/* Opt this document into cross-document view transitions, so navigating to
   a case study (and back) morphs rather than cutting instantly. Unsupported
   browsers (Firefox, older Safari) just keep today's instant navigation -
   this is additive, not a replacement for anything. */
@view-transition {
  navigation: auto;
}

/* Named card -> case-study-hero pairs animate as one continuous shape (the
   "grow into the page" effect). The curve is cubic-bezier(0.16, 1, 0.3, 1)
   - a fast-start, long-smooth-decel "expo out" - which is the actual shape
   of Apple's own card-expand transitions (App Store product pages, Photos
   detail view): quick to commit, no bounce/overshoot at the end, because
   overshoot on something resizing its literal box reads as a glitch, not
   a spring. Old/new get the identical curve and duration so the box-resize
   and the content crossfade land in the same rhythm instead of visibly
   drifting apart, which is a common cause of transitions feeling "off." */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Root (everything without its own view-transition-name, images included)
   just cuts out instantly on exit - no fade, no scale - and fades in on
   entry. The named groups above (card/headline) carry all the motion, via
   their own box interpolation; root just needs to get out of the way
   cleanly rather than adding a second, separate-feeling transition on top. */
@keyframes cs-root-in {
  from { opacity: 0; }
}
::view-transition-old(root) { animation: none; }
::view-transition-new(root) { animation-name: cs-root-in; }

:root {
  /* ==== Reference tokens ====
     The raw scales. Nothing consumes these directly - components reference
     a *role* below, which points at one step of a scale. Redefining a step
     here (what "lg" actually equals) ripples to every role assigned to it;
     reassigning a role to a different step doesn't touch the scale. This
     is the same reference/system split m3.material.io uses for its own
     token set. */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;

  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 24px;
  --radius-xl: 32px;
  /* 100px, not the more conventional 999px: every pill-shaped element on
     this site is under 50px tall, so 100 already guarantees a full round
     the same as 999 would - and a slider bounded 0-100 stays useful,
     whereas one that had to reach 999 would be all but unusable for the
     visually-relevant 0-40px range. */
  --radius-full: 100px;

  /* ==== Color tokens (semantic - name the role, not the hue) ==== */
  --color-surface: #f9e0d6;
  --color-on-surface: #301e18;
  --color-surface-inverse: #2e1b15;
  --color-surface-bright: #fdfbf8;
  /* Sits on --color-surface-inverse (the dark sticky panel), so it defaults
     to the bright value for contrast, not --color-on-surface - "the color
     for interactive text/links" is contextual, not one fixed hue. */
  --color-interactive: #fdfbf8;
  /* The .card-glass tint - an RGB triple + separate alpha so color and
     strength can be tuned independently, composed as
     rgba(var(--color-glass-tint), var(--color-glass-tint-alpha)) where
     it's used. */
  --color-glass-tint: 255, 240, 240;
  --color-glass-tint-alpha: 0.5;
  /* Shade laid over inactive cards (hover dimming and the tab filter).
     Color + strength are separate so either can be tuned alone; the hover
     dim uses 70% of the strength, the filter uses all of it. */
  --color-dim: #301e18;
  /* Bright text at reduced strength, for copy on the dark inverse panel. */
  --color-on-inverse-muted: rgba(253, 251, 248, 0.62);
  --color-on-inverse-subtle: rgba(253, 251, 248, 0.34);
  /* Redline coral for the card selection box (matches --thinking), plus a
     darker step for text that has to clear contrast on the peach surface. */
  --color-accent: #e8505b;
  --color-accent-strong: #b8323d;
  --color-outline-strong: rgba(48, 30, 24, 0.18);
  --dim-strength: 42%;

  /* ==== Type tokens ====
     Headings use the shared roles in type.css (--font-heading-*); these
     are the homepage-only text sizes that have no role there. */
  --type-label: 10px;
  --type-body: 14px;
  --type-body-lg: 17px;
  --type-nav: 14px;
  --type-button: 13px;

  /* ==== Motion tokens ==== */
  --motion-hover: 0.35s;
  --motion-fade: 0.45s;

  /* ==== Size tokens (standalone dimensions - not steps of a repeating
     scale, so they live outside --space-*) ==== */
  --size-panel-width: 336px;
  /* How far the panel's scalloped edge reaches past its width - offsets
     to the right of the panel are measured from the scallops' tips. */
  --size-scallop: 25px;

  /* ==== Component role assignments ====
     Which scale step each spacing/shape role actually uses. Change the
     step referenced here (e.g. --gap: var(--space-xl)) to reassign the
     role; change the scale itself above to redefine what that step means
     everywhere it's used. */
  --gap: var(--space-md);
  /* One margin all the way round the cards: top, right, bottom, and from
     the panel's scallops on the left. The background grid's frame sits
     halfway between the cards and the edge, on the same rule. */
  --space-edge: var(--space-xl);
  --space-frame: var(--space-md);
  --radius: var(--radius-md);
  --btn-radius: var(--radius-full);
  --tabs-radius: var(--radius-full);
  --tab-radius: var(--radius-full);
  /* Content gap (title -> image) for the three cards where that's still a
     meaningful per-card knob. Previously drifted to a hardcoded 20px on
     .card-bg-img when the image treatment was simplified to pre-composed
     mockups - reconnected here so the role tokens actually mean something
     again rather than sitting unused in :root. */
  --card-gap-teal: var(--space-md);
  --card-gap-purple: var(--space-md);
  --card-gap-orangesky: var(--space-md);

  /* ==== Component-specific tokens (continuous values with no shared
     scale to draw from) ==== */
  --card-glass-blur: 2px;
  --card-glass-saturate: 100%;
  --card-mh-mini: 260px;
  --card-mh-photo: 340px;
  --img-phone-width: 58%;
  --img-phones-width: 88%;
  --img-spacecentre-x: -20px;

  /* Card glass: layered inner highlights + drop shadow, kept as single
     component tokens because the layers only make sense together. */
  --shadow-card:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.95),
    inset 0 0 0 1px rgba(255, 255, 255, 0.15),
    inset 0 -12px 20px -14px rgba(255, 255, 255, 0.5),
    inset 0 10px 18px -16px rgba(48, 30, 24, 0.25),
    0 10px 30px rgba(48, 30, 24, 0.14),
    0 2px 8px rgba(48, 30, 24, 0.08);
  --shadow-card-hover:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.95),
    inset 0 0 0 1px rgba(255, 255, 255, 0.2),
    inset 0 -12px 20px -14px rgba(255, 255, 255, 0.55),
    inset 0 14px 24px -18px rgba(48, 30, 24, 0.35),
    0 20px 46px rgba(48, 30, 24, 0.22),
    0 4px 12px rgba(48, 30, 24, 0.1);
  --shadow-card-glass:
    inset 1px 0 0 rgba(255, 0, 170, 0.05),
    inset -1px 0 0 rgba(0, 220, 255, 0.05),
    inset 0 14px 22px -18px rgba(255, 255, 255, 0.6),
    inset 0 -14px 22px -18px rgba(48, 30, 24, 0.22);
  --card-sheen:
    radial-gradient(130% 70% at 8% -8%, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 55%),
    linear-gradient(-45deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 45%);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  font-family: var(--font-family-body);
  font-size: var(--type-body);
  min-height: 100vh;
  /* Inherited, so this reaches every wrapping label/line on the page
     without hand-listing each class - avoids a lone last-line word (an
     "orphan") wherever text wraps to more than one line. Short headline-
     style text (tagline, card titles) gets the stronger text-wrap: balance
     below instead, which evens every line rather than only nudging the
     last one - reads better on 2-3 line titles specifically. Both are
     no-ops in browsers that don't support them yet. */
  text-wrap: pretty;
}

.panel-tagline, .card-title { text-wrap: balance; }

a { color: inherit; text-decoration: none; }

button { font-family: inherit; cursor: pointer; }
ul { margin: 0; padding: 0; list-style: none; }

.page {
  position: relative;
  display: flex;
  min-height: 100vh;
}

/* ---- Sticky left panel ---- */
.sticky-panel {
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  width: var(--size-panel-width);
  flex: 0 0 var(--size-panel-width);
  background: var(--color-surface-inverse);
  color: var(--color-surface-bright);
  z-index: 5;
}

.sticky-panel::after {
  content: "";
  position: absolute;
  top: 0;
  right: calc(var(--size-scallop) * -1);
  bottom: 0;
  width: calc(var(--size-scallop) * 2);
  background-image: radial-gradient(circle var(--size-scallop) at var(--size-scallop) var(--size-scallop), var(--color-surface-inverse) var(--size-scallop), transparent calc(var(--size-scallop) + 1px));
  background-size: calc(var(--size-scallop) * 2) calc(var(--size-scallop) * 2);
  background-repeat: repeat-y;
}

.panel-inner {
  padding: 68px 40px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.panel-hello {
  font-family: var(--font-heading-large-family);
  font-size: var(--font-heading-large-size);
  font-weight: var(--font-heading-large-weight);
  line-height: var(--font-heading-large-line-height);
  letter-spacing: var(--font-heading-large-tracking);
  margin: 0;
}

.panel-tagline {
  font-family: var(--font-heading-xxlarge-family);
  font-size: var(--font-heading-xxlarge-size);
  font-weight: var(--font-heading-xxlarge-weight);
  line-height: var(--font-heading-xxlarge-line-height);
  letter-spacing: var(--font-heading-xxlarge-tracking);
  margin: 0;
  max-width: 320px;
}

.panel-links {
  margin: 12px 0 0;
  font-family: var(--font-body-small-family);
  font-size: var(--font-body-small-size);
  font-weight: var(--font-body-small-weight);
  line-height: var(--font-body-small-line-height);
  letter-spacing: var(--font-body-small-tracking);
}

.panel-links a { color: var(--color-interactive); text-decoration: underline; }
.panel-links span { margin: 0 8px; opacity: 0.6; }

/* Intro paragraph: project sentences are tied to their cards (script.js).
   Muted by default with the role sentence at full strength, like a lede;
   hovering a sentence or its card brings that sentence forward and steps
   the rest back. */
.panel-intro {
  margin: 0;
  max-width: 320px;
  font-size: var(--font-size-300);
  line-height: var(--font-body-small-line-height);
  color: var(--color-on-inverse-muted);
}

.panel-intro span { transition: color 0.2s ease; }
.panel-intro .intro-key { color: var(--color-surface-bright); }
.panel-intro .intro-close { display: block; margin-top: 0.7em; }
.panel-intro.is-spot span { color: var(--color-on-inverse-subtle); }
.panel-intro.is-spot span.is-on { color: var(--color-surface-bright); }

@media (hover: hover) and (pointer: fine) {
  .panel-intro [data-work] { cursor: default; }
}

/* Short laptop screens: the panel is one viewport tall, so tighten it, and
   if it still doesn't fit let the inner column scroll rather than clip.
   The scroll lives on .panel-inner, not .sticky-panel, so the scalloped
   ::after edge isn't clipped with it. */
@media (min-width: 961px) {
  .panel-inner { max-height: 100vh; overflow-y: auto; scrollbar-width: none; }
}

@media (min-width: 961px) and (max-height: 940px) {
  .panel-inner { padding-top: 44px; padding-bottom: 32px; gap: 18px; }
  .panel-tagline { font-size: var(--font-size-1200); }
  .panel-intro { font-size: var(--font-size-200); line-height: var(--font-body-small-line-height); }
}

/* ---- Main content ---- */
.content {
  position: relative;
  flex: 1;
  min-width: 0;
  background-color: var(--color-surface);
  padding: var(--space-edge) var(--space-edge) 100px calc(var(--size-scallop) + var(--space-edge));
}

/* Grid pattern lives on its own layer (a real element, not a pseudo -
   script.js needs to measure its rendered width) inset from the top/left/
   right of the content area, rather than painted directly on .content -
   that's what leaves a plain-color margin on those three sides instead of
   the pattern running edge-to-edge. It's absolutely positioned against
   .page (which is position:relative) rather than fixed to the viewport, so
   it scrolls together with the cards as one piece instead of staying put
   while they scroll past it. The 1040x641 size below is only a fallback
   for the instant before JS runs - script.js recalculates it on load/resize
   so a whole number of tiles always fits exactly, keeping a full line at
   the right edge instead of a partial tile getting clipped. */
.bg-grid {
  position: absolute;
  top: var(--space-frame);
  left: calc(var(--size-panel-width) + var(--size-scallop) + var(--space-frame));
  right: var(--space-frame);
  bottom: 0;
  z-index: 1;
  pointer-events: none;
  background-image: url('assets/background-grid-landscape.svg');
  background-repeat: repeat;
  background-position: center top;
  background-size: 1040px 641px;
}

/* Animated mesh gradient sitting behind .bg-grid and the cards, scoped to
   the .content column the same way .bg-grid is (left offset skips the
   sticky panel). z-index: 0 keeps it under .bg-grid (1) and .masonry (2).
   position: fixed (not absolute) because the gradient's canvas only ever
   renders viewport-sized content - anchoring it to the full, much-taller
   scroll height of .page left everything past the first screen unpainted. */
.bg-gradient {
  position: fixed;
  top: 0;
  left: var(--size-panel-width);
  right: 0;
  bottom: 0;
  z-index: 0;
  pointer-events: none;
}


/* ---- Masonry columns ---- */
.masonry {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  align-items: start;
  max-width: 1400px;
  margin: 0 auto;
}

.col {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  /* Without this, a grid item's min-width defaults to its content's
     min-content size - and a hero image sitting in normal flow (see
     .card-bg-img) contributes its natural pixel width to that, which
     blew the masonry grid's two columns wildly out of proportion instead
     of respecting the 1fr/1.1fr split. */
  min-width: 0;
}

/* ---- Cards ---- */
.card {
  position: relative;
  display: block;
  min-width: 0;
  border-radius: calc(var(--radius) + 6px);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  color: var(--color-on-surface);
  transition: transform var(--motion-hover) cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow var(--motion-hover) cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1), filter var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1);
}

/* The frosted-glass look lives on this inner layer rather than on .card
   itself. Cross-document view transitions snapshot the element carrying
   view-transition-name, and Chromium can't correctly capture an element
   that also has an SVG-filter backdrop-filter directly on it - the capture
   silently fails and the whole transition falls back to an instant swap.
   Keeping .card filter-free (and putting the glass on a plain absolute
   child instead) keeps the exact same look while letting the card ->
   hero morph actually run.

   The tint background moved down here too, and that part isn't optional:
   backdrop-filter blurs whatever paints behind IT specifically. Left on
   .card, this layer would sit on top of .card's own background and blur
   that flat tint instead of the actual page behind the card - which reads
   as a grey, hazy box instead of glass. Keeping both on the same element
   restores the original stacking (blur the page, then lay the tint over
   the blur).

   z-index: -1, not 0: cards like .card-highlights and .card-mini have
   content with no position set (static), and a positioned element - even
   at z-index: 0 - always paints above static content regardless of DOM
   order. At 0, this layer sat on top of that text and blurred it. -1
   drops it below static content while staying above .card's own
   background, which is what "behind the content, in front of the page"
   actually requires. */
/* Figma "Glass" material, ported 1:1 where CSS has a real equivalent and
   approximated where it doesn't (that engine renders true refraction/
   dispersion; backdrop-filter can't bend light, only blur+saturate it):
     Frost 12       -> blur(); pushed well past the literal 12px to 52px.
                       .bg-grid's pattern repeats on a ~1040px tile (see
                       script.js), so a blur radius has to get genuinely
                       large before it stops just softening the lines and
                       starts actually averaging them out - 22px still left
                       them readable enough to fight with text on cards with
                       a lot of bare glass (Highlights, Seat selection). A
                       thin white wash is layered in below for the same
                       reason blur alone won't fully solve: it also masks
                       the pattern, without going back to an opaque tint.
     Refraction 80  -> saturate(180%) - backdrop-filter's closest analog to
                       "how much the glass bends/intensifies what's behind it"
     Light -45/80%  -> the -45deg linear glint below; dialed down from the
                       literal 80% to 40% peak opacity - at 80% it read as a
                       wash strong enough to look like a strikethrough over
                       title text on the same bare-glass cards above
     Depth 20       -> the inset highlight/shadow pair (light rim top-left,
                       dark rim bottom-right)
     Dispersion 50  -> the two hairline magenta/cyan inset shadows - a
                       restrained stand-in for chromatic-aberration fringing
     Splay 0        -> nothing extra; 0 means no additional spread

   One shared rule, no per-card override: every card gets the identical
   treatment regardless of how much of it its own image covers - card-mini-
   tan/-cream used to paint their own flat tan/cream fill here, which is
   part of why cards disagreed on what the material even looked like.

   The other part: with saturate(180%), a card that's mostly bare glass
   (Highlights, Seat selection) picks up a strong warm wash off the pink
   page behind the blur, while a card that's mostly its own screenshot
   (Turn Tool, Space Centre) barely shows any glass at all - so the exact
   same rule still read as two different materials depending on coverage,
   just because of how much pink it had left to saturate. Trading most of
   that saturation for a heavier white wash instead is what makes a fully-
   exposed card and a sliver of one look like the same surface: closer to
   frosted white glass than tinted glass, so coverage stops being visible
   in the result. */
.card-glass {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background: rgba(var(--color-glass-tint), var(--color-glass-tint-alpha));
  backdrop-filter: blur(var(--card-glass-blur)) saturate(var(--card-glass-saturate));
  -webkit-backdrop-filter: blur(var(--card-glass-blur)) saturate(var(--card-glass-saturate));
  box-shadow: var(--shadow-card-glass);
}

/* The "front" glint that keeps text readable on a now-transparent glass
   surface: a top-left highlight sitting right under .card-top's label/
   title, plus the -45deg sweep from the Light setting. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: var(--card-sheen);
}

/* ---- Hover (card-motion.js) ----
   The card tilts in 3D toward the cursor and grows a touch, after React
   Bits' Tilted Card (card-motion.js writes the transform), and its shadow
   deepens. Every card on the grid gets the same, Highlights included. */
a.card:hover,
.card-highlights:hover,
a.card:focus-visible { box-shadow: var(--shadow-card-hover); }
a.card:focus-visible { transform: translateY(-3px); outline: 1.5px solid var(--color-on-surface); outline-offset: 4px; }

/* Cards filtered out by a tab sit under a veil of the espresso ink. */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 8;
  border-radius: inherit;
  pointer-events: none;
  background: color-mix(in srgb, var(--color-dim) var(--dim-strength), transparent);
  opacity: 0;
  transition: opacity var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1);
}

.card.is-disabled::after,
.mini-row.is-disabled .card::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  a.card:focus-visible { transform: none; }
  .highlights-viewport .highlights-line { transition: none; }
  /* Page-to-page morphs become a plain cut. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

.card.is-disabled,
.mini-row.is-disabled {
  pointer-events: none;
}


.card-top { position: relative; z-index: 2; padding: 32px 32px 0; }

.card-label {
  font-family: var(--font-breadcrumb-small-family);
  font-weight: var(--font-breadcrumb-small-weight);
  font-size: var(--font-breadcrumb-small-size);
  letter-spacing: 0.14em;
  line-height: var(--font-breadcrumb-small-line-height);
  text-transform: uppercase;
  margin: 0 0 8px;
  /* 0.82, not the original 0.75 - small text needs the full 4.5:1 AA ratio
     (no "large text" exception at 10px), and this keeps a comfortable
     margin above it rather than sitting close to the line. */
  opacity: 0.82;
}

.card-title {
  font-family: var(--font-heading-medium-family);
  font-size: var(--font-heading-medium-size);
  font-weight: var(--font-heading-medium-weight);
  line-height: var(--font-heading-medium-line-height);
  letter-spacing: var(--font-heading-medium-tracking);
  margin: 0;
}

/* UltraBasic/Seat Selection/Turn Tool/Space Centre: title block in normal
   flow, then one image below it. A flow-based box (not position: absolute)
   means there's no min-height/top-offset arithmetic that can drift out of
   sync with a longer title - the image always starts exactly where the
   title block ends.

   Every source here is a pre-composed mockup exported straight from Figma
   (a real device frame - laptop or phone bezel - already rendered around
   the actual screenshot, already cropped to its final look), not a raw
   screenshot needing CSS to fake a crop/frame around it. So there's no
   object-fit: cover / aspect-ratio window doing work here anymore - each
   image just displays at its own native size and ratio. Only Seat
   Selection and UltraBasic (phone sources) get a narrower, centered width;
   Turn Tool and Space Centre (laptop sources, already close to the card's
   own width) run full-bleed edge to edge at the base width below. */
.card-bg-img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}

/* UltraBasic: a single phone source, so it stays noticeably narrower than
   the two-phone Seat Selection image below - full width would blow a
   single phone silhouette up far past its natural size. */
.card-teal .card-bg-img {
  width: var(--img-phone-width);
  margin-top: var(--card-gap-teal);
  margin-left: auto;
  margin-right: auto;
}

/* Seat Selection: two phones side by side in one source image, already
   wider than a single phone but still narrower than the full-bleed laptop
   cards - full width would stretch the pair past a natural phone size. */
.card-purple .card-bg-img {
  width: var(--img-phones-width);
  margin-top: var(--card-gap-purple);
  margin-left: auto;
  margin-right: auto;
}

.card-sky .card-bg-img,
.card-orange .card-bg-img {
  margin-top: var(--card-gap-orangesky);
}

/* Designing with AI: the token panel itself, cropped out of a live
   screenshot (rounded top corners baked into the PNG's alpha). Centered
   and narrower than the laptop cards, like the single-phone UltraBasic
   tile, so the panel reads at roughly its real on-screen size. */
.card-ai .card-bg-img {
  width: 62%;
  margin: var(--card-gap-orangesky) auto 0;
  filter: drop-shadow(0 12px 28px rgba(48, 30, 24, 0.22));
}

/* Space Centre: left edge offset from the card's own edge - a plain
   x-position, not a width recalculation, so the right edge keeps bleeding
   off the card's own edge regardless of this value; only the left side
   moves. Negative pulls it left past the card edge (bleeding, like the
   right side already does); 32px would align it with .card-top's own left
   padding instead, flush with the title/label above it. */
.card-orange .card-bg-img {
  margin-left: var(--img-spacecentre-x);
}

/* ---- Highlights card ---- */
.card-highlights {
  padding: 36px 32px 16px;
  display: flex;
  flex-direction: column;
}

.card-eyebrow {
  font-family: var(--font-heading-xxsmall-family);
  font-size: var(--font-heading-xxsmall-size);
  font-weight: var(--font-heading-xxsmall-weight);
  line-height: var(--font-heading-xxsmall-line-height);
  letter-spacing: var(--font-heading-xxsmall-tracking);
  margin: 0 0 36px;
  flex: none;
}

.highlights-viewport {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.highlights-line {
  margin: 0;
  font-family: var(--font-lead-family);
  font-size: var(--font-lead-size);
  font-weight: var(--font-lead-weight);
  line-height: var(--font-lead-line-height);
  letter-spacing: var(--font-lead-tracking);
  overflow: hidden;
  flex: none;
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.highlights-line.is-active {
  /* Fallback only - script.js sizes this to the tallest attribute at the
     card's current width. */
  height: 72px;
  font-size: var(--type-body-lg);
  opacity: 1;
}

.highlights-line.is-swapping {
  opacity: 0;
  transform: translateY(6px);
}

.highlights-dots {
  display: flex;
  gap: 8px;
  margin-top: 20px;
  flex: none;
}

/* A button with a 24px hit area (WCAG 2.5.8) around a 6px visible dot. */
.highlights-dot {
  position: relative;
  width: 24px;
  height: 24px;
  margin: -9px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.highlights-dot::before {
  content: "";
  position: absolute;
  inset: 9px;
  border-radius: 50%;
  background: var(--color-outline-strong);
  transition: background-color 0.3s ease, transform 0.3s ease;
}

.highlights-dot:focus-visible { outline: 2px solid var(--color-on-surface); outline-offset: -4px; border-radius: 50%; }

.highlights-dot.is-active::before {
  background: var(--color-on-surface);
  transform: scale(1.4);
}

/* ---- Mini cards ---- */
.mini-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  transition: opacity var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1), filter var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1);
}

.card-mini {
  min-height: var(--card-mh-mini);
  display: flex;
}

.card-mini img {
  position: absolute;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

/* card-mini-tan/-cream intentionally carry no .card-glass override of their
   own anymore - they use the exact same shared glass as every other card
   (see the comment on .card-glass) so all four "bare glass" cards read as
   one consistent material instead of two of them being flat tinted panels. */

/* Sized/positioned per image rather than a shared rule - the sandwich photo
   and the showcase illustration have different native crops, so one
   "bigger + shifted" value doesn't fit both. Bleeding past the card edge
   (cropped by .card's own overflow: hidden) is what makes each read as
   large as the reference instead of a contained thumbnail floating in
   empty padding. */
.card-mini-tan img {
  width: 112%;
  height: 112%;
  top: 6%;
  left: 8%;
}

.card-mini-cream img {
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  padding: 20px;
  object-position: 52% 42%;
}

/* ---- Photo card ---- */
.card-photo {
  min-height: var(--card-mh-photo);
}

.card-photo img {
  width: 100%;
  height: 100%;
  min-height: var(--card-mh-photo);
  object-fit: cover;
  display: block;
}

/* ---- Buttons on cards ---- */
.card-btn {
  position: absolute;
  left: 24px;
  bottom: 24px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--color-surface-bright);
  color: var(--color-on-surface);
  font-family: var(--font-label-family);
  font-size: var(--font-label-size);
  font-weight: var(--font-label-weight);
  line-height: var(--font-label-line-height);
  letter-spacing: var(--font-label-tracking);
  padding: 8px 16px;
  border-radius: var(--btn-radius);
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
}

.card-highlights .card-btn { display: none; }

.card-btn svg { width: 12px; height: 12px; }

/* ---- Bento grid ----
   Cells of different sizes on one 20-column grid, like a brand bento board:
   Seat Selection is the hero cell; Highlights runs wide and short beside it
   with the two Exploration tiles as squares underneath; the bottom row is
   two project cells and a tall portrait for the photo. Row edges don't all
   line up on purpose - the mix of sizes is what makes it read as a bento
   rather than a table.

   Cards are placed by these rules, not by their order in index.html, so
   the .col / .mini-row wrappers step aside (display: contents). Below
   960px the wrappers come back and everything stacks in one column. */
@media (min-width: 961px) {
  .masonry {
    grid-template-columns: repeat(20, minmax(0, 1fr));
    grid-template-rows: auto 240px 340px;
    align-items: stretch;
    max-width: none;
  }

  .masonry > .col,
  .mini-row { display: contents; }

  .card-purple      { grid-column: 1 / 11;  grid-row: 1 / 3; }
  .card-highlights  { grid-column: 11 / 21; grid-row: 1; }
  .card-mini-tan    { grid-column: 11 / 16; grid-row: 2; }
  .card-mini-cream  { grid-column: 16 / 21; grid-row: 2; }
  .card-sky         { grid-column: 1 / 9;   grid-row: 3; }
  .card-orange      { grid-column: 9 / 17;  grid-row: 3; }
  .card-photo       { grid-column: 17 / 21; grid-row: 3; }

  .card-mini { min-height: 0; }
  .card-photo,
  .card-photo img { min-height: 0; height: 100%; }

  .card-highlights { padding: 24px 28px 16px; }
  .card-eyebrow { margin-bottom: 16px; }
}

/* Small laptops and tablets in landscape (961-1199px): the same board on
   four columns, with the photo taking its own wide row at the bottom - at
   these widths a fifth of the row would leave it a sliver. */
@media (min-width: 961px) and (max-width: 1199px) {
  .masonry {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: auto 240px 360px 320px;
  }

  .card-purple      { grid-column: 1 / 3; grid-row: 1 / 3; }
  .card-highlights  { grid-column: 3 / 5; grid-row: 1; }
  .card-mini-tan    { grid-column: 3 / 4; grid-row: 2; }
  .card-mini-cream  { grid-column: 4 / 5; grid-row: 2; }
  .card-sky         { grid-column: 1 / 3; grid-row: 3; }
  .card-orange      { grid-column: 3 / 5; grid-row: 3; }
  .card-photo       { grid-column: 1 / 5; grid-row: 4; }
  .card-photo img   { object-position: center 30%; }
}

/* On a laptop or bigger the bento fills exactly the space under the tabs:
   no page scroll. The top row fits Highlights; the rest of the height
   splits between the tiles' row and the taller bottom row, so every card is
   sized by the screen rather than by its image - mockups keep their
   natural size and run off the card's bottom edge, where overflow: hidden
   crops them. The background grid stops repeating and stretches to frame
   the view (script.js leaves its size alone in this mode). Under 1200 x
   700 the bento keeps fixed row heights and the page scrolls instead,
   since cells would get too small to read. */
@media (min-width: 1200px) and (min-height: 700px) {
  .content {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    padding: var(--space-edge) var(--space-edge) var(--space-edge) calc(var(--size-scallop) + var(--space-edge));
  }

  .site-controls { flex: none; }

  .masonry {
    flex: 1;
    min-height: 0;
    width: 100%;
    grid-template-rows: auto minmax(0, 1fr) minmax(0, 1.5fr);
  }

  .card-top { padding: 24px 24px 0; }

  .bg-grid {
    position: fixed;
    top: var(--space-frame);
    bottom: var(--space-frame);
    left: calc(var(--size-panel-width) + var(--size-scallop) + var(--space-frame));
    right: var(--space-frame);
    background-repeat: no-repeat;
    background-size: 100% 100%;
  }
}

/* ---- Responsive ---- */
@media (max-width: 960px) {
  .page { flex-direction: column; }

  .sticky-panel {
    position: relative;
    width: 100%;
    height: auto;
    flex: none;
  }

  .sticky-panel::after { display: none; }

  .bg-grid { left: 40px; }
  .bg-gradient { left: 0; }

  .masonry { grid-template-columns: 1fr; }

  .content { padding: 28px 20px 80px; }
}

@media (max-width: 560px) {
  .mini-row { grid-template-columns: 1fr; }
  .panel-tagline { font-size: var(--font-size-1000); }
}

/* ---- Personality layer ----
   Small, decorative touches. Everything that moves sits inside
   prefers-reduced-motion: no-preference, so with reduced motion on the page
   renders in its finished state: underline drawn, cards in
   place. Nothing here carries meaning, so nothing here is announced. */

/* Hand-drawn coral underline under the key word of the tagline. It's a
   background stroke under the text, so the word's own contrast is
   untouched. */
.scribble {
  position: relative;
  white-space: nowrap;
}

.scribble::after {
  content: "";
  position: absolute;
  left: -2%;
  right: -2%;
  bottom: -0.12em;
  height: 0.32em;
  pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 14' preserveAspectRatio='none'%3E%3Cpath d='M3 9.5C38 4.5 92 3 197 6.5M18 12c46-4 103-4.5 164-2.5' fill='none' stroke='%23e8505b' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
}

@media (prefers-reduced-motion: no-preference) {
  /* Underline draws itself left to right, just after the tagline lands. */
  .is-arriving .scribble::after { animation: scribble 0.7s 0.65s cubic-bezier(0.65, 0, 0.35, 1) both; }

  /* First arrival only (see the script in index.html's head): the panel's
     lines rise in, lightly staggered; the cards follow from card-motion.js.
     Short on purpose - it's all over in under a second and nothing waits
     on it to be readable or clickable. */
  .is-arriving .panel-inner > * {
    animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  }
  .is-arriving .panel-inner > :nth-child(2) { animation-delay: 0.06s; }
  .is-arriving .panel-inner > :nth-child(3) { animation-delay: 0.12s; }
  .is-arriving .panel-inner > :nth-child(n+4) { animation-delay: 0.18s; }
}

@keyframes rise {
  from { opacity: 0; translate: 0 14px; }
}

@keyframes scribble {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

/* ---- Card motion (card-motion.js) ----
   Motion springs the cards' transform from script, so CSS stops
   transitioning it once that's running - two animators on one property
   fight. Before a card's turn in the reveal it waits hidden; the head
   script in index.html only sets that up on first arrival with motion
   allowed, and drops it if card-motion.js never loads. */
.has-card-motion .card {
  transition: box-shadow var(--motion-hover) cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1), filter var(--motion-fade) cubic-bezier(0.22, 1, 0.36, 1);
}
.cards-pending .masonry .card:not(.is-shown) { opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  /* Focus lift comes from card-motion.js, on the same spring as hover. */
  .has-card-motion a.card:focus-visible { transform: none; }
}
