/* ============================================================
   ALVINOTS® — Design System
   Single source of truth for the entire site.
   Edit tokens once, all pages update.
   ============================================================ */

/* ---------- Reset ---------- */

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html { scroll-behavior: smooth; }

body {
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  min-height: 100vh;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; }
/* height:auto is load-bearing now that every <img> carries intrinsic width and
   height attributes. Those attributes are presentational hints: with a CSS
   `width: 100%` and no CSS height, the attribute height would be used as a
   fixed pixel height and the image would distort. `auto` restores proportional
   scaling while browsers still read the two attributes to reserve the right
   aspect ratio before the bytes arrive — which is the whole point of adding
   them. Rules that genuinely want a fixed height (.hero-portrait, .reel-facade,
   .project-still) declare it themselves and outrank this by specificity. */
img { display: block; max-width: 100%; height: auto; }


/* ---------- Design tokens ---------- */

:root {
  /* Colors */
  --cream: #F1F1F1;
  --surface: #E2E2E2;
  --surface-dark: #1A1A18;
  --ink: #0E0E0C;
  --muted: #555550;
  --muted-soft: rgba(14, 14, 12, 0.15);
  --blue: #1F1FDA;
  /* Kuya blue on ink is 2.09:1 — it fails WCAG AA for normal text AND for
     large text, so it cannot carry any blue-on-dark accent. This is the brand
     book's designated "accents on dark" value and measures 9.07:1 on --ink.
     Rule: --blue on cream, --blue-pale on ink. Never --blue on a dark ground. */
  --blue-pale: #A8AAFF;

  /* Fonts — canonical ALVINOTS® stack (see the alvinots-brand skill).
     Two families only. Archivo Black / Inter / JetBrains Mono were free
     stand-ins and have been retired.

     Display weight is 700, Space Grotesk's heaviest cut. Archivo Black was a
     single-weight face that rendered black at weight 400, so every rule using
     --display now declares 700 explicitly — without it the headlines would
     drop to regular and the whole page would go soft. */
  --display: 'Space Grotesk', 'Helvetica Neue', system-ui, sans-serif;
  --body: 'Space Grotesk', 'Helvetica Neue', system-ui, -apple-system, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, 'Menlo', monospace;
  /* The third family, and the only one with a job this narrow. Falls back to
     --display, so if Doto fails to load every numeral below just renders in
     Space Grotesk at the same size — degraded, never broken. */
  --font-numeral: 'Doto', 'Space Grotesk', system-ui, sans-serif;

  /* Spacing */
  --margin: 60px;
}





/* ---------- Typography utilities ---------- */

/* Visually hidden but available to assistive tech and search. Not display:none
   and not visibility:hidden — both remove the element from the accessibility
   tree, which defeats the point. The 1px-clip technique is the one that keeps
   it announced. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link: hidden until focused, then pinned over the transmission bar so it
   is the first thing a keyboard user can act on. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 1000;
  background: var(--blue);
  color: var(--cream);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 14px 20px;
}
.skip-link:focus {
  left: 0;
}

.mono {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.mono-sm {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.display {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 0.9;
  text-transform: uppercase;
}
.hairline { height: 1px; background: var(--ink); width: 100%; }
.hairline-blue { height: 2px; background: var(--blue); width: 100%; }
.blue { color: var(--blue); }


/* ---------- TRANSMISSION BAR (sticky top) ---------- */

.transmission {
  background: var(--ink);
  color: var(--cream);
  padding: 10px 0;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  position: sticky;
  top: 0;
  z-index: 99;
  border-bottom: 1px solid var(--blue);
}
/* Two copies of the segment set, travelling exactly half the track width —
   that is what makes the seam invisible. */
.transmission-track {
  display: flex;
  align-items: center;
  gap: 20px;
  width: max-content;
  white-space: nowrap;
  animation: scroll-transmission 48s linear infinite;
  will-change: transform;
}
/* gap, not literal spaces: inline-flex makes the children flex items and
   collapses the whitespace between them ("23:44:59MYT", "STATUS:●AVAILABLE"). */
.tx-seg {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
}
.tx-sep { color: var(--blue-pale); }
.tx-dot { color: var(--blue-pale); }
@keyframes scroll-transmission {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
.transmission .pulse {
  display: inline-block;
  flex: none; /* a flex item now — must not squash */
  width: 7px; height: 7px;
  background: var(--blue);
  border-radius: 50%;
  margin-right: 8px;
  animation: pulse 1.4s ease-in-out infinite;
  vertical-align: middle;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}


/* ---------- TOP NAV ---------- */

nav.top {
  padding: 22px var(--margin);
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: var(--cream);
  border-bottom: 1px solid var(--ink);
  position: sticky;
  top: 30px;
  z-index: 98;
}
.nav-left { display: flex; gap: 36px; flex-wrap: wrap; }
/* Vertical padding cancelled by an equal negative margin: grows the tap target
   from the 12px text height to ~36px without moving anything. Same trick as
   .nav-monogram below. Horizontal is left alone so the flex gap stays exact. */
.nav-link {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.22em;
  color: var(--ink);
  transition: color 80ms linear;
  text-transform: uppercase;
  padding: 12px 0;
  margin: -12px 0;
}
.nav-link:hover, .nav-link.active { color: var(--blue); }
.nav-monogram {
  font-family: var(--display);
  font-weight: 700;
  font-size: 24px;
  letter-spacing: -0.02em;
  padding: 8px 12px;
  margin: -8px -12px;
  display: inline-flex;
  align-items: center;
  transition: color 80ms linear;
}
.nav-monogram:hover { color: var(--blue); }
.nav-monogram sup {
  font-family: var(--mono);
  font-size: 0.45em;
  letter-spacing: 0;
  font-weight: 400;
  vertical-align: top;
  position: relative;
  top: 0.1em;
  margin-left: 0.1em;
}


/* ---------- HERO ---------- */

/* ---------- HERO STAGE ----------
   The portrait is pinned while the veil below scrolls across it. .hero is the
   sticky element and keeps its own overflow:hidden (it clips the wordmark
   marquee); stickiness is resolved against .hero-stage, so clipping here is
   safe. The stage must NOT have overflow hidden or the sticky breaks. */
.hero-stage {
  position: relative;
}

.hero {
  height: calc(100vh - 80px);
  min-height: 680px;
  position: sticky;
  top: 80px; /* clears the transmission bar (30px) + sticky nav */
  overflow: hidden;
  background: var(--cream);
  border-bottom: 2px solid var(--blue);
  z-index: 1;
}

.hero-veil {
  position: relative;
  z-index: 3;
}

/* ---------- PROFILE ON GLASS ----------
   The panel IS the glass. It rises over the pinned portrait, refracts it
   through vertical flutes, and darkens as it goes so it lands on --ink, the
   colour CAPABILITIES opens in. Nothing opaque crosses the figure. */

.profile {
  position: relative;
  overflow: hidden;
  /* Sets how long the hero stays pinned, which is the whole ballgame for the
     half-covered composition. The hero is sticky inside .hero-stage, so it
     un-pins once the stage has been consumed — i.e. after roughly this much
     scrolling. At 62vh it un-pinned at ~606px while the panel did not finish
     crossing until ~738px, so the portrait had already slid away by the time
     the glass was over it and the half-covered frame never actually existed.
     112vh buys ~225px of scroll where the panel is across a still-pinned
     portrait. The extra height is clear glass at the top of the panel, which
     is where the figure shows through — it is the picture, not padding. */
  min-height: 112vh;
  display: flex;
  flex-direction: column;
  /* Packed to the bottom, not spread. The panel is deliberately taller than
     its content now (see min-height above), and space-between turned that
     spare height into a hole with the PROFILE label floating in it. Bottom-
     anchored keeps the text as one block, and hands the whole top of the panel
     to clear glass — which is where the portrait reads through. It also lands
     the block in the dense part of the tint, where 11px mono is legible. */
  justify-content: flex-end;
  padding: 0 var(--margin) 90px;
  color: var(--cream);
  isolation: isolate;
}

/* Full-width, no blur, no ribs. Two jobs the half-width glass can no longer do
   on its own: carry the cream -> ink transition all the way across so
   CAPABILITIES opens seamlessly, and put enough tone under the status row and
   the PROFILE label — both of which sit over the bare portrait now — to keep
   them legible. Stays clear at the top so the figure reads before it sinks. */
.profile-wash {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    to bottom,
    rgba(14, 14, 12, 0.00) 0%,
    rgba(14, 14, 12, 0.30) 34%,
    rgba(14, 14, 12, 0.62) 62%,
    rgba(14, 14, 12, 0.88) 82%,
    var(--ink) 100%
  );
}

/* Real glass, not a painted reconstruction. The panel is transparent and blurs
   whatever is behind it, so the actual pinned hero shows through — including
   the blue wordmark, which the old duplicate-and-displace approach could never
   carry because it only ever copied the portrait.
   No opaque base, so there is nothing to ghost against: the original is the
   only copy. The fade-in mask stays here so the blur and the ribs arrive
   together rather than the blur snapping on at the panel edge. */
.profile-glass {
  position: absolute;
  /* Right-hand panel, not the full width: the portrait has moved left by the
     time this is over it, so the glass takes its inboard half and leaves the
     face clear. The number is tied to the portrait transform below — move one
     and the figure stops being cut where it should be. */
  inset: 0 0 0 34%;
  z-index: 1;
  background: transparent;
  -webkit-backdrop-filter: blur(7px) saturate(1.08);
  backdrop-filter: blur(7px) saturate(1.08);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 9%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 9%, #000 100%);
}

/* Without backdrop-filter there is no frosting, so the statement would sit on
   a bare portrait. Fall back to a solid blue panel: the tint below assumes it
   is compositing over something, and on its own it is too sheer to read on. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .profile-glass { background: var(--blue); }
}


/* Rib highlights + the darkening tint, in one layer above the refraction.
   The tint is what makes the statement legible over a busy portrait, and it
   carries the cream -> ink transition into the next section. */
.profile-ribs {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      90deg,
      rgba(241, 241, 241, 0.00) 0px,
      rgba(241, 241, 241, 0.16) 3px,
      rgba(241, 241, 241, 0.03) 7px,
      rgba(14, 14, 12, 0.10) 12px,
      rgba(241, 241, 241, 0.00) 18px
    ),
    /* Neutral ink scrim. Blue was tried and reverted — over the real blurred
       backdrop it tinted the whole figure, and the panel started competing
       with the blue wordmark showing through rather than framing it.
       Runs slightly ahead of .profile-wash at every stop so the glass reads as
       a denser material than the open left side, and lands on the same ink so
       the two halves resolve together. */
    linear-gradient(
      to bottom,
      rgba(14, 14, 12, 0.20) 0%,
      rgba(14, 14, 12, 0.56) 32%,
      rgba(14, 14, 12, 0.76) 56%,
      rgba(14, 14, 12, 0.90) 78%,
      var(--ink) 100%
    );
}

.profile-status {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  padding: 18px 0;
  /* Just enough glass to establish the material before the content starts.
     This was 26vh, which meant a long empty run of ribs before anything to
     read — the glass became the section instead of the surface it sits on. */
  margin-top: 9vh;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(241, 241, 241, 0.62);
  border-bottom: 1px solid rgba(241, 241, 241, 0.16);
}
.profile-status .v { color: var(--cream); }

/* Pushed to the right edge, still left-aligned inside itself. margin-left:auto
   on a fit-content box does both: the block sits right, the ragged edge stays
   on the right where left-aligned text puts it. */
.profile-content {
  position: relative;
  z-index: 2;
  width: fit-content;
  max-width: 100%;
  margin-left: auto;
  text-align: left;
}
/* Sibling of .profile-content, not a child — it holds the left rail while the
   statement sits right. Generous top margin so it reads as the start of a new
   block rather than a caption hanging off the status row. */
.profile-label {
  position: relative;
  z-index: 2;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(241, 241, 241, 0.55);
  margin-top: 7vh;
  margin-bottom: 28px;
}
/* Always three lines. The <br>s set the breaks; nowrap stops the box from
   re-breaking them at narrow widths, which was turning three lines into five.
   The vw term is sized so the longest line ("Great storytelling") still fits
   the smallest screen — raise it and the statement starts clipping. */
.profile-statement {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(27px, 5.4vw, 84px);
  line-height: 0.96;
  letter-spacing: -0.045em;
  color: var(--cream);
  white-space: nowrap;
}

/* Kuya blue on near-black is a genuinely low-contrast pairing, and neither
   fix-in-place worked: the glow went hazy, the offset copy read as a printing
   error. So the word stops fighting the background and gets its own — a solid
   blue block with the text knocked out in cream.
   Blue still owns the word, contrast is now cream-on-blue rather than
   blue-on-black, and it matches the LATEST badge in the hero, which is the
   same move. Alternatives, if this reads too heavy: cream text with a 2px
   blue rule under it, or blue text with a cream -webkit-text-stroke. */
/* The payoff line gets air above it. Two tight lines then a beat, so "It's
   felt." lands rather than continuing the sentence. */
.profile-last {
  display: block;
  margin-top: 0.34em;
}

/* Set in IBM Plex Mono against Space Grotesk everywhere else. The site has
   exactly two families, so this is the only contrast available that does not
   introduce a third — and the switch to mono makes the word read as a stamped
   label rather than more headline, which is what gives it the emphasis.
   Sized down to 0.74em because mono runs wide and tall at matched px, and
   given positive tracking since the display face's -0.045em is wrong for it. */
.profile-statement .blue {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.74em;
  letter-spacing: 0.005em;
  color: var(--cream);
  background: var(--blue);
  padding: 0.1em 0.22em 0.16em;
  margin-left: 0.04em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
.profile-more {
  display: inline-block;
  margin-top: 34px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cream);
  border-bottom: 1px solid var(--blue);
  padding: 6px 0;
  transition: color 120ms linear;
}
/* The glass is dark by the time this is on screen, so hover goes pale. */
.profile-more:hover { color: var(--blue-pale); }

/* Everything textual moves onto the frosted half. Once the glass stopped being
   full width, the status row and the PROFILE label were left sitting on the
   bare, brightly-lit portrait and stopped being readable — they are 11px mono
   at 55-62% opacity, which needs a surface under it.
   Keeping them on the glass also leaves the left half as pure photograph,
   which is the better picture anyway.
   Width, not margin-left: a percentage margin here would resolve against the
   padded content box while the glass is positioned against the border box, so
   the two would not line up. 61% + auto puts the edge within a couple of
   pixels of the glass at every desktop width. */
@media (min-width: 901px) {
  .profile-status,
  .profile-label {
    width: 68%;
    margin-left: auto;
    padding-left: 26px;
  }
}

@media (max-width: 900px) {
  .profile {
    min-height: 0;
    padding-bottom: 64px;
  }
  /* Slightly more lead-in than desktop — the portrait is smaller here so the
     glass needs a little longer to register — but nowhere near the 34vh this
     was, which pushed the statement most of a screen down. */
  .profile-status { margin-top: 15vh; }
  .profile-content { max-width: none; }

  /* A half-width panel here would leave the statement about 160px of column,
     which re-breaks the three nowrap lines. Glass goes back to full width, and
     the portrait stops shifting left since there is no clear half to shift
     into — it keeps the zoom, which is the part that still reads at this size. */
  .profile-glass { inset: 0; }
}

.hero-portrait {
  position: absolute;
  inset: 0;
  z-index: 2;
}
/* --focus is written by initHeroFocus() in site.js: 0 while the hero stands
   alone, 1 once the profile panel has fully risen over it. Everything below is
   a pure function of it, so the whole move is one number.

   The transform lives on the container, not the <img>. Both the translate and
   the origin are percentages, and a percentage resolves against the box it is
   written on — putting them here means they resolve against one stable
   1440x818-ish box instead of the image's own letterboxed geometry.

   scale 2.2, walked back from 4.2 -> 3.2 -> 2.2. Nothing here is limited by
   the asset (it ships 2600px wide and 2.2x samples down hard); the crop is
   purely a framing call.

   The three values below are coupled — change the scale and both of the others
   have to move with it:
   - Origin 40.5% tracks the head. As the scale drops the origin has to rise or
     the crown drifts toward the top edge; this leaves ~50px of headroom at
     full zoom. It holds across viewport heights because the figure's headroom
     is a fixed fraction of the image.
   - translateX -20% grows as the zoom shrinks. A smaller scale makes the
     figure narrower, so without a bigger shift it just sits centred and the
     leftward move stops reading.
   Together they put the figure's midline within a couple of pixels of the
   glass edge, which is what makes it read as exactly half-covered.
   translateX before scale, so the shift is in the container's space and does
   not get multiplied by the zoom. */
.hero-portrait {
  transform-origin: 50% 40.5%;
  transform:
    translateX(calc(var(--focus, 0) * -20%))
    scale(calc(1 + var(--focus, 0) * 1.2));
  will-change: transform;
}

/* MUST stay below the desktop rule above. Both blocks select .hero-portrait at
   equal specificity, so source order is the only thing deciding the winner —
   these lived up in the max-width:900px section for a while and were silently
   losing to the desktop transform at every width. If you move either block,
   check a phone width before trusting it.

   No left shift here: the glass is full width on mobile, so there is no clear
   half to shift into. */
@media (max-width: 900px) {
  .hero-portrait {
    transform-origin: 50% 33%;
    transform: scale(calc(1 + var(--focus, 0) * 1.2));
  }
}

/* Pulls the zoom upward on phones.

   object-fit: contain resolves differently depending on the hero's shape. On a
   phone the box is far taller than the image is, so the image is *width*-
   constrained: it fills only the bottom ~560px of a ~730px hero, and
   object-position: center bottom parks that band on the floor. Add the 25%
   headroom baked into the export and the crown sat ~400px down the frame at
   full zoom — a screenful of empty cream above a head pinned to the bottom.

   Raising the origin makes the scale itself lift the figure, which beats a
   translateY: one value, and it stays proportional as the hero grows.

   The aspect-ratio gate is load-bearing, not belt-and-braces. A tablet in
   portrait (768x1024) is *height*-constrained, so its geometry is the desktop
   case wearing the mobile breakpoint — 70% there drives the head above the top
   edge and decapitates it. 3/5 admits phones (375x812 = 0.46) and keeps
   tablets (0.75) and every landscape orientation out. */
@media (max-width: 900px) and (max-aspect-ratio: 3/5) {
  .hero-portrait { transform-origin: 50% 70%; }
}
.hero-portrait img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
}

/* No scroll-linked reframing for anyone who asked for less motion; the static
   full-figure hero is the honest fallback. */
@media (prefers-reduced-motion: reduce) {
  .hero-portrait { transform: none; }
}

.hero-wordmark {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  height: 38vw;
  min-height: 260px;
  display: flex;
  align-items: center;
}
.hero-wordmark-track {
  display: flex;
  gap: 6vw;
  animation: scroll-wordmark 30s linear infinite;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: 36vw;
  letter-spacing: -0.075em;
  line-height: 0.85;
  color: var(--blue);
  text-transform: uppercase;
  will-change: transform;
  /* No text-stroke here. It was added to fatten the glyphs past Space Grotesk's
     700 ceiling, but it thickens the ® superscript by the same ratio and at
     0.22em that closes the counters — the mark turned into a blob. The scale
     increase carries the weight on its own. */
}
.hero-wordmark-track span sup {
  font-size: 0.22em;
  font-family: var(--mono);
  letter-spacing: 0;
  vertical-align: top;
  position: relative;
  top: 0.6em;
  margin-left: 0.05em;
  font-weight: 400;
}
@keyframes scroll-wordmark {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

.hero-corners {
  position: absolute;
  top: 40px;
  left: var(--margin);
  right: var(--margin);
  z-index: 5;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}
.hero-corner {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  line-height: 1.7;
}
.hero-corner.left { color: var(--muted); }
.hero-corner.left .v { color: var(--ink); }

.latest-badge {
  background: var(--blue);
  color: var(--cream);
  width: 88px; height: 88px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 12px;
}
.latest-badge .label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
/* Part of the numeral system. This was the system's one exemption — at 34px on
   blue the dot grid broke up and lost to Space Grotesk — but it is Alvin's
   call, and at 34px it clears the 30px floor honestly rather than by exception.
   "062" measures 61px against 64px of inner width, so it fits without the badge
   growing on desktop. (Measure the text with a Range, not the .num element —
   that is a block and always reports the full container width, which makes any
   size look like an exact fit.) */
.latest-badge .num {
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: 34px;
  letter-spacing: 0;
  line-height: 1;
}

.hero-rail-l,
.hero-rail-r {
  position: absolute;
  top: 50%;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--muted);
  text-transform: uppercase;
  white-space: nowrap;
  z-index: 5;
}
/* translateX sign must push the rail back DOWN by half its own length after
   rotation, or the label drifts up out of the hero and collides with the
   corner metadata (worst on narrow screens). Left rotates -90deg so +x maps
   to -y and needs -50%; right rotates +90deg so +x maps to +y and needs +50%. */
.hero-rail-l {
  left: 18px;
  transform: rotate(-90deg) translateX(-50%);
  transform-origin: left center;
}
.hero-rail-r {
  right: 18px;
  transform: rotate(90deg) translateX(50%);
  transform-origin: right center;
}

/* Scroll-progress hairline at the very bottom of the hero, no overlap */
.hero .hairline-blue {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 6;
}

/* The status row now sits BELOW the hero, not on top of the portrait */


/* ---------- CAPABILITIES (dark room) ---------- */

.capabilities {
  background: var(--ink);
  color: var(--cream);
  padding: 100px var(--margin) 80px;
  position: relative;
  overflow: hidden;
}
.cap-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 60px;
  flex-wrap: wrap;
  gap: 16px;
}
.cap-header .mono { color: rgba(241, 241, 241, 0.5); }
.cap-header .blue { color: var(--blue-pale); }

/* Deliberately un-boxed. The rules and the tidy left rail were doing the
   opposite of what this section wants — each item now sits at its own indent
   and its own size, so the block reads as a scatter of statements rather than
   a table of services. Offsets are hand-set per item, not generated: the
   irregularity is the point and a formula would even it back out. */
.cap-list {
  display: block;
  position: relative;
}

.cap-item {
  padding: 18px 0;
  outline: none;
  position: relative;
  width: fit-content;
  max-width: 100%;
}
.cap-item:nth-child(1) { margin-left: 0; }
.cap-item:nth-child(2) { margin-left: 52%; }
.cap-item:nth-child(3) { margin-left: 6%; }
.cap-item:nth-child(4) { margin-left: 48%; }

/* Between the mobile breakpoint and ~1200px the indents have to come in or the
   two right-hand items run out of room — at 910px a 52% indent left FILMMAKING
   with zero clearance, one render tweak away from wrapping. The bold offsets
   above only work once there is width to spend. */
@media (max-width: 1200px) {
  .cap-item:nth-child(2) { margin-left: 38%; }
  .cap-item:nth-child(4) { margin-left: 34%; }
}

/* Sizes vary per item too — a single clamp for all four is what made it feel
   like a list. 02 and 04 read louder, 03 sits back. */
.cap-item:nth-child(1) .cap-title { font-size: clamp(34px, 7.2vw, 108px); }
.cap-item:nth-child(2) .cap-title { font-size: clamp(30px, 5.4vw, 82px); }
.cap-item:nth-child(3) .cap-title { font-size: clamp(38px, 8.6vw, 128px); }
.cap-item:nth-child(4) .cap-title { font-size: clamp(32px, 6.2vw, 94px); }

/* The number breaks out of the flow and hangs off the title's top-left, the
   way a plate number sits on a contact sheet. */
.cap-head {
  display: block;
  position: relative;
  padding-left: 0;
}

.cap-num {
  position: absolute;
  top: 0.2em;
  /* Wide enough to clear the title. "[ 01 ]" is ~4em at this size, so a
     shallower hang tucks the number under the first letter. */
  left: -4.4em;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.2em;
  color: rgba(241, 241, 241, 0.42);
  transition: color 140ms linear;
  white-space: nowrap; /* "[ 01 ]" must never break across two lines */
}
.cap-item.featured .cap-num { color: var(--blue-pale); }

.cap-title {
  /* inline-block, not block: the cue has to sit beside the title, and a block
     h3 pushes it onto its own line. */
  display: inline-block;
  vertical-align: baseline;
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 0.9;
  color: var(--cream);
  text-transform: uppercase;
  transition: color 140ms linear, transform 260ms cubic-bezier(0.2, 0.7, 0.3, 1);
  transform-origin: left center;
}
/* Pale rather than a knockout block: the .profile-statement fix works for a
   short word set in mono, but a solid blue slab behind "Animation" at 94px
   would read as a redaction bar, not an accent. Featured still reads as
   featured; it just stops failing contrast at 2.09:1. */
.cap-item.featured .cap-title { color: var(--blue-pale); }

/* The cue trails the title inline rather than sitting in a right-hand column —
   a column is the boxiness this section is getting rid of. */
.cap-cue {
  display: inline-block;
  vertical-align: super;
  margin-left: 0.28em;
  font-family: var(--mono);
  font-size: 15px;
  color: rgba(241, 241, 241, 0.4);
  transition: transform 260ms cubic-bezier(0.2, 0.7, 0.3, 1), color 140ms linear;
}

/* 0fr -> 1fr animates cleanly; height:auto does not. */
.cap-reveal {
  display: grid;
  grid-template-rows: 0fr;
  /* 240ms, not 320ms. This fires on hover, a tens-of-times-a-visit
     interaction, and the band for those is short. 320ms read as the panel
     lagging the cursor. */
  transition: grid-template-rows 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
/* The clipped box carries no padding of its own — any padding here would
   render at 0fr and leave a permanent gap under every collapsed row. Spacing
   lives on .cap-desc, inside the clip. min-height:0 lets the grid row shrink. */
.cap-reveal-inner {
  overflow: hidden;
  min-height: 0;
}

.cap-desc {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.15em;
  line-height: 1.7;
  color: rgba(241, 241, 241, 0.7);
  text-transform: uppercase;
  max-width: 54ch;
  padding-top: 18px;
  opacity: 0;
  transition: opacity 220ms linear 60ms;
}

/* Open state. Hover only where a real pointer exists, so a tap on touch does
   not leave a row stuck open; :focus-within is what makes this keyboard-usable. */
@media (hover: hover) {
  .cap-item:hover .cap-reveal { grid-template-rows: 1fr; }
  .cap-item:hover .cap-desc { opacity: 1; }
  .cap-item:hover .cap-title { transform: translateX(10px); }
  .cap-item:hover .cap-cue { transform: rotate(45deg); color: var(--blue-pale); }
  .cap-item:not(.featured):hover .cap-title { color: var(--blue-pale); }
  .cap-item:not(.featured):hover .cap-num { color: rgba(241, 241, 241, 0.85); }
}
.cap-item:focus-within .cap-reveal { grid-template-rows: 1fr; }
.cap-item:focus-within .cap-desc { opacity: 1; }
.cap-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 4px;
}

/* Touch has no hover: never hide the copy behind an interaction that
   cannot happen. Everything is open, no cue, no transitions. */
@media (hover: none) {
  .cap-reveal { grid-template-rows: 1fr; }
  .cap-desc { opacity: 1; }
  .cap-cue { display: none; }
}

/* Below 900px the stagger is dropped entirely and the list goes back to being
   a list. A 36% indent on a 375px screen is 135px of nothing, and with the
   titles already wrapping the irregularity reads as broken rather than
   deliberate — the effect needs width to be legible as an effect. Uniform
   indent, uniform size, number above the title. */
@media (max-width: 900px) {
  .cap-item { margin-left: 0 !important; width: auto; }
  .cap-item:nth-child(1) .cap-title,
  .cap-item:nth-child(2) .cap-title,
  .cap-item:nth-child(3) .cap-title,
  .cap-item:nth-child(4) .cap-title {
    font-size: clamp(34px, 9vw, 56px);
  }
  .cap-num {
    position: static;
    display: block;
    margin-bottom: 8px;
  }
  .cap-desc { padding-top: 14px; }
}

.cap-footer {
  margin-top: 80px;
  padding-top: 28px;
  border-top: 2px solid var(--blue);
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cream);
  flex-wrap: wrap;
  gap: 16px;
}
.cap-footer .blue { color: var(--blue-pale); }


/* ---------- WORKS (homepage selected work) ---------- */

.works {
  padding: 100px var(--margin) 80px;
  position: relative;
}
.works-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 18px;
  flex-wrap: wrap;
  gap: 12px;
}

/* Tile A — standard layout */
.tile-a {
  display: grid;
  grid-template-columns: 780fr 460fr;
  gap: 48px;
  padding: 60px 0 50px;
  border-bottom: 1px solid var(--muted-soft);
}

/* The frame is padding now, not a 36px inset on an absolutely positioned
   child. That was doing two things at once: drawing a very heavy border, and
   forcing the actual thumbnail to whatever ratio was left over from the outer
   78/44 box — which was never 16:9. Padding on the outside, aspect-ratio on
   the inside, so the image is a true 16:9 and the frame is just a frame. */
.tile-thumb {
  width: 100%;
  background: var(--surface);
  padding: 12px;
  position: relative;
  overflow: hidden;
  cursor: pointer;
  transition: transform 240ms cubic-bezier(0.2, 0.7, 0.2, 1);
  /* Without this the grid stretches the frame to the height of the info
     column and the grey runs on well below the 16:9 image. It used to be held
     up by the old fixed aspect-ratio; now the content sets the height. */
  align-self: start;
}
.tile-thumb:hover { transform: translateX(-4px); }

.tile-thumb-inner {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  background-size: cover;
  background-position: center;
}
.tile-thumb-inner.blue-bg { background: var(--blue); }

.tile-thumb-inner .play-stripe {
  position: absolute;
  inset: 30% 12%;
  background: rgba(241, 241, 241, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
}
.tile-thumb-inner .play-text {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(24px, 3vw, 42px);
  letter-spacing: -0.02em;
  color: var(--cream);
  text-transform: uppercase;
  text-align: center;
}
.tile-thumb-inner.blue-bg .play-text { color: var(--ink); }

.tile-corner {
  position: absolute;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--cream);
  text-transform: uppercase;
}
.tile-corner.tl { top: 16px; left: 16px; }
.tile-corner.tr { top: 16px; right: 16px; }
.tile-corner.bl { bottom: 16px; left: 16px; }
.tile-corner.br { bottom: 16px; right: 16px; }
.tile-thumb-inner.blue-bg .tile-corner { color: var(--ink); }

.tile-info {
  display: flex;
  flex-direction: column;
  padding-top: 8px;
}
.tile-num {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  color: var(--blue);
  text-transform: uppercase;
  margin-bottom: 16px;
}
.tile-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(48px, 6vw, 96px);
  letter-spacing: -0.05em;
  line-height: 0.85;
  margin-bottom: 30px;
  text-transform: uppercase;
}
.tile-title .blue { color: var(--blue); }

.tile-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 32px;
}
.tile-meta-row {
  display: grid;
  grid-template-columns: 110px 1fr;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 4px 0;
  border-bottom: 1px solid var(--muted-soft);
}
.tile-meta-row .v { color: var(--ink); }

.tile-explore {
  align-self: flex-start;
  background: var(--blue);
  color: var(--cream);
  padding: 14px 24px;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  transition: background 80ms linear;
}
.tile-explore:hover { background: var(--ink); }

/* Tile B — full-bleed type overlay */
.tile-b {
  margin: 0 calc(-1 * var(--margin));
  position: relative;
  height: 720px;
  background: var(--ink);
  overflow: hidden;
  border-bottom: 1px solid var(--muted-soft);
  cursor: pointer;
}
.tile-b-bg {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(45deg, transparent 0 8px, rgba(31, 31, 218, 0.08) 8px 9px),
    radial-gradient(ellipse at center, rgba(31, 31, 218, 0.4) 0%, transparent 70%),
    var(--blue);
}
.tile-b-overlay {
  position: relative;
  z-index: 2;
  height: 100%;
  padding: 50px var(--margin);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  color: var(--cream);
}
.tile-b-top {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  gap: 16px;
  flex-wrap: wrap;
}
.tile-b-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(72px, 17vw, 240px);
  letter-spacing: -0.07em;
  line-height: 0.82;
  text-transform: uppercase;
  color: var(--cream);
}
.tile-b-title .stroke {
  -webkit-text-stroke: 2px var(--cream);
  color: transparent;
}
.tile-b-bottom {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 40px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  align-items: end;
}
.tile-b-bottom .label {
  color: rgba(241, 241, 241, 0.5);
  display: block;
  margin-bottom: 4px;
}
.tile-b-bottom .v { color: var(--cream); }
.tile-b-explore {
  justify-self: end;
  background: var(--cream);
  color: var(--ink);
  padding: 14px 24px;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.22em;
  display: inline-flex;
  gap: 12px;
}

/* Tile C — mirrored */
.tile-c {
  display: grid;
  grid-template-columns: 460fr 780fr;
  gap: 48px;
  padding: 60px 0;
  position: relative;
}
.tile-c .tile-blueblock {
  position: absolute;
  left: -60px;
  top: 60px;
  bottom: 60px;
  width: 12px;
  background: var(--blue);
}


/* ---------- CTA (blue room) ---------- */

.cta-block {
  background: var(--blue);
  color: var(--cream);
  padding: 90px var(--margin);
  border-top: 2px solid var(--ink);
  border-bottom: 2px solid var(--ink);
  position: relative;
  overflow: hidden;
}
/* A <button>, so the reset has to undo the UA styling the rest of the site
   never needed: full width, left-aligned, inherited colour, no padding. */
.cta-row {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(40px, 7vw, 116px);
  letter-spacing: -0.05em;
  line-height: 0.95;
  text-transform: uppercase;
  display: flex;
  align-items: baseline;
  gap: 0.18em;
  flex-wrap: wrap;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.cta-row .stroke {
  -webkit-text-stroke: 2px var(--cream);
  color: transparent;
  transition: color 160ms linear;
}
.cta-row .arrow {
  margin-left: 0.2em;
  transition: transform 220ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
/* The whole line is the target, so it has to look like one on hover. */
@media (hover: hover) {
  .cta-row:hover .stroke { color: var(--cream); }
  .cta-row:hover .arrow { transform: translate(0.12em, -0.12em); }
}
.cta-row:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 8px;
}
.cta-meta {
  margin-top: 60px;
  padding-top: 24px;
  border-top: 1px solid rgba(241, 241, 241, 0.3);
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  flex-wrap: wrap;
  gap: 12px;
}


/* ---------- MARQUEE (filename strip) ---------- */

.marquee {
  overflow: hidden;
  background: var(--cream);
  padding: 14px 0;
  border-bottom: 1px solid var(--ink);
}
.marquee-track {
  display: flex;
  gap: 48px;
  animation: scroll-files 60s linear infinite;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 14px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}
.marquee-track span { color: var(--ink); }
.marquee-track .blue { color: var(--blue); }
@keyframes scroll-files {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}


/* ---------- FOOTER RIBBON ---------- */

.ribbon { 
  background: var(--ink);
  color: var(--cream);
  padding: 18px var(--margin);
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 24px;
  align-items: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.ribbon .center { text-align: center; }
.ribbon .right { text-align: right; }
.ribbon .blue { color: var(--blue-pale); }
/* inline-block + cancelled vertical padding lifts these from the 15px text
   height to ~27px so they're tappable on phones, without altering the ribbon
   layout or the "·" separators between them. */
.ribbon a:hover { color: var(--blue-pale); }

/* Tappable inline links.
   These mono links sit at 11px, so their natural hit area is ~15px tall — too
   small to hit reliably on a phone. inline-block plus padding cancelled by an
   equal negative margin grows the target without moving anything or disturbing
   the "·" separators between them. Same trick as .nav-link and .nav-monogram.

   The bar here is WCAG 2.2 SC 2.5.8 (AA), which is 24x24 CSS px — not the 44x44
   of SC 2.5.5, which is AAA. Vertical padding alone already cleared 24px, but
   the shortest labels ("IG", "YT", "FB") were only 18px WIDE and still failed.
   The horizontal padding fixes that; it is cancelled by negative margin so the
   separators do not drift. */
.ribbon a,
.cap-footer a,
.works-header a,
.cta-meta a {
  transition: color 80ms linear;
  display: inline-block;
  padding: 8px 4px;
  margin: -8px -4px;
}


/* ============================================================
   WORK INDEX PAGE
   ============================================================ */

.index-page {
  padding: 60px var(--margin) 100px;
}
.index-header {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(48px, 8vw, 120px);
  letter-spacing: -0.06em;
  line-height: 0.9;
  text-transform: uppercase;
  margin-bottom: 48px;
}
.index-header .blue { color: var(--blue); }

.index-meta {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 16px;
}

.filter-chips {
  display: flex;
  gap: 12px;
  margin-bottom: 40px;
  flex-wrap: wrap;
}
.chip {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 8px 16px;
  border: 1px solid var(--ink);
  background: var(--cream);
  color: var(--ink);
  cursor: pointer;
  /* Named, not `all`: the chip also carries a transform on :active, and `all`
     would put that on the same 80ms colour timing instead of its own. */
  /* press: transform is part of this shorthand, not a later override */
  transition: color 80ms linear, background-color 80ms linear,
              border-color 80ms linear,
              transform 120ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.chip.active, .chip:hover {
  background: var(--blue);
  border-color: var(--blue);
  color: var(--cream);
}

.index-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 13px;
}
.index-table thead {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}
.index-table th {
  text-align: left;
  padding: 12px 0;
  border-bottom: 1px solid var(--ink);
  font-weight: 400;
}
.index-table tbody tr {
  border-bottom: 1px solid var(--muted-soft);
  transition: background 80ms linear;
}
/* Keyed to [data-href] since the rows stopped carrying inline onclick — the
   attribute is still what marks a row as navigable, so rows without a detail
   page keep the default cursor and no hover state. */
.index-table tbody tr[data-href] { cursor: pointer; }
.index-table tbody tr[data-href]:hover { background: var(--surface); }
.index-table tbody tr[data-href]:hover .row-title,
.index-table tbody tr[data-href]:hover .row-title a { color: var(--blue); }
/* The title is a real link now. It inherits so the row's hover colour still
   drives it, and the focus ring is explicit because the surrounding row gives
   a keyboard user no other indication of where they are. */
.index-table .row-title a {
  color: inherit;
  text-decoration: none;
}
.index-table .row-title a:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}
.index-table td { padding: 18px 0; }
.index-table .col-num { color: var(--blue); width: 80px; }
.index-table .row-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 18px;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--ink);
  transition: color 80ms linear;
}
.index-table .col-year { width: 80px; }


/* ============================================================
   PROJECT DETAIL PAGE
   ============================================================ */

.project-page { background: var(--cream); }

.project-meta-strip {
  padding: 14px var(--margin);
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  border-bottom: 1px solid var(--ink);
  flex-wrap: wrap;
  gap: 16px;
}

.project-reel {
  margin: 0 var(--margin);
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--ink);
  overflow: hidden;
}

/* Vertical and square pieces. Full-width at 9:16 would be absurdly tall, so
   these are height-bounded and the player is centred at its true aspect —
   the ink either side reads as a frame rather than a mistake. */
.project-reel.is-vertical,
.project-reel.is-square {
  aspect-ratio: auto;
  height: min(78vh, 900px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.project-reel.is-vertical > .reel-facade,
.project-reel.is-vertical > iframe { aspect-ratio: 9 / 16; }
.project-reel.is-square > .reel-facade,
.project-reel.is-square > iframe { aspect-ratio: 1 / 1; }
.project-reel.is-vertical > .reel-facade,
.project-reel.is-vertical > iframe,
.project-reel.is-square > .reel-facade,
.project-reel.is-square > iframe {
  position: relative;
  inset: auto;
  height: 100%;
  width: auto;
  max-width: 100%;
}
.project-reel.is-vertical .reel-facade img,
.project-reel.is-square .reel-facade img { object-fit: contain; }
.project-reel iframe,
.project-reel video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.project-reel-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Click-to-load video facade. Holds the reel's 16:9 box with a local poster
   until the viewer asks to play, so no third-party player script loads on
   pages nobody watches. */
.reel-facade {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: var(--ink);
  cursor: pointer;
  display: block;
}
.reel-facade img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.85;
  transition: opacity 160ms linear;
}
.reel-facade:hover img { opacity: 1; }
.reel-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--blue);
  color: var(--cream);
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(14px, 2vw, 24px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 18px 44px;
  white-space: nowrap;
}
@media (max-width: 620px) { .reel-play { padding: 12px 22px; } }
.project-reel-placeholder .play-btn {
  background: var(--blue);
  color: var(--cream);
  padding: 24px 56px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 24px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 16px;
}
.project-reel-corner {
  position: absolute;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--cream);
  opacity: 0.6;
  text-transform: uppercase;
}
.project-reel-corner.tl { top: 20px; left: 20px; }
.project-reel-corner.tr { top: 20px; right: 20px; }
.project-reel-corner.bl { bottom: 20px; left: 20px; }
.project-reel-corner.br { bottom: 20px; right: 20px; }

/* Below ~620px the opposing corner labels are wider than the reel and print on
   top of each other. Stack them down the left edge instead of dropping any —
   the metadata density is the point, so it reflows rather than disappears. */
@media (max-width: 620px) {
  .project-reel-corner { font-size: 9px; letter-spacing: 0.14em; }
  .project-reel-corner.tl { top: 8px; left: 14px; }
  .project-reel-corner.tr { top: 24px; left: 14px; right: auto; }
  .project-reel-corner.bl { bottom: 24px; left: 14px; }
  .project-reel-corner.br { bottom: 8px; left: 14px; right: auto; }
  /* the centred play button has to clear both stacked bands */
  .play-btn { padding: 14px 20px; font-size: 12px; }
}

.project-title-block {
  padding: 80px var(--margin) 40px;
}
.project-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(48px, 9vw, 140px);
  letter-spacing: -0.06em;
  line-height: 0.88;
  text-transform: uppercase;
}
.project-title .blue { color: var(--blue); }

.project-body {
  padding: 40px var(--margin) 80px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 80px;
}
.project-body-section .section-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 28px;
}
.project-brief {
  font-family: var(--body);
  font-size: clamp(20px, 1.8vw, 28px);
  line-height: 1.45;
  color: var(--ink);
}
.project-credits {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.project-credits-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  padding: 14px 0;
  border-bottom: 1px solid var(--muted-soft);
  color: var(--muted);
}
.project-credits-row .v {
  text-align: right;
  color: var(--ink);
}

.project-stills {
  padding: 40px var(--margin) 80px;
}
.project-stills-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 28px;
}
.project-stills-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
.project-still {
  aspect-ratio: 4 / 3;
  background: var(--surface);
  position: relative;
  overflow: hidden;
}
.project-still img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* ---------- Photo project hero ----------
   Photo sets lead with a frame instead of the 16:9 video well. Capped at 80vh
   so a tall portrait frame cannot push the title block off the first screen. */
.project-hero-photo {
  margin: 0 var(--margin);
  position: relative;
  background: var(--ink);
  max-height: 80vh;
  overflow: hidden;
}
.project-hero-photo img {
  display: block;
  width: 100%;
  max-height: 80vh;
  object-fit: cover;
  object-position: center;
}
/* Sets with no landscape frame: show the whole portrait rather than crop it
   to a strip. It pillarboxes against the ink, which reads as deliberate. */
.project-hero-photo.is-contain {
  height: 80vh;
}
.project-hero-photo.is-contain img {
  height: 100%;
  object-fit: contain;
}

/* ---------- Campaign cuts ----------
   Variants of one campaign. Each is its own click-to-load facade, so a set of
   five cuts still loads zero player scripts until one is asked for. */
.project-cuts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}
.cut-frame { margin: 0; }
.cut-frame .reel-facade {
  position: relative;
  inset: auto;
  aspect-ratio: 1 / 1;
  width: 100%;
  height: auto;
}
.cut-frame .reel-play {
  font-size: 14px;
  padding: 10px 18px;
  letter-spacing: 0;
}
.cut-frame figcaption {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  padding-top: 8px;
}

/* ---------- Mixed-orientation gallery ----------
   The sets mix portrait, landscape and square. A fixed grid would have to crop
   to make them line up, which throws away the composition — so columns, where
   each frame keeps its own aspect ratio and the column heights fall where they
   fall. break-inside is what stops a frame splitting across a column. */
.project-gallery {
  columns: 2;
  column-gap: 16px;
}
.gallery-frame {
  break-inside: avoid;
  margin: 0 0 16px;
  position: relative;
  background: var(--surface);
}
.gallery-frame img {
  display: block;
  width: 100%;
  height: auto;
}
.gallery-frame figcaption {
  position: absolute;
  bottom: 10px;
  left: 12px;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.22em;
  color: var(--cream);
  opacity: 0.75;
  mix-blend-mode: difference;
  pointer-events: none;
}
@media (min-width: 1100px) { .project-gallery { columns: 3; } }
@media (max-width: 700px)  { .project-gallery { columns: 1; } }

.project-still-cap {
  position: absolute;
  bottom: 12px;
  left: 12px;
  right: 12px;
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--cream);
  opacity: 0.7;
  text-transform: uppercase;
}

.project-nav {
  display: flex;
  justify-content: space-between;
  padding: 24px var(--margin);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid var(--ink);
  border-bottom: 1px solid var(--muted-soft);
}

.project-next {
  padding: 60px var(--margin);
  display: grid;
  grid-template-columns: 1fr 440px;
  gap: 40px;
  align-items: center;
}
.project-next-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(36px, 5vw, 64px);
  letter-spacing: -0.04em;
  line-height: 0.92;
  text-transform: uppercase;
}
.project-next-cta {
  background: var(--blue);
  color: var(--cream);
  padding: 50px;
  text-align: center;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(20px, 2vw, 32px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: background 80ms linear;
}
.project-next-cta:hover { background: var(--ink); }


/* ============================================================
   INFO PAGE
   ============================================================ */

.info-page { padding: 60px var(--margin) 0; }

.info-meta {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 16px;
}

.info-hero {
  padding: 60px 0;
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 80px;
  border-top: 1px solid var(--ink);
}
.info-manifesto {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(48px, 7vw, 96px);
  letter-spacing: -0.05em;
  line-height: 0.88;
  text-transform: uppercase;
}
.info-manifesto .blue { color: var(--blue); }
.info-bio .bio-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 24px;
}
.info-bio p {
  font-family: var(--body);
  font-size: 17px;
  line-height: 1.6;
  margin-bottom: 18px;
}
/* The craft line is a caption on the work, not part of the biography, so it
   sits in mono with the rest of the site's metadata. */
.info-bio .bio-craft {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  line-height: 1.8;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid var(--muted-soft);
  padding-top: 18px;
  margin-top: 26px;
}
.info-bio-cta {
  margin-top: 32px;
  display: inline-flex;
  background: var(--ink);
  color: var(--cream);
  padding: 14px 24px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* ---------- INFO: TRAJECTORY + SPECIALITIES ----------
   Both sections come straight from the CV. Shared label style so they sit in
   the same family as the clients and contact labels above and below. */
.section-label {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 44px;
}
.section-label span:last-child { color: var(--muted); }

.info-trajectory,
.info-skills {
  padding: 80px 0;
  border-top: 1px solid var(--muted-soft);
}

.traj-row {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 32px;
  padding: 28px 0;
  border-bottom: 1px solid var(--muted-soft);
}
.traj-row:last-child { border-bottom: none; }

.traj-when {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.traj-dates { color: var(--ink); }
.traj-dur { color: var(--blue); }

.traj-role {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(20px, 2.3vw, 30px);
  letter-spacing: -0.03em;
  line-height: 1.06;
  color: var(--ink);
}
.traj-role .blue { color: var(--blue); }
.traj-org {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 10px;
}
.traj-note {
  font-size: 14px;
  line-height: 1.62;
  color: var(--muted);
  margin-top: 14px;
  max-width: 68ch;
}

.skills-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 40px;
}
.skill-head {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
  padding-bottom: 12px;
  border-bottom: 1px solid var(--muted-soft);
  margin-bottom: 16px;
}
.skill-list { list-style: none; }
.skill-list li {
  font-family: var(--display);
  font-weight: 400;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--ink);
  padding: 7px 0;
}
.skill-note {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--blue);
}

@media (max-width: 1000px) {
  .skills-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; }
}
@media (max-width: 760px) {
  .info-trajectory,
  .info-skills { padding: 56px 0; }
  .section-label { margin-bottom: 28px; }
  .traj-row { grid-template-columns: 1fr; gap: 12px; padding: 22px 0; }
  .traj-when { flex-direction: row; gap: 14px; }
}

.info-clients {
  padding: 80px 0;
  border-top: 1px solid var(--muted-soft);
}
.clients-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 40px;
}
.clients-grid {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(28px, 4.4vw, 56px);
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-transform: uppercase;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 48px;
}
.clients-grid span:nth-child(even) { color: var(--muted); }
.clients-grid .more {
  font-size: 0.5em;
  align-self: end;
  color: var(--blue) !important;
}

.info-recognition {
  padding: 80px 0;
  border-top: 1px solid var(--muted-soft);
}
.recognition-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 32px;
}
.recognition-row {
  display: grid;
  grid-template-columns: 100px 1fr;
  padding: 16px 0;
  border-bottom: 1px solid var(--muted-soft);
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}
.recognition-row .year { color: var(--muted); }
.recognition-row .entry { color: var(--ink); }

.info-contact {
  background: var(--blue);
  color: var(--cream);
  margin: 0 calc(-1 * var(--margin));
  padding: 80px var(--margin) 100px;
  border-top: 2px solid var(--ink);
}
.contact-label {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  margin-bottom: 40px;
}
/* A <button> now, so the UA styling has to be undone — see .cta-row. */
.contact-line {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(40px, 7vw, 116px);
  letter-spacing: -0.05em;
  line-height: 0.95;
  text-transform: uppercase;
  margin-bottom: 60px;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.contact-line .stroke {
  -webkit-text-stroke: 2px var(--cream);
  color: transparent;
  transition: color 160ms linear;
}
@media (hover: hover) {
  .contact-line:hover .stroke { color: var(--cream); }
}
.contact-line:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 8px;
}
.contact-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  padding-top: 28px;
  border-top: 1px solid rgba(241, 241, 241, 0.3);
}


/* ============================================================
   CONTACT MODAL (full-screen overlay, triggered from nav)
   ============================================================ */

.contact-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--blue);
  color: var(--cream);
  display: flex;
  flex-direction: column;
  opacity: 0;
  pointer-events: none;
  transform: scale(1.02);
  transition: opacity 200ms ease-out, transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
  /* Content runs ~830px tall. On a 667px phone (SE/8, or any landscape) the
     foot — including "ESC TO CLOSE" — falls off the bottom, and with the body
     scroll-locked there was no way to reach it. No effect on tall screens. */
  overflow-y: auto;
  overscroll-behavior: contain;
}
.contact-modal.is-open {
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
}

/* Top bar of modal — looks like the transmission bar */
.contact-modal-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px var(--margin);
  border-bottom: 1px solid rgba(241, 241, 241, 0.25);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
}
.contact-modal-close {
  background: var(--cream);
  color: var(--ink);
  padding: 8px 16px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  cursor: pointer;
  /* press: transform is part of this shorthand, not a later override */
  transition: background 80ms linear, color 80ms linear,
              transform 120ms cubic-bezier(0.2, 0.7, 0.3, 1);
  border: none;
}
.contact-modal-close:hover {
  background: var(--ink);
  color: var(--cream);
}

/* Main body of the modal */
.contact-modal-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 60px var(--margin);
  max-width: 1400px;
  width: 100%;
  margin: 0 auto;
}

.contact-modal-label {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  margin-bottom: 32px;
  opacity: 0.7;
}

.contact-modal-headline {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(48px, 9vw, 140px);
  letter-spacing: -0.06em;
  line-height: 0.88;
  text-transform: uppercase;
  margin-bottom: 80px;
}
.contact-modal-headline .stroke {
  -webkit-text-stroke: 2px var(--cream);
  color: transparent;
}

/* Contact rows — large clickable */
.contact-rows { display: flex; flex-direction: column; }
.contact-row {
  display: grid;
  grid-template-columns: 180px 1fr auto;
  align-items: center;
  padding: 28px 0;
  border-top: 1px solid rgba(241, 241, 241, 0.25);
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: padding 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.contact-row:last-child { border-bottom: 1px solid rgba(241, 241, 241, 0.25); }
.contact-row:hover { padding-left: 16px; }

.contact-row .label { opacity: 0.55; }
.contact-row .value {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(20px, 2.4vw, 36px);
  letter-spacing: -0.02em;
  text-transform: none;
}
.contact-row .action {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.25em;
  opacity: 0;
  transition: opacity 160ms linear;
}
.contact-row:hover .action { opacity: 1; }

/* Bottom availability row */
.contact-modal-foot {
  display: flex;
  justify-content: space-between;
  padding: 16px var(--margin);
  border-top: 1px solid rgba(241, 241, 241, 0.25);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  flex-wrap: wrap;
  gap: 12px;
}

/* Lock body scroll when modal is open */
body.modal-open { overflow: hidden; }

/* Toast for copy-to-clipboard confirmations */
.contact-toast {
  position: fixed;
  bottom: 40px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: var(--ink);
  color: var(--cream);
  padding: 14px 24px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  z-index: 300;
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms linear, transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.contact-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@media (max-width: 900px) {
  .contact-row {
    grid-template-columns: 1fr;
    gap: 6px;
    padding: 20px 0;
  }
  .contact-row .action { display: none; }
  .contact-modal-headline { margin-bottom: 40px; }
}

@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: fade-out 200ms cubic-bezier(0.4, 0, 1, 1);
}
::view-transition-new(root) {
  animation: fade-in 320ms cubic-bezier(0, 0, 0.2, 1);
}
@keyframes fade-out {
  to { opacity: 0; }
}
@keyframes fade-in {
  from { opacity: 0; }
}


/* ============================================================
   RESPONSIVE
   ============================================================ */

@media (max-width: 1100px) {
  /* The .cap-grid rules that lived here were left over from the two-column
     layout and were re-adding a border under items 3 and 4, which showed as a
     stray rule floating under STILLS. The staggered list handles its own
     responsive behaviour further up. */
  .project-body { grid-template-columns: 1fr; gap: 40px; }
  .info-hero { grid-template-columns: 1fr; gap: 40px; }
}

@media (max-width: 900px) {
  :root { --margin: 24px; }

  /* Nav scrolls sideways rather than wrapping. Four links at a readable size
     do not fit 375px, and wrapping doubled the header height — which then ate
     into the sticky offset the hero is pinned against. min-width:0 is what
     lets the flex item actually shrink so overflow can kick in. */
  .nav-left {
    gap: 22px;
    flex-wrap: nowrap;
    overflow-x: auto;
    min-width: 0;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-right: 12px;
  }
  .nav-left::-webkit-scrollbar { display: none; }
  .nav-link { font-size: 10px; white-space: nowrap; }
  /* The monogram sits above the scroll track and carries the page background,
     so links pass cleanly behind it instead of colliding with the mark. */
  .nav-monogram {
    flex: none;
    position: relative;
    z-index: 1;
    background: var(--cream);
    padding-left: 14px;
  }

  /* The manual-scroll and hide-mobile rules that lived here are gone: the bar
     is an auto-scrolling ticker at every width now, so nothing needs hiding
     and nothing wraps. It only needs to be smaller. */
  .transmission { font-size: 8px; padding: 8px 0; }

  .hero-wordmark-track { font-size: 46vw; }
  .hero-corners { top: 24px; }
  /* 60px held a 22px numeral, which is well under the 30px floor — at that
     size Doto stops resolving into digits entirely. Rather than drop back to
     Space Grotesk here (the same badge in two different faces depending on
     viewport is worse than either choice on its own), the badge grows enough
     to carry a legal numeral: 30px Doto measures 54px, so 76px wide with 8px
     padding leaves 60px of room and 6px of slack. */
  .latest-badge { width: 76px; height: 76px; padding: 8px; }
  .latest-badge .num { font-size: 30px; }
  .latest-badge .label { font-size: 8px; }

  .cap-item { grid-template-columns: 50px 1fr; column-gap: 16px; padding: 40px 0; }
  .cap-num { padding-top: 10px; }

  .tile-a, .tile-c { grid-template-columns: 1fr; gap: 24px; }
  .tile-c .tile-blueblock { display: none; }
  /* Tile C is mirrored on desktop — info left, thumb right — so stacking it
     leaves the thumbnail below the title while every other tile has it above.
     One tile out of step looks like a bug, not a rhythm. */
  .tile-c .tile-thumb { order: -1; }
  .tile-title { font-size: clamp(40px, 8vw, 64px); }
  .tile-b { height: 480px; }
  .tile-b-bottom { grid-template-columns: 1fr; gap: 16px; }

  .cta-row { white-space: normal; }

  .marquee-track { font-size: 11px; }

  .ribbon { grid-template-columns: 1fr; text-align: left; }
  .ribbon .center, .ribbon .right { text-align: left; }

  .project-stills-grid { grid-template-columns: 1fr; }
  .project-next { grid-template-columns: 1fr; }

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

  .index-table thead { display: none; }
  /* Two lines per row, not four:
       062  Philips Baristina × Jason Loo
       Philips Malaysia   2026   Social Media · Social · 4K 9:16

     Flex-wrap rather than a grid. A grid puts each cell in a fixed track, which
     is what forced client / year / type / format onto four separate rows and
     crushed the long ones into the 68px numeral column. Here the number and
     title fill the first line exactly, so the four meta cells wrap together and
     flow inline on the second — and if they run out of room they simply wrap
     again instead of overlapping. */
  .index-table tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 14px;
    row-gap: 7px;
    /* The whole row is indented to the title's edge and the number is pulled
       back out into the gutter (below). Indenting via padding rather than a
       margin on the first meta cell matters: the meta can wrap to a third line
       on a narrow phone, and a margin would only indent the first of those
       lines while the rest fell back to the far left. */
    padding: 16px 0 16px 82px;
    position: relative;
  }
  /* .col-num / .col-year carry fixed widths for the desktop table; both are
     class selectors, so they outrank a bare `td` rule and have to be reset by
     name or the year keeps an 80px column and leaves a hole mid-line. */
  .index-table td,
  .index-table .col-year { padding: 0; width: auto; }

  /* The number hangs in the gutter the row's padding just opened up. Taken out
     of flow so it cannot push the title, and so it stays put no matter how many
     lines the meta wraps to. */
  .index-table .col-num {
    position: absolute;
    left: 0;
    top: 14px;
  }
  /* Title takes the full indented width, which is what pushes the meta cells
     onto line two — where they now start at the title's left edge. */
  .index-table .row-title { flex: 1 1 100%; }

  /* Line two — meta, sized to content so it reads as one continuous string. */
  .index-table tbody td:nth-child(n + 3) { flex: 0 0 auto; }
  /* A separator only between TYPE and FORMAT; client and year are spaced by the
     column gap, matching the reference.

     Attached to the END of TYPE rather than the start of FORMAT. They are
     separate flex items, so on a narrow screen FORMAT wraps to its own line —
     and a leading "·" would sit orphaned at the start of that line looking like
     a bullet. Trailing it keeps the separator with the text it follows. */
  .index-table tbody td:nth-child(5)::after {
    content: '·';
    margin-left: 8px;
    color: var(--muted);
  }
}

/* ============================================================
   TEXT-STROKE FALLBACK
   -webkit-text-stroke is unprefixed-unsupported in some engines;
   without this, the outline word falls back to transparent = invisible.
   ============================================================ */
@supports not (-webkit-text-stroke: 1px #000) {
  .tile-b-title .stroke,
  .cta-row .stroke,
  .contact-line .stroke,
  .contact-modal-headline .stroke {
    -webkit-text-stroke: none;
    color: var(--cream);
  }
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .transmission .pulse { animation: none; opacity: 1; }
  /* Ticker stops and returns to the start, so the first segment is the one
     left on screen rather than a random mid-scroll fragment. */
  .transmission-track { animation: none; transform: none; }
  .hero-wordmark-track { animation: none; }
  .marquee-track { animation: none; }

  /* Capabilities still reveal — they just arrive instantly. */
  .cap-reveal,
  .cap-desc,
  .cap-title,
  .cap-cue { transition: none; }
  .cap-item:hover .cap-title { transform: none; }

  /* Unpin the hero. The scroll-driven pass across the portrait is exactly the
     kind of motion this setting asks us to drop; the page becomes a plain
     stack. site.js makes the matching call and skips the refraction sync. */
  .hero { position: relative; top: auto; }
  .cta-row .arrow,
  .cta-row .stroke,
  .contact-line .stroke { transition: none; }
  .cta-row:hover .arrow { transform: none; }
}


/* ============================================================
   The numeral system lives at the END of this file on purpose.

   .cap-num, .index-table .col-num, .project-title-block and the four label
   classes below all have earlier definitions elsewhere in this stylesheet, at
   equal specificity. Source order is the only thing deciding the winner, so
   defining these any higher up means the originals silently override them —
   which is exactly what happened on the first pass: .cap-num kept rendering as
   12px IBM Plex Mono while every other numeral had switched.

   If you move this block, re-check a rendered page before trusting it.
   ============================================================ */

/* ---------- NUMERAL SYSTEM (Doto) ----------

   THE RULE: Doto owns standalone display numerals at 30px and above, and
   nothing else. Every word, label and headline stays --display or --mono.
   Every numeral below sets 'wght' 900 and 'ROND' 0: 900 fills the dot grid as
   solidly as the face allows, and ROND 0 keeps the dots square, which sits with
   the no-border-radius rule everywhere else.

   Why it holds together: the site already numbers every section / 00 .. / 04
   on every page, so the numeral has a real recurring slot rather than being
   sprinkled on. That repetition is what makes it read as an identity element
   instead of a novelty font.

   THE 30px FLOOR IS LOAD-BEARING. Doto is a dot-matrix face: below roughly
   30px the dots stop resolving into digits and it turns to grey mush. Anything
   smaller — the meta strip, the ticker, the clock, the [ 062 / 062 ] counters —
   stays in --mono. That does mean the same digit can appear in two faces on
   one page; the size gap is what keeps that reading as a decision.

   The LATEST badge was originally exempt — the dots break up against the blue
   and it is weaker there than Space Grotesk was — but Alvin chose to bring it
   in, so it now follows the same rule as everything else and the mobile badge
   was enlarged so its numeral could stay above the floor. Nothing below 30px
   is exempt-by-negotiation; the floor still holds everywhere. */

/* The section numeral: the "/ NN" lifted out of the 11px label and set large
   and ghosted at the end of the header row. Decorative — every one of these is
   aria-hidden in the markup, because the label beside it already names the
   section and a screen reader announcing "zero three" adds nothing. */
.sec-num {
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: clamp(30px, 4.2vw, 58px);
  line-height: 0.82;
  letter-spacing: 0;
  /* Blue rather than neutral: these sit next to blue section labels on cream,
     and a grey numeral read as a rendering artefact rather than a mark. */
  color: rgba(31, 31, 218, 0.20);
  flex: none;
  user-select: none;
  /* No margin-left:auto here. These headers are justify-content:space-between
     and several carry a second span of meta text on the right; an auto margin
     on the last child swallows all the free space, which collapsed the label
     and the meta together at the left edge. Letting space-between do the work
     keeps the label at one end and the numeral at the other, with the meta
     distributed between them as it was. */
}
/* On ink the blue disappears, so the ghost switches to cream. */
.capabilities .sec-num,
.profile .sec-num { color: rgba(241, 241, 241, 0.20); }

/* Headers that were already flex rows just need to stop stretching the numeral
   to the row height. Those that were plain divs become flex rows here rather
   than in their own blocks, so the whole pattern stays in one place. */
.cap-header,
.works-header,
.info-meta,
.section-label {
  align-items: center;
}
.clients-label,
.contact-label,
.profile-label,
.project-stills-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

/* Works index — the highest-volume use on the site at 62 rows. Sits right on
   the floor at 30px; going smaller to save vertical space is exactly the
   trade that would make the whole system look like a mistake. */
.index-table .col-num {
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: 30px;
  line-height: 0.82;
  letter-spacing: 0;
  /* 92px fits "062" at 30px with room to breathe. It was 80px for the old 13px
     mono; going much wider than this starts taking width off CLIENT. */
  width: 92px;
}
/* The cells had no horizontal padding at all, so long values butted straight
   into the next column — "AIA Public Takaful" ran into "2026". Latent before,
   but the wider number column above is what made it show, so it gets fixed
   here rather than left for later.

   Desktop only: below 900px the row becomes a grid whose `gap` already does
   this, and the extra padding just ate into a 60px column that was already
   too tight. */
@media (min-width: 901px) {
  .index-table th,
  .index-table td { padding-right: 20px; }
  .index-table th:last-child,
  .index-table td:last-child { padding-right: 0; }
}

/* Capability numerals. The mock had these inline in a tidy row; the real
   section is a deliberate scatter with hand-set indents and four different
   title sizes, and item 01 sits at margin-left:0 with its number already
   hanging into the page margin. A 44px hanging numeral would land outside the
   canvas, so the number goes ABOVE the title instead — the scatter survives
   and Doto gets the size it needs. */
.cap-num {
  position: static;
  display: block;
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: clamp(30px, 3.4vw, 44px);
  line-height: 0.82;
  letter-spacing: 0;
  margin-bottom: 14px;
  /* 0.34 measured 2.84:1 on ink, which fails even the 3:1 large-text floor —
     and unlike .sec-num these numerals are not aria-hidden, because 01..04
     carry the ordering of the list. 0.42 is 3.73:1 and is the value the old
     12px mono marker used, so the ghost is one this design already proved. */
  color: rgba(241, 241, 241, 0.42);
}

/* Info stat row — the strongest fit on the site: big standalone numbers with a
   mono key under each. Every figure is derived, never typed: projects and
   clients come from _data/site.js, the years from the CV. */
.info-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px;
  margin-bottom: 64px;
}
.info-stat-n {
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: clamp(48px, 7vw, 96px);
  line-height: 0.82;
  letter-spacing: 0;
  color: var(--blue);
}
.info-stat-k {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 12px;
}

/* Project watermark. At 12% the dot grid is texture rather than type, which is
   the whole point — legibility stops mattering, so the one thing Doto is bad
   at never comes up. Clipped by the block and pointer-events:none so it can
   never eat a click on the title. */
.project-title-block { position: relative; overflow: hidden; }
.project-wm {
  position: absolute;
  left: var(--margin);
  top: 4px;
  font-family: var(--font-numeral);
  font-variation-settings: 'wght' 900, 'ROND' 0;
  font-weight: 900;
  font-size: clamp(110px, 17vw, 230px);
  line-height: 0.82;
  letter-spacing: 0;
  color: rgba(31, 31, 218, 0.12);
  pointer-events: none;
  user-select: none;
  z-index: 0;
}
.project-title { position: relative; z-index: 1; }

@media (max-width: 900px) {
  .info-stats { grid-template-columns: 1fr; gap: 20px; }
  /* The scatter indents collapse on mobile, so the numeral can sit tighter. */
  .cap-num { margin-bottom: 10px; }
}

/* ---------- FEATURED TILE REVEAL ----------

   The homepage has one strong motion moment — the hero reframing as the glass
   rises — and then goes completely still. This carries a little of that energy
   into SELECTED WORK without turning the page into a scroll demo.

   Deliberately restrained, because the design's whole argument is restraint:
   opacity plus an 18px lift, one easing, ~300ms. No scale, no blur, no
   horizontal drift, nothing that moves twice.

   The hidden state is gated behind .js-reveal, which site.js only adds when it
   is actually going to observe. Without JS — or with reduced motion, where the
   class is never added — the tiles simply render, so the content can never be
   stranded invisible by a script that failed to run. That failure mode is the
   whole reason this is not styled the obvious way round. */
.works-tile { display: block; }

.js-reveal .works-tile {
  opacity: 0;
  transform: translateY(18px);
}
.js-reveal .works-tile.is-revealed {
  opacity: 1;
  transform: none;
  /* Ease-out: fast at the start, settling at the end — the tile arrives rather
     than launches. */
  transition: opacity 300ms cubic-bezier(0.2, 0.7, 0.3, 1),
              transform 300ms cubic-bezier(0.2, 0.7, 0.3, 1);
}


/* ---------- FOCUS VISIBILITY & PRESS FEEDBACK ----------

   Focus: only five elements had an explicit ring; everything else fell back to
   the UA default. That is visible, but undesigned — and on the ink surfaces
   (modal, footer, capabilities, the glass) the default ring is nearly invisible
   against a near-black ground. Ring colour follows the same rule as every other
   blue here: --blue on cream, --blue-pale on ink.

   :focus-visible rather than :focus, so a mouse click never leaves a ring
   behind — only keyboard and other non-pointer focus shows it.

   Press: nothing on the site acknowledged a press. A control that does not
   react feels broken on touch, where there is no hover state to fall back on. */

.nav-link:focus-visible,
.nav-monogram:focus-visible,
.chip:focus-visible,
.works-tile:focus-visible,
.works-header a:focus-visible,
.cta-meta a:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}

/* On ink, --blue is 2.09:1 — a ring you cannot see. */
.ribbon a:focus-visible,
.cap-footer a:focus-visible,
.profile-more:focus-visible,
.reel-facade:focus-visible,
.contact-row:focus-visible,
.contact-modal-close:focus-visible {
  outline: 2px solid var(--blue-pale);
  outline-offset: 3px;
}

/* Two scales, because these are not the same size of thing: a 40px chip and a
   116px headline need different travel to read as the same gesture — 0.97 on
   the headline would look like a glitch. 120ms sits in the 100-160ms band for
   press feedback.

   The transform timing is declared on each element's OWN transition (search
   "press:" above) rather than in a shared rule here. A shared
   `transition: transform` at this point in the file replaces the whole
   shorthand on .chip and .contact-modal-close, which silently dropped their
   colour transitions and made hover snap. */
.reel-facade { transition: transform 120ms cubic-bezier(0.2, 0.7, 0.3, 1); }

.chip:active,
.contact-modal-close:active,
.reel-facade:active { transform: scale(0.97); }

.cta-row:active,
.contact-line:active { transform: scale(0.995); }

/* Press feedback is feedback, not decoration, so it survives reduced-motion —
   only the easing goes, so the change is instant rather than animated. */
@media (prefers-reduced-motion: reduce) {
  .chip,
  .contact-modal-close,
  .reel-facade { transition: none; }
}
