  /* ── PALETTE: 8 spec tokens (per vmanwrites-palette-spec v1.0) ── */
  :root {
    --ground:      #0E1A2B;  /* page background        */
    --surface:     #152538;  /* cards, grid, footer     */
    --border:      #24384F;  /* hairlines, dividers     */
    --cream:       #EFE6D3;  /* display + body text     */
    --muted:       #9AAABF;  /* metadata, >=12px only   */
    --gold:        #C9A24B;  /* static gold             */
    --gold-bright: #E7C46A;  /* hover / active / bindu  */
    --copper:      #C1502E;  /* NEW badges only         */

    --font-display: 'Fraunces', Georgia, serif;
    --font-body:    'Fraunces', Georgia, serif;
    --font-ui:      'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --font-mono:    'IBM Plex Mono', 'JetBrains Mono', ui-monospace, monospace;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }
    /* CSS scroll-snap: snap between two points — the top of the hero
       (scrollY=0) and the bottom of the hero (sentinel at scrollY=heroHeight).
       Using `proximity` (not `mandatory`) so the user can freely scroll
       through work section content; the snap only kicks in when the user
       lets go near a snap point. `scroll-behavior: smooth` provides the
       glide animation between snap points. The browser handles everything
       atomically — no JS scroll-snap state machine needed. */
    html {
      scroll-behavior: smooth;
      scroll-snap-type: y proximity;
    }
    /* (2026-09-24) The first snap point used to be `.hero { scroll-snap-align:
       start }`. The hero sits 4rem below the page top, so the browser kept
       nudging it flush to the window edge. The first snap point is now a
       1px sentinel at scrollY=0 (created in site.js), so the page rests
       with its top margin. The second snap point (below) is unchanged. */
    /* The .snap-stop element (a 1px sentinel inserted just below the hero
       at the heroHeight mark) is the second snap point. When the user
       scrolls down, the page snaps so this sentinel is at the top of the
       viewport — which is scrollY=heroHeight, exactly the "hero out of view"
       position. */
    .snap-stop { scroll-snap-align: start; }
      body {
        background: var(--ground); color: var(--cream);
        font-family: var(--font-body); line-height: 1.55;
        -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
      }
  a { color: inherit; text-decoration: none; }
  em { font-style: normal; color: var(--gold); }

  /* ── HERO REEL (no overlay text — name lives below) ───── */
  .hero {
    position: relative;
    height: 70vh; min-height: 420px; max-height: 620px;
    overflow: hidden;
    background: var(--ground);
    max-width: 1180px; margin: 4rem auto 0;   /* same column as everything else */
    border: 1px solid var(--border);
  }
  @media (max-width: 900px) {
    .hero { margin: 2rem auto 0; height: 60vh; min-height: 360px; max-width: none; }
  }
  /* On phones the hero spans full viewport width and uses an explicit 16:9 aspect
     so the showreel video fills the full width without horizontal cropping.
     (object-fit: cover on a tall portrait hero crops the video to the middle;
     matching the aspect ratio to the video's source gives cover == contain.) */
  @media (max-width: 640px) {
    .hero {
      margin: 1.25rem 0 0;
      height: auto;
      aspect-ratio: 16 / 9;
      min-height: 0;
      max-height: none;
    }
  }

  /* ── MEDIA PLAYER (overlay on hero video, hover-to-show) ─ */
  .media-player {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    z-index: 3;
    display: flex; align-items: center; gap: 0.6rem;
    padding: 0.55rem 0.9rem;
    background: rgba(14, 26, 43, 0.78);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-top: 1px solid var(--border);
    font-family: var(--font-mono);
    color: var(--cream);
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 0.18s ease, transform 0.18s ease;
    pointer-events: none;
  }
  .hero .hover-zone:hover,
  .media-player:hover,
  .media-player:focus-within,
  .media-player.is-visible {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
  /* Invisible hover strip at the bottom of the hero reveals the player.
     Sized large enough to be discoverable but small enough that normal
     video-viewing hover doesn't trigger the UI. The strip intercepts
     pointer events for hover detection, so clicking the bottom-most
     ~70px of the hero reveals controls but doesn't toggle video play/pause
     (toggle via the player button or by clicking higher in the video). */
  .hero .hover-zone {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 70px;
    z-index: 2; /* above the video (z-index 0) but below the player (z-index 3) */
    pointer-events: auto;
    background: transparent;
  }
  .mp-btn {
    background: none; border: 0; padding: 0;
    color: var(--cream);
    font: inherit; line-height: 1;
    cursor: pointer; min-width: 1.6rem;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .mp-btn:hover { color: var(--gold-bright); }
  .mp-icon-play,  .mp-icon-unmuted { display: inline-block; }
  .mp-icon-pause, .mp-icon-muted   { display: none; }
  .media-player.is-playing .mp-icon-play  { display: none; }
  .media-player.is-playing .mp-icon-pause { display: inline-block; }
  .media-player.is-muted   .mp-icon-unmuted { display: none; }
  .media-player.is-muted   .mp-icon-muted   { display: inline-block; }
  .mp-seek {
    flex: 1 1 auto; min-width: 0;
    -webkit-appearance: none; appearance: none;
    height: 4px; background: var(--border); border-radius: 2px; outline: 0;
    cursor: pointer;
  }
  .mp-seek::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--gold-bright);
    border: 0; cursor: pointer;
  }
  .mp-seek::-moz-range-thumb {
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--gold-bright); border: 0; cursor: pointer;
  }
  .mp-time {
    font-size: 0.7rem; color: var(--muted);
    letter-spacing: 0.05em; white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }
  .mp-vol-slider {
    -webkit-appearance: none; appearance: none;
    width: 70px; height: 4px; background: var(--border);
    border-radius: 2px; outline: 0; cursor: pointer;
    margin-left: 0.3rem;
  }
  .mp-vol-slider::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 10px; height: 10px; border-radius: 50%;
    background: var(--cream); border: 0; cursor: pointer;
  }
  .mp-vol-slider::-moz-range-thumb {
    width: 10px; height: 10px; border-radius: 50%;
    background: var(--cream); border: 0; cursor: pointer;
  }
  .mp-vol-slider { margin-left: 0.3rem; }
  .mp-fullscreen {
    font-size: 1.05rem;
    letter-spacing: -0.04em;
  }
  .hero .video-stage {
    position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%;
    object-fit: cover;
    cursor: pointer;
    background:
      radial-gradient(ellipse at 28% 38%, rgba(201,164,73,0.32) 0%, transparent 55%),
      radial-gradient(ellipse at 72% 62%, rgba(14,26,43,0.55) 0%, transparent 55%),
      linear-gradient(135deg, #1a2640 0%, #0e1a2b 50%, #0E1A2B 100%);
  }
  @media (max-width: 640px) {
    .media-player { padding: 0.45rem 0.6rem; gap: 0.45rem; }
    .mp-vol-slider { display: none; }
    .mp-time { font-size: 0.62rem; }
  }

  /* When the video has no source, show a subtle film-grain overlay */
  .hero .video-stage::after {
    content: ""; position: absolute; inset: 0;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.45 0'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>");
    mix-blend-mode: overlay;
  }
  .hero .reel-mark {
    position: absolute; right: 1.25rem; top: 1rem; z-index: 2;
    font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.18em;
    text-transform: uppercase; color: rgba(239,230,211,0.55);
  }
  .hero .reel-mark .dot { color: var(--gold-bright); margin-right: 0.4rem; }
  .hero .reel-reset {
    position: absolute; right: 1.25rem; top: 1rem; z-index: 4;
    font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.18em;
    text-transform: uppercase; color: var(--cream); background: rgba(14,26,43,0.7);
    border: 1px solid var(--gold); padding: 0.5rem 0.85rem;
    cursor: pointer; backdrop-filter: blur(8px);
  }
  .hero .reel-reset:hover { background: var(--gold); color: var(--ground); }

  /* ── NAME BLOCK (sits in the column, not over the video) ─ */
  .name-block {
    max-width: 1180px; margin: 4rem auto 0;
    padding: 0 4rem;
    text-align: center;
  }
  .name-block .bullet {
    width: 7px; height: 7px; background: var(--gold-bright);
    border-radius: 50%; margin: 0 auto 1.5rem;
    box-shadow: 0 0 8px var(--gold-bright);
  }
  .name-block .name {
    font-family: var(--font-display); font-weight: 400;
    font-size: clamp(2rem, 4.4vw, 3.6rem); line-height: 1;
    letter-spacing: -0.025em; color: var(--cream);
    white-space: nowrap;
    display: flex; align-items: baseline; justify-content: center; gap: 0.6rem;
  }
  .name-block .name .med   { font-weight: 500; }
  .name-block .name .light { font-weight: 200; }
  .name-block .role {
    font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.22em;
    text-transform: uppercase; color: var(--gold-bright);
    margin-top: 1.25rem;
  }
  @media (max-width: 900px) {
    .name-block { padding: 0 1.5rem; margin-top: 2.5rem; }
  }
  @media (max-width: 640px) {
    .name-block { padding: 0 1rem; }
    .name-block .name { font-size: 1.7rem; gap: 0.4rem; }
  }

  /* ── ABOUT (matches the same column frame as everything else) ─ */
  .about {
    max-width: 1180px; margin: 0 auto;
    padding: 5rem 4rem 4rem;
    text-align: center;
  }
  .about .label {
    font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.22em;
    text-transform: uppercase; color: var(--gold);
    margin-bottom: 2.5rem;
  }
  .about p {
    font-family: var(--font-body); font-size: 1.15rem; line-height: 1.55;
    color: var(--cream); margin: 0 auto 1.25rem; max-width: 60ch;
  }
  .about p:last-of-type { margin-bottom: 0; }
  .about p em { color: var(--gold); }
  .about p .drop {
    float: none;
    font-family: var(--font-display); font-size: 1.4em; line-height: 1;
    color: var(--gold); font-weight: 500; padding-right: 0.05em;
  }
  /* Cannes Lions credit — small gold mono line beneath the about prose */
  .about .cannes-credit {
    margin-top: 2.5rem;
    font-family: var(--font-mono); font-size: 0.72rem;
    letter-spacing: 0.22em; text-transform: uppercase;
    color: var(--gold);
    display: inline-flex; align-items: center; gap: 0.6rem;
  }
  .about .cannes-credit .dot {
    width: 6px; height: 6px; background: var(--gold);
    border-radius: 50%; display: inline-block;
    box-shadow: 0 0 6px var(--gold);
    flex: none;
    vertical-align: middle;
  }
  /* When the dot is swapped to an inline SVG <circle> (PDF composition),
     strip the CSS-circle styling — sizing comes from the SVG attributes. */
  .about .cannes-credit .dot:not(span) {
    background: none;
    box-shadow: none;
  }
  @media (max-width: 900px) {
    .about { padding: 3.5rem 1.5rem 3rem; }
  }
  @media (max-width: 640px) {
    .about { padding: 3rem 1rem 2.5rem; }
  }

  /* ── INDEX WALL ─────────────────────────────────────────── */
  /* All sections share the same 4rem horizontal padding so they line up.
     All section headings use the same pattern: small mono brass label + meta on right. */
  .work-section {
    max-width: 1180px; margin: 0 auto;
    padding: 5rem 4rem 5rem;
  }
  .work-section .head {
    margin-bottom: 2.5rem;
    display: flex; justify-content: space-between; align-items: baseline;
    flex-wrap: wrap; gap: 1rem;
  }
  .section-label {
    font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.22em;
    text-transform: uppercase; color: var(--gold);
  }
  .section-label .marker { color: var(--gold-bright); }
  .work-section h2.section-label { font-family: var(--font-mono); font-weight: 400; }
  .section-meta {
    font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.15em;
    text-transform: uppercase; color: var(--muted);
  }
  .work-section .grid {
    display: grid; gap: 1rem;
    grid-template-columns: repeat(12, 1fr);
  }
  .card {
    grid-column: span 3;
    background: var(--surface);
    color: var(--cream);
    padding: 1.4rem;
    min-height: 220px;
    border: 1px solid var(--border);
    display: flex; flex-direction: column; justify-content: space-between;
    cursor: pointer; position: relative;
    transition: transform 0.25s, box-shadow 0.25s, border-color 0.25s;
  }
  .card:hover {
    transform: translateY(-2px);
    box-shadow: 0 1px 0 var(--gold-bright), 0 12px 28px rgba(14,26,43,0.35);
    border-color: var(--gold-bright);
  }
  /* Static card: looks like a .card but isn't clickable. Used for projects
     that don't have a project page (e.g. 100+ Coverages — internal work
     that lives as a portfolio entry only). Same dimensions, no hover/click. */
  .card.card--static { cursor: default; }
  .card.card--static:hover {
    transform: none;
    box-shadow: none;
    border-color: var(--border);
  }
  .card .laurel {
    display: inline-flex; align-items: center; gap: 0.3rem;
    font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--gold);
    margin-left: 0.4rem;
  }
  .card .laurel svg { width: 14px; height: 14px; fill: var(--gold); }
  .card.featured { grid-column: span 8; min-height: 280px; }
  .card .cat {
    font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.18em;
    text-transform: uppercase; color: var(--muted);
    display: flex; justify-content: space-between; align-items: center;
  }
  .card .cat .stamp {
    width: 9px; height: 9px; background: var(--gold);
    border-radius: 50%;
  }
  .card .new-tag {
    display: inline-block; font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.2em;
    color: var(--cream); background: var(--copper);
    padding: 0.2rem 0.5rem; border-radius: 1px; margin-left: 0.6rem; vertical-align: middle;
  }
  .card h3 {
    font-family: var(--font-display); font-weight: 400;
    font-size: 1.25rem; line-height: 1.15; margin-top: 0.6rem;
  }
  .card.featured h3 { font-size: 1.7rem; }
  .card .role {
    font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    color: var(--gold); margin-top: 0.3rem;
  }
  .card .desc {
    font-size: 0.88rem; color: var(--muted);
    margin-top: 0.5rem; line-height: 1.5;
  }
  .card .yr {
    font-family: var(--font-mono); font-size: 0.68rem; color: var(--muted);
    letter-spacing: 0.1em;
    display: flex; justify-content: space-between; align-items: center;
    margin-top: 1rem; padding-top: 0.6rem;
    border-top: 1px solid var(--border);
  }
  .card .yr .id { letter-spacing: 0.15em; }
  .card .yr .arrow { color: var(--gold-bright); }

  @media (max-width: 1000px) {
    .card { grid-column: span 6; }
    .card.featured { grid-column: span 12; }
  }
  @media (max-width: 640px) {
    .card, .card.featured { grid-column: span 12; }
    .work-section { padding: 3rem 1.5rem 4rem; }
    .worked-with { padding: 3rem 1.5rem; }
  }

  /* ── COLLABORATORS WHO ARE FANS ────────────────────────────── */
  .worked-with {
    max-width: 1180px; margin: 0 auto;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    padding: 4rem 4rem;
  }
  .worked-with .head { margin-bottom: 2.5rem; }
  .brands {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    border-top: 1px solid var(--border);
  }
  .brands .brand {
    font-family: var(--font-display); font-weight: 400;
    font-size: 1.4rem; color: var(--cream);
    text-align: center; padding: 2rem 0.75rem;
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    transition: color 0.3s;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 0.4rem;
    /* Grid items have `min-width: auto` by default, which equals the
       min-content size of the content. For a cell containing an <img>,
       that's the image's intrinsic pixel width — which can exceed the
       column width on phones and force horizontal overflow. Override to
       0 so the cell shrinks to fit its column. */
    min-width: 0;
  }
  .brands .brand:hover { color: var(--gold-bright); }
  /* Empty grid placeholder — takes up cell space but renders no border/background.
     Each .brand cell now owns its OWN left/right/bottom borders (rather than
     relying on .brands's outer border-left), so an empty cell with border:0
     becomes truly invisible — no edges, no background, no interactions. */
  .brands .brand--empty {
    border: 0;
    background: transparent; pointer-events: none;
  }
  /* Renault (row 3, col 2 = 14th child) sits next to the leftmost empty cell.
     The empty cell has no border-right, so the vertical line between them must
     come from Renault's own edge. Use box-shadow (inset on the left edge) to
     draw a 1px line that visually matches the rest of the grid borders, since
     border-left alone gets visually swallowed by the empty cell's transparent
     background on the same edge. */
  .brands .brand:nth-child(14) {
    box-shadow: inset 1px 0 0 var(--border);
  }
  /* Cells in column 1 of rows 1 and 2 (children 1 and 7) form the grid's left
     edge. They use box-shadow so the line is independent of the .brand rule's
     border-right / border-bottom stack. */
  .brands .brand:nth-child(1),
  .brands .brand:nth-child(7) {
    box-shadow: inset 1px 0 0 var(--border);
  }
  /* ── LOGO GRID — monochrome per spec section 3 ─────────────── */
  /* All 12 logos render as cream tint at 70% opacity — unified monochrome per spec. */
  .brands .brand .logo {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Fixed height = tallest logo size (.logo--big at 90px). Smaller
       logos are vertically centered inside this container, so the
       subtitle <small> below always sits at the same Y position —
       eliminates the staggered-baseline issue. */
    height: 90px;
  }
  .brands .brand .logo img {
    /* Display:block removes inline baseline gap. width:100% makes the
       image fill its cell horizontally (the cell already constrains
       it via min-width:0). max-width:160 caps the desktop size.
       height:auto preserves aspect ratio. max-height:64 caps vertical.
       object-fit:contain handles cases where the box and image aspect
       ratios differ. min-width:0 lets the image shrink below its
       intrinsic pixel width on phones (e.g. 5417px visa.png). */
    display: block;
    width: 100%;
    height: auto;
    max-height: 64px; max-width: 160px;
    min-width: 0;
    object-fit: contain;
    filter: grayscale(1) brightness(1.6);
    opacity: 0.7;
    transition: filter 0.3s, transform 0.3s, opacity 0.3s;
  }
  .brands .brand .logo img.wide {
    max-height: 38px; max-width: 140px;
  }
  .brands .brand .logo img.compact {
    max-height: 32px; max-width: 110px;
  }
  /* Pre-tinted logos (already cream at source — don't re-brighten):
     PNGs that are ALREADY cream at source use the .pre class to bypass the filter.
     The grayscale+brightness filter would over-brighten already-cream art toward white. */
  .brands .brand .logo img.pre {
    filter: none;
    opacity: 0.85;
  }
  /* Larger render for logos that need more detail visible (Joe & the Juice's
     barista emblem + small text reads better at full cell height). */
  .brands .brand .logo img.logo--big {
    max-height: 90px;
    max-width: 90px;
  }
  /* Thin-stroke logos (J&J, Disney Hotstar, CWC, Dabur, AVIVA) — same filter as the
     default to keep brightness parity with the bold wordmarks. The .bold class
     only marks them so future per-logo tweaks are easy to find. */
  .brands .brand .logo img.bold {
    opacity: 0.7;
    filter: grayscale(1) brightness(1.6);
  }
  .brands .brand:hover .logo img {
    transform: scale(1.04);
    opacity: 1;
  }
  /* Cannes Bronze tag — appears under brand films descriptor for the winning work */
  .brands .brand .cannes-tag {
    display: inline-block;
    font-family: var(--font-mono); font-size: 0.55rem;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--gold); margin-top: 0.35rem;
    border-top: 1px solid var(--gold); padding-top: 0.25rem;
  }
  .brands .brand .name {
    font-family: var(--font-display); font-weight: 400; font-size: 1.25rem;
    color: var(--cream); letter-spacing: 0.01em;
  }
  .brands .brand small {
    display: block; font-family: var(--font-mono); font-size: 0.55rem;
    color: var(--muted); letter-spacing: 0.15em; text-transform: uppercase;
    margin-top: 0.1rem; font-weight: 400;
  }
  /* Mobile (≤640px): switch from 6 columns to 4 columns, hide the empty
     cells (they'd just be dead space on a phone), and let the parent
     .brands draw its own left border. The nth-child box-shadow rules
     above target positions that only make sense in the 6-col layout,
     so we explicitly turn them off here. Desktop (≥641px) inherits the
     base `.brands { grid-template-columns: repeat(6, 1fr) }` rule.

     Why 4 cols at phone: with 16 visible logos, 3 cols = 5 rows + 1
     spillover (Lionsgate alone in row 6). 4 cols = exactly 4 rows × 4
     logos. Clean grid, no orphaned cell. */
  @media (max-width: 640px) {
    .brands {
      grid-template-columns: repeat(4, 1fr);
      border-left: 1px solid var(--border);
    }
    .brands .brand--empty { display: none; }
    .brands .brand:nth-child(1),
    .brands .brand:nth-child(7),
    .brands .brand:nth-child(14) {
      box-shadow: none;
    }
  }

  /* ── PROJECT PAGE (expanded card view) ────────────────── */
  /* Hidden by default via the [hidden] attribute. When .is-open is added,
     we fade + slide the page in. The work-sections above it are dimmed
     via .work-section.is-hidden-by-project so the project page owns visual focus. */
  .project-page {
    /* FIXED OVERLAY that covers the viewport when open. Driven by the
       View Transitions API: when the page is unhidden via JS, the
       .project-stage (which has view-transition-name: project-media)
       is the morph target. The page itself sits at full opacity so the
       browser can capture it as the NEW view during the morph.
       When [hidden] is set, display:none hides everything cleanly. */
    position: fixed;
    top: 0; left: 0; right: 0;
    min-height: 100vh;
    max-height: 100vh;
    overflow-y: auto;
    background: var(--ground);
    z-index: 50;
    max-width: 100%;
    padding: 0;
    opacity: 1;
    transform: none;
    clip-path: none;
  }
  /* Inner container keeps the old padding/max-width so the content
     doesn't run edge-to-edge. The fixed page itself uses 0 padding. */
  .project-shell {
    box-sizing: border-box;
    min-height: 100vh;
    max-width: 1180px;
    margin: 0 auto;
    /* Top padding 0 — name+role lands right at viewport top (when
       migrated). Right/left padding 1.5rem on phones, 4rem desktop.
       Bottom padding 4rem so the contact section (or end of body)
       has breathing room before scrolling. */
    padding: 0.25rem 4rem 4rem;
  }
  /* .is-open rule removed: animation now driven by JS Web Animations API. */
    .project-page[hidden] {
    display: none;
  }

  /* ── Inner-block reveal timing ──
     Each inner sub-element starts hidden and animates in only when the
     JS state machine triggers it via class toggle. CSS owns the easing
     curves + durations; JS owns the orchestration. */

  /* Project header: name + role subtitle. Reveals as the page settles. */
  
  

  /* Top bar (back button + tag). */
  
  

  /* Video stage: scale-in from 0.88, fades from 0. The 0.88 start makes
     the morph clearly visible — at 0.96 it was too subtle to read as a
     separate moment. 560ms duration paired with the page reveal gives a
     satisfying "settling" feel as the video lands. */
  
  

  /* Clip nav row (label + prev/next + counter): slides up from below. */
  
  

  /* Body: title chars + partner/year + description words. The .is-typing
     class on .project-body triggers the per-char/word animation; staggered
     animation-delay is set inline by JS so it scales to text length without
     hand-tuning. The chars are invisible UNTIL .is-typing is added — the
     preceding opacity 0 prevents a flash of unstyled chars on initial load. */
  /* Body fades in WHILE char/word animations run. Per-char (.ch) and
     per-word (.wd) spans handle their own timing via animation-delay
     (set inline by JS), so the trigger here is just the body's own fade. */
  
  

  /* REAL TYPEWRITER (per char / per word):
     Each character has its own 80ms "snap" animation with a sharp
     ease-out-back. JS staggers each char at 90ms (so the eye reads
     one character per step, not a smear). animation-fill-mode is
     `backwards` so chars are invisible BEFORE their delay fires —
     without this they all paint visible briefly then the animation
     just plays a "settling" motion on already-visible chars
     (which is why this used to look like a fade). */
  .project-title .ch,
  .project-desc .wd {
    display: inline-block;
    opacity: 0;
    transform: translateY(-2px);
    /* Longhand (NOT the `animation:` shorthand) so the fill-mode is
       unambiguous and we can specify BOTH. Without this, the `forwards`
       keyword in the shorthand is overridden by `animation-fill-mode:
       backwards`, causing each char to disappear after its 80ms flash. */
    animation-name: chIn;
    animation-duration: 80ms;
    animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
    animation-fill-mode: both;
  }
  .project-title .ch.ch-sp { width: 0.28em; }
  /* Words describe a different motion: brief overshoot + settle, no
     blur (blur was being read as a fade because it happens during a
     paint-heavy phase). */
  .project-desc .wd {
    animation-duration: 110ms;
  }
  /* Caret that follows the cursor. We place a single absolute caret
     next to the .project-title and animate its position by reading
     the offset of the last revealed .ch via JS. See .caret below. */
/* Caret removed for v1 — see DOX. */
  @keyframes chIn {
    0%   { opacity: 0; transform: translateY(-2px) scale(0.94); }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }

  /* Close: nested inner-blocks ride out with the page's exit easing. */
  /* .is-closing rules removed: animation now driven by JS Web Animations API. */

  /* prefers-reduced-motion: collapse everything to a single 120ms fade.
     The state machine already detects this via matchMedia and sets
     short animation timings; CSS ensures the spans are visible instantly
     and any clip-path animation is skipped. */

  /* ── VIEW TRANSITIONS — card ↔ project stage morph ──────────────
     Shared element: the .card on the homepage and the .project-stage
     in the project page both use the same view-transition-name
     "project-media". The browser handles the rect morph automatically.

     During open: openProject tags the clicked .card with
     view-transition-name: project-media just before calling
     document.startViewTransition. The browser captures the card's
     rect, runs the DOM update (which makes .project-stage visible
     and the homepage blocks fade), then animates from the captured
     card rect to .project-stage's natural rect.

     During close: closeProject tags .project-stage with the same
     name (it's the CSS-default already on the element), runs the
     DOM update, and the browser animates back to the .card's last
     known position. */

  ::view-transition-group(project-media) {
    animation-duration: 520ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* Keep video frames continuous during the morph — both old and
     new views stay opacity 1 and don't fade, so the video plays
     through the transition without a freeze-frame snap. */
  ::view-transition-old(project-media),
  ::view-transition-new(project-media) {
    animation: none;
    mix-blend-mode: normal;
    height: 100%;
    overflow: clip;
  }
  /* OLD view IS the card (small thumbnail). object-fit: contain so
     it stays visually centered as the rect expands. */
  ::view-transition-old(project-media) {
    object-fit: contain;
  }
  /* NEW view IS the full-size stage. object-fit: cover so the video
     fills the new dimensions. */
  ::view-transition-new(project-media) {
    object-fit: cover;
  }

  /* Root: subtle crossfade. The page doesn't slide horizontally;
     the user sees the card grow into the stage and the rest of
     the page content fade out behind it. */
  ::view-transition-old(root) {
    animation: 280ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
  }
  ::view-transition-new(root) {
    animation: 280ms cubic-bezier(0.4, 0, 1, 1) both fade-in;
  }

  @media (prefers-reduced-motion: reduce) {
    .project-page,
    .project-stage,
    .project-header,
    .project-topbar,
    .project-controls,
    .project-body { transition-duration: 120ms !important; transform: none !important; }
    .project-page { clip-path: none !important; }
    .project-body .ch,
    .project-body .wd { opacity: 1; transform: none; animation: none; }
    /* View Transitions: collapse to instant for reduced motion */
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
      animation: none !important;
    }
  }
  /* Inner-block reveal via .is-shown class:
     Added by openProject after the morph completes (~560ms). The
     class triggers CSS transitions on opacity, so inner blocks fade
     in cleanly. We use transitions (not animations) so the final
     state is always committed (no fill-mode gotchas). */
  .project-body { opacity: 0; transition: opacity 240ms ease-out; }
  .project-body.is-shown { opacity: 1; }
  .project-title { opacity: 0; transition: opacity 280ms ease-out 240ms; }
  .project-title.is-shown { opacity: 1; }

  /* ── HERO HEADER ANIMATION (opt-in via project.headerAnimation === 'hero') ──
     The project-header starts offscreen above and drops in showing the
     full subtitle. After landing, the role text erodes front-to-back
     (one character from the start at a time) until only the final
     role remains. Driven by JS (_animateHeroHeader) which sets inline
     transform/opacity + the role text directly.
     Reduced-motion collapses the drop to instant and skips the
     erase (lands at final state immediately). */
  .project-header {
    transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 600ms ease-out;
  }
  .project-header.is-hero-dropping {
    transform: translateY(-100vh);
    opacity: 0;
  }
  .project-header.is-hero-settled {
    transform: translateY(0);
    opacity: 1;
  }
  .project-header-role { transition: none; }

  /* ── PASSWORD PROMPT OVERLAY (opt-in via project.passwordHash) ──────
     Full-viewport modal that blocks the rest of the page until the
     user enters the correct password. On correct entry, the prompt
     fades out and openProject proceeds. On wrong entry, an error
     message appears below the input. */
  .password-prompt {
      position: fixed;
      inset: 0;
      z-index: 100;
      display: flex;
      align-items: center;
      justify-content: center;
      /* Fully opaque so the hero video (which may still be autoplaying
         briefly behind the prompt on direct-link entry) is COMPLETELY
         hidden. The previous 0.92 alpha left 8% transparency — enough
         for the video frames to bleed through and look like the video
         is still playing. The prompt is the foreground; the video
         should not show AT ALL behind it. */
      background: rgb(14, 26, 43);
      backdrop-filter: blur(8px);
      opacity: 0;
      pointer-events: none;
      transition: opacity 240ms ease-out;
    }
  .password-prompt.is-open {
    opacity: 1;
    pointer-events: auto;
  }
  .password-prompt-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 2rem 2.4rem;
    max-width: 440px;
    width: calc(100% - 2rem);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  }
  .password-prompt-card h3 {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: 1.4rem;
    color: var(--cream);
    margin: 0 0 0.4rem;
  }
  .password-prompt-card p {
    font-family: var(--font-ui);
    font-size: 0.86rem;
    color: var(--muted);
    line-height: 1.5;
    margin: 0 0 1.2rem;
  }
  .password-prompt-input {
    width: 100%;
    padding: 0.7rem 0.9rem;
    background: var(--ground);
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--cream);
    font-family: var(--font-mono);
    font-size: 0.95rem;
    outline: none;
    transition: border-color 160ms ease-out;
  }
  .password-prompt-input:focus {
    border-color: var(--gold);
  }
  .password-prompt-input.is-wrong {
    border-color: var(--copper);
  }
  .password-prompt-actions {
    display: flex;
    gap: 0.6rem;
    margin-top: 1rem;
    justify-content: flex-end;
  }
  .password-prompt-btn {
    padding: 0.55rem 1.1rem;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--cream);
    font-family: var(--font-mono);
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    cursor: pointer;
    transition: border-color 160ms ease-out, color 160ms ease-out;
  }
  .password-prompt-btn:hover {
    border-color: var(--gold);
    color: var(--gold-bright);
  }
  .password-prompt-btn.primary {
    background: var(--gold);
    color: var(--ground);
    border-color: var(--gold);
  }
  .password-prompt-btn.primary:hover {
    background: var(--gold-bright);
    color: var(--ground);
  }
  .password-prompt-error {
    margin-top: 0.6rem;
    font-family: var(--font-mono);
    font-size: 0.74rem;
    color: var(--copper);
    min-height: 1em;
  }

  /* Dim the work-section above when a project is open. Keep it in flow
     (don't collapse height) so the back-button scroll position stays stable. */
  /* When a project page is open, HIDE everything above it (not just dim).
     Spec: "project page is its own page, not a pop-up". Only the project
     page + contact section should be visible. We use display:none so the
     page truly swaps — no scroll-back to find the homepage content.
     Hero, both work-sections (Now + Archive), and the collaborators
     (worked-with) all get hidden. Contact stays. */
  /* When a project page is open, the homepage blocks either:
     - DISPLAY: NONE after a fade (work sections, collaborators, hero), or
     - FLIP-MIGRATE to the project page (the .name-block, handled by JS).
     We deliberately do NOT include .name-block or .about here — the JS
     state machine manages those separately (FLIP for .name-block, a
     slide-up fade for .about). */
  .hero.is-hidden-by-project,
  .work-section.is-hidden-by-project,
  .worked-with.is-hidden-by-project {
    opacity: 0 !important;
    transform: translateY(-12px) !important;
    transition: opacity 480ms cubic-bezier(0.22, 1, 0.36, 1),
                transform 480ms cubic-bezier(0.22, 1, 0.36, 1),
                visibility 0s linear 480ms !important;
    pointer-events: none;
  }
  .hero.is-hidden-by-project { display: none !important; }
  .work-section.is-hidden-by-project,
  .worked-with.is-hidden-by-project { display: none !important; }
  /* .about gets its own slightly longer slide-up but no display:none —
     it just goes invisible by the end of its fade. JS un-clears its
     is-hidden-by-project class on close. */
  .about.is-hidden-by-project {
    opacity: 0 !important;
    transform: translateY(-14px) !important;
    transition: opacity 540ms cubic-bezier(0.22, 1, 0.36, 1),
                transform 540ms cubic-bezier(0.22, 1, 0.36, 1) !important;
    pointer-events: none;
  }
  /* The clicked card gets a subtle gold ring so the user knows which
     card they came from. Just a box-shadow — no transform, since the
     card stays in place while the project page opens below. */
  .card.is-source {
    border-color: var(--gold-bright);
    box-shadow: 0 0 0 1px var(--gold-bright);
  }

  

  .project-shell {
    display: flex; flex-direction: column; gap: 0.6rem;
  }

  /* ── Project page header ────────────────────────────────
     Re-uses the same name-block pattern from the homepage:
     a small gold bullet, the name in display serif, and the
     project's role (e.g. "AI ARTIST") in mono caps below.
     This is the page identity — sits at the top of the project page. */
  .project-header {
    text-align: center;
    padding: 0.25rem 0 0.4rem;
  }
  .project-header .bullet {
    width: 6px; height: 6px; background: var(--gold-bright);
    border-radius: 50%; margin: 0 auto 0.7rem;
    box-shadow: 0 0 6px var(--gold-bright);
  }
  .project-name {
    font-family: var(--font-display); font-weight: 400;
    font-size: clamp(1.3rem, 2.8vw, 2rem); line-height: 1;
    letter-spacing: -0.025em; color: var(--cream);
    white-space: nowrap;
    display: flex; align-items: baseline; justify-content: center; gap: 0.35rem;
    margin: 0;
  }
  .project-name .med   { font-weight: 500; }
  .project-name .light { font-weight: 300; }
  .project-header-role {
    font-family: var(--font-mono); font-size: 0.74rem;
    letter-spacing: 0.22em; text-transform: uppercase;
    color: var(--gold-bright);
    margin-top: 0.6rem;
  }

  /* Compact top bar: back button (left) + tag (right). Thin, no
     border — the divider above is the project-header's bottom space. */
  .project-topbar {
    display: flex; justify-content: space-between; align-items: center;
    gap: 1rem;
    padding: 0.3rem 0 0.5rem;
  }
  .project-back {
    display: inline-flex; align-items: center; gap: 0.5rem;
    background: transparent; border: 0;
    color: var(--cream); font-family: var(--font-mono);
    font-size: 0.72rem; letter-spacing: 0.18em;
    text-transform: uppercase;
    padding: 0.4rem 0; cursor: pointer;
    transition: color 0.2s;
  }
  .project-back:hover { color: var(--gold-bright); }
  .project-back .back-arrow { font-size: 1rem; line-height: 1; }
  .project-meta-line {
    display: flex; align-items: center; gap: 1.2rem;
    font-family: var(--font-mono); font-size: 0.68rem;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--muted);
  }
  .project-tag { color: var(--gold); }

  /* Video stage. The custom transport bar (.project-player) sits
     inside the stage and is positioned absolute over the bottom edge,
     identical to the hero reel's .media-player. Same backdrop blur,
     same buttons, same hidden-by-default visibility (cursor-trail + touch
     behavior is bound by the same showPlayer/HidePlayer functions).

     The stage has TWO renderer modes:
     - video (default): 16:9 landscape, fills stage, video element grows to fill.
     - pdf:              container is taller (portrait A4-letter or 9:16 video),
     bigger than the video stage by default, with the PDF iframe filling it.
     The .is-renderer-pdf class on .project-stage triggers the PDF sizing.
  */
  .project-stage {
    position: relative;
    background: #000;
    border: 1px solid var(--border);
    overflow: hidden;
    /* View Transitions API: this is the SHARED ELEMENT name that the
       card uses during the open transition. The browser morphs the
       card rect → stage rect automatically. See openProject/closeProject
       JS and the ::view-transition-group(project-media) rule below. */
    view-transition-name: project-media;
  }
  .project-video {
    display: block; width: 100%; height: auto;
    aspect-ratio: 16 / 9;
    background: #000;
  }
  /* PDF iframe wrapper. Hidden by default; the stage only shows it
     when .is-renderer-pdf is applied. Sized to letter/A4 portrait
     (~85vh tall, capped) — large enough to read, leaves header/body
     visible. The <object> gets 100% of wrapper width/height; the
     browser's built-in PDF viewer handles scroll/zoom. */
  .project-pdf-wrap {
    display: none;
    width: 100%;
    height: 100%;
    position: absolute; inset: 0;
    background: #0d1521;
  }
  .pdf-canvas-stage {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: auto;
    /* Lockdown: no text select, no right-click (handled by JS), no drag. */
    -webkit-user-select: none;
    -moz-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  .project-pdf-wrap canvas {
    display: block;
    /* Page-sized but capped so tall pages scroll within the wrapper. */
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    /* Drag/save prevention */
    -webkit-user-drag: none;
    user-drag: none;
    pointer-events: none;
    background: #fff;
    box-shadow: 0 4px 24px rgba(0,0,0,0.5);
  }
  .pdf-watermark {
    position: absolute;
    inset: 0;
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2;
    /* Repeated diagonal watermark via SVG data URI */
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><g transform='rotate(-35 200 200)' fill='%23ffffff' fill-opacity='0.08' font-family='monospace' font-size='38' font-weight='700'><text x='40' y='120'>PREVIEW</text><text x='40' y='200'>PREVIEW</text><text x='40' y='280'>PREVIEW</text><text x='40' y='360'>PREVIEW</text></g></svg>");
    background-repeat: repeat;
  }
  .pdf-canvas-loading {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--cream);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    z-index: 3;
  }
  .pdf-canvas-loading.is-hidden { display: none; }
  /* Stills wrapper. Hidden by default; the stage only shows it when
     .is-renderer-stills is applied. The <img> gets object-fit: contain
     so portrait stills don't get cropped (they show side-by-side with
     letterboxing on a landscape stage). */
  .project-stills-wrap {
    display: none;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
    overflow: hidden;
    position: relative;
  }
  .project-still {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: #000;
  }
  /* Stills projects: stage needs an explicit height since no <video>
     rule exists to drive aspect. Mirror the video's 16:9 behavior. */
  .project-stage.is-renderer-stills {
    aspect-ratio: 16 / 9;
  }
  /* Portrait projects on stills: lock to 9:16 like videos */
  .project-stage.is-renderer-stills.is-portrait {
    aspect-ratio: 9 / 16;
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
  }
  .project-stage.is-renderer-stills.is-portrait .project-stills-wrap {
    aspect-ratio: 9 / 16;
  }
  .project-stage.is-renderer-stills .project-stills-wrap {
    display: block;
  }
  /* ── Stage aspect sizing ── */
  /* Default (video, landscape): the video element drives aspect via its
     own 16:9 rule. Stage grows naturally to fit. */
  .project-stage:not(.is-renderer-pdf) {
    /* video drives height; nothing to set here */
  }
  /* PDF renderer: the stage is taller than a typical 16:9 stage, and the
     iframe fills it. Tall projects: clamp min/max height to keep within
     viewport on desktop but allow fullscreen on mobile. */
  .project-stage.is-renderer-pdf {
    aspect-ratio: auto;
    /* Tall enough to read a letter page. On desktop we cap at 85vh;
       on mobile (when portrait frame is wider than tall) the cap is
       reduced so the body text below stays visible. */
    min-height: 600px;
    height: clamp(600px, 85vh, 1100px);
  }
  .project-stage.is-renderer-pdf .project-pdf-wrap {
    display: block;
  }
  /* Portrait-video projects: aspect-lock the stage at 9:16 so vertical
     content renders full-bleed. Videos still <video> → object-fit: cover
     via the .project-video rule. */
  .project-stage.is-portrait {
    aspect-ratio: 9 / 16;
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
  }
  .project-stage.is-portrait .project-video {
    aspect-ratio: 9 / 16;
    object-fit: cover;
  }
  /* (PDF protection rules now live on .pdf-canvas-stage above.) */
  /* Mixable toggle visibility — hidden unless .is-mixable on stage */
  .project-stage:not(.is-mixable) .mp-toggle { display: none !important; }
  /* On PDF stage, hide the play/seek/time/mute/vol controls (PDF has
     no audio). Keep fullscreen + toggle. */
  .project-stage.is-renderer-pdf #ppPlay,
  .project-stage.is-renderer-pdf #ppSeek,
  .project-stage.is-renderer-pdf #ppTime,
  .project-stage.is-renderer-pdf #ppMute,
  .project-stage.is-renderer-pdf #ppVol {
    display: none !important;
  }
  /* Stills: same as PDF — no audio, no play/pause. Keep fullscreen + toggle. */
  .project-stage.is-renderer-stills #ppPlay,
  .project-stage.is-renderer-stills #ppSeek,
  .project-stage.is-renderer-stills #ppTime,
  .project-stage.is-renderer-stills #ppMute,
  .project-stage.is-renderer-stills #ppVol {
    display: none !important;
  }
  .project-stage.is-renderer-pdf .project-player {
    /* Center the remaining controls (fullscreen + toggle) on the PDF stage. */
    justify-content: flex-end;
    gap: 0.5rem;
  }
  /* When on PDF stage but mixable=true (showing PDF), the toggle label
     flips to "View Video" so users know what's behind it. JS toggles this. */

  /* Compact clip nav row below the video. Right-aligned: clip label
     (left) + prev/next + counter (right). No dots — scales to projects
     with many clips without dominating the page. */
  .project-controls {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.5rem;
    padding: 0.6rem 0.2rem 0.4rem;
  }
  .project-controls .clip-label {
    font-family: var(--font-mono); font-size: 0.7rem;
    letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--muted);
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .clip-nav {
    display: flex; align-items: center; gap: 0.7rem;
    flex-shrink: 0;
  }
  .clip-btn {
    background: transparent; border: 1px solid var(--border);
    color: var(--cream); font-family: var(--font-mono);
    font-size: 0.95rem; line-height: 1;
    width: 2.2rem; height: 2.2rem;
    padding: 0;
    border-radius: 2px; cursor: pointer;
    transition: border-color 0.2s, color 0.2s, background 0.2s;
  }
  .clip-btn:hover:not(:disabled) {
    border-color: var(--gold-bright);
    color: var(--gold-bright);
    background: rgba(201, 162, 75, 0.08);
  }
  .clip-btn:disabled {
    opacity: 0.3; cursor: not-allowed;
  }
  .project-counter {
    font-family: var(--font-mono); font-size: 0.72rem;
    letter-spacing: 0.18em; color: var(--muted);
    min-width: 3.5rem; text-align: center;
  }

  /* Project text body */
  .project-body {
    display: flex; flex-direction: column; gap: 0.7rem;
    padding-top: 1.5rem;
  }
  .project-title {
    font-family: var(--font-display); font-weight: 400;
    font-size: 2.2rem; line-height: 1.15;
    color: var(--cream);
  }
  .project-meta {
    font-family: var(--font-mono); font-size: 0.78rem;
    letter-spacing: 0.1em;
    color: var(--gold);
    display: flex; flex-wrap: wrap; gap: 0.4rem 0.6rem;
  }
  .meta-sep { color: var(--border); }
  .project-credit {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--cream);
    padding-top: 0.4rem;
    margin-bottom: 0.6rem;
    border-top: 1px solid var(--border);
  }
  .project-desc {
    font-size: 1.02rem; line-height: 1.6;
    color: var(--cream); opacity: 0.86;
    max-width: 68ch;
  }
  .project-desc p { margin-bottom: 0.7rem; }
  .project-desc p:last-child { margin-bottom: 0; }
  .clip-label {
    font-family: var(--font-mono); font-size: 0.72rem;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--muted);
    padding-top: 0.6rem;
  }

  /* Phone: reduce padding, smaller title, controls stack tighter */
    @media (max-width: 640px) {
      .project-page { padding: 2rem 1.25rem 3rem; }
      .project-shell { padding: 0.25rem 1.25rem 3rem; }
      .project-title { font-size: 1.6rem; }
      .project-topbar { flex-direction: column; align-items: flex-start; gap: 0.6rem; }
      .project-controls { gap: 1rem; }
    }

  /* ── CONTACT BAR (matching section language) ────────────── */
  .contact {
    max-width: 1180px; margin: 0 auto;
    border-top: 1px solid var(--border);
    padding: 4rem 4rem 5rem;
    display: grid; grid-template-columns: 1fr 1fr; gap: 3rem;
  }
  .contact .links { display: flex; flex-direction: column; gap: 0.9rem; }
  .contact .links a {
    font-family: var(--font-display); font-weight: 400;
    font-size: 1.25rem; color: var(--cream);
    transition: color 0.2s;
    display: inline-flex; align-items: center; gap: 0.85rem;
  }
  .contact .links a:hover { color: var(--gold-bright); }
  .contact .links a .icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px;
    border: 1px solid var(--gold);
    color: var(--gold);
    flex-shrink: 0;
  }
  .contact .links a:hover .icon { background: var(--gold); color: var(--ground); }
  .contact .links a .icon svg { width: 16px; height: 16px; }
  .contact .links a .icon .stroke { fill: none; stroke: currentColor; stroke-width: 1.6; }
  .contact .copy {
    font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted);
    letter-spacing: 0.12em; text-transform: uppercase;
    /* Pin to bottom of the right-hand column of the 2-col contact grid.
       align-self: end only works when the row has extra vertical space
       beyond the item's intrinsic height, which it does here because
       the left column (h2 + 3 links) is taller than the .copy block.
       Explicit grid-column/row + justify-self: end keeps it locked
       to the bottom-right corner regardless of viewport height. */
    grid-column: 2; grid-row: 1;
    align-self: end;
    justify-self: end;
    text-align: right;
    line-height: 1.7;
  }
  @media (max-width: 700px) { .contact { grid-template-columns: 1fr; padding: 3rem 1.5rem; gap: 2rem; } }

  /* ── MOBILE: portrait-video + PDF stage ──
     On phones we shrink the PDF viewer height cap to leave room for
     body text + bottom nav. Portrait video projects get a wider stage
     since the 9:16 frame leaves a lot of vertical breathing room. */
  @media (max-width: 700px) {
    .project-stage.is-renderer-pdf {
      min-height: 480px;
      height: clamp(480px, 70vh, 900px);
    }
    .project-stage.is-portrait {
      max-width: 100%;
      aspect-ratio: 9 / 16;
    }
  }
  @media (max-width: 480px) {
    .project-stage.is-renderer-pdf {
      min-height: 400px;
      height: clamp(400px, 65vh, 800px);
    }
  }

  /* ════════════════════════════════════════════════════════════════
     SITE SPLIT (2026-09-23) — hub + /production + /ai
     Everything below is new. It only uses existing palette tokens and
     the existing type scale; no new colours. See AGENTS.md → "Site split".
     ════════════════════════════════════════════════════════════════ */

  /* Lead card: 2 per row (e.g. adidas + Pepsi on /production,
     Pirate's Portrait + Heroes in Training on /ai). Set per track via
     `featured: ['<track>']` in projects.js. */
  .card.card--lead { grid-column: span 6; min-height: 280px; }
  .card.card--lead h3 { font-size: 1.7rem; }
  @media (max-width: 640px) { .card.card--lead { grid-column: span 12; } }

  /* Compact section (Writing in development): 3 per row, shorter cards. */
  .work-section--compact { padding-top: 3.5rem; padding-bottom: 3.5rem; }
  /* 4 per row (was 3) since the Jack Stalwart script card made it 4 cards (2026-09-24). */
  .work-section--compact .card { grid-column: span 3; min-height: 170px; }
  .work-section--compact .card h3 { font-size: 1.1rem; }
  .work-section--compact .card .desc { font-size: 0.82rem; }
  @media (max-width: 1000px) { .work-section--compact .card { grid-column: span 6; } }
  @media (max-width: 640px)  { .work-section--compact .card { grid-column: span 12; } }

  /* Short prose under a section heading (e.g. "Production background"). */
  .section-intro {
    font-family: var(--font-body); font-size: 1.05rem; line-height: 1.6;
    color: var(--cream); max-width: 62ch; margin: -1rem 0 2.5rem;
  }

  /* Quiet cross-link between tracks. One line, not a banner. */
  .track-switch {
    display: inline-block; margin-top: 1.1rem;
    font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.12em;
    color: var(--muted); transition: color 0.2s;
  }
  .track-switch:hover { color: var(--gold-bright); }
  .track-switch .arrow { color: var(--gold); margin-left: 0.35rem; }
  .contact .track-switch { margin-top: 1.75rem; }

  /* ── Tools and workflows (AI page) ── */
  .tools-section {
    max-width: 1180px; margin: 0 auto;
    padding: 5rem 4rem 5rem;
  }
  .tools-section .head {
    margin-bottom: 2.5rem;
    display: flex; justify-content: space-between; align-items: baseline;
    flex-wrap: wrap; gap: 1rem;
  }
  .tools-section h2.section-label { font-weight: 400; }
  .tools-list {
    list-style: none;
    display: grid; grid-template-columns: repeat(3, 1fr);
    border-top: 1px solid var(--border); border-left: 1px solid var(--border);
  }
  .tools-list li {
    padding: 1.4rem;
    border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
    min-width: 0;
  }
  .tools-list h3 {
    font-family: var(--font-display); font-weight: 400;
    font-size: 1.15rem; line-height: 1.2; color: var(--cream);
  }
  .tools-list p { font-size: 0.88rem; color: var(--muted); margin-top: 0.5rem; line-height: 1.5; }
  .tools-line {
    margin-top: 2rem;
    font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.12em;
    color: var(--muted); line-height: 1.9;
  }
  .tools-line .label { color: var(--gold); letter-spacing: 0.22em; text-transform: uppercase; margin-right: 0.6rem; }
  @media (max-width: 1000px) { .tools-list { grid-template-columns: repeat(2, 1fr); } }
  @media (max-width: 640px) {
    .tools-section { padding: 3rem 1.5rem 4rem; }
    .tools-list { grid-template-columns: 1fr; }
  }
  .tools-section.is-hidden-by-project { display: none !important; }

  /* ── Hub (/) ── */
  .hub-line {
    max-width: 1180px; margin: 0 auto;
    padding: 3rem 4rem 0; text-align: center;
  }
  .hub-line p {
    font-family: var(--font-body); font-size: 1.15rem; line-height: 1.55;
    color: var(--cream); margin: 0 auto; max-width: 52ch;
  }
  .doors { padding-top: 3.5rem; }
  .doors .card { grid-column: span 6; min-height: 240px; }
  .doors .card h3 { font-size: 1.7rem; }
  .doors .card .desc { font-size: 0.98rem; }
  @media (max-width: 640px) {
    .hub-line { padding: 2rem 1rem 0; }
    .doors .card { grid-column: span 12; min-height: 200px; }
  }

  /* Pre-existing phone bug (also on the pre-split live site): .contact
     collapses to 1 column at ≤700px but .copy kept grid-column: 2, which
     created an implicit 2nd column and ~46px horizontal overflow on a
     390px phone. Stack it under the links instead. */
  @media (max-width: 700px) {
    .contact .copy { grid-column: 1; grid-row: auto; justify-self: start; text-align: left; }
  }

  /* Pre-existing bug (also on the pre-split live site): nothing hid the
     <video> in stills mode, so the 16:9 video box sat on top of the stage
     and the still rendered BELOW it, clipped out of view (Bollywoof showed
     an empty black player). Found 2026-09-23 when Pirate's Portrait became
     the first mixed video + stills carousel. */
  .project-stage.is-renderer-stills .project-video { display: none; }

  /* /ai "Production background": summary paragraph + cross-link, no cards. */
  .production-summary .section-intro { margin-bottom: 1.25rem; }
  .production-summary .track-switch { margin-top: 0; font-size: 0.72rem; color: var(--gold); }
  .production-summary .track-switch:hover { color: var(--gold-bright); }

  /* Card footer: keep a gap between year and company when a card is narrow
     (4-up Screenwriting row); long company names wrap right-aligned. */
  .card .yr { gap: 0.75rem; }
  .card .yr > span:last-child { text-align: right; }
  .card .yr .id { white-space: nowrap; }

  /* ── Stills take their own shape (2026-10-02) ──
     site.js sets --still-ar (width / height) from the loaded image and
     adds .has-still-ar. Height is capped at 78vh, so tall stills narrow
     and centre instead of running off screen. Overrides the 16:9 and
     .is-portrait 9:16 stills rules above. */
  .project-stage.is-renderer-stills.has-still-ar {
    aspect-ratio: var(--still-ar);
    max-width: min(100%, calc(78vh * var(--still-ar)));
    margin-left: auto;
    margin-right: auto;
  }
  .project-stage.is-renderer-stills.has-still-ar .project-stills-wrap {
    aspect-ratio: var(--still-ar);
  }

  /* ── Fullscreen: fill the screen, and add prev/next on the stage ── */
  .project-stage:fullscreen { max-width: none !important; aspect-ratio: auto !important; }
  .project-stage:-webkit-full-screen { max-width: none !important; aspect-ratio: auto !important; }
  .project-stage:fullscreen .project-stills-wrap { aspect-ratio: auto !important; height: 100%; }
  .project-stage:-webkit-full-screen .project-stills-wrap { aspect-ratio: auto !important; height: 100%; }
  .fs-nav { display: none; }
  .project-stage:fullscreen .fs-nav,
  .project-stage:-webkit-full-screen .fs-nav {
    display: flex; align-items: center; justify-content: center;
    position: absolute; top: 50%; transform: translateY(-50%); z-index: 6;
    width: 3.2rem; height: 3.2rem; border-radius: 50%;
    border: 1px solid var(--border); background: rgba(14, 26, 43, 0.6);
    color: var(--cream); font-size: 1.3rem; cursor: pointer;
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  }
  .project-stage:fullscreen .fs-prev, .project-stage:-webkit-full-screen .fs-prev { left: 1.25rem; }
  .project-stage:fullscreen .fs-next, .project-stage:-webkit-full-screen .fs-next { right: 1.25rem; }
  .project-stage .fs-nav:hover:not([disabled]) { border-color: var(--gold); color: var(--gold-bright); }
  .project-stage .fs-nav[disabled] { opacity: 0.25; cursor: default; }
  .project-stage.fs-nav-off .fs-nav { display: none !important; }

  /* Stills: the transport bar only holds the fullscreen button, so it
     goes transparent (no band over the bottom of the image, which
     mattered for before/after comparisons). The button gets its own
     small backing so it reads on light images. */
  .project-stage.is-renderer-stills .project-player {
    background: transparent;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    border-top: 0;
    pointer-events: none;
  }
  .project-stage.is-renderer-stills .project-player .mp-btn {
    pointer-events: auto;
    background: rgba(14, 26, 43, 0.6);
    border-radius: 3px;
  }

  /* /ai/ showcase: one section, equal cards, 2 per row (2026-10-05: 2x2
     with four pieces; was 3 per row, which left the 4th card alone). */
  .work-section--thirds .card { grid-column: span 6; min-height: 240px; }
  @media (max-width: 640px)  { .work-section--thirds .card { grid-column: span 12; } }

  /* ── Media cards (card.thumb, 2026-10-05) ──
     Visual roles need visual cards: the image shows by default with a
     title strip; the normal text panel fades in on hover or keyboard
     focus. The text panel stays IN FLOW (just transparent), so a media
     card is exactly as tall as the same card in text form; the image is
     a layer behind it. Touch screens (no hover) stack image above text. */
  .card.card--media { padding: 0; overflow: hidden; background: var(--ground); }
  .card--media .card-thumb {
    position: absolute; inset: 0; z-index: 0;
    background-size: cover; background-position: center;
    transition: transform 0.6s ease;
  }
  .card--media .card-strip {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
    padding: 3rem 1.4rem 1.1rem;
    background: linear-gradient(to top, rgba(14, 26, 43, 0.95) 0%, rgba(14, 26, 43, 0.8) 50%, rgba(14, 26, 43, 0) 100%);
    transition: opacity 0.25s ease;
  }
  .card--media .strip-cat {
    font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.2em;
    text-transform: uppercase; color: var(--gold);
  }
  .card--media .card-strip h3 { margin-top: 0.3rem; }
  .card--media .card-text {
    position: relative; z-index: 2; flex: 1; padding: 1.4rem;
    display: flex; flex-direction: column; justify-content: space-between;
    background: rgba(21, 37, 56, 0.94);
    opacity: 0; transition: opacity 0.25s ease;
  }
  .card--media:hover .card-text, .card--media:focus-visible .card-text { opacity: 1; }
  .card--media:hover .card-strip, .card--media:focus-visible .card-strip { opacity: 0; }
  .card--media:hover .card-thumb { transform: scale(1.04); }
  /* /ai/ showcase cards are wide: hold them at 16:9 (grows if text needs it). */
  .work-section--thirds .card.card--media { aspect-ratio: 16 / 9; min-height: 0; }
  @media (hover: none) {
    .card.card--media, .work-section--thirds .card.card--media { aspect-ratio: auto; }
    .card--media .card-thumb { position: relative; inset: auto; aspect-ratio: 16 / 9; flex: none; }
    .card--media .card-strip { display: none; }
    .card--media .card-text { opacity: 1; background: var(--surface); }
  }
  @media (prefers-reduced-motion: reduce) {
    .card--media .card-thumb, .card--media .card-text, .card--media .card-strip { transition: none; }
    .card--media:hover .card-thumb { transform: none; }
  }

  /* Hub doors as media cards (2026-10-06): wide, so held at 16:9. */
  .doors .card.card--media { aspect-ratio: 16 / 9; min-height: 0; }
  .doors .card--media .card-strip h3 { font-size: 1.7rem; }
  @media (hover: none) { .doors .card.card--media { aspect-ratio: auto; } }
