/* ==========================================================================
   NEUE MONTREAL — Dani's licensed PP set (Graphic Assets/FONTS/PP®F Font
   Starter Pack), self-hosted. The pack has Light, Regular and Bold; there is
   no Medium or SemiBold, so the weight ranges below make 500 fall to Regular
   and 600 fall to Bold rather than to a faux-bold. Archivo, which this
   replaces, came from Google Fonts; the mono (IBM Plex Mono) and the serif
   still do. Files are ~24 KB each. font-display: swap so text never waits.
   ========================================================================== */
@font-face { font-family: 'Neue Montreal'; src: url('../fonts/NeueMontreal-Light.woff2') format('woff2');      font-weight: 200 300; font-style: normal; font-display: swap; }
/* (5 Oct: her name and the project names were tried in Bold, by asking for
   800 on .logo, .crumb__name, .wname, .pcard__title and .pgal__t, and went
   back to Regular the same day. Bold was too heavy; she asked about a Medium
   or Semibold. The family has both, but only Light, Regular and Bold are in
   assets/fonts. With a Medium file added, give it its own @font-face at 500
   and narrow Regular's range to 400, then ask for 500 on those five.)

   Dani, Sept: the whole site in REGULAR. Every weight the stylesheet asks
   for between 400 and 700 — the 500 metas, the 600 display headings —
   resolves to the Regular file; Bold only answers 800+, which nothing uses.
   Change the ranges here, not the 30-odd font-weight rules below. */
@font-face { font-family: 'Neue Montreal'; src: url('../fonts/NeueMontreal-Regular.woff2') format('woff2');    font-weight: 400 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Neue Montreal'; src: url('../fonts/NeueMontreal-Bold.woff2') format('woff2');       font-weight: 800 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Neue Montreal'; src: url('../fonts/NeueMontreal-Italic.woff2') format('woff2');     font-weight: 300 700; font-style: italic; font-display: swap; }
@font-face { font-family: 'Neue Montreal'; src: url('../fonts/NeueMontreal-BoldItalic.woff2') format('woff2'); font-weight: 800 900; font-style: italic; font-display: swap; }

/* ==========================================================================
   DANIELLE SHAMI — STUDIO SITE
   Design system v1.0
   Token-first CSS. Every value here maps to a Framer/Webflow variable.
   See HANDOFF.md for the token table.
   ========================================================================== */

/* --- 1. TOKENS ----------------------------------------------------------- */
:root {
  /* Neutral base. Projects introduce colour, the shell never does. */
  --ink:        #111110;
  --ink-2:      #2A2A27;
  --paper:      #FFFFFF;
  /* The alternate band went neutral with it. Against a true white, the old warm
     #EAE7DF read as a cream panel rather than a quiet step down. */
  --paper-2:    #F3F3F4;
  --muted:      #656158; /* 5.74:1 on white, 5.44:1 on paper-2 — passes AA at small sizes */
  --hairline:   rgba(17,17,16,.14);
  --hairline-2: rgba(17,17,16,.07);

  /* Inverted surface */
  --dark:       #0E0E0C;
  --dark-2:     #1A1A17;
  --on-dark:    #F4F2ED;
  --on-dark-mut:#9C978B;
  --hairline-d: rgba(255,255,255,.16);

  /* Accent — used sparingly for state, never decoration */
  --accent:     #1B3BE8;
  /* Highlight — the marker swipe on hovered rows. The one bit of colour in the shell. */
  --hl:         #CFF24D;

  /* Corner radius. One token, site-wide, at Dani's request — "slight corner
     rounding on all rectangles so nothing is too sharp".

     6px is chosen rather than picked: at the sizes used here it is visible as
     softness on an edge but never as a rounded box, which is the difference
     between an editorial page and a dashboard. Anything under 4px reads as a
     rendering artefact on a hairline border; anything over 10px starts to look
     like a card component.

     `--radius-lg` is for the two floating previews only, which are small and
     sit over artwork, so they need slightly more to read as separate objects.

     NOT applied to full-bleed elements: a rounded corner on something that runs
     to the edge of the window shows the page background through the notch and
     reads as a mistake. */
  --radius:     6px;
  --radius-lg:  10px;

  /* Type families */
  --sans:  'Neue Montreal', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --serif: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --mono:  'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Fluid type scale — clamp(min, fluid, max) */
  --t-mega:  clamp(3.1rem, 12.5vw, 12rem);
  --t-xxl:   clamp(2.5rem, 7.6vw, 6.6rem);
  --t-xl:    clamp(2rem, 4.6vw, 3.9rem);
  --t-lg:    clamp(1.5rem, 2.7vw, 2.3rem);
  --t-md:    clamp(1.18rem, 1.5vw, 1.45rem);
  --t-body:  clamp(1rem, 1.05vw, 1.09rem);
  --t-sm:    .875rem;
  --t-meta:  .72rem;

  /* Spacing */
  --gut:     clamp(1.15rem, 3.2vw, 2.6rem);
  --sec:     clamp(4.5rem, 11vh, 9.5rem);
  --sec-sm:  clamp(2.75rem, 6vh, 5rem);
  --max:     1680px;

  /* Motion */
  --ease:     cubic-bezier(.22, .61, .36, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --dur:      .5s;
  --dur-fast: .22s;
}

/* --- 2. RESET ------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Offset for the fixed header when jumping to anchors */
  scroll-padding-top: 6rem;
}
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -.005em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* `clip` not `hidden`: overflow:hidden here creates a scroll container and
     silently breaks position:sticky on the pinned homepage hero. `clip` stops
     the horizontal overflow from .bleed without that side effect. */
  overflow-x: clip;
}
img, svg, video { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; padding: 0; }

/* Accessible focus — visible, never removed. The colour is the surface's own
   ink, NOT --accent: the blue ring fired on mouse click in text inputs too, so
   filling the form drew a bright blue rectangle round every field. Overridden
   further down for form fields, which have their own designed focus state. */
:focus-visible {
  outline: 1.5px solid var(--ink);
  outline-offset: 2px;
  border-radius: 1px;
}
.dark :focus-visible, .u-invert :focus-visible, .closing :focus-visible { outline-color: var(--on-dark); }

.skip {
  position: absolute; left: -999px; top: 0; z-index: 999;
  background: var(--ink); color: var(--paper);
  padding: .8rem 1.2rem; font: 500 var(--t-sm)/1 var(--mono);
}
.skip:focus { left: .6rem; top: .6rem; }
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- 3. LAYOUT ---------------------------------------------------------- */
.wrap { width: 100%; max-width: var(--max); margin-inline: auto; padding-inline: var(--gut); }
.wrap--narrow { max-width: 1140px; }
.wrap--text { max-width: 760px; }

.sec { padding-block: var(--sec); }
.sec--sm { padding-block: var(--sec-sm); }
.sec--tight { padding-block: 0 var(--sec); }

.grid { display: grid; gap: var(--gut); grid-template-columns: repeat(12, 1fr); }
.g2 { display: grid; gap: var(--gut); grid-template-columns: repeat(2, 1fr); }
.g3 { display: grid; gap: var(--gut); grid-template-columns: repeat(3, 1fr); }
.g4 { display: grid; gap: var(--gut); grid-template-columns: repeat(4, 1fr); }
/* .g5 did not exist. Rows written as five-up fell back to plain block layout —
   one full-width image per row — on both case-study pages, and my own row check
   passed the whole time: the aspect-ratio normalisation gives every item in a
   row an identical ratio, so at full width they are all exactly the same
   height. A collapsed grid is perfectly even. Evenness cannot detect it;
   comparing an item's width against its container can. */
.g5 { display: grid; gap: var(--gut); grid-template-columns: repeat(5, 1fr); }

.rule { height: 1px; background: var(--hairline); border: 0; }
.rule--d { background: var(--hairline-d); }

.dark { background: var(--dark); color: var(--on-dark); }
.dark .rule { background: var(--hairline-d); }
.dark .meta, .dark .lede { color: var(--on-dark-mut); }
.paper-2 { background: var(--paper-2); }

/* --- 4. TYPOGRAPHY ------------------------------------------------------ */
h1, h2, h3, h4, h5 {
  font-weight: 600;
  line-height: .95;
  letter-spacing: -.03em;
  text-wrap: balance;
}
.d-mega { font-size: var(--t-mega); line-height: .84; letter-spacing: -.045em; font-weight: 600; }
.d-xxl  { font-size: var(--t-xxl);  line-height: .9;  letter-spacing: -.038em; }
.d-xl   { font-size: var(--t-xl);   line-height: .96; letter-spacing: -.03em; }
.d-lg   { font-size: var(--t-lg);   line-height: 1.05; letter-spacing: -.025em; }
.d-md   { font-size: var(--t-md);   line-height: 1.2;  letter-spacing: -.02em; font-weight: 500; }

.serif  { font-family: var(--serif); font-weight: 400; letter-spacing: -.02em; }
.serif-it { font-family: var(--serif); font-style: italic; font-weight: 400; letter-spacing: -.015em; }

.meta {
  font-family: var(--mono);
  font-size: var(--t-meta);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: 1.4;
  font-weight: 400;
}
.meta--ink { color: var(--ink); }
.lede { font-size: var(--t-md); line-height: 1.42; color: var(--ink-2); max-width: 46ch; letter-spacing: -.015em; }
.body-lg { font-size: 1.09rem; line-height: 1.62; max-width: 66ch; }
.prose p + p { margin-top: 1.1em; }
.prose p { max-width: 68ch; line-height: 1.62; }
.prose h3 { margin-top: 2.4em; margin-bottom: .7em; }
.prose ul { margin-top: 1em; }
.prose li { padding-left: 1.15em; position: relative; margin-bottom: .45em; line-height: 1.55; }
.prose li::before { content: ""; position: absolute; left: 0; top: .62em; width: 5px; height: 5px; background: var(--ink); }
.dark .prose li::before { background: var(--on-dark); }

.u-mut { color: var(--muted); }
.u-max { max-width: 52ch; }

/* Section label — the small numbered eyebrow used throughout */
.eyebrow {
  display: flex; align-items: baseline; gap: .9rem;
  padding-bottom: 1.4rem; margin-bottom: 2.2rem;
  border-bottom: 1px solid var(--hairline);
}
.dark .eyebrow { border-color: var(--hairline-d); }
.eyebrow__num { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; color: var(--muted); }
.eyebrow__title { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .14em; text-transform: uppercase; }
.eyebrow__end { margin-left: auto; }

/* --- 5. HEADER / NAV ---------------------------------------------------- */
.hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  padding-block: 1.05rem;
  background: transparent;
  transition: background var(--dur) var(--ease), backdrop-filter var(--dur) var(--ease), padding var(--dur) var(--ease);
}
.hdr::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 1px;
  background: var(--hairline); opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
/* No background on the bar itself — the progressive blur below handles legibility.
   A translucent panel plus a blur strip would double up and read as a band. */
.hdr.is-stuck { padding-block: .7rem; }
.hdr.is-stuck::after { opacity: 0; }

/* --- 5a. PROGRESSIVE BLUR -------------------------------------------------
   A fixed strip that blurs whatever scrolls under it and then FADES THE BLUR
   OUT rather than ending on a line.

   Three details make it work, and each is easy to get wrong:
   1. It starts ABOVE the viewport (top: -128px, height: 208px) so the strongest
      part of the blur is off-screen and only its falloff is visible. Starting
      at 0 gives a hard top edge.
   2. `mask-image` fades the ELEMENT, and because the element's only paint is a
      backdrop-filter, fading it fades the blur itself — a gradient of blur,
      which is not something backdrop-filter can do on its own.
   3. `pointer-events: none`, or it swallows every click in the top 80px.

   It carries no background colour at all. The legibility comes from the blur,
   so the page's own colour still shows through and the bar never reads as a
   panel sitting on top of the page. */
.blurtop {
  position: fixed;
  top: -128px; left: 0; right: 0;
  height: 208px;
  z-index: 99;                     /* under the header (100), over everything else */
  pointer-events: none;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  -webkit-mask-image: linear-gradient(#000 70%, transparent);
  mask-image: linear-gradient(#000 70%, transparent);
}
/* Safari on iOS has historically mis-composited a masked backdrop-filter into a
   grey block. If it cannot do both, better to do neither than to paint a slab. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .blurtop { display: none; }
}
/* .is-hidden retired — the header no longer hides on scroll-down. */
.hdr { transition: transform .42s var(--ease), background var(--dur) var(--ease), padding var(--dur) var(--ease); }

.hdr__in { display: flex; align-items: center; gap: var(--gut); }
/* THE WORDMARK — Dani, 5 Oct. Her name, and only her name, is set in PP
   Editorial New Regular, mixed case. It got here by elimination the same day:
   Neue Montreal Bold was too heavy, Archivo SemiBold (the site's face before
   Neue Montreal) in caps was "not there yet", and of four Pangram Pangram
   faces tried in this block she chose this one. A sharp serif against the mono
   line under it. The size is larger than the sans it replaced because a
   high-contrast serif reads small at the same em.

   The font file is the .woff2 Dani supplied on 5 Oct from her Pangram Pangram
   pack (it replaced one converted here from the .otf). Whether that pack's
   licence covers web use is hers to confirm. */
@font-face { font-family: 'Editorial New'; src: url('../fonts/EditorialNew-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
.logo {
  font-family: 'Editorial New', var(--serif);
  font-weight: 400; font-size: 1.25rem; letter-spacing: -.01em;
  line-height: 1; display: flex; flex-direction: column; gap: .18rem;
}
.logo span { font-family: var(--mono); font-size: .58rem; letter-spacing: .2em; text-transform: uppercase; color: var(--muted); font-weight: 400; }

/* Nav as a single frosted pill holding the links, rather than free-floating
   text. Against a white page the group needs an edge to sit on; against a
   full-bleed image it needs the frosting to stay legible. One element does both. */
.nav {
  margin-left: auto;
  display: flex; align-items: center;
  gap: clamp(.15rem, 1vw, .6rem);
  padding: 0 .25rem;
  border-radius: 5px;
  background: rgb(17 17 16 / .05);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.nav a {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em;
  text-transform: uppercase;
  position: relative;
  padding: .5rem .7rem;
  border-radius: 4px;
  color: var(--muted);
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.nav a:hover { color: var(--ink); background: rgb(17 17 16 / .05); }
/* The current page is marked by weight and a solid chip, not an underline —
   an underline under a pill reads as two competing shapes. */
.nav a[aria-current="page"] {
  color: var(--ink);
  background: rgb(255 255 255 / .75);
}

.burger {
  display: none; margin-left: auto; background: none; border: 0;
  width: 44px; height: 44px; cursor: pointer; position: relative;
}
.burger i {
  position: absolute; left: 10px; width: 24px; height: 1.5px; background: currentColor;
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast);
}
.burger i:nth-child(1) { top: 17px; }
.burger i:nth-child(2) { top: 25px; }
.burger[aria-expanded="true"] i:nth-child(1) { transform: translateY(4px) rotate(45deg); }
.burger[aria-expanded="true"] i:nth-child(2) { transform: translateY(-4px) rotate(-45deg); }

.mnav {
  position: fixed; inset: 0; z-index: 99;
  background: var(--dark); color: var(--on-dark);
  padding: 6rem var(--gut) 2rem;
  display: flex; flex-direction: column;
  clip-path: inset(0 0 100% 0);
  transition: clip-path .55s var(--ease-out);
  overflow-y: auto;
}
.mnav.is-open { clip-path: inset(0 0 0 0); }
.mnav a.mnav__link {
  font-size: clamp(2.2rem, 11vw, 3.4rem); font-weight: 600; letter-spacing: -.04em;
  line-height: 1.05; padding-block: .42rem; display: block;
  border-bottom: 1px solid var(--hairline-d);
}
.mnav__foot { margin-top: auto; padding-top: 2.5rem; display: grid; gap: .5rem; }
/* Services sub-items sit inline on mobile — no nested disclosure to mis-tap */
.mnav__sub {
  display: grid; gap: .1rem;
  padding: .7rem 0 1rem 1rem;
  border-bottom: 1px solid var(--hairline-d);
}
.mnav__sub a {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em;
  text-transform: uppercase; color: var(--on-dark-mut);
  padding-block: .5rem;
}
.mnav__sub a:hover { color: var(--on-dark); }

/* --- 6. BUTTONS / LINKS ------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: .7rem;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em;
  text-transform: uppercase; padding: 1.05rem 1.6rem;
  background: var(--ink); color: var(--paper); border: 1px solid var(--ink);
  cursor: pointer; transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.btn:hover { background: transparent; color: var(--ink); }
.btn:active { transform: translateY(1px); }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { background: var(--ink); color: var(--paper); }
.dark .btn { background: var(--on-dark); color: var(--dark); border-color: var(--on-dark); }
.dark .btn:hover { background: transparent; color: var(--on-dark); }
.dark .btn--ghost { background: transparent; color: var(--on-dark); }
.dark .btn--ghost:hover { background: var(--on-dark); color: var(--dark); }

/* Arrow link */
.alink {
  display: inline-flex; align-items: center; gap: .55rem;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em; text-transform: uppercase;
  padding-bottom: .35rem; border-bottom: 1px solid currentColor;
}
.alink svg { transition: transform var(--dur-fast) var(--ease); }
.alink:hover svg { transform: translateX(4px); }

/* --- 7. HERO ------------------------------------------------------------ */
.hero { padding-top: clamp(7.5rem, 17vh, 12rem); padding-bottom: var(--sec-sm); position: relative; }
.hero__stmt { max-width: 19ch; }
.hero__row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--gut) clamp(2rem,6vw,6rem); margin-top: clamp(2.5rem,6vh,4.5rem); }
.hero__disc { display: flex; flex-wrap: wrap; gap: .45rem; }
.chip {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--hairline); padding: .5rem .8rem; color: var(--muted);
}
.dark .chip { border-color: var(--hairline-d); }

/* --- 8. PROJECT CARDS / ARCHIVE ---------------------------------------- */
.pcard { display: block; position: relative; }
/* The ratio is the intent; the cap is what makes it usable. At two columns on a
   laptop these measured 694x867 — the picture alone was taller than the 828px
   viewport, so the title and the year underneath it could never be on screen at
   the same time as the image they belong to. You scrolled past a photograph,
   then past its caption, and had to hold one in your head to read the other.

   At 82svh the card measures about 745px against an 828px viewport: the image
   is long again — 680px, most of the way back to the uncapped 867 — and the
   title and year still land on the same screen, which was the whole point.
   70svh was too cautious. The 4/5 ratio still governs every narrow viewport,
   where the cap never bites.

   `width: 100%` is load-bearing and not decoration. A max-height reduces an
   element's INTRINSIC size, and a grid column sized from content will then
   shrink to match it — which silently narrows the card instead of shortening
   it. This grid has fixed columns so it would survive without the line, but the
   homepage tiles did not, and lost a chunk of their width twice before the
   cause was found. */
.pcard__media {
  position: relative; overflow: hidden; background: var(--paper-2);
  aspect-ratio: 4 / 5;
  width: 100%;
  max-height: 82svh;
}
.pcard__media img {
  width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.001);
  transition: transform .85s var(--ease-out), filter .5s var(--ease);
}
.pcard:hover .pcard__media img { transform: scale(1.045); }
/* A cover can be a film. The studio lets Dani drop one onto a Work card, and
   a <video> gets none of the img rules above for free: without these it sits
   at its intrinsic size in the top-left of the frame instead of filling it. */
.pcard__media video {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.001);
  transition: transform .85s var(--ease-out);
}
.pcard:hover .pcard__media video { transform: scale(1.045); }
.pcard--flat:hover .pcard__media video { transform: none; }
.pcard__media--wide { aspect-ratio: 16 / 10; }
.pcard__media--sq { aspect-ratio: 1 / 1; }
/* .pcard__idx removed with the numbered index on the Work cards — Dani's call.
   Dead CSS is worse than untidy: the next reader takes it for a live component. */
.pcard__bar {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding-top: .85rem; margin-top: .85rem; border-top: 1px solid var(--hairline);
}
.dark .pcard__bar { border-color: var(--hairline-d); }
.pcard__title { font-size: clamp(1.15rem, 1.7vw, 1.5rem); font-weight: 600; letter-spacing: -.028em; line-height: 1.1; }
.pcard__meta { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin-top: .4rem; }
.pcard__yr { font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); letter-spacing: .1em; flex: none; }

/* Filters */
.filters { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.filter {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase;
  padding: .62rem .95rem; border: 1px solid var(--hairline); background: transparent; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.filter:hover { border-color: var(--ink); }
.filter[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.archive__item { transition: opacity .4s var(--ease), transform .4s var(--ease); }
.archive__item[hidden] { display: none; }
.archive__count { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; color: var(--muted); }

/* --- 9. CAPABILITIES LIST ---------------------------------------------- */
.caps { border-top: 1px solid var(--hairline); }
.dark .caps { border-color: var(--hairline-d); }
/* Every .cap is its own grid, so an `auto` track sized by that row's own tag
   made each row start its text at a different x. Two equal columns instead —
   the same 50% axis as .g2 — so descriptions line up with each other AND with
   the intro paragraph above them. Sub-tracks live inside each half. */
.cap {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gut);
  align-items: start;
  padding-block: clamp(1.5rem, 3.2vw, 2.3rem);
  border-bottom: 1px solid var(--hairline);
}
.dark .cap { border-color: var(--hairline-d); }
.cap__l { display: grid; grid-template-columns: 4.5rem minmax(0,1fr); gap: var(--gut); align-items: start; }
.cap__r { display: grid; grid-template-columns: minmax(0,1fr) 7rem; gap: var(--gut); align-items: start; }
/* Foldable variant: the third track holds the open/close indicator */
.cap--fold .cap__r { grid-template-columns: minmax(0,1fr) 7rem 1.2rem; }
.cap__n {
  font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); letter-spacing: .1em;
  padding-top: .35rem; transition: color var(--dur-fast) var(--ease);
}
.cap:hover .cap__n { color: var(--ink); }
.dark .cap:hover .cap__n { color: var(--on-dark); }
.cap__h { font-size: clamp(1.4rem, 2.5vw, 2.1rem); font-weight: 600; letter-spacing: -.03em; line-height: 1.02; }

/* --- FOLDABLE CAPABILITY ROWS ---------------------------------------------
   Closed by default; hover opens on a fine pointer, click or Enter toggles
   for keyboard and touch. Animating grid-template-rows 0fr → 1fr is what lets
   an auto-height block transition — display:none can't be animated, and a
   fixed max-height would either clip long copy or ease against dead space. */
.cap--fold { cursor: pointer; }
.cap__btn {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  letter-spacing: inherit; line-height: inherit;
}
.cap__fold {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .5s var(--ease-out), opacity .35s var(--ease);
}
.cap__fold > div { overflow: hidden; }
.cap--fold.is-open .cap__fold { grid-template-rows: 1fr; opacity: 1; }

/* A quiet plus that becomes a minus */
.cap__ind {
  position: relative; width: 1.2rem; height: 1.2rem; margin-top: .25rem;
  justify-self: end; flex: none;
}
.cap__ind::before, .cap__ind::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  background: var(--muted);
  transition: transform .4s var(--ease-out), background var(--dur-fast) var(--ease);
}
.cap__ind::before { width: 11px; height: 1.5px; transform: translate(-50%, -50%); }
.cap__ind::after  { width: 1.5px; height: 11px; transform: translate(-50%, -50%); }
.cap--fold.is-open .cap__ind::after { transform: translate(-50%, -50%) scaleY(0); }
.cap--fold.is-open .cap__ind::before,
.cap--fold:hover .cap__ind::before,
.cap--fold:hover .cap__ind::after { background: var(--ink); }

/* Closed rows sit tighter — the point is a scannable list, not a wall.
   Titles are one line each: a hardcoded <br> was doubling every row's height
   and is what made this read heavier than the homepage version. */
.cap--fold { padding-block: clamp(.85rem, 1.6vw, 1.2rem); }
.cap--fold .cap__btn { line-height: 1.08; font-size: clamp(1.3rem, 2.4vw, 2rem); }
/* A narrower number column keeps the longest title on one line all the way
   down to the mobile breakpoint, and tightens the left edge at the same time */
.cap--fold .cap__l { grid-template-columns: 3.2rem minmax(0,1fr); }
/* Nudge the number and duration onto the title's optical line */
.cap--fold .cap__n { padding-top: .5rem; }
.cap--fold .cap__t { padding-top: .55rem; }
.cap--fold .cap__ind { margin-top: .45rem; }

@media (prefers-reduced-motion: reduce) {
  .cap__fold { transition: none; }
  .cap__ind::before, .cap__ind::after { transition: none; }
}

/* --- MARKER HIGHLIGHT ---------------------------------------------------
   A colour swipe that wipes in behind the heading text on hover. Replaces the
   old indent-on-hover, which read as a layout glitch rather than a state.
   width:fit-content keeps the swipe on the text, not the whole grid column. */
.cap__h, .cap__btn {
  width: fit-content;
  background-image: linear-gradient(var(--hl), var(--hl));
  background-repeat: no-repeat;
  background-size: 0% 100%;
  background-position: 0 0;
  box-decoration-break: clone;
  transition: background-size .48s var(--ease-out), color .28s var(--ease);
}
.cap:hover .cap__h,
.cap--fold:hover .cap__btn,
.cap--fold:focus-within .cap__btn,
.cap:focus-within .cap__h,
/* On dark sections the swipe needs dark text to stay legible */
.dark .cap:hover .cap__h,

@media (prefers-reduced-motion: reduce) {
  .cap__h, .cap__btn { transition: none; }
}
.cap__d { color: var(--ink-2); font-size: .97rem; line-height: 1.55; }
.dark .cap__d { color: var(--on-dark-mut); }
.cap__t { font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); letter-spacing: .1em; text-transform: uppercase; text-align: right; padding-top: .3rem; }

/* --- 10. SPRINT CARDS -------------------------------------------------- */
.sprint {
  display: flex; flex-direction: column; gap: 1rem;
  padding: clamp(1.4rem,2.4vw,2rem);
  border: 1px solid var(--hairline); background: transparent;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
  height: 100%;
}
.dark .sprint { border-color: var(--hairline-d); }
.sprint:hover { background: var(--paper-2); border-color: var(--ink); }
.dark .sprint:hover { background: var(--dark-2); border-color: var(--on-dark); }
.sprint__top { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.sprint__h { font-size: clamp(1.2rem,1.8vw,1.55rem); font-weight: 600; letter-spacing: -.028em; line-height: 1.08; }
.sprint__d { color: var(--ink-2); font-size: .95rem; line-height: 1.55; }
.dark .sprint__d { color: var(--on-dark-mut); }
.sprint__foot { margin-top: auto; padding-top: 1.1rem; border-top: 1px solid var(--hairline); display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.dark .sprint__foot { border-color: var(--hairline-d); }

/* Placeholder marker — every unconfirmed value carries this */
.ph {
  font-family: var(--mono); font-size: .62rem; letter-spacing: .12em; text-transform: uppercase;
  background: rgba(27,59,232,.09); color: var(--accent);
  border: 1px dashed rgba(27,59,232,.45); padding: .18rem .42rem; white-space: nowrap;
  vertical-align: middle; display: inline-block;
}
.dark .ph { background: rgba(169,184,255,.13); color: #B9C4FF; border-color: rgba(169,184,255,.5); }

/* --- 11. CASE STUDY --------------------------------------------------- */
.cs-hero { position: relative; }
.cs-hero__media { aspect-ratio: 16 / 9; overflow: hidden; background: var(--paper-2); }
.cs-hero__media img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 720px) { .cs-hero__media { aspect-ratio: 4 / 5; } }

.factsheet { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--gut) var(--gut); }
.fact { border-top: 1px solid var(--hairline); padding-top: .8rem; }
.dark .fact { border-color: var(--hairline-d); }
.fact dt { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: .45rem; }
.fact dd { font-size: .97rem; line-height: 1.4; letter-spacing: -.01em; }

.cs-split { display: grid; grid-template-columns: minmax(0, 27%) minmax(0, 1fr); gap: clamp(2rem, 6vw, 6rem); align-items: start; }
.cs-split__aside { position: sticky; top: 6.5rem; }
/* `overflow: hidden` because the reveal scales the media from 1.09 to 1 — an
   unclipped figure lets a scaled child spill over its neighbour in a grid for
   the 1.3s the transition lasts. */
.cs-figure { margin-block: clamp(2.5rem, 6vw, 5rem); overflow: hidden; }
.cs-figure img { width: 100%; }
.cs-figure figcaption { margin-top: .8rem; font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
/* --- FULL BLEED, WITHOUT A TRANSFORM -------------------------------------
   This was `margin-left: 50%; transform: translateX(-50%)`, and the transform
   was a latent collision: `.js [data-rev] { transform: translateY(18px) }` and
   its `.is-in { transform: none }` are the SAME property on the SAME element,
   so any bleed figure that also carried a reveal kept the 50% margin and lost
   the -50% pull. The result renders as a half-width image starting at the
   centre of the page — it looks like a sizing mistake, not a broken animation,
   which is why it survived review on the Paninoteca page.

   Centring with margin alone cannot collide with anything: reveals animate
   `transform` and `opacity`, and neither is used here now. Two properties that
   both want `transform` on one element will always eventually meet. */
.bleed {
  /* `100vw` includes the scrollbar; the body's content box does not. So a
     100vw element on a scrolling page is a scrollbar wider than the space it
     has, and centring it hangs half a scrollbar off each side — 7px here.
     `overflow-x: clip` on the body hides the consequence, which is why this
     survived: nothing scrolls sideways, the artwork is just quietly cropped.

     --sbw is measured once by site.js. The fallback of 0px is the old
     behaviour exactly, so no JS means no regression. */
  width: calc(100vw - var(--sbw, 0px));
  margin-left: calc(50% - 50vw + var(--sbw, 0px) / 2);
}

.stat { border-top: 1px solid var(--hairline); padding-top: 1rem; }
.dark .stat { border-color: var(--hairline-d); }
.stat__n { font-size: clamp(2.2rem,4.5vw,3.6rem); font-weight: 600; letter-spacing: -.04em; line-height: .95; }
.stat__l { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-top: .6rem; }

/* --- 12. TRENDS ------------------------------------------------------- */
.scanning {
  display: inline-flex; align-items: center; gap: .6rem;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .14em; text-transform: uppercase;
}
.scanning__dot {
  width: 7px; height: 7px; border-radius: 50%; background: #57C25B; flex: none;
  animation: pulse 1.9s var(--ease) infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.72); } }

/* .signal / .hsig removed — the Trends signals list became a .crow list and
   nothing used them any more. Dead CSS is worse than untidy: the next reader
   takes it for a live component and reasons about a system that is gone.
   tools/check-dead-css.py exists because of this one. */

.report { display: flex; flex-direction: column; gap: .9rem; }
.report__media { aspect-ratio: 3 / 2; overflow: hidden; background: var(--dark-2); }
.report__media img { width: 100%; height: 100%; object-fit: cover; transition: transform .8s var(--ease-out); }
.report:hover .report__media img { transform: scale(1.04); }

/* --- 13. SHOP -------------------------------------------------------- */
.prod { display: flex; flex-direction: column; gap: .9rem; }
.prod__media { aspect-ratio: 1/1; overflow: hidden; background: var(--paper-2); position: relative; }
.prod__media img { width: 100%; height: 100%; object-fit: cover; transition: transform .8s var(--ease-out); }
.prod:hover .prod__media img { transform: scale(1.04); }
.prod__bar { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.prod__h { font-size: 1.05rem; font-weight: 600; letter-spacing: -.022em; line-height: 1.15; }
.prod__p { font-family: var(--mono); font-size: var(--t-sm); letter-spacing: .02em; flex: none; }

/* --- 14. FORMS ------------------------------------------------------- */
.form { display: grid; gap: 1.6rem; }
.field { display: grid; gap: .55rem; }
.field label { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.field input, .field select, .field textarea {
  background: transparent; border: 0; border-bottom: 1px solid var(--hairline);
  padding: .75rem 0; font-size: 1.02rem; letter-spacing: -.012em; border-radius: 0;
  transition: border-color var(--dur-fast) var(--ease);
  width: 100%;
}
.dark .field input, .dark .field select, .dark .field textarea { border-color: var(--hairline-d); }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--ink); outline-offset: 4px; }
.dark .field input:focus, .dark .field select:focus, .dark .field textarea:focus { border-color: var(--on-dark); }

/* --- A FORM ON A DARK SECTION -------------------------------------------
   Every dark-form rule on this site was written against `.closing` or
   `#support` — the two places a form had ever appeared on black. Putting the
   Trends digest form on `.sec.dark` inherited none of them, and the failures
   are the quiet kind: labels and the note are `--muted` (#656158), which is
   3.43:1 on `--dark` and fails AA at the 11px these are set in; the UA paints
   `input { color: fieldtext }`, which does NOT inherit, so typed text stays
   near-black on black; and the placeholder and unchecked checkbox are drawn in
   the browser's LIGHT scheme.

   `color-scheme` is what fixes the native controls — `accent-color` only
   colours the CHECKED state and cannot touch an unchecked box. */
.sec.dark { color-scheme: dark; }
.sec.dark .field label,
.sec.dark .form__note,
.sec.dark .u-mut { color: var(--on-dark-mut); }          /* 6.63:1 */
.sec.dark .field input,
.sec.dark .field select,
.sec.dark .field textarea { color: var(--on-dark); }
.sec.dark .field input::placeholder,
.sec.dark .field textarea::placeholder { color: rgb(244 242 237 / .5); }
.field textarea { min-height: 8rem; resize: vertical; line-height: 1.5; }
.field select { appearance: none; background-image: none; cursor: pointer; }
.field--sel { position: relative; }
.field--sel::after { content: "↓"; position: absolute; right: .2rem; bottom: .85rem; pointer-events: none; font-family: var(--mono); font-size: .8rem; color: var(--muted); }
.hint { font-size: var(--t-sm); color: var(--muted); line-height: 1.45; }

/* Radio / checkbox pills */
.pills { display: flex; flex-wrap: wrap; gap: .45rem; }
.pill { position: relative; }
.pill input { position: absolute; opacity: 0; width: 100%; height: 100%; left: 0; top: 0; margin: 0; cursor: pointer; }
.pill span {
  display: block; font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase;
  padding: .68rem 1rem; border: 1px solid var(--hairline);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.dark .pill span { border-color: var(--hairline-d); }
.pill input:hover + span { border-color: var(--ink); }
.dark .pill input:hover + span { border-color: var(--on-dark); }
.pill input:checked + span { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.dark .pill input:checked + span { background: var(--on-dark); color: var(--dark); border-color: var(--on-dark); }
.pill input:focus-visible + span { outline: 1.5px solid currentColor; outline-offset: 2px; }

.consent { display: flex; gap: .8rem; align-items: flex-start; }
.consent input[type="checkbox"] {
  appearance: none; width: 19px; height: 19px; flex: none; margin-top: .16rem;
  border: 1px solid var(--muted); background: transparent; cursor: pointer; border-radius: 0;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.consent input[type="checkbox"]:checked { background: var(--ink); border-color: var(--ink); }
.dark .consent input[type="checkbox"]:checked { background: var(--on-dark); border-color: var(--on-dark); }
.consent input[type="checkbox"]:checked::after { content: ""; display: block; width: 5px; height: 9px; border: solid var(--paper); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg) translate(3px, 1px); }
.dark .consent input[type="checkbox"]:checked::after { border-color: var(--dark); }
.consent label { font-size: var(--t-sm); line-height: 1.5; color: var(--ink-2); }
.dark .consent label { color: var(--on-dark-mut); }
.consent a { text-decoration: underline; text-underline-offset: 2px; }

/* Inline newsletter */
.nl { display: flex; gap: 0; border-bottom: 1px solid currentColor; max-width: 520px; align-items: center; }
.nl input { flex: 1; background: transparent; border: 0; padding: .9rem 0; font-size: 1.05rem; }
.nl button { background: none; border: 0; cursor: pointer; padding: .9rem 0 .9rem 1rem; font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em; text-transform: uppercase; }
.nl button:hover { opacity: .6; }
.form__note { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.form__ok { padding: 1rem 1.1rem; border: 1px solid currentColor; font-family: var(--mono); font-size: var(--t-sm); letter-spacing: .04em; }

/* Intro pair: a display heading beside a body paragraph. Top-aligned, with the
   paragraph nudged down so its first line sits on the heading's x-height rather
   than floating above it. Bottom-aligning these two reads as a mistake. */
.g2--intro { align-items: start; }
.g2--intro > p { padding-top: .45rem; }
@media (max-width: 760px) { .g2--intro > p { padding-top: 0; } }

/* --- 14b. STUDIO SUPPORT — plans, badges, availability ---------------- */
/* Values come from assets/js/studio-support.js. Edit that, not this. */
.badge {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em;
  text-transform: uppercase;
  padding: .45rem .8rem;
  background: var(--hl); color: var(--ink);
  border: 1px solid var(--ink);
}
.badge::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ink); flex: none;
}
.badge--quiet { background: transparent; border-color: var(--hairline); color: var(--muted); }
.badge--quiet::before { background: currentColor; }
.badge[data-state="waitlist"] { background: #F2C33D; }
.badge[data-state="closed"] { background: transparent; color: var(--muted); border-color: var(--hairline); }
.dark .badge--quiet { border-color: var(--hairline-d); color: var(--on-dark-mut); }

/* A link whose destination isn't set yet shouldn't pretend to work */
.is-placeholder {
  opacity: .45; pointer-events: none; text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.plan {
  display: flex; flex-direction: column; gap: 1.2rem;
  padding: clamp(1.5rem, 2.8vw, 2.4rem);
  border: 1px solid var(--hairline);
  height: 100%;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.plan:hover { border-color: var(--ink); }
.plan--priority { background: var(--paper-2); border-color: var(--ink); }
.dark .plan { border-color: var(--hairline-d); }
.plan__name { font-size: clamp(1.3rem, 2.1vw, 1.8rem); font-weight: 600; letter-spacing: -.03em; line-height: 1.05; }
.plan__note { color: var(--muted); font-size: .95rem; line-height: 1.5; margin-top: .5rem; max-width: 40ch; }
.plan__price {
  font-size: clamp(1.9rem, 3.4vw, 2.9rem); font-weight: 600;
  letter-spacing: -.04em; line-height: 1;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem;
  padding-bottom: 1.2rem; border-bottom: 1px solid var(--hairline);
}
.dark .plan__price { border-color: var(--hairline-d); }
.plan__per {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); font-weight: 400;
}
.plan__list { font-size: .95rem; }
.plan__list li { margin-bottom: .55rem; }
.plan__foot {
  margin-top: auto; padding-top: 1.2rem;
  border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: space-between;
}
.dark .plan__foot { border-color: var(--hairline-d); }

@media (max-width: 760px) {
  .plan__foot { flex-direction: column; align-items: stretch; }
  .plan__foot .alink { align-self: flex-start; }
}

/* --- 14c. NAV DROPDOWN ------------------------------------------------ */
/* CSS-only: hover and focus-within both open it, so keyboard users get the
   same behaviour without any JS or an ARIA menu pattern to get wrong. */
.nav__group { position: relative; display: flex; align-items: center; }
.nav__panel {
  position: absolute; top: 100%; left: -1rem; z-index: 20;
  min-width: 15rem;
  background: var(--paper);
  border: 1px solid var(--hairline);
  padding: .5rem;
  display: grid; gap: .1rem;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), visibility var(--dur-fast);
  box-shadow: 0 12px 32px rgba(17,17,16,.07);
}
.nav__group:hover .nav__panel,
.nav__group:focus-within .nav__panel {
  opacity: 1; visibility: visible; transform: none;
}
.nav__panel a {
  padding: .62rem .7rem;
  text-transform: none; letter-spacing: -.01em;
  font-family: var(--sans); font-size: .92rem;
  transition: background var(--dur-fast) var(--ease);
}
.nav__panel a::after { display: none; }
.nav__panel a:hover { background: var(--paper-2); }
.nav__panel small {
  display: block; font-family: var(--mono); font-size: .62rem;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
  margin-top: .2rem;
}
/* A hover bridge so the pointer can cross the gap without the panel closing */
.nav__group::after { content: ""; position: absolute; top: 100%; left: 0; right: 0; height: .9rem; }

/* --- 14d. FX RUNNER — scroll-driven effect sequence -------------------- */
/* A tall track with a pinned stage. Scroll position selects which of the five
   treatments is showing. Pinning is CSS position:sticky — remember that
   `overflow: hidden` on any ancestor silently breaks it; the body uses
   `overflow-x: clip` for exactly this reason. */
.fxtrack { position: relative; height: 340vh; }
.fxstage {
  position: sticky; top: 0;
  height: 100svh;
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--dark);
  color: var(--on-dark);
}
.fxstage__head {
  position: relative; z-index: 2;
  padding: clamp(5rem, 11vh, 7rem) var(--gut) 0;
  display: flex; flex-wrap: wrap; gap: .8rem var(--gut); align-items: baseline;
}
.fx { position: relative; flex: 1; min-height: 0; }
.fx canvas { display: block; width: 100%; height: 100%; }

/* Step index down the side of the frame */
.fxsteps {
  position: absolute; z-index: 2; left: var(--gut); bottom: clamp(1.4rem, 4vh, 2.4rem);
  display: grid; gap: .5rem;
}
.fxstep {
  display: flex; align-items: center; gap: .6rem;
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em;
  text-transform: uppercase; color: rgba(244,242,237,.42);
  transition: color .4s var(--ease);
}
.fxstep i {
  display: block; width: 14px; height: 1px; background: currentColor;
  transition: width .4s var(--ease);
}
.fxstep.is-on { color: var(--on-dark); }
.fxstep.is-on i { width: 34px; }

.fxnow {
  position: absolute; z-index: 2; right: var(--gut); bottom: clamp(1.4rem, 4vh, 2.4rem);
  font-family: var(--mono); font-size: clamp(1.1rem, 2.4vw, 1.9rem);
  letter-spacing: -.02em; text-align: right;
}

@media (max-width: 760px) {
  .fxtrack { height: 260vh; }
  .fxstage__head { padding-top: 5.2rem; }
  .fxsteps { gap: .35rem; }
  .fxnow { font-size: 1rem; }
}

/* No pin, no sequence — one static frame in the studio's own treatment */
@media (prefers-reduced-motion: reduce) {
  .fxtrack { height: auto; }
  .fxstage { position: static; height: auto; min-height: 78svh; }
  .fxsteps { display: none; }
}

/* --- 15. MARQUEE (clients) ------------------------------------------- */
.marq { overflow: hidden; position: relative; padding-block: 1.1rem; border-block: 1px solid var(--hairline); }
.dark .marq { border-color: var(--hairline-d); }
.marq__t { display: flex; gap: 3.5rem; width: max-content; animation: slide 34s linear infinite; }
.marq__t span { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .16em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.marq:hover .marq__t { animation-play-state: paused; }

/* --- 16. FOOTER ----------------------------------------------------- */
.ftr { background: var(--dark); color: var(--on-dark); padding-block: var(--sec-sm) 2rem; }
.ftr__cta { padding-bottom: clamp(3rem,7vh,5.5rem); }
.ftr__grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: var(--gut); padding-block: 2.6rem; border-top: 1px solid var(--hairline-d); }
.ftr .ftr-h { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em; text-transform: uppercase; color: var(--on-dark-mut); font-weight: 400; margin-bottom: 1.1rem; line-height: 1.4; }
.ftr li { margin-bottom: .55rem; }
.ftr li a { font-size: .95rem; letter-spacing: -.012em; transition: opacity var(--dur-fast); }
.ftr li a:hover { opacity: .55; }
.socials { display: flex; gap: .55rem; margin-top: 1.4rem; }
.socials a {
  width: 38px; height: 38px; border: 1px solid var(--hairline-d);
  display: grid; place-items: center; transition: background var(--dur-fast), color var(--dur-fast);
}
.socials a:hover { background: var(--on-dark); color: var(--dark); }
.socials svg { width: 15px; height: 15px; }
.ftr__base { display: flex; flex-wrap: wrap; gap: .6rem 1.6rem; padding-top: 1.8rem; border-top: 1px solid var(--hairline-d); }
.ftr__base p, .ftr__base a { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .1em; text-transform: uppercase; color: var(--on-dark-mut); }
.ftr__base a:hover { color: var(--on-dark); }

/* --- 17. MOTION / REVEALS -------------------------------------------
   EVERY hidden state is scoped to `.js`, which a one-line script in <head>
   adds before the first paint. Without that scope the initial state is
   `opacity: 0` / `clip-path: inset(0 0 100% 0)` — so if the JS never arrives,
   errors, or is simply slow, the page renders as a blank white sheet with
   nothing on it. A hero headline that is invisible until a script says
   otherwise is not a reveal, it is a single point of failure.

   With the scope: no JS, no animation, all content visible. */
.js [data-rev] { opacity: 0; transform: translateY(18px); }
.js [data-rev].is-in {
  opacity: 1; transform: none;
  transition: opacity .8s var(--ease-out), transform .8s var(--ease-out);
  transition-delay: var(--d, 0ms);
}
/* The mask reveal ends OUTSIDE the box: display headings run at line-height
   .9, so descenders on the last line hang below the border box, and an
   inset(0) left in place after the animation clipped them (the g in
   "building", the y in "everyday"). The end state lets .3em spill below
   and .15em above; the start state is still fully hidden. */
.js [data-rev="mask"] { opacity: 1; transform: none; clip-path: inset(-.15em 0 100% 0); }
.js [data-rev="mask"].is-in { clip-path: inset(-.15em 0 -.3em 0); transition: clip-path 1.05s var(--ease-out); transition-delay: var(--d, 0ms); }
/* The slow zoom-out applies to <video> as well as <img>. It was img-only, which
   was invisible while case studies were mostly photographs — on the Liminal
   page, 41 films and 15 stills, half the grid eased and half arrived flat.
   (This also cost me a measurement: an <img> held at scale(1.09) reports a
   bounding rect 9% larger than its computed width, which reads as a broken
   grid row until you remember the transform is mid-flight.) */
.js [data-rev="img"] .pcard__media img,
.js [data-rev="img"] > img,
.js [data-rev="img"] > video { transform: scale(1.09); }
.js [data-rev="img"].is-in .pcard__media img,
.js [data-rev="img"].is-in > img,
.js [data-rev="img"].is-in > video { transform: scale(1); transition: transform 1.3s var(--ease-out); }

/* Page-load and page-exit transition veil */
.veil {
  position: fixed; inset: 0; z-index: 200; background: var(--dark);
  pointer-events: none; transform-origin: top;
  animation: veilOut .75s var(--ease-out) forwards;
}
@keyframes veilOut { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 100% 0); } }
.veil.is-leaving { animation: veilIn .4s var(--ease) forwards; }
@keyframes veilIn { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }

/* Reduced motion — honour the OS setting completely */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  [data-rev], [data-rev="mask"] { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  [data-rev="img"] .pcard__media img,
  [data-rev="img"] > img,
  [data-rev="img"] > video { transform: none !important; }
  .veil { display: none; }
  .marq__t { animation: none; }
  .scanning__dot { animation: none; }
}

/* --- 18. RESPONSIVE ------------------------------------------------- */
/* Tablet */
@media (max-width: 1080px) {
  .cap__l { grid-template-columns: 3rem minmax(0,1fr); }
  .cap__r { grid-template-columns: minmax(0,1fr); }
  .cap--fold .cap__r { grid-template-columns: minmax(0,1fr) 1.2rem; }
  .cap__t { display: none; }
  .factsheet { grid-template-columns: repeat(3, 1fr); }
  .g4 { grid-template-columns: repeat(2, 1fr); }
  .g5 { grid-template-columns: repeat(3, 1fr); }
  .ftr__grid { grid-template-columns: 1fr 1fr; gap: 2.4rem var(--gut); }
  .cs-split { grid-template-columns: 1fr; gap: 2rem; }
  .cs-split__aside { position: static; }
}

/* Mobile — hierarchy is rebuilt, not stacked */
@media (max-width: 760px) {
  :root { --t-meta: .68rem; --gut: 1.15rem; }
  .nav { display: none; }
  .burger { display: block; }

  /* Hero: statement leads, everything else becomes a scannable list */
  .hero { padding-top: 6.5rem; }
  .hero__stmt { max-width: 100%; }
  .hero__row { flex-direction: column; align-items: stretch; gap: 1.8rem; }
  .lede { max-width: 100%; font-size: 1.1rem; }

  .g2, .g3, .g4 { grid-template-columns: 1fr; gap: 2.2rem; }
  .g5 { grid-template-columns: repeat(2, 1fr); gap: 1rem; }

  /* Capabilities become a definition list, not a broken 4-col grid */
  .cap { grid-template-columns: 1fr; gap: .7rem; padding-block: 1.5rem; }
  .cap__l, .cap__r { grid-template-columns: 1fr; gap: .5rem; }
  .cap--fold .cap__r { grid-template-columns: minmax(0,1fr) 1.2rem; }
  .cap--fold .cap__fold { grid-column: 1; }
  .cap__n { padding-top: 0; }

  /* Facts: two per row keeps them legible */
  .factsheet { grid-template-columns: 1fr 1fr; }

  /* Signals lose the fixed date column */

  /* First card in a featured grid stays large; the rest go 2-up */
  .m-2up { grid-template-columns: 1fr 1fr !important; gap: 1rem !important; }
  .m-2up .pcard__title { font-size: 1.02rem; }
  .m-2up .pcard__meta { font-size: .62rem; }

  .eyebrow { margin-bottom: 1.6rem; padding-bottom: 1rem; }
  .btn { padding: 1rem 1.3rem; width: 100%; justify-content: space-between; }
  .btn-row { display: grid; gap: .6rem; }
  .ftr__grid { grid-template-columns: 1fr; }
  .bleed { margin-left: 0; transform: none; width: 100%; }
  .cs-hero__media { aspect-ratio: 3/4; }
  .stat__n { font-size: 2.4rem; }
  .nl { max-width: 100%; }
  .prose p { max-width: 100%; }
}

@media (max-width: 420px) {
  .m-2up { grid-template-columns: 1fr !important; }
}

/* --- 19. PRINT ------------------------------------------------------ */
@media print {
  .hdr, .ftr, .veil, .mnav { display: none; }
  body { background: #fff; color: #000; }
}

/* --- SUB-BLOCK inside a section -------------------------------------------
   Used where a page absorbs what used to be its own page (Studio Support into
   Services). It reads as a distinct movement without opening a new <section>,
   which would break the page's heading outline. */
.ssub {
  margin-top: clamp(2.6rem, 6vh, 4.5rem);
  padding-top: clamp(2rem, 4vh, 3rem);
  border-top: 1px solid rgb(255 255 255 / .14);
}
.sec:not(.dark) .ssub { border-top-color: var(--hairline); }

/* --- FLAT ARCHIVE ENTRY ---------------------------------------------------
   Not every piece of work earns a case study. A flat entry is a <div>, not an
   <a> — it carries the image, title, role and year and goes nowhere, so the
   archive can show real range without inventing a narrative for each item.
   It must not look disabled or broken, so it keeps the card's full presence
   and only drops the hover affordances a link would have. */
.pcard--flat { cursor: default; }
.pcard--flat .pcard__media img { transition: none; }
.pcard--flat:hover .pcard__media img { transform: none; }
.pcard__flat {
  align-self: center;
  color: var(--muted);
  white-space: nowrap;
}

/* --- FOOTER CONTACT -------------------------------------------------------
   An address and one button, no closing statement. The address is the largest
   thing here because it is the actual instruction — a sentence above it was
   asking for the same action twice in a row. */
.ftr__mail {
  display: inline-block;
  font-size: clamp(1.5rem, 4vw, 3rem);
  letter-spacing: -.035em;
  line-height: 1.05;
  color: var(--on-dark);
  border-bottom: 1px solid rgb(255 255 255 / .28);
  padding-bottom: .1em;
  transition: border-color var(--dur-fast) var(--ease);
}
.ftr__mail:hover { border-bottom-color: var(--on-dark); }

/* --- NAV: DIM THE SIBLINGS ------------------------------------------------
   The reference dims inactive items to 0.30 opacity. That is 1.99:1 against
   white — well under the 4.5:1 small text owes — so the effect is copied but
   the value is not: 0.60 is the lowest opacity that still passes, measured.
   The hover lift is what carries the interaction either way. */
.nav a { opacity: .6; }
.nav a:hover,
.nav a:focus-visible,
.nav a[aria-current="page"] { opacity: 1; }
/* Hovering anywhere in the group pushes the rest back further — but only to
   the same compliant floor, never below it. */
.nav:hover a:not(:hover):not([aria-current="page"]) { opacity: .6; }
.nav a { transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); }

/* --- NAV DROPDOWN, matched to the pills ----------------------------------- */
.nav__panel {
  /* .72 looked right over the white page and measured 3.19:1 over the black
     hero film — the panel floats over BOTH, so the weaker one is the real
     number. .92 keeps the frost visible and holds 5.19:1 everywhere. */
  background: rgb(255 255 255 / .92);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  border: 1px solid rgb(17 17 16 / .08);
  border-radius: 5px;
  box-shadow: 0 14px 40px rgb(17 17 16 / .10);
  transform: translateY(-6px) scale(.985);
  transform-origin: top center;
  transition: opacity .22s var(--ease-out), transform .28s var(--ease-out), visibility .22s;
}
.nav__group:hover .nav__panel,
.nav__group:focus-within .nav__panel { transform: none; }
.nav__panel a { opacity: 1; border-radius: 4px; }
.nav__panel a:hover { background: rgb(17 17 16 / .06); }
@media (prefers-reduced-motion: reduce) {
  .nav__panel { transition: opacity .01ms, visibility .01ms; transform: none; }
}

/* --- NAV: ONE SLIDING CHIP ------------------------------------------------
   Why this instead of a per-link background: fading a chip in on the hovered
   item and out on the last one is two opacity transitions that overlap, and it
   reads as a flicker between neighbours no matter how you time it. ONE element
   that travels between items is continuous — the eye follows a single moving
   object, which is where the reference's smoothness comes from.

   It is `aria-hidden` and behind the links; JS only ever writes transform and
   width to it. */
.nav { position: relative; }
.nav__chip {
  position: absolute;
  left: 0; top: 50%;
  height: calc(100% - .5rem);
  border-radius: 4px;
  background: rgb(255 255 255 / .78);
  box-shadow: 0 1px 2px rgb(17 17 16 / .05);
  opacity: 0;
  pointer-events: none;
  transform: translate3d(0, -50%, 0);
  /* Transform and width are cheap to animate; the opacity is separate so the
     chip can fade in where it lands rather than flying in from the last item. */
  transition: transform .34s var(--ease-out), width .34s var(--ease-out), opacity .2s var(--ease);
  will-change: transform, width;
}
.nav__chip.is-on { opacity: 1; }
.nav > a, .nav__group { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) {
  .nav__chip { transition: opacity .2s var(--ease); }
}


/* --- HEADER AS SEPARATE BLOCKS -------------------------------------------
   The reference header is not a bar — it is discrete rounded blocks with air
   between them: identity, navigation, and (inside a project only) where you
   are. Matching it is mostly about giving the wordmark the same block
   treatment the nav already had, and making every block the same height so
   they read as one family rather than three unrelated shapes.

   Kept from the reference: 4px radius, ~6px gaps, translucent fill, and the
   1px seam that splits a block into panes.
   Not kept: collapsing the items behind a "Menu" chip. Dani's nav carries Shop
   and Contact, which people go looking for — hiding them behind a hover costs
   more than the theatre is worth. */
:root { --block-h: 44px; }

.hdr__in { gap: 6px; }

/* The wordmark keeps the block's height so it lines up with the nav, but no
   fill — Dani's call. It sits alone on the left with nothing to sit against,
   so the frame was doing work that wasn't needed. */
.logo {
  min-height: var(--block-h);
  justify-content: center;
}

.nav { min-height: var(--block-h); border-radius: 4px; }

/* --- The project block ---------------------------------------------------- */
.crumb {
  position: relative;                /* the project list hangs off this */
  margin-left: auto;                 /* it, not the nav, takes the free space */
  min-height: var(--block-h);
}
.crumb__link {
  display: flex; align-items: stretch;
  gap: 1px;                          /* the seam: the page shows through it */
  min-height: var(--block-h);
  border-radius: 4px;
  overflow: hidden;
}
.hdr__in .crumb + .nav { margin-left: 0; }

.crumb__name, .crumb__back {
  display: flex; align-items: center;
  background: rgb(17 17 16 / .05);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.crumb__name {
  padding: 0 .8rem;
  border-radius: 4px 0 0 4px;
  font-family: var(--sans); font-size: .92rem; letter-spacing: -.01em;
  color: var(--ink);
  white-space: nowrap;
}
.crumb__back {
  padding: 0 .7rem;
  border-radius: 0 4px 4px 0;
  font-size: 1rem; line-height: 1;
  color: var(--ink-2);
}
.crumb__link:hover .crumb__name, .crumb__link:hover .crumb__back { background: rgb(17 17 16 / .09); }
.crumb__link:hover .crumb__back { color: var(--ink); }
.crumb__link::after { display: none; }   /* the global link underline */

@media (max-width: 860px) {
  /* The burger owns the header at this width; two blocks plus a crumb is a
     queue, not a header. */
  .crumb__name { max-width: 8rem; overflow: hidden; text-overflow: ellipsis; }
}
@media (max-width: 620px) { .crumb { display: none; } }

/* --- CHIP OWNS THE BACKGROUND, NOTHING ELSE ------------------------------
   `.nav a:hover` and `.nav a[aria-current]` each painted their own background
   before the sliding chip existed. Left in place they fight it: the chip glides
   to an item that is already lit, so you see a block appear instantly and the
   chip catch up a beat later. That lag is what still read as "less smooth".
   One object, one background. */
.nav a:hover, .nav a:focus-visible { background: none; }
.nav a[aria-current="page"], .nav a[aria-current="true"] { background: none; }

/* A section page (aria-current="true", e.g. a project under Work) must read as
   current too — previously only ="page" did, so Work sat greyed out on its own
   project pages while the chip rested underneath it. */
.nav a[aria-current] { color: var(--ink); opacity: 1; }
.nav:hover a:not(:hover):not([aria-current]) { opacity: .6; }

/* --- THE PROJECT LIST ------------------------------------------------------
   A column of names that floats — no container, no card, no panel. Each name is
   its own small frosted block, and the artwork opens to the RIGHT of whichever
   one the cursor is on, aligned to it.

   The preview is anchored to its own pill on both axes: top edge to top edge,
   and immediately to the right of that pill rather than at a fixed column. So it
   tracks the name it belongs to instead of floating in a reserved lane, and a
   long name pushes its own preview across without pushing anyone else's. */
.crumb__panel {
  position: absolute;
  top: calc(100% + 6px); left: 0;
  z-index: 20;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 3px;
  /* Both the pill and the preview are sized from these, so the preview's
     vertical centring is arithmetic rather than a magic number that drifts the
     moment the type changes. */
  --pill-h: 1.9rem;
  --pill-bd: 1px;
  --shot-h: 6.75rem;
  opacity: 0; visibility: hidden;
  transition: opacity .01s linear, visibility .01s linear;
}
.crumb:hover .crumb__panel,
.crumb:focus-within .crumb__panel { opacity: 1; visibility: visible; }

/* A transparent bridge so the pointer can cross the 6px gap without the panel
   closing under it. */
.crumb__panel::before {
  content: ""; position: absolute;
  left: 0; right: 0; top: -8px; height: 8px;
}

/* --- the names ------------------------------------------------------------ */
.wname {
  position: relative;            /* the preview anchors to this */
  display: flex; align-items: center;
  height: var(--pill-h);
  max-width: 15rem;
  padding: 0 .62rem;
  border-radius: 4px;
  /* Light, not dark. The column hangs over whatever the page is showing, and on
     a project page that is a black hero. A dark translucent pill measured
     3.05:1 there; a light one holds 7.1:1 against both the white page above and
     the black image below, because the text stays dark either way. */
  background: rgb(252 252 251 / .78);
  /* A hairline, because the fill has to stay light enough to read over the black
     hero — and against the white page a light fill alone is invisible. The edge
     is what makes it a block on white; the fill is what makes it legible on
     black. Neither does both on its own. */
  border: var(--pill-bd) solid rgb(17 17 16 / .07);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  font-family: var(--sans); font-size: .95rem; line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--ink-2);
  white-space: nowrap;
  /* Each name drops in a beat after the one above it. --i comes from the markup. */
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity .22s var(--ease-out), transform .32s var(--ease-out),
              background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  transition-delay: 0s;                     /* closing: all leave together */
}
.crumb:hover .wname, .crumb:focus-within .wname {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--i) * 38ms);  /* opening: one after another */
}
.wname:hover, .wname:focus-visible {
  background: #fff;
  border-color: rgb(17 17 16 / .12);
  color: var(--ink);
}
.wname::after { display: none; }            /* the global link underline */

/* The project you are already reading stays in the list but stops pretending
   to be a destination. */
.wname.is-here { background: var(--ink); border-color: var(--ink); color: #fff; pointer-events: none; }

/* --- the preview ---------------------------------------------------------- */
.wname__shot {
  position: absolute;
  /* Offsets resolve against the pill's PADDING box, which sits inside its 1px
     border — so a plain `top: 0` sat 1px low and the gap came out 7px, not 8.
     Both are corrected by the border width rather than by eyeballed numbers. */
  left: calc(100% + 8px + var(--pill-bd));   /* just past its own pill */
  top: calc(var(--pill-bd) * -1);            /* top edge to top edge */
  width: 12rem; height: var(--shot-h);           /* 16:9 */
  border-radius: 4px;
  overflow: hidden;
  background: var(--paper-2);
  box-shadow: 0 12px 32px rgb(17 17 16 / .16);
  opacity: 0;
  transform: scale(.97);
  transform-origin: left center;
  transition: opacity .22s var(--ease-out), transform .3s var(--ease-out);
  pointer-events: none;          /* it is a preview, not a target */
}
.wname:hover .wname__shot, .wname:focus-visible .wname__shot {
  opacity: 1; transform: none;
}
.wname__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (max-width: 1040px) { .wname__shot { display: none; } }  /* no room beside it */
@media (max-width: 620px)  { .crumb { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .wname { transition: opacity .2s linear, background .2s linear, color .2s linear; transform: none; }
  .crumb:hover .wname, .crumb:focus-within .wname { transition-delay: 0s; }
  .wname__shot { transition: opacity .2s linear; transform: none; }
}

/* --- FOOTER: THE CLOSING IMAGE -------------------------------------------
   Replaces the flat dark slab. A vertical gradient from the page's ink down
   into the studio yellow, with a looping flower sitting in the warm half.

   The palette is derived rather than borrowed. The reference runs near-black
   into terracotta, which is that studio's brand; running ink into #F7FFA2 —
   the one accent this site already uses — gets the same warming-as-you-descend
   move without importing someone else's identity.

   Structural bonus: the top staying dark means every existing footer colour
   (light text on ink) still holds where it already sits. Only the base row
   crosses into the yellow, and it flips to dark type there. */
.ftr {
  position: relative;
  isolation: isolate;                 /* keep the film behind the content */
  /* The warm half sits BELOW the content, not through it. First attempt spread
     the gradient evenly and the olive midtones rose under the footer's muted
     greys — measured 2.67:1 on the studio blurb, 2.94 on the availability line,
     3.58 on the column headings, all of which were designed against flat ink.
     Holding ink to the 52% mark keeps every existing colour valid and puts the
     warming where the flower actually is. */
  background: linear-gradient(
    180deg,
    var(--dark)  0%,
    #16170F     52%,
    #4A5327     70%,
    #B9C95A     87%,
    #F7FFA2    100%);
  --bloom-rise: -16vh;
  min-height: 100vh;
  display: flex; flex-direction: column;
  padding-block: var(--sec-sm) 2rem;
}
.ftr > .wrap { position: relative; z-index: 1; }
.ftr__base { margin-top: auto; }   /* rides the bottom of the screen again */

.ftr__bloom {
  /* Full height, behind everything — Dani's call, and it is the right one for
     the image: the flower is the closing statement, not a motif in a margin.
     Legibility is handled where it belongs, on the type (see .ftr__base and the
     scrim below), rather than by shrinking the artwork. */
  position: absolute; inset: 0;
  /* NO z-index here. Setting one made this a stacking context, and
     mix-blend-mode only blends within its own stacking context — so the film
     multiplied against this transparent layer instead of the footer's gradient
     and rendered as a white card over the link columns. `isolation: isolate`
     on .ftr is what confines the blend; the paint order is handled by .wrap
     carrying z-index 1 above this. */
  overflow: hidden;
  pointer-events: none;
  /* Anchored to the TOP, not the bottom. The flower is taller than the footer
     (760 x 1403 in an ~830px box), so alignment decides which part you see —
     bottom-anchored put the head low with the stem owning the visible area.
     --bloom-rise then lifts it so the head reads against the email line. */
  display: grid; place-items: start center;
}
.ftr__film {
  /* Frames are now the sequence's NATIVE 566px — the flower's true pixel width
     in the source, so there is no more detail to be had from these assets. The
     earlier 440px/quality-45 encode was the low-res look: 3.5x upscale on a
     retina screen, with WebP mush on top of it. Native at quality 78 costs
     1.65 MB against 1.26 MB — a 1.29x resolution gain and a far cleaner encode
     for 0.4 MB, which measured much better than the frame count did. */
  width: min(62vw, 760px);
  height: auto;
  aspect-ratio: 566 / 1044;
  display: block;
  margin-top: var(--bloom-rise);
  /* Real alpha, so it composites directly. No blend mode: `multiply` eats the
     chrome highlights and `screen` washes out against the yellow. A chrome
     object over a light background needs alpha; no blend trick substitutes. */
}

/* The base row is the only content that crosses onto the yellow. */
.ftr__base, .ftr__base a, .ftr__base p { color: var(--dark); }
.ftr__base a:hover { opacity: .6; }

@media (max-width: 860px) {
  .ftr { min-height: 0; --bloom-rise: -6vh; }   /* a screen-tall footer on a phone is a wall */
  .ftr__film { width: 88vw; }
}
/* Reduced motion is handled in bloom-scroll.js: the flower is still drawn, it
   simply stops following the scroll. The reader loses the movement, not the
   picture. */

/* `.meta` defaults to --muted, which is tuned for the white page. In the footer
   it sat on ink at 3.04:1 — and it did so long before the gradient arrived; the
   flat dark footer measured 3.01:1. Found by measuring the new background, but
   it was always broken. --on-dark-mut is the token that belongs on dark. */
.ftr .meta { color: var(--on-dark-mut); }
.ftr__base .meta { color: var(--dark); }

/* A `.dark .ph` variant already existed; the footer is `.ftr`, not `.dark`, so
   it never picked it up and the placeholder chips sat at 2.54:1 on ink. */
.ftr .ph { background: rgba(169,184,255,.13); color: #B9C4FF; border-color: rgba(169,184,255,.5); }


:root {
  --dark-rgb: 14 14 12;          /* #0E0E0C — components, so ramps can vary alpha */
  --paper-2-rgb: 243 243 244;    /* #F3F3F4 */
}

/* --- FOOTER SURFACE -------------------------------------------------------
   Two layers. The top one softens the seam where the footer meets the page;
   the bottom one is the long warm ramp into the studio yellow.

   THE BUG THIS FIXES: the ramp layer was written as a plain 2-stop gradient,
   and a gradient holds its LAST colour all the way to 100% — so the ink layer
   painted the entire footer and buried the yellow completely. A background
   layer has to be given a size, or it fills the box. */
.ftr {
  padding-block: var(--sec) 2rem;
  background:
    linear-gradient(180deg,
      rgb(var(--dark-rgb) / 0)    0,
      rgb(var(--dark-rgb) / .216) 30%,
      rgb(var(--dark-rgb) / .648) 60%,
      rgb(var(--dark-rgb) / .972) 90%,
      rgb(var(--dark-rgb) / 1)    100%)
      0 0 / 100% calc(var(--edge) * .8) no-repeat,
    linear-gradient(180deg,
      var(--dark)  0%,
      #16170F     52%,
      #4A5327     70%,
      #B9C95A     87%,
      #F7FFA2    100%);
}

/* --- KEEPING TYPE READABLE OVER THE FLOWER -------------------------------
   With the bloom full-height and behind the content, footer text crosses bright
   chrome. Measured by sampling the canvas pixels behind each line, the worst
   cases were 1.02:1 — invisible.

   The fix is on the TYPE, not the artwork: a wide, soft shadow in the surface's
   own colour. It reads as nothing — there is no visible edge, box or panel, so
   the "no colour blocks" rule still holds — but it gives every glyph a local
   backdrop of the right polarity, which is exactly what contrast needs. Cheaper
   and far less intrusive than a scrim, and it does not shrink the flower. */
.ftr__grid,
.ftr__cta { text-shadow: 0 0 10px rgb(var(--dark-rgb) / .92), 0 0 26px rgb(var(--dark-rgb) / .78); }
.ftr__base { text-shadow: 0 0 10px rgb(247 255 162 / .95), 0 0 24px rgb(247 255 162 / .85); }
.socials a { backdrop-filter: blur(2px); }

/* The old "go opaque once stuck" rules lived here. They are gone: the surface
   flip in site.js handles the bright-chrome case and every other case properly,
   by knowing what is behind the bar rather than assuming that scrolling implies
   danger. Worth noting why they had to go rather than just be overridden —
   `.hdr.is-stuck .nav a` is THREE classes and quietly outranked
   `.hdr--dark .nav a`, so the dark state applied its background but kept ink
   type. The bar went dark and the text stayed dark with it. */

/* --- NEWSLETTER + FOOTER AS ONE CLOSING SURFACE ---------------------------
   Dani: "unite the newsletter section to the footer so it's continuous and the
   flower's edge is fully visible."

   Two problems solved by the same move. The flower was clipped at the top
   because the bloom box could not extend past the footer, and the newsletter
   sat on its own grey slab directly above — the exact hard boundary the whole
   soft-edge pass was meant to remove.

   So the newsletter now IS the top of the closing surface: same ink, no
   background of its own, no bottom fade, butted straight into the footer. The
   two read as one block, and the bloom is free to rise into it. */
.closing {
  position: relative;
  background: linear-gradient(180deg,
    rgb(var(--dark-rgb) / 0)    0,
    rgb(var(--dark-rgb) / .216) calc(var(--edge) * .3),
    rgb(var(--dark-rgb) / .648) calc(var(--edge) * .6),
    rgb(var(--dark-rgb) / .972) calc(var(--edge) * .9),
    rgb(var(--dark-rgb) / 1)    var(--edge));   /* fade in at the top, then hold */
  color: var(--on-dark);
  padding-bottom: 0;                            /* it runs straight into .ftr */
}
.closing .meta { color: var(--on-dark-mut); }
.closing .meta--ink { color: var(--on-dark); }
.closing .u-mut { color: var(--on-dark-mut); }
.closing .ph { background: rgba(169,184,255,.13); color: #B9C4FF; border-color: rgba(169,184,255,.5); }
.closing label { color: var(--on-dark-mut); }
.closing input, .closing select, .closing textarea {
  background: rgb(255 255 255 / .06);
  border-color: var(--hairline-d);
  color: var(--on-dark);
}
.closing input::placeholder { color: rgb(255 255 255 / .38); }
.closing .btn { background: var(--on-dark); color: var(--dark); border-color: var(--on-dark); }

/* On the homepage the newsletter runs straight into the footer, so the footer
   must NOT draw its own opening fade — a second ramp would redraw the very line
   we are removing. Everywhere else the footer still meets an ordinary section
   and needs it, so this is scoped to the page that has a .closing surface
   rather than applied globally. Ten of eleven pages end on something else. */
body.has-closing .ftr {
  background: linear-gradient(180deg,
    var(--dark)  0%,
    #16170F     52%,
    #4A5327     70%,
    #B9C95A     87%,
    #F7FFA2    100%);
  padding-top: var(--sec-sm);
}
/* The bloom only reaches up into a .closing surface; on other pages it stays
   inside the footer, where there is nothing above it to rise into. */
body.has-closing .ftr__bloom { inset: calc(-1 * var(--bloom-reach)) 0 0 0; }

/* The bloom starts above the footer, inside the newsletter, so the top of the
   flower is in frame instead of being clipped by the footer's edge. */
.ftr { --bloom-reach: 46vh; }
.ftr__film { margin-top: 0; }

/* Newsletter type crosses the flower now, so it takes the same soft shadow the
   rest of the closing surface uses. */
.closing .wrap { position: relative; z-index: 1; }
.closing .d-xl, .closing .body-lg, .closing .shead, .closing .form {
  text-shadow: 0 0 10px rgb(var(--dark-rgb) / .92), 0 0 26px rgb(var(--dark-rgb) / .78);
}

/* --edge survives the removal of the section fades because the closing run
   still uses it: the footer's opening ramp and the sky's bottom padding are both
   measured in it. Deleting the declaration while eight `calc(var(--edge) ...)`
   references remained made every one of those declarations INVALID — and an
   invalid value drops the whole declaration, which is why the footer lost its
   background entirely and rendered white on every page that does not have the
   .has-closing override. A missing custom property fails silently and at a
   distance; grep for remaining uses before removing one. */
:root { --edge: 9rem; }

/* --- SECTION EDGES: SHARP AGAIN ------------------------------------------
   The soft fades are gone at Dani's request, and one of their side effects
   shows why they had to be: the ramp overhung each tinted section by --edge
   (144px) at BOTH ends, and Partners' own top padding is only 91px — so the
   dark section above reached past that padding and put "03 Partners" on black.
   A fade long enough to read as soft was necessarily long enough to invade its
   neighbour's content.

   Soft transitions are kept ONLY where the page is meant to read as one
   continuous surface: the sky, the newsletter, the footer. Everywhere else a
   section has an edge again. */
.sec.dark { background: var(--dark); }
.sec.paper-2 { background: var(--paper-2); }


/* The closing surface no longer fades in at all: the sky hands it solid ink at
   exactly its top edge, so a fade here would be a second curve laid over the
   first — which is precisely the join Dani could see. One gradient, one curve. */
.closing { background: var(--dark); }

/* --- THE LAYER ORDER OF THE CLOSING RUN ----------------------------------
   Four things overlap here and every one of them has to land in the right
   order. Written out because getting it wrong produced two different bugs in
   two consecutive attempts:

       sky background   (#trends, not positioned)
       ink fade         (.closing::before, overhangs --edge upward)
       the flower       (.ftr__bloom, overhangs --bloom-reach upward)
       newsletter type  (.closing .wrap)

   `z-index: -1` on the fade put it BEHIND the sky — a negative-z child sits
   behind any ancestor background in the same stacking context — so the ink
   began abruptly at the newsletter's top edge.

   `z-index: 1` on .closing fixed that but lifted the whole newsletter above
   .ftr, so the newsletter's background covered the flower.

   The answer needs neither. The fade is a POSITIONED element, so it already
   paints above the sky's background without any z-index at all; .ftr follows in
   document order and so paints above the fade; and `.closing .wrap` carries
   z-index 1, which is what keeps the type above the flower. Document order does
   almost all of the work — the only explicit z-index needed is the one on the
   text. */


/* --- THE SKY -------------------------------------------------------------
   One continuous fall from the white page to the newsletter's ink, in a single
   gradient. The earlier version was two overlapping curves — a sky on the
   section and an ink fade overhanging up from the newsletter — and no amount of
   easing hides the place where two different curves cross. This replaces both.

   It is built as two background LAYERS that meet on an identical colour, which
   is not the same thing as two curves crossing: the base runs white -> #BCC9DE
   and then holds, and the plunge layer starts at exactly #BCC9DE and is sized to
   the section's bottom padding. The seam is a colour match, so there is nothing
   to see at it.

   `z-index: -1` lets it overhang upward as far as it likes — a negative-z
   pseudo-element paints above the page canvas but BELOW every block's normal
   content, so the sky can begin half a screen above the section without ever
   covering the text up there.

   The palette is the reference's sky, desaturated: on a site that is otherwise
   achromatic plus one yellow, this has to read as atmosphere. */
.sky {
  position: relative;
  background: none;
  /* Keeps the section's own type clear of the plunge to ink at the bottom. */
  padding-bottom: calc(var(--sec) + var(--edge));
}
.sky::before {
  content: ""; position: absolute; z-index: -1;
  inset: calc(-1 * var(--sky-rise)) 0 0 0;
  pointer-events: none;
  background:
    /* the plunge: pale blue -> ink, occupying exactly the bottom padding */
    linear-gradient(180deg,
      #BCC9DE   0%,
      #9FB0C8  22%,
      #77879F  44%,
      #46505F  66%,
      #21252B  84%,
      #0E0E0C 100%)
      bottom / 100% calc(var(--sec) + var(--edge)) no-repeat,
    /* the sky: white -> that same pale blue, holding once it arrives */
    linear-gradient(180deg,
      #FFFFFF   0%,
      #FBFCFE  18%,
      #F2F6FB  34%,
      #E6EDF6  50%,
      #D8E3F0  64%,
      #C9D6E7  76%,
      #BCC9DE  calc(100% - var(--sec) - var(--edge)),
      #BCC9DE 100%);
}
:root { --sky-rise: 42vh; }
@media (max-width: 860px) { :root { --sky-rise: 18vh; } }


/* --- 1. THE TWO DROPDOWNS LINE UP ----------------------------------------
   The Services panel hung 1rem left of its trigger while the project list sits
   flush with the crumb's left edge, so the two menus in the same bar started on
   different verticals. Both are flush now. */
.nav__panel { left: 0; }

/* --- 3. THE PROJECT PREVIEW CLEARS EVERY PILL ----------------------------
   It was offset from the HOVERED pill (`left: calc(100% + 8px)`), so a short
   name like "tm:rw" put the image where a longer one — "Paninoteca" — already
   was, and the artwork sat behind the list.

   It is offset from a fixed column instead. Every pill shares the crumb's left
   edge, so one distance measured from any pill is the same x for all of them;
   --wname-max caps the pill width so nothing can ever reach past it. */
.crumb__panel { --wname-max: 8.5rem; }   /* 136px: the widest current name,
   "Paninoteca", is 95px, so this leaves headroom for a longer one without
   parking the preview miles away from the list. A name that exceeds it
   truncates rather than pushing the artwork off its column. */
.wname { max-width: var(--wname-max); }
/* The ellipsis goes on the LABEL, not the pill: `overflow: hidden` on .wname
   clips its own absolutely-positioned child, which is the preview — the image
   vanished entirely rather than truncating a name. */
.wname__label { overflow: hidden; text-overflow: ellipsis; }
.wname__shot { left: calc(var(--wname-max) + 10px); }

/* --- 4. ROOM INSIDE THE NEWSLETTER FIELDS --------------------------------
   These inputs are underline-only on the white pages, where `padding: .75rem 0`
   is right. On the closing surface they gained a filled background, and a fill
   with no horizontal padding puts the text hard against its own edge. */
.closing .field input,
.closing .field select,
.closing .field textarea {
  padding: .95rem 1.1rem;
  border-radius: 4px 4px 0 0;
}
.closing .field input::placeholder { opacity: .7; }

/* --- SERVICES PANEL: SAME GEOMETRY AS THE PROJECT LIST -------------------
   `top: 100%` measured the GROUP, and the group is vertically centred inside
   the nav pill — so the panel started partway up the pill and overlapped it.
   Stretching the group to the pill's full height makes 100% mean "the bottom of
   the bar", which is where the project list already hangs from, so the two
   menus now open from the same line with the same 6px gap. */
.nav__group { align-self: stretch; display: flex; align-items: center; }
.nav__panel { top: calc(100% + 6px); }
/* The group's own hover bridge is no longer needed — the group now reaches the
   pill's edge, so there is no gap to cross. */
.nav__group::after { display: none; }

/* --- NAV LINK RULES MUST NOT REACH INTO THE PANELS ------------------------
   `.nav a` also matches every link inside the Services dropdown and the project
   list, because both live inside the bar. The smoked-glass rule therefore
   painted the Services panel's titles WHITE — on a white panel. Everything that
   styles a nav ITEM is scoped to the direct children from here on. */
.nav__panel a { color: var(--ink); opacity: 1; }
.nav__panel small { color: var(--muted); }
body.has-closing .hdr:not(.is-stuck) .nav__panel a { color: var(--ink); opacity: 1; }
body.has-closing .hdr:not(.is-stuck) .nav__panel small { color: var(--muted); }
body.has-closing .hdr:not(.is-stuck) .nav__panel { background: rgb(255 255 255 / .92); }

/* --- NAV: THE REFERENCE'S GLASS ------------------------------------------
   Studio Nari's bar, measured off the page: rgba(208,208,208,.3) behind
   blur(16px), 4px radius, NO border and NO shadow, with black type. The grey
   tint is the part I had wrong — I kept reaching for white, and white glass on
   a white page is just a white pill. A grey tint is what makes the blur legible
   as glass over a light background.

   Their values on this site measure:

       over the white page          ink 16.71:1   light  1.01:1
       over the dark sections       ink  2.07:1   light  8.16:1
       over a bright chrome petal   ink 14.71:1   light  1.15:1

   So the glass can be exactly theirs — but the TYPE has to know what it is
   sitting on, which no amount of tuning the fill can solve. site.js adds
   `.hdr--dark` when the bar overlaps a dark surface, and that flips the tint
   and the type together. That is the whole trick, and it is why every previous
   attempt failed: I was tuning one variable when the problem had two. */
.nav {
  background: rgb(208 208 208 / .3);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 0;
  border-radius: 4px;
  box-shadow: none;
}
.nav a { color: var(--ink); opacity: .62; }   /* .62 is the floor: 5.08:1 dimmed */
.nav a:hover, .nav a:focus-visible, .nav a[aria-current] { color: var(--ink); opacity: 1; }
.nav:hover a:not(:hover):not([aria-current]) { opacity: .62; }
.nav__chip { background: rgb(255 255 255 / .55); box-shadow: none; }

.logo, .crumb__name, .crumb__back {
  background: rgb(208 208 208 / .5);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

/* Over ink: the same glass, inverted. */
.hdr--dark .nav { background: rgb(40 40 38 / .38); }
.hdr--dark .nav a { color: var(--on-dark); opacity: .68; }
.hdr--dark .nav a:hover, .hdr--dark .nav a[aria-current] { color: #fff; opacity: 1; }
.hdr--dark .nav__chip { background: rgb(255 255 255 / .16); }
.hdr--dark .logo { background: rgb(40 40 38 / .5); color: var(--on-dark); }
.hdr--dark .logo span { color: var(--on-dark-mut); }
.hdr--dark .crumb__name, .hdr--dark .crumb__back { background: rgb(40 40 38 / .5); color: var(--on-dark); }
.hdr--dark .burger i { background: var(--on-dark); }

/* --- SERVICES DROPDOWN = THE PROJECT LIST --------------------------------
   Same object, so it gets the same construction: no card, no border, no
   shadow — a floating column of frosted pills. */
.nav__panel {
  background: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  min-width: 0;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 3px;
}
.nav__panel a {
  display: block;
  padding: .42rem .62rem;
  border-radius: 4px;
  background: rgb(252 252 251 / .78);
  border: 1px solid rgb(17 17 16 / .07);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  font-family: var(--sans); font-size: .95rem; line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--ink-2); opacity: 1;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.nav__panel a:hover, .nav__panel a:focus-visible {
  background: #fff; border-color: rgb(17 17 16 / .12); color: var(--ink);
}
.nav__panel small {
  display: block; margin-top: .12rem;
  font-family: var(--mono); font-size: .58rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}

/* --- THE PREVIEW SITS IN FRONT OF THE NAMES ------------------------------
   It was pushed clear of the widest pill, which kept it out of the way but read
   as detached. In the reference it overlaps the column and sits ON TOP.

   `z-index: 3` is what puts it there. The pills are positioned with z-index
   auto, so they do not create stacking contexts — a child promoted to 3 is
   lifted into the shared context above ALL of them, not just its own pill.
   Without it the preview slips behind every name below its own. */
.wname__shot {
  /* Back beside its OWN name, 10px off the end of the hovered pill — which is
     what the reference does, and what I had originally. When Dani said the
     image was "behind the names" I read it as a positioning fault and pushed
     the preview clear of the widest pill; that fixed the symptom by moving the
     artwork away from the list, and made it read as detached. The fault was
     layering all along. `z-index: 3` is the actual fix: the pills are
     positioned with z-index auto and so create no stacking context, which
     means a child promoted to 3 is lifted above ALL of them rather than only
     its own. It can now overlap the longer names below and sit on top of them,
     exactly as in the reference. */
  left: calc(100% + 10px + var(--pill-bd));
  z-index: 3;
  box-shadow: 0 12px 40px rgb(17 17 16 / .28);
}

/* --- THE WORDMARK NEEDS AIR ---------------------------------------------- */
.logo {
  padding: 0 1.15rem;
  min-height: calc(var(--block-h) + 6px);
  /* Same 4px as the nav pill. The radius went away with the background when the
     wordmark was un-blocked, and did not come back when the glass did — so the
     one square corner in a bar of rounded ones. */
  border-radius: 4px;
}

/* Adding `backdrop-filter` to the pills silently made every one of them a
   STACKING CONTEXT, which trapped the preview inside its own pill — so the
   names below, being later siblings, painted over it again. `z-index: 3` on the
   child could not escape.

   Raising the hovered pill itself is what works: it carries its preview up with
   it, above every sibling. Worth remembering that backdrop-filter, transform,
   opacity < 1, filter and will-change all create stacking contexts — adding
   glass to something can break layering somewhere else entirely. */
.wname { z-index: 1; }
.wname:hover, .wname:focus-visible, .wname:focus-within { z-index: 4; }


/* --- THE PANELS USE THE BAR'S MATERIAL, FULL STOP ------------------------
   One material for the whole header system: the same rgb(208 208 208 / .3)
   behind blur(16px) the nav bar uses, no border, no shadow. I had drifted off
   it twice — first to a near-white .78 that was opacity rather than glass, then
   to drawn edges and shadows to fake glass over a flat backdrop. Both were me
   solving a contrast problem by changing the material.

   The contrast problem is real — this tint reads 16.71:1 over the white page
   and 2.07:1 over ink — but it is already solved elsewhere, by the same surface
   flip the bar uses. `.hdr--dark` inverts the tint and the type together, so
   the material stays constant and only the state changes. */
.nav__panel a,
.wname {
  background: rgb(208 208 208 / .3);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 0;
  box-shadow: none;
  color: var(--ink);
}
.nav__panel a:hover, .nav__panel a:focus-visible,
.wname:hover, .wname:focus-visible {
  background: rgb(230 230 230 / .55);
  color: var(--ink);
}
.nav__panel small { color: var(--ink-2); opacity: .7; }
.wname.is-here { background: rgb(20 20 18 / .55); color: #fff; }

/* Over a dark surface the same material inverts, exactly as the bar does. */
.hdr--dark .nav__panel a,
.hdr--dark .wname { background: rgb(40 40 38 / .38); color: var(--on-dark); }
.hdr--dark .nav__panel a:hover,
.hdr--dark .wname:hover { background: rgb(60 60 57 / .5); color: #fff; }
.hdr--dark .nav__panel small { color: var(--on-dark-mut); opacity: 1; }
.hdr--dark .wname.is-here { background: rgb(240 240 238 / .5); color: var(--ink); }

/* --- WHY THE DROPDOWN COULD NEVER LOOK LIKE GLASS ------------------------
   The pills had the bar's exact material and still rendered as flat tint. The
   cause is in the spec, not the values: an element with `backdrop-filter`
   becomes a BACKDROP ROOT, and its descendants can only sample what is inside
   that root. The panels are children of `.nav`, and they hang BELOW the nav's
   box — so their blur had literally nothing to sample. The cursor looked right
   all along because it is a direct child of <body>.

   Fix: the bar keeps its glass, but on a pseudo-element instead of on itself.
   `.nav` stops being a backdrop root, so the panels inside it now sample the
   page. Verified by removing the nav's filter by hand first and watching the
   pills come alive.

   Worth remembering: nesting one glass surface inside another does not work.
   The inner one goes dead, silently, with no warning and no error. */
.nav {
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.nav::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  border-radius: 4px;
  background: rgb(208 208 208 / .3);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  pointer-events: none;
}
.hdr--dark .nav { background: none; }
.hdr--dark .nav::before { background: rgb(40 40 38 / .38); }

/* --- SHOP: THREE STOREFRONTS ---------------------------------------------
   The page's whole job is to route you to the right store, so the stores ARE
   the layout — one row of three, image over name over one line of what is in
   it. No product taxonomy: the stores already have search. */
.stores {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--gut);
  grid-template-columns: repeat(3, 1fr);
}
.store__link { display: block; }
.store__link::after { display: none; }          /* the global link underline */
.store__media {
  margin: 0 0 1.1rem;
  border-radius: 4px;
  overflow: hidden;
  background: var(--paper-2);
  aspect-ratio: 4 / 3;
}
.store__media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .6s var(--ease-out);
}
.store__link:hover .store__media img,
.store__link:focus-visible .store__media img { transform: scale(1.04); }
.store__name {
  font-size: clamp(1.15rem, 2vw, 1.5rem); letter-spacing: -.02em;
  margin: 0 0 .35rem; color: var(--ink);
}
.store__what { color: var(--ink-2); font-size: .95rem; max-width: 34ch; margin: 0 0 .7rem; }
.store__go {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .13em; text-transform: uppercase; color: var(--muted);
  transition: color var(--dur-fast) var(--ease);
}
.store__link:hover .store__go, .store__link:focus-visible .store__go { color: var(--ink); }

@media (max-width: 900px) { .stores { grid-template-columns: 1fr 1fr; } }
@media (max-width: 620px) { .stores { grid-template-columns: 1fr; } }

/* --- SERVICES: THE COMPARISON --------------------------------------------
   A real table would be the semantic choice, but it cannot reflow to a stacked
   layout on a phone without losing the row labels. This is a grid with ARIA
   table roles: correct to a screen reader, and free to become three stacked
   cards below 860px, where each card repeats its row label so the cells still
   mean something on their own. */
.cmp {
  display: grid;
  grid-template-columns: minmax(5.5rem, .8fr) repeat(3, 1fr);
  border-top: 1px solid var(--hairline);
}
.cmp__row { display: contents; }
.cmp__row > * {
  padding: 1.05rem 1rem 1.05rem 0;
  border-bottom: 1px solid var(--hairline);
  font-size: .96rem; line-height: 1.45; color: var(--ink-2);
}
.cmp__rh {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .13em; text-transform: uppercase; color: var(--muted);
}
.cmp__h {
  font-family: var(--sans); font-size: clamp(1.02rem, 1.5vw, 1.2rem);
  letter-spacing: -.015em; color: var(--ink);
  display: flex; gap: .55rem; align-items: baseline;
}
.cmp__num { font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); }
.cmp__row--cta > * { border-bottom: 0; }

@media (max-width: 860px) {
  .cmp { grid-template-columns: 1fr; border-top: 0; }
  .cmp__row { display: grid; grid-template-columns: 7rem 1fr; }
  .cmp__row--head .cmp__rh { display: none; }
  .cmp__row--head { grid-template-columns: 1fr; margin-top: 2rem; }
  .cmp__row--head .cmp__h { border-bottom: 1px solid var(--ink); padding-bottom: .5rem; }
  .cmp__row > * { padding: .7rem 0; }
}

/* --- SERVICES: THE DISCIPLINE / SPRINT LISTS -----------------------------
   The old page gave each of these a paragraph, which is what made it read as
   three essays. A row you can scan carries the same information. */
.slist { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--hairline); }
.slist li {
  display: grid;
  grid-template-columns: 3rem minmax(11rem, 1.1fr) 2fr auto;
  gap: 1rem;
  align-items: baseline;
  padding: 1rem 0;
  border-bottom: 1px solid var(--hairline);
}
.slist__n { font-family: var(--mono); font-size: var(--t-meta); color: var(--muted); }
.slist__t { font-size: 1.05rem; letter-spacing: -.015em; color: var(--ink); }
.slist__d { color: var(--ink-2); font-size: .95rem; }
.slist__w {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
  white-space: nowrap; text-align: right;
}
.sec.dark .slist, .sec.dark .slist li { border-color: var(--hairline-d); }
.sec.dark .slist__t { color: var(--on-dark); }
.sec.dark .slist__d { color: var(--on-dark-mut); }

@media (max-width: 860px) {
  .slist li { grid-template-columns: 2.5rem 1fr; row-gap: .3rem; }
  .slist__d { grid-column: 2; }
  .slist__w { grid-column: 2; text-align: left; }
}


/* --- CUSTOM CURSOR -------------------------------------------------------
   Moved here from home.css. The script had already been made site-wide, but
   the STYLES had not — so on Work and Shop the element mounted correctly and
   rendered as nothing at all. A JS-only move is half a move. */
.cursor {
  position: fixed; top: 0; left: 0; z-index: 140; pointer-events: none;
  will-change: transform;
}
.cursor__in {
  width: 96px; height: 96px; margin: -48px 0 0 -48px;
  border-radius: 50%;
  display: grid; place-items: center; text-align: center;
  background: var(--ink); color: var(--paper);
  font-family: var(--mono); font-size: .62rem; letter-spacing: .13em; text-transform: uppercase;
  transform: scale(0);
  transition: transform .4s var(--ease-out), background .3s var(--ease), color .3s var(--ease);
}
.cursor.is-on .cursor__in { transform: scale(1); }
.cursor.is-dark .cursor__in { background: var(--paper); color: var(--ink); }
.cursor__label { opacity: 0; transition: opacity .25s var(--ease); max-width: 74px; line-height: 1.25; }
.cursor.is-on .cursor__label { opacity: 1; }
/* --- THE CURSOR USES THE SAME MATERIAL AS THE BAR ------------------------
   Same glass as the nav and the dropdowns — rgb(208 208 208 / .3) behind
   blur(16px), no outline, no shadow. It was a solid disc, which covered the
   thing you were pointing at; translucent, you can see through to it.

   `.is-dark` already existed and means "the section behind is dark". The
   material does not change with it — only the label colour does, because the
   same tint measures 16.71:1 with ink type over the white page and 8.16:1 with
   light type over ink. One material, two label colours. */
.cursor__in {
  background: rgb(208 208 208 / .3);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 0;
  box-shadow: none;
  color: var(--ink);
}
.cursor.is-dark .cursor__in {
  background: rgb(208 208 208 / .3);
  color: #fff;
}

/* A sixth slot in the selected-work grid. The five existing spans deliberately
   never line up — the rhythm comes from the mismatch — so --f takes the wide
   right-hand column the run was missing and sits high, against --a's low start. */
.wcard--f { grid-column: 6 / span 7; }
.wcard--f .wcard__media { aspect-ratio: 5/4; }
@media (max-width: 900px) {
  .wcard--f { grid-column: 1 / -1; padding-top: 0; }
}

/* --- SHOP HEADER (the /learn layout) -------------------------------------
   Headline left across most of the width, one small square image off to the
   right, and a short bold block beneath. The image is deliberately small: on
   the reference the type is the event and the picture is punctuation. A
   full-bleed banner here — which is what this page had — says the opposite. */
.shophead__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(220px, 28vw, 440px);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
  margin-top: clamp(1rem, 2vw, 1.6rem);
}
/* Dani, Sept: the plant should be bigger and come out of the edge of the
   frame. The column is ~2x what it was, and the figure is wider than its
   column by the gutter plus a slice of viewport, so it runs off the right
   edge; the section clips it. The film is white-matted, so what leaves the
   frame is the growth itself, not a rectangle. */
.shophead { overflow: hidden; }
.shophead__fig { width: calc(100% + var(--gut) + 8vw); }
.shophead__h {
  font-size: clamp(2.4rem, 6.4vw, 6rem);
  line-height: .96;
  letter-spacing: -.035em;
  margin: 0;
  max-width: 15ch;
}
.shophead__fig { margin: 0; align-self: center; }
.shophead__fig img {
  width: 100%; aspect-ratio: 1/1; object-fit: cover;
  background: var(--paper-2); display: block;
}
.shophead__new {
  margin-top: clamp(2.5rem, 7vw, 6rem);
  display: flex; flex-direction: column; gap: .1rem;
  font-weight: 600; letter-spacing: -.02em;
  font-size: clamp(1.1rem, 2vw, 1.5rem);
  line-height: 1.15;
}
.shophead__new-k { text-transform: uppercase; }
@media (max-width: 720px) {
  .shophead__grid { grid-template-columns: 1fr; }
  .shophead__fig { max-width: 260px; width: 100%; }
  /* Stacked, the figure no longer runs off the page, so the right edge of the
     film is on screen and the bough would stop dead at it. Fade that edge. */
  .shophead__fig video {
    -webkit-mask-image: linear-gradient(to right, #000 80%, transparent);
            mask-image: linear-gradient(to right, #000 80%, transparent);
  }
}


/* --- HOVER PREVIEW FLOAT (Partners, Capabilities) ---------------------
   Moved from home.css alongside hover-float.js. */

.pnames {
  font-size: clamp(1.6rem, 4.6vw, 3.5rem);
  line-height: 1.16;
  letter-spacing: -.035em;
  font-weight: 500;
  color: var(--ink);
  max-width: 22ch;
}
.pnames .ph { color: var(--muted); }
.pnames span[data-img] { cursor: pointer; }
.pfloat {
  position: fixed;
  top: 0; left: 0;
  z-index: 130;
  width: clamp(150px, 13vw, 210px);
  aspect-ratio: 3 / 4;
  pointer-events: none;
  will-change: transform;
  /* Parked off-screen until JS positions it. Without this the element sits at
     0,0, and on the first hover it becomes visible in the top-left corner for
     however long it takes the rAF loop to write a transform — a real glitch
     that only appears on the very first hover, which is exactly the kind that
     survives testing. */
  transform: translate3d(-9999px, -9999px, 0);
  /* NO transition on transform — this element is moved every frame by rAF, and
     a transition would fight the loop and produce jitter. */
}
/* NO ROUNDED BOX AND NO CROP. These are cut-outs with real transparency, and a
   3/4 card with `overflow: hidden` was slicing each specimen into a rectangle —
   the thing that made them look pasted rather than floating. The card is now
   just a stage: `contain` fits the whole shape inside it, so what you see is
   the silhouette, not a crop of it.

   The assets are trimmed to their own alpha bounding box at 560px on the long
   edge, so nothing is letterboxed either — each one fills the stage in the
   dimension it actually needs. */
.pfloat__in {
  position: absolute; inset: 0;
  overflow: visible;
  opacity: 0;
  transform: scale(.9);
  transition: opacity .32s var(--ease), transform .42s var(--ease-out);
}
.pfloat.is-on .pfloat__in { opacity: 1; transform: scale(1); }
.pfloat :is(img, video) {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain;
  opacity: 0;
  transition: opacity .3s var(--ease);
}
.pfloat :is(img, video).is-live { opacity: 1; }
.pnames[data-pnames] span[data-img] { cursor: pointer; }
@media (hover: none), (pointer: coarse) { .pfloat { display: none; } }
@media (prefers-reduced-motion: reduce) { .pfloat { display: none; } }

/* --- SERVICES: THE ROUTE ROW ---------------------------------------------
   One form serves all three routes now, so it has to capture WHICH — otherwise
   every enquiry arrives indistinguishable and the triage moves into Dani's
   inbox. A radio row costs one line, against the three fields that came out.

   A fieldset/legend rather than a label: three radios are one question, and a
   screen reader should announce the question before the options. */
.field--routes { border: 0; padding: 0; margin: 0; }
.field--routes .field__l { padding: 0; }
.route {
  display: inline-flex; align-items: center; gap: .5rem;
  margin: .55rem 1.4rem .2rem 0;
  font-size: .98rem; letter-spacing: -.01em;
  cursor: pointer;
}
.route input { width: 1rem; height: 1rem; accent-color: var(--ink); }
.closing .route input, .sec.dark .route input { accent-color: var(--on-dark); }

/* The capability list is four times longer than the Partners list it borrows
   from, so it takes a step down in size — at the Partners scale, 22 items fill
   two screens and stop reading as a list. Still large enough to be the page's
   main statement. */
.pnames--caps { font-size: clamp(1.5rem, 3.4vw, 3.1rem); line-height: 1.12; }


/* --- CAPABILITIES: THE FULL MEASURE --------------------------------------
   .pnames carries `max-width: 22ch` because the Partners list is five names —
   at full page width five names would be one short line adrift in white space,
   so the narrow measure is what makes THAT list read as a block.

   Twenty-two capabilities is a different object. The same 22ch measure turned
   it into a 626px column beside 886px of nothing: eleven lines tall, and the
   eye tracked it as a vertical ribbon rather than a list. Released to the full
   wrap it runs about five lines, which is a paragraph-shaped statement — and
   with the width back, the type can go up a step instead of down. */
.pnames--caps {
  max-width: none;
  font-size: clamp(1.55rem, 3.9vw, 3.5rem);
  line-height: 1.1;
}

/* --- SEND A BRIEF: ONE COLOUR, LESS AIR ----------------------------------
   Dani's call on both. The greys (--muted labels, --on-dark-mut body, muted
   form note) were doing hierarchy work that size, case and the mono face
   already do here — on ink they mostly read as "slightly switched off". White
   throughout, and the structure survives on typography alone.

   Contrast is not a risk in this direction: #F4F2ED on #0E0E0C measures
   17.4:1, against 5.6:1 for the muted grey it replaces. */
#support .field label,
#support .field__l,
#support .meta,
#support .body-lg,
#support .hint,
#support .form__note,
#support legend,
#support .badge--quiet { color: var(--on-dark); }
#support input::placeholder,
#support textarea::placeholder { color: rgb(244 242 237 / .5); }

/* The vertical rhythm was set for a form half again as long. Nothing here is
   removed — the gaps between rows come down, the textarea stops reserving eight
   rems for three lines, and the block loses roughly a third of its height. */
.form--tight { gap: 1.05rem; }
.form--tight .g2 { gap: var(--gut) 1.6rem; }
.form--tight .field { gap: .35rem; }
.form--tight .field input,
.form--tight .field textarea { padding: .55rem 0; }
.form--tight .field textarea { min-height: 5.2rem; }
.form--tight .field--check { margin-top: .2rem; }
.form--tight .form__note { margin-top: -.35rem; }

/* The sub-block no longer sits under a second heading, so it does not need the
   rule and the clearance that separated it from one. */
.ssub--flush { margin-top: 0; padding-top: 0; border-top: 0; }

/* The badge rides the section rule at the right-hand end. */
.eyebrow--split { flex-wrap: wrap; }
.eyebrow--split .badge { margin-left: auto; }

/* --- THE AVAILABILITY DOT ------------------------------------------------
   A status light, so it behaves like one: the studio's yellow, and a slow
   pulse rather than a blink. 3.2s and a floor of .28 opacity — fast enough to
   register as live, slow enough that it never competes with reading. Anything
   under ~2s in peripheral vision reads as an alert.

   Honoured through `prefers-reduced-motion`, where it holds at full strength:
   the dot still means "open", it just stops moving. */
#support .badge--live::before { background: var(--hl); }
@media (prefers-reduced-motion: no-preference) {
  #support .badge--live::before { animation: dotPulse 3.2s var(--ease) infinite; }
}
@keyframes dotPulse {
  0%, 100% { opacity: 1;   transform: scale(1); }
  50%      { opacity: .28; transform: scale(.82); }
}

/* --- RUNNING INTO THE FOOTER ---------------------------------------------
   Two separate faults made this join visible.

   1. The footer's opening ramp fades UP to transparent so it can soften the
      meeting with a WHITE section. On a page that already ends in ink, that
      transparency lets the page canvas through and paints a pale band across
      the footer's top 115px — a seam manufactured by the very thing meant to
      prevent one. `dark-end` is set at build time by inspecting the last
      section of each fragment, so it cannot drift out of sync.

   2. Even with matching colour, a full --sec of padding at the bottom of the
      section plus a full --sec at the top of the footer reads as a gap, which
      is the other half of "merge seamlessly". The section keeps enough room to
      breathe below the button and hands off. */
body.dark-end .ftr {
  background: linear-gradient(180deg,
    var(--dark)  0%,
    #16170F     52%,
    #4A5327     70%,
    #B9C95A     87%,
    #F7FFA2    100%);
  padding-top: var(--sec-sm);
}

/* --- THE CLOSING FILM RUNS INTO THE FOOTER -------------------------------
   Measured on tm:rw before this: 154px of flat black between the bottom of the
   last film and the top of the footer, holding nothing but the "Next" line.
   The COLOUR was already seamless — the footer gradient starts at exactly the
   page background — so what read as "not merging" was the dead space plus the
   film's hard bottom edge sitting on it.

   The fade is a mask rather than a gradient overlay, so the picture dissolves
   into whatever is actually behind it. An overlay would need to know the page
   colour, and would have been wrong the moment tm:rw went from #0E0E0C to
   pure black. `runs-out` is applied by build.py to whichever section currently
   ends a dark page.

   The mask is on the FIGURE, not the media: `object-fit: cover` means the
   media box and the visible frame are not the same rectangle, and masking the
   media fades a region that may be cropped away.

   SCOPED TO tm:rw. build.py tags the closing section on every page-dark page,
   so MUR and Liminal already carry `runs-out` and this becomes a one-line
   change if the same handoff is wanted there — but Dani asked for the black to
   stay on tm:rw alone, and quietly restyling the ending of two other pages in
   the same breath would not be keeping to that. */
body.is-tmrw .runs-out { padding-bottom: 0; }
body.is-tmrw .runs-out .cs-figure { margin-block-end: 0; }
body.is-tmrw .runs-out .cs-figure:last-of-type {
  -webkit-mask-image: linear-gradient(180deg, #000 58%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 58%, transparent 100%);
}
/* Whatever follows the closing film — the "Next" line — sits close under it
   instead of in its own full-height block, and the footer starts straight
   away, because its first 52% is already the page colour. */
body.is-tmrw .runs-out + section { padding-block: clamp(1rem, 2.5vh, 1.7rem); }
/* The footer is outside <main>, so it is not a sibling of the section — a
   combinator cannot reach it from here and `:has()` on the body can. */
body.is-tmrw.dark-end:has(.runs-out) .ftr { padding-top: 0; }
.sec--tofooter { padding-bottom: clamp(2rem, 4.5vh, 3.4rem); }

/* --- TWO FIELD BUGS THIS SECTION MADE VISIBLE -----------------------------
   Both predate the tightening; shortening the form is simply what brought them
   into one screen.

   1. `.field label` styles the mono caps label — but a <legend> is not a
      <label>, so "Which route?" was rendering as plain sentence-case sans
      beside six mono-caps siblings. The class the markup already carries,
      .field__l, had no rule of its own at all. It does now, so the styling
      follows the class rather than the tag.

   2. `.field input { width: 100% }` was also hitting the consent CHECKBOX,
      which stretched it across the column and centred it — the stray square
      floating mid-row. Checkboxes need their own row shape. */
.field__l {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}
.dark .field__l, .closing .field__l { color: var(--on-dark-mut); }

.field--check {
  grid-template-columns: 1rem 1fr;
  align-items: start;
  gap: .7rem;
}
.field--check input[type="checkbox"] {
  width: 1rem; height: 1rem; margin: .1rem 0 0; padding: 0;
  border-bottom: 0; accent-color: var(--ink); flex: none;
}
.dark .field--check input[type="checkbox"],
.closing .field--check input[type="checkbox"] { accent-color: var(--on-dark); }
/* A consent line is read, not scanned — mono caps is the wrong face for a
   sentence with a verb in it. */
.field--check .field__l {
  font-family: inherit; font-size: .92rem; letter-spacing: -.01em;
  text-transform: none; line-height: 1.45;
}

/* `.field` is a grid, so each .route became its own row and three radios ran
   down the page like three separate questions. They are one question. */
.field--routes { display: block; }
.field--routes .field__l { display: block; margin-bottom: .15rem; }

/* Native radios and checkboxes were drawn in the browser's LIGHT scheme — solid
   white discs on ink, which read as three already-selected options. Declaring
   the scheme is the fix; `accent-color` only colours the checked state and
   cannot touch how an unchecked control is painted. */
#support, .closing { color-scheme: dark; }

/* --- FOOTER TYPE: WHITE, AND NO SHADOW -----------------------------------
   Dani's call. The shadow was load-bearing — it was what made the muted greys
   survive crossing the flower, where sampling the canvas behind the type
   measured 1.02:1 in the worst places. Removing it and keeping the greys would
   be illegible, so the two changes go together: at full white the type has
   4.5x the contrast of the grey it replaces against every part of the bloom,
   including the pale yellow at the bottom of the gradient.

   Measured after the change rather than assumed — see the note in HANDOFF. */
.ftr,
.ftr .ftr-h,
.ftr .meta,
.ftr__base p,
.ftr__base a,
.ftr li a,
.ftr .lede,
.ftr p { color: var(--on-dark); }
.ftr .ftr-h { opacity: 1; }
.ftr,
.ftr .wrap,
.ftr .d-xl,
.ftr .body-lg,
.ftr .shead,
.ftr .form,
.ftr__base,
.ftr__cta { text-shadow: none; }
.closing .d-xl,
.closing .body-lg,
.closing .shead,
.closing .form { text-shadow: none; }
.closing, .closing p, .closing .meta, .closing label, .closing .field__l { color: var(--on-dark); }

/* Higher-specificity kill for the two remaining shadow rules — .ftr__grid and
   .ftr__base were not in the earlier list. */
.ftr .ftr__grid,
.ftr .ftr__cta,
.ftr .ftr__base { text-shadow: none; }

/* --- WHERE THE TYPE CAN AND CANNOT BE WHITE ------------------------------
   Removing the shadow means the gradient alone has to carry the contrast, and
   it cannot everywhere. Measured against #F4F2ED:

       #0E0E0C  ( 0%)   17.4:1
       #16170F  (52%)   16.2:1
       #4A5327  (70%)    7.2:1
       #B9C95A  (87%)    1.65:1   FAIL
       #F7FFA2 (100%)    1.02:1   FAIL — invisible

   White is safe down to roughly the 72% mark and unreadable past about 78%.
   The legal row rides `margin-top: auto` to the very bottom, which is exactly
   the worst place on the gradient — it measured 2.21:1 there.

   The answer is not to put the shadow back or to dim the glow; it is to stop
   putting type on top of the glow. The bottom padding clears the bright band,
   so the yellow becomes an unobstructed field of light below the content —
   which is what it looks like in the reference anyway. Content now ends around
   the 70% mark, where white still measures over 7:1. */
.ftr { padding-bottom: clamp(6rem, 34vh, 20rem); }
@media (max-width: 860px) { .ftr { padding-bottom: clamp(4rem, 14vh, 8rem); } }

/* The mobile panel's current row is marked with data-current rather than
   aria-current — see the note in initCurrent(). Styling follows the attribute
   that is actually there. */
.mnav__link[data-current] { color: var(--on-dark); }
.mnav__link[data-current]::after { content: " ·"; opacity: .5; }


/* --- NUMBERED SECTION HEAD ------------------------------------------------
   Moved here from home.css, which is loaded on ONE page. Services and the
   project case studies both use this component, so both were rendering it
   unstyled: the number ran straight into the title and the right-hand column
   fell beneath the left. Nothing errored, and a section head that is merely
   plainer than intended does not look like a bug — which is how it survived.

   Rule of thumb this earns: if a class appears in more than one part file, it
   belongs in site.css. `grep -l 'class="x' parts/*.html` settles it. */
/* --- 9. NUMBERED SECTION HEAD ---------------------------------------- */
.shead {
  display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: var(--gut);
  align-items: end;
  padding-bottom: 1.3rem; margin-bottom: clamp(2rem,5vh,3.4rem);
  border-bottom: 1px solid var(--hairline);
}
.dark .shead { border-color: var(--hairline-d); }
.shead__l { display: flex; gap: .8rem; align-items: baseline; }
.shead__r { display: flex; gap: 1.4rem; align-items: baseline; justify-content: flex-end; }
@media (max-width: 760px) {
  .shead { grid-template-columns: 1fr; gap: .9rem; align-items: start; }
  .shead__r { justify-content: flex-start; }
}

/* A <video> in a case-study figure fills its column exactly as an <img> does.
   Without this it lays out at its intrinsic pixel size, so a 720px render sat
   in a 1400px column with white beside it. */
.cs-figure video { width: 100%; height: auto; display: block; }


/* --- A PAGE THAT IS INK ALL THE WAY DOWN ---------------------------------
   `page-dark` on <body> rather than `.dark` on each section. Three reasons it
   has to be the body:

   1. The footer already runs from ink to yellow. If the page above it is white
      and only the last section is dark, there is a hard line to hide; if the
      whole page is ink, the footer is simply where the page finally warms up.
      `dark-end` is set automatically for these pages in build.py, so the
      footer drops the opening ramp it draws over white sections.
   2. Overscroll. Section backgrounds stop at the section — pull past the top
      of the page on a trackpad and a white band appears above an ink page.
      The body's background paints the canvas, so there is nothing to expose.
   3. Twenty-six sections would each have needed the class, and the next one
      added would have been forgotten.

   Everything below is scoped to `main` on purpose. The header runs its own
   surface flip and the footer has its own palette; neither should inherit
   from the page, or a fix here silently changes both. */
body.page-dark { background: var(--dark); color: var(--on-dark); }

/* --- PURE BLACK, ON ONE PAGE ---------------------------------------------
   `--dark` is #0E0E0C, a warm near-black, and it is shared: the footer starts
   from it on EVERY page, white ones included, and MUR and Liminal are built on
   it too. tm:rw wants true black, so it redefines the token for itself rather
   than changing it globally — every rule that reads `var(--dark)` inside this
   page then follows, including the footer's gradient, with nothing else on the
   site touched.

   One token, one page. */
body.page-dark.is-tmrw { --dark: #000000; }

/* --- THE PLAYHOUSE NAVY, ON ONE PAGE (8 Oct 2026) -------------------------
   The same move as tm:rw above: Playhouse runs on the brand's own navy, the
   colour its posters, posts and logo films are set on, so the artwork sits in
   the page instead of on tiles. Sampled off the artwork; the pieces vary by a
   level or two around it (#100D20 to #130F23). */
body.page-dark.is-playhouse { --dark: #110E21; }
/* Six loops are rendered ON the navy (the logo, the three chrome word banners,
   the alpaca, the chrome symbol). A film never decodes to exactly the CSS
   colour: these measure one level off, which is enough to draw a faint box
   round each one. The outer 7% of the frame is empty navy in all six, so it is
   faded out and the page shows through instead. On the wrapper, by position:
   the studio rewrites figures and would drop a class put on one. */
.ph-float .cs-figure video {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%),
                      linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%),
                      linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

body.page-dark main .meta { color: var(--on-dark-mut); }
body.page-dark main .meta--ink { color: var(--on-dark); }
body.page-dark main .lede { color: var(--on-dark); }
body.page-dark main .shead { border-bottom-color: var(--hairline-d); }
body.page-dark main .rule { background: var(--hairline-d); }

/* Captions were --muted (#656158), which is a grey chosen against paper — on
   ink it measures 2.6:1 and reads as switched off rather than secondary.
   --on-dark-mut is the grey that was designed for this surface: 7.4:1. */
body.page-dark main .cs-figure figcaption { color: var(--on-dark-mut); }

/* The breadcrumb's underlined link and the "Next" link inherit currentColor,
   so they follow the surface without their own rule — but the hover state was
   an ink tint that vanishes here. */
body.page-dark main a { color: inherit; }
body.page-dark main .alink:hover { color: var(--hl); }

/* The reveal fades from `opacity: 0`, which on a dark page means fading up
   from the page rather than from white — no change needed, noted because the
   instinct is to look for a light flash that is not there. */

/* A caption under a FULL-BLEED figure has to come back onto the grid. The
   figure is 100vw by design, and the caption was inheriting that — starting
   hard against the left edge of the viewport while every other line on the page
   sits on the gutter. The artwork bleeds; the type never does.

   First surfaced on the AI campaigns page, which is the first bleed figure on
   the site to carry a caption at all. */
.cs-figure.bleed figcaption {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gut);
}


/* --- HOVER-TO-OPEN ROW LIST WITH A LINKED PREVIEW ------------------------
   Moved here from home.css, which loads on exactly one page. Used by the
   homepage capabilities, the homepage trend section, and now the Trends
   page's Latest signals. */
.cpx { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,.82fr); gap: clamp(2rem,5vw,4.5rem); align-items: start; }
.cpx__view { position: sticky; top: 7rem; }
.cpx__stack { position: relative; aspect-ratio: 4/5; overflow: hidden; background: var(--dark-2); }
.cpx__stack :is(img, video) {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transform: scale(1.06);
  transition: opacity .6s var(--ease-out), transform 1.2s var(--ease-out);
}
.cpx__stack :is(img, video).is-live { opacity: 1; transform: scale(1); }
.cpx__cap { margin-top: .85rem; display: flex; justify-content: space-between; gap: 1rem; }
.cpx__list { border-top: 1px solid var(--hairline-d); }
.crow {
  display: grid; grid-template-columns: 3.2rem minmax(0,1fr) auto;
  gap: 1.2rem; align-items: center;
  padding-block: clamp(1.15rem, 2.1vw, 1.75rem);
  border-bottom: 1px solid var(--hairline-d);
  cursor: pointer; position: relative;
  transition: padding-left .5s var(--ease);
}
.crow:hover, .crow.is-active { padding-left: .7rem; }
.crow__n { font-family: var(--mono); font-size: var(--t-meta); color: var(--on-dark-mut); letter-spacing: .1em; }
/* The heading is the only link. Its ::after stretches over the whole row so the
   click target is large, while the description stays out of the accessible name. */
.crow__h {
  font-size: clamp(1.35rem, 2.5vw, 2.15rem); font-weight: 600;
  letter-spacing: -.032em; line-height: 1;
  transition: color .4s var(--ease), opacity .4s var(--ease);
}
/* Resolves against .crow (the nearest positioned ancestor), so the hit area is
   the full row. .crow__h must NOT be position:relative for this to work. */
.crow__h::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.crow__go { font-family: var(--mono); font-size: var(--t-meta); color: var(--on-dark-mut); letter-spacing: .1em; opacity: 0; transform: translateX(-6px); transition: opacity .35s var(--ease), transform .35s var(--ease); }
.crow:hover .crow__go, .crow.is-active .crow__go { opacity: 1; transform: none; }
/* Dim the siblings so the hovered row leads */
.cpx__list:hover .crow:not(:hover) .crow__h { opacity: .38; }

.crow__d {
  grid-column: 2 / -1;
  font-size: .95rem; line-height: 1.55; color: var(--on-dark-mut);
  max-width: 52ch;
  display: grid; grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .55s var(--ease-out), opacity .4s var(--ease), padding-top .45s var(--ease);
}
.crow__d > span { overflow: hidden; }
.crow.is-active .crow__d { grid-template-rows: 1fr; opacity: 1; padding-top: .85rem; }

/* The preview panel */
@media (max-width: 1080px) {
  .cpx { grid-template-columns: 1fr; }
  .cpx__view { display: none; }
  .crow__d { grid-template-rows: 1fr; opacity: 1; padding-top: .8rem; }
  .crow { align-items: start; }
  .cpx__list:hover .crow:not(:hover) .crow__h { opacity: 1; }
}


/* --- SIGNALS VARIANT: image LEFT, 4:3, category and date -----------------
   The homepage panel is on the right in 4:5 with a two-part caption. This one
   mirrors it deliberately — the two are composed as opposites so the mechanic
   reads as a system rather than as the same trick twice. That was written into
   the original CSS as a warning; this is it being honoured rather than
   discovered later. */
.cpx--sig { grid-template-columns: minmax(0,.78fr) minmax(0,1fr); }
.cpx--sig .cpx__view { order: -1; }
.cpx--sig .cpx__stack { aspect-ratio: 4/3; }
@media (max-width: 1080px) {
  .cpx--sig { grid-template-columns: 1fr; }
}
/* A signal keeps its number, date and category — the row is wider than a
   capability row because it carries both ends. */
.crow--sig { grid-template-columns: 6.5rem minmax(0,1fr) auto; }
.crow--sig .crow__h { font-size: clamp(1.05rem, 1.75vw, 1.5rem); line-height: 1.15; }
.crow--sig .crow__d { max-width: 56ch; }

/* --- A HERO THAT MEETS A COLOUR CHANGE NEEDS CLEARANCE -------------------
   Six heroes on this site carry `padding-bottom: 0`, and on five of them it is
   invisible: the next section is the same colour, so the zero gap costs
   nothing and the section's own top padding does all the work.

   Trends is the exception — its hero is followed by `.sec.dark`, so there is a
   hard edge, and at zero padding that edge landed on the button row. The
   buttons were sitting ON the boundary.

   The inline override is gone from that page, and this rule states the
   intent so the next hero followed by a dark section gets it automatically
   rather than by someone noticing. An inline style would beat this, which is
   why the override had to be deleted rather than countered. */
.hero:has(+ .sec.dark),
.hero:has(+ .closing) { padding-bottom: var(--sec); }

/* --- SIGNAL ROWS: THE ACTIVE ONE IS WHITE, THE REST ARE DIMMED -----------
   The base component only dimmed siblings WHILE the list was hovered, so at
   rest every row sat at full strength and nothing indicated which one the
   image belonged to. On a dark section that reads as five equal headlines
   above an unrelated picture.

   Dimming is now the resting state and the active row is the exception, which
   is what makes the pairing legible before the pointer arrives.

   .52, not the .38 the base component uses on hover. Measured against the ink:

       .38  rgb(101,101,98)   3.30:1   passes AA-large only
       .52  rgb(134,133,129)  5.23:1   passes AA at any size

   These rows are LINKS to real reports, not decoration, and at the small end
   of their clamp they are 16.8px — under the 18.66px where AA-large applies.
   .38 would have been a fail at that size dressed up as a design choice. */
.cpx--sig .crow__h { color: var(--on-dark); opacity: .52; }
.cpx--sig .crow.is-active .crow__h { opacity: 1; }
.cpx--sig .crow:hover .crow__h { opacity: 1; }
/* The description belongs to the open row, so it is full-strength white
   rather than the muted grey the base component uses. */
.cpx--sig .crow__d { color: var(--on-dark); }
.cpx--sig .crow__n,
.cpx--sig .crow__go { color: var(--on-dark-mut); }
.cpx--sig .crow.is-active .crow__n { color: var(--on-dark); }

/* --- SITE-WIDE CORNER ROUNDING -------------------------------------------
   Dani's call: "a slight corner rounding on all rectangles so nothing is too
   sharp". One token, applied to the surfaces that actually read as rectangles
   — media boxes, cards, controls — rather than blanket-applied to every
   element, which would also round things that are not boxes (hairlines, table
   cells, the reveal masks) and produce artefacts.

   DELIBERATELY EXCLUDED, and this is the part that matters:
     · .bleed and .opener__film — anything running to the window edge. A
       rounded corner there exposes the page background in the notch and reads
       as a rendering fault, not a detail.
     · the footer flower canvas and the hero WebGL canvas — transparent
       elements with no visible box to round.
   ------------------------------------------------------------------------ */
.cs-figure img,
.cs-figure video,
.pcard__media,
.pgal__frame,
.tsplit__stack,
.cpx__stack,
.wcard__media,
.plan,
.sprint,
.fact,
.stat,
.cap,
.form__ok,
.badge,
.chip,
.filter,
.btn,
.pfloat__in { border-radius: var(--radius); }

/* The Partners float sits ON artwork rather than on the page, so it needs a
   touch more to read as an object lifted off it. (`.gfloat` was its twin on the
   work tiles; that interaction is now the .pgal__peek cascade and the class is
   gone.) */
.pfloat__in { border-radius: var(--radius-lg); }

/* A rounded frame with a square image inside it shows four sharp corners
   poking out, because the image is a separate box. The overflow clip on the
   frame is what actually does the rounding — this just makes sure nothing
   inside can paint over it. */
.pcard__media,
.pgal__frame,
.tsplit__stack,
.cpx__stack,
.wcard__media { overflow: hidden; }

/* Form controls are underlined, not boxed — a radius on a bottom border does
   nothing except round the two ends of the line, which looks like a mistake.
   Only the checkbox, which IS a box, takes one. */
.consent input[type="checkbox"],
.field--check input[type="checkbox"] { border-radius: 2px; }

/* Full-bleed media keeps its square corners. This has to come after the block
   above, because `.cs-figure img` and `.bleed` land on the same element. */
.bleed,
.bleed img,
.bleed video,
.opener__film { border-radius: 0; }

/* ==========================================================================
   PROJECT OPENER — the film owns the first screen, the facts sit on it.

   Replaces the old arrangement, which was a band of white carrying a
   breadcrumb and a headline, and only THEN the image. That order spends the
   most valuable screen on the page explaining what you are about to see
   instead of showing it. Modelled on the reference Dani sent
   (vincentschwenk.de/works/blue-campaign): full-bleed media, a hairline across
   the middle with the facts hung beneath it, and the project name set large in
   the bottom corner.

   NO SCRIM AND NO TEXT SHADOW, deliberately and at Dani's instruction — she
   had both removed from the homepage opener for the same reason. The type is
   white and the media underneath it is whatever the project's opener happens
   to be, so on a pale frame the facts row will run low-contrast. The reference
   does exactly the same thing and accepts it. Everything here is repeated as
   real text further down each page, so nothing is only available in this
   layer; if it ever needs solving, the header's glass-pill material and the
   frame sampler in site.js are the pieces to reach for.

   The heading is the project NAME rather than the tagline. The taglines are
   too long to set at this size, and they read better as the first statement
   after the opener, which is where they now live.
   ========================================================================== */
.popener {
  position: relative;
  height: 100svh;
  min-height: 460px;
  /* NO margin here. The section after the opener always brings its own
     padding-block, so a margin on this side is a second gap stacked on the
     first — 66px + 50px at 1512x828, which is most of what read as "so much
     white space after the header". The gap is declared once, below. */
  margin-bottom: 0;
  overflow: hidden;
  background: var(--ink);   /* the poster decodes into this, never a white flash */
}
.popener__media,
.popener__media img,
.popener__media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 0;         /* full bleed — the site-wide rounding must not apply */
}

/* The overlay is a grid so the facts row can sit on the horizontal centre line
   while the title stays pinned to the bottom, without either one having to
   know the other's height. */
.popener__over {
  position: absolute; inset: 0;
  display: grid;
  grid-template-rows: 1fr auto;
  align-content: stretch;
  color: var(--on-dark);
  pointer-events: none;     /* the breadcrumb re-enables it for itself */
}
.popener__mid { align-self: center; width: 100%; }
.popener__rule {
  height: 1px; width: 100%;
  background: rgb(255 255 255 / .45);
  margin-bottom: clamp(.9rem, 2vh, 1.4rem);
}
.popener__facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1rem, 3vw, 2.5rem);
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
}
.popener__facts dt { opacity: .62; margin: 0 0 .35em; }
.popener__facts dd { margin: 0; line-height: 1.55; }
.popener__t {
  margin: 0;
  padding-bottom: clamp(1.2rem, 4vh, 2.6rem);
  line-height: .92;
  letter-spacing: -.02em;
  font-size: clamp(2.6rem, 9vw, 8.5rem);
  font-weight: 400;   /* was the heading default; Dani asked for less weight */
}
/* The wrapper is what is positioned, not the nav, so the breadcrumb still sits
   in the page gutter. It must be a child of .popener__over rather than of
   .popener__mid — see the note in the markup about backdrop-filter creating a
   containing block. */
.popener__crumbwrap {
  position: absolute; top: 0; left: 0; right: 0;
  padding-top: clamp(5.5rem, 12vh, 7.5rem);
  pointer-events: none;
}
.popener__crumb { pointer-events: auto; }
.popener__crumb a { text-decoration: underline; text-underline-offset: 3px; }

/* The tagline that used to be the <h1>, now the first statement after the
   opener. It keeps the display size it had; it just no longer has to compete
   with a full-screen image for the same moment. */
.popener-lead { margin-block: 0 clamp(1.6rem, 4vh, 2.8rem); max-width: 22ch; }

/* The single gap under the opener. Overrides the `padding-block` shorthand on
   `.sec--sm` because a longhand at higher specificity wins regardless of the
   shorthand's source order. */
.popener + * { padding-block-start: clamp(3.25rem, 7vh, 5.5rem); }

/* Paninoteca: the tagline is the only thing in its section, followed by a
   full-bleed frame rather than a lede. Its bottom margin would stack on the
   section's own padding, which is the same doubling all over again. */
.popener-lead:last-child { margin-block-end: 0; }

@media (max-width: 760px) {
  .popener { height: 82svh; min-height: 380px; }
  .popener__facts { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 1.1rem; }
}

/* NO PANEL BEHIND THE FACTS — Dani's call, and the third time this site has
   traded a measured contrast floor for the look she wants. The numbers are
   kept here rather than deleted with the rule.

   Measured per column through `object-fit: cover` at 1512x828, the brightest
   column behind this row runs from .218 (Sokol) to .856 (Shabour) — 3.5:1 down
   to 1.0:1 against white type. Shabour's ROLE column was observed rendering as
   blank window before the panel went in.

   Everything in the row is repeated as real text further down each page, so
   nothing is only available here. tools/opener-contrast.py still reports the
   numbers if the openers change. */

/* The Shop hero is a film now, where a still used to be. Both live in
   .shophead__fig, so the video inherits the figure's box and only needs the
   object-fit rules an <img> got for free. The 4/5 crop matches the portrait
   source, so nothing is squeezed.

   (This block replaced .cpx__film, which put the same clip beside the homepage
   capability list. Dani moved it here.) */
/* The plant film is already cropped to its subject and matted onto the page
   colour, so it needs no crop and no placeholder tint — a --paper-2 panel
   behind it would show as a grey rectangle around a white-matted video. */
.shophead__fig video {
  display: block; width: 100%; height: auto;
  /* No aspect-ratio here. It was pinned to 480 / 610, the first film's own
     shape, so any film swapped in through the studio would have been
     letterboxed inside that box. The width and height attributes on the tag
     give the browser the ratio, and the studio rewrites those with the file. */
  background: transparent;
}

/* ==========================================================================
   FORM CHROME — Dani's notes, 25 Aug
   ========================================================================== */

/* 1. The focus ring is set at the top of this file — recoloured from
   --accent to the surface ink, for the reason recorded there. */

/* Text fields already HAVE a designed focus state — the hairline under them
   goes to full ink. That is the indicator; a second ring around the outside was
   always redundant. The line thickens on keyboard focus so it stays obvious
   without drawing a box. */
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible { outline: none; border-bottom-width: 2px; padding-bottom: calc(.75rem - 1px); }

/* 2. THE TICK WAS NOT CENTRED. It was drawn with
   `transform: rotate(45deg) translate(3px, 1px)` — a hand-nudged offset that
   only ever looked right at one box size. Now the box centres it and the
   translate only compensates for the rotation, which is what it is for. */
.consent input[type="checkbox"] { display: grid; place-content: center; }
.consent input[type="checkbox"]:checked::after {
  width: 5px; height: 9px;
  border: solid var(--paper); border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}

/* 3. THE SUBSCRIBE BUTTON HAD NO WORKING HOVER, and the reason is worth
   recording: every dark-surface button rule on this site is written against
   `.dark`, but the newsletter sits on `.sec.closing`, which is a dark surface
   that does not carry that class. So the button fell through to the LIGHT
   rules — and `.btn:hover` sets `color: var(--ink)` on a transparent
   background, i.e. near-black type on a near-black section. The hover state
   existed; it just made the label vanish. */
.closing .btn {
  background: var(--on-dark); color: var(--dark); border-color: var(--on-dark);
}
/* Hover swaps the solid fill for the site's glass — the button stops being an
   object and becomes frosted, with the chrome flower showing through it.

   The tint is WHITE with the label staying ink, and that is a measured choice,
   not a stylistic one. This button floats over a photograph that runs from
   near-black to near-white, so both ends have to hold. An ink wash with light
   type — the obvious inversion — scores 18:1 over the dark parts and **1.5:1
   over the chrome**, i.e. the label disappears exactly where the picture is
   brightest. Even at 72% ink it is only 2.8:1. A white wash with ink type holds
   9.5:1 over black and 19:1 over white. */
.closing .btn:hover {
  background: rgb(255 255 255 / .48);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  color: var(--dark);
  border-color: rgb(244 242 237 / .6);
}
.closing .btn--ghost { background: transparent; color: var(--on-dark); }
.closing .btn--ghost:hover { background: var(--on-dark); color: var(--dark); }
/* The arrow slides on hover — the button is an action, so it should read as
   one. Suppressed under reduced-motion by the global rule at the end of file. */
.btn svg { transition: transform var(--dur-fast) var(--ease); }
.btn:hover svg { transform: translateX(3px); }

/* 4. THE DROPDOWN. A <select> is styleable; the list it opens is not — that
   panel is drawn by the OS, which is why it arrived as a translucent macOS
   menu in the system font with a system-blue tick. assets/js/select.js builds
   a real listbox and hides the native control; `.sel` only exists once that
   has succeeded, so with no JS the native select is still there and the form
   still submits. */
.sel { position: relative; }
.sel select {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;   /* still submits; just not shown */
}
.sel__btn {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  width: 100%; background: transparent; cursor: pointer;
  border: 0; border-bottom: 1px solid var(--hairline); border-radius: 0;
  padding: .75rem 0; font-size: 1.02rem; letter-spacing: -.012em;
  text-align: left; color: inherit;
  transition: border-color var(--dur-fast) var(--ease);
}
.dark .sel__btn, .closing .sel__btn, .sec.dark .sel__btn { border-color: var(--hairline-d); }
.sel__btn:hover { border-color: currentColor; }
.sel.is-open .sel__btn { border-bottom-width: 2px; padding-bottom: calc(.75rem - 1px); }
.sel.is-empty .sel__val { color: var(--muted); }
.sec.dark .sel.is-empty .sel__val,
.closing .sel.is-empty .sel__val { color: rgb(244 242 237 / .5); }
.sel__caret { width: 10px; height: 6px; flex: none; transition: transform var(--dur) var(--ease); }
.sel.is-open .sel__caret { transform: rotate(180deg); }

/* Same glass as the nav dropdown — that panel is the site's existing answer
   to "a list floating over unpredictable artwork", so this borrows its
   material instead of inventing a second one. */
.sel__list {
  position: absolute; z-index: 60; left: 0; right: 0; top: calc(100% + 6px);
  margin: 0; padding: .35rem; list-style: none;
  background: rgb(255 255 255 / .92);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  color: var(--ink);
  border: 1px solid rgb(17 17 16 / .10); border-radius: 5px;
  box-shadow: 0 14px 40px rgb(17 17 16 / .18);
  max-height: 15rem; overflow: auto;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), visibility var(--dur-fast);
}
.sel.is-open .sel__list { opacity: 1; visibility: visible; transform: none; }
/* On a dark surface the panel is ink with light type — the same inversion the
   rest of the site uses, rather than a second palette. */
/* On a dark surface the tint inverts. .86 rather than something lighter is a
   contrast floor, not a taste call: this panel floats over the footer's chrome
   photograph, and a lighter wash leaves the option text near 3:1 over the
   brightest part of it. */
.dark .sel__list, .closing .sel__list, .sec.dark .sel__list {
  background: rgb(14 14 12 / .86);
  color: var(--on-dark);
  border-color: rgb(244 242 237 / .18);
  box-shadow: 0 14px 40px rgb(0 0 0 / .45);
}

/* No backdrop-filter (Firefox with it off, older Safari): each wash has to
   carry it alone, so both go opaque. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .closing .btn:hover { background: rgb(255 255 255 / .82); }
  .sel__list { background: var(--paper); }
  .dark .sel__list, .closing .sel__list, .sec.dark .sel__list { background: #14140F; }
}
.sel__opt {
  padding: .6rem .7rem; cursor: pointer; border-radius: 3px;
  font-size: .98rem; letter-spacing: -.01em;
}
.sel__opt.is-active { background: var(--ink); color: var(--paper); }
.dark .sel__opt.is-active, .closing .sel__opt.is-active, .sec.dark .sel__opt.is-active {
  background: var(--on-dark); color: var(--dark);
}
.sel__opt[aria-selected="true"] { font-weight: 500; }

/* ---- 3D scan portrait (about) — scan3d.js ---- */
.scan3d { position: relative; aspect-ratio: 4 / 5; overflow: hidden; background: #fff; }
.scan3d > img { display: block; width: 100%; height: 100%; object-fit: cover; transition: opacity .6s ease; }
.scan3d.is-live > img { opacity: 0; }
.scan3d__cv { position: absolute; inset: 0; width: 100% !important; height: 100% !important; }

/* ---- Scan portrait (about) ----------------------------------------------
   The host keeps the fallback <img> in flow so the box has its height before
   Three loads; the canvas is laid over it and the image fades once the cloud
   is live. --scan-ink / --scan-hot are read by scan.js. */
.scan { position: relative; aspect-ratio: 4 / 5; overflow: hidden; background: var(--paper-2);
        --scan-ink: var(--ink); --scan-hot: #FF5A1F; }
.scan > img { display: block; width: 100%; height: 100%; object-fit: cover; transition: opacity .9s ease; }
.scan.is-live > img { opacity: 0; }
.scan__cv { position: absolute; inset: 0; width: 100% !important; height: 100% !important; }
.dark .scan { background: var(--dark); --scan-ink: var(--on-dark); }


/* ==========================================================================
   LIMINAL — CASE-STUDY RHYTHM (round 4, 2 Oct 2026)

   The page was wall-to-wall media at one size: every row a two-up or a
   three-up of films, edge to edge of the wrap. These helpers give a case study
   the three things it was missing — rows of different width, columns of
   different weight, and text that sits beside the work instead of above it.

   They wrap a studio row; they never replace its grid class. The studio only
   owns what is between the markers, so a row dragged or re-gridded in the
   back office keeps working, and the wrapper just stops applying if its row
   no longer has two columns.

   Everything asymmetric lives behind `min-width: 761px` so the stacked phone
   layout in §responsive is untouched.
   ========================================================================== */
.lim-rows > * + * { margin-top: clamp(1rem, 2vw, 1.6rem); }
.lim-rows > .lim-gap { margin-top: clamp(2.5rem, 6vw, 5rem); }
.lim-sub { display: flex; gap: .8rem; align-items: baseline; margin-top: clamp(3rem, 7vw, 6rem); }
.lim-note { max-width: 44ch; color: var(--on-dark-mut); font-size: var(--t-body); line-height: 1.5; }
.d-md + .lim-note { margin-top: .9rem; }
.lim-plates .cs-figure img { box-shadow: inset 0 0 0 1px var(--hairline-d); outline: 1px solid var(--hairline-d); outline-offset: -1px; }
@media (min-width: 761px) {
  /* columns of unequal weight: --c is the template, e.g. "1fr 1.6fr" */
  .lim-cols > .g2, .lim-cols > .g3, .lim-cols > .g4 { grid-template-columns: var(--c, 1fr 1fr); align-items: start; }
  /* a row narrower than the wrap: --w is its width; --l / --r pin it to a side */
  /* On the WRAPPER, not the figure: every figure carries an inline `margin:0`
     (the studio writes it), and an inline margin beats any rule here — the
     first attempt set the margin on the figure and every row sat hard left. */
  .lim-w { width: var(--w, 72%); margin-inline: auto; }
  .lim-w.lim-w--l { margin-inline: 0 auto; }
  .lim-w.lim-w--r { margin-inline: auto 0; }
  /* copy beside the work */
  .lim-split { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--gut); align-items: end; }
  .lim-split--top { align-items: start; }
}
@media (max-width: 760px) {
  .lim-split > * + * { margin-top: 1.6rem; }
}

/* ==========================================================================
   KORRO case study, round 2 (2 Oct 2026)
   --------------------------------------------------------------------------
   The page reuses the layout helpers written for Liminal (.lim-split,
   .lim-cols, .lim-w). These are the few things Korro needs on top, and the
   light-page versions of the ones whose colours were written for a dark page.
   ========================================================================== */
.kor-rows > * + * { margin-top: clamp(1rem, 2vw, 1.6rem); }
.kor-rows > .kor-gap { margin-top: clamp(2.5rem, 6vw, 5rem); }
.kor-note { max-width: 44ch; color: var(--ink-2); font-size: var(--t-body); line-height: 1.5; }
.d-md + .kor-note { margin-top: .9rem; }
/* guideline pages are cream on a near-white page: a hairline gives them an edge */
.kor-plates .cs-figure img { outline: 1px solid var(--hairline); outline-offset: -1px; }
/* the client's headlines, set as a short list under the note */
.kor-lines { list-style: none; padding: 0; margin: 1.6rem 0 0; }
.kor-lines li { font-size: var(--t-md); line-height: 1.25; letter-spacing: -.02em; padding-block: .55rem; border-top: 1px solid var(--hairline); }
.kor-lines li:last-child { border-bottom: 1px solid var(--hairline); }
/* Stories in phones. The row is an ordinary three-up (grid still, story,
   story); this wrapper draws a phone frame round the two films. The screen is
   9:16, the same as the film, so nothing is cropped. On the wrapper and by
   position, not a class on the figure: the studio rewrites figures and would
   drop the class. */
.kor-phones .g3 > .cs-figure:has(> video) {
  width: min(100%, 19rem); margin-inline: auto !important;
  background: #16181b; border-radius: 2.3rem; padding: .55rem;
  box-shadow: 0 1.2rem 2.4rem rgba(20, 24, 30, .16);
}
.kor-phones .g3 > .cs-figure:has(> video) video { display: block; width: 100%; height: auto; aspect-ratio: 9 / 16; object-fit: cover; border-radius: 1.8rem; }
@media (min-width: 761px) {
  .kor-phones .g3 { align-items: center; }
}

/* ==========================================================================
   SHOP HEADER, TIGHTER — Dani, 5 Oct: "too much white space"
   Three places the air was coming from: the generic hero top padding (sized
   for pages that open on a huge headline alone), a film column tall enough to
   push the headline 230px below its eyebrow, and the "New" line under it,
   which is hidden for now (commented out in parts/shop.html). The stores now
   start inside the first screen.
   ========================================================================== */
.shophead { padding-top: clamp(5.75rem, 11vh, 7.5rem); }
.shophead__grid { margin-top: clamp(.25rem, 1vw, .75rem); }
/* Above the stacking breakpoint only: unscoped, this would override the
   single-column rule at 720px and put the film beside the headline on a phone. */
@media (min-width: 721px) {
  .shophead__grid { grid-template-columns: minmax(0, 1fr) clamp(200px, 23vw, 360px); }
}
/* Lowered twice more at Dani's request: clamp(1.25rem, 3vh, 2.25rem), then
   clamp(2.75rem, 7vh, 4.75rem), now this. */
.shophead + .sec--sm { padding-top: clamp(4.5rem, 12vh, 8rem); }

/* "Everything": at this size the r's arm and the y's left stroke nearly touch.
   The headline is tracked at -.035em and the font's own r-y kern cannot absorb
   that, so that one pair is opened up. The span wraps the r because
   letter-spacing is added AFTER a glyph. */
.shophead__h .kern-ry { letter-spacing: .005em; }

/* ==========================================================================
   WORK: THREE PROJECTS IN A ROW — Dani, 6 Oct, after mouthwash.studio/work
   Three across on a desktop, with a narrower gutter between the columns (set
   inline on the grid in parts/work.html). The grid is still a .g2 underneath,
   so between 761px and 1080px it stays two across, and on a phone it is one
   column, exactly as before.

   At three across a cover is about 440px wide on a laptop and 550px tall,
   which is under the 82svh height cap, so the covers now show at their full
   4:5 instead of being trimmed at the bottom as they were at two across.
   ========================================================================== */
@media (min-width: 1081px) {
  .archive { grid-template-columns: repeat(3, 1fr); }
  .archive .pcard__title { font-size: clamp(1.05rem, 1.35vw, 1.3rem); }
}


/* ============================ 2MRW ======================================
   The opener is a 3:2 photograph with the can on the right. Keep the can in
   frame when the opener crops to a phone. Scoped to this page
   (body.is-2mrw, set in build.py). */
body.is-2mrw .popener__media img { object-position: 74% 50%; }
/* The photograph is sunlit and busy: a pale newspaper under the title, a hand
   and a book on the centre line. So the facts row moves up under the
   breadcrumb, and the frame is shaded at the top and the bottom, only as much
   as the white type needs. */
body.is-2mrw .popener__media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to bottom, rgb(0 0 0 / .58), rgb(0 0 0 / 0) 42%),
    linear-gradient(to top, rgb(0 0 0 / .62), rgb(0 0 0 / 0) 40%);
}
body.is-2mrw .popener__mid { align-self: start; margin-top: clamp(8.5rem, 20vh, 11rem); }
@media (max-width: 760px) { body.is-2mrw .popener__mid { margin-top: 8.6rem; } }

/* The moving strip of social cards on the 2MRW page. */
.mq2 {
  width: calc(100vw - var(--sbw, 0px));
  margin-left: calc(50% - 50vw + var(--sbw, 0px) / 2);
  margin-top: clamp(2.5rem, 6vw, 5rem);
  padding-block: clamp(2rem, 5vw, 4.5rem);
  background: #222222; overflow: hidden;
}
.mq2__track { display: flex; gap: clamp(.8rem, 1.6vw, 1.5rem); width: max-content; padding-left: clamp(.8rem, 1.6vw, 1.5rem); animation: mq2 48s linear infinite; }
.mq2__card { margin: 0; flex: none; width: clamp(180px, 22vw, 340px); aspect-ratio: 9 / 16; border-radius: clamp(14px, 1.6vw, 26px); overflow: hidden; background: #333; }
.mq2__card img, .mq2__card video { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 0; }
@keyframes mq2 { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .mq2__track { animation: none; } .mq2 { overflow-x: auto; } }

/* ============================ 2MRW wall ================================
   The whole 2MRW page is one ruled wall, after the brand banner: cells edge
   to edge with a 2px ink line between them, four surfaces (cream, ink, navy,
   gradient), large serif lines and small mono tags. Photographs and films are
   cells in the same grid. Scoped to .tw, which only this page uses. */
.tw {
  --tw-ink: #222222; --tw-cream: #F6F1E8; --tw-navy: #273250;
  --tw-grad: linear-gradient(100deg, #AEEBE7 0%, #A9C5F0 50%, #B3A8F6 100%);
  --tw-pad: clamp(1.1rem, 2.2vw, 2.2rem);
  width: calc(100vw - var(--sbw, 0px));
  margin-left: calc(50% - 50vw + var(--sbw, 0px) / 2);
  background: var(--tw-ink); color: var(--tw-ink);
  overflow: hidden;
}
.popener + .tw { padding-block-start: 0; }
.tw__row { display: grid; grid-template-columns: var(--c, 1fr); }
.tw__c {
  position: relative; min-width: 0;
  box-shadow: 0 0 0 1px var(--tw-ink);
  padding: var(--tw-pad);
  display: flex; flex-direction: column; justify-content: space-between; gap: clamp(2rem, 5vw, 5rem);
}
.tw__c--cream { background: var(--tw-cream); }
.tw__c--paper { background: #F4EFE3; }
.tw__c--ink   { background: var(--tw-ink); color: var(--tw-cream); }
.tw__c--navy  { background: var(--tw-navy); color: var(--tw-cream); }
.tw__c--grad  { background: var(--tw-grad); }
.tw__c--media { padding: 0; gap: 0; justify-content: stretch; }
.tw__c--media .cs-figure { margin: 0; flex: 1; display: flex; overflow: hidden; }
.tw .cs-figure img, .tw .cs-figure video { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 0; }
.tw__c--contain .cs-figure img { object-fit: contain; padding: var(--tw-pad); }
.tw__c--45 { aspect-ratio: 4 / 5; }
.tw__c--bar { padding-block: clamp(.8rem, 1.2vw, 1.1rem); }
.tw__tag { margin: 0; font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; }
.tw__split { display: flex; justify-content: space-between; gap: 1rem; }
.tw__split--bar { padding: clamp(.7rem, 1vw, 1rem) var(--tw-pad); box-shadow: 0 1px 0 0 var(--tw-ink), 0 2px 0 0 var(--tw-ink); position: relative; z-index: 1; }
.tw__big { margin: 0; font-family: 'Editorial New', var(--serif); font-weight: 400; letter-spacing: -.02em; line-height: .92; }
.tw__big--xl { font-size: clamp(3.4rem, 9.2vw, 9.6rem); }
.tw__big--lg { font-size: clamp(2.6rem, 5.4vw, 5.6rem); }
.tw__big--sm { font-size: clamp(1.7rem, 2.7vw, 2.9rem); line-height: 1; }
.tw__note { margin: clamp(1rem, 2vw, 1.6rem) 0 0; max-width: 40ch; font-size: var(--t-body); line-height: 1.5; opacity: .86; }
.tw__caps { margin: 0; font-size: clamp(1rem, 1.5vw, 1.45rem); line-height: 1.22; letter-spacing: -.005em; text-transform: uppercase; font-weight: 500; }
.tw__icons { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(.8rem, 1.6vw, 1.6rem); }
.tw__icon { display: block; width: 100%; height: auto; }
.tw__wm { display: block; width: 100%; height: auto; }
.tw__c--fact { gap: clamp(1.6rem, 3.4vw, 3.4rem); }
.tw__sw { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.tw__sw li { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: .7rem 0; border-top: 1px solid rgb(246 241 232 / .3);
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; }
.tw__sw li::before { content: ""; flex: none; width: clamp(2.4rem, 5vw, 4.6rem); height: 1.5rem; background: var(--s, transparent); border: 1px solid rgb(246 241 232 / .4); }
.tw__sw li span:first-of-type { margin-right: auto; }
.tw--strip .mq2 { width: 100%; margin: 0; }
@media (max-width: 900px) {
  .tw__row { grid-template-columns: 1fr; }
  .tw__row--facts, .tw__row--credits { grid-template-columns: 1fr 1fr; }
  .tw__row--posters { grid-template-columns: 1fr; }
  .tw__c { gap: 2rem; }
  .tw__c--fact { min-height: 0; }
}
.tw__c--obj { aspect-ratio: 4 / 5; }
.tw__c--obj .tw__tag--on { color: #F6F1E8; text-shadow: 0 0 12px rgb(0 0 0 / .55); }
.tw__tag--on { position: absolute; left: var(--tw-pad); top: var(--tw-pad); z-index: 1; }
@media (max-width: 900px) {
  .tw__c--45:not(.tw__c--media) { aspect-ratio: auto; min-height: 15rem; }
  .tw__row--objs { grid-template-columns: repeat(3, 1fr); }
  .tw__row--objs .tw__tag--on { left: .5rem; top: .5rem; font-size: .56rem; }
}

/* 2MRW: the landscape crossword film sits in a 4:5 cell; keep the can and the hands in the crop */
body.is-2mrw .tw video[poster*="m-crossword"] { object-position: 66% 50%; }
