/* ==========================================================================
   HOMEPAGE ART-DIRECTION LAYER
   Loaded only by index.html, after site.css. Nothing here affects other pages.
   Delete this file and the homepage falls back to the base design system.
   ========================================================================== */

/* --- 0. GRAIN — sits above everything, ignores pointer events -------------- */
.grain {
  position: fixed; inset: -50%; z-index: 150; pointer-events: none;
  opacity: .038; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grainShift 8s steps(6) infinite;
}
@keyframes grainShift {
  0%   { transform: translate(0,0); }
  20%  { transform: translate(-1.5%, 1%); }
  40%  { transform: translate(1%, -1.5%); }
  60%  { transform: translate(-1%, -1%); }
  80%  { transform: translate(1.5%, 1.5%); }
  100% { transform: translate(0,0); }
}

/* --- 1. CUSTOM CURSOR — desktop, fine pointer only ------------------------ */
/* Two elements on purpose: the outer one is moved by rAF every frame (no
   transition, or it would fight the animation loop), the inner one owns the
   scale and colour transitions. */
/* Cursor styles moved to site.css — it is loaded on every page, and the
   cursor is not homepage-only any more. */
@media (hover: none), (pointer: coarse) { .cursor { display: none; } }

/* --- 2. HERO — full viewport, type-led ----------------------------------- */
.vhero {
  min-height: 100svh;
  padding-top: clamp(5.5rem, 12vh, 8rem);
  padding-bottom: 2rem;
  display: flex; flex-direction: column;
  position: relative;
  overflow: hidden;
}

/* --- 2a. HERO BACKGROUND SLOT -------------------------------------------
   Empty and inert until a treatment is dropped in, so the hero looks exactly
   as it did before. Everything a background needs is set up here:

   - It sits at z-index 0 under the content (which is z-index 1), and under
     the fixed nav (100), grain (150) and cursor (140) — no stacking surprises.
   - `--hero-scrim` fades paper back in behind the type, strongest at the
     top-left where the headline sits.
   - Anything inside is forced to cover the box, so a treatment can't letterbox.
   - Under reduced-motion any animation inside is stopped rather than the
     background being hidden — removing it entirely would change the layout's
     contrast, which is worse than a still image.

   To use: put an <img>, <canvas>, <video> or a background on .vhero__bg. */
.vhero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;   /* never steals a click from the masthead links */
  /* No background of its own — an empty slot must be invisible */
}
.vhero__bg > * {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.vhero__video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The readability guard. Only paints once the slot has something in it — the
   slot is empty right now, so none of this is active. Kept because it is the
   hard-won part: it was measured against a real moving background, and those
   numbers are the reason it works.

   TWO gradients, because one is not enough for a background that is pale at
   the top and dark at the bottom, and the hero has text at both ends:
     - the angled pass protects the headline (top-left), where the outlined
       `-webkit-text-stroke` line is the first thing to fail;
     - the vertical pass protects the base row (status, clock, scroll cue),
       which is --muted #656158 and would otherwise sit on near-black grass.
   Values were measured across every frame of a moving background, not a single
   still — a moving image's worst frame is rarely the one you would screenshot.
   At .86 the headline bottomed out at 4.44:1; .90 gives 4.72:1. That was tuned
   when the third headline line was a 1.15px `-webkit-text-stroke` outline,
   which behaves like thin text and owes 4.5:1. Every line is solid serif now,
   so the headline is unambiguously large text and only owes 3:1 — .90 is
   generous, and .80 would let considerably more through.
   Raise --hero-scrim toward 1 to wash out further. */
.vhero__bg:not(:empty)::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(to top,
      rgb(255 255 255 / var(--hero-scrim-base, .92)) 0%,
      rgb(255 255 255 / calc(var(--hero-scrim-base, .92) * .45)) 22%,
      transparent 42%),
    linear-gradient(105deg,
      rgb(255 255 255 / var(--hero-scrim, .90)) 0%,
      rgb(255 255 255 / calc(var(--hero-scrim, .90) * .66)) 46%,
      rgb(255 255 255 / calc(var(--hero-scrim, .90) * .28)) 100%);
}
/* When — and only when — the slot has a background in it, darken the hero's
   small type from --muted to --ink-2.

   This is measured, not taste. Over the grass loop the base row (status, clock,
   scroll cue) lands at 2.45:1 in --muted, against a 4.5:1 requirement. The
   obvious fix — a stronger bottom scrim — does not work: that text sits in the
   gradient's mid-band, so even a FULLY OPAQUE bottom stop only reaches 3.74:1,
   and getting there erases the most interesting part of the image anyway.
   Darkening the text instead reaches 5.92:1 and leaves the picture intact.

   Scoped with :has() so it is self-managing — empty the slot and the muted
   type comes back on its own. */
.vhero:has(.vhero__bg:not(:empty)) .meta,
.vhero:has(.vhero__bg:not(:empty)) .masthead a,
.vhero:has(.vhero__bg:not(:empty)) .masthead span {
  color: var(--ink-2);
}
.vhero:has(.vhero__bg:not(:empty)) .masthead a:hover { color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .vhero__bg *, .vhero__bg::after { animation: none !important; transition: none !important; }
}
/* On a phone the type occupies nearly the whole screen, so it needs more
   protection, not less — the background becomes texture rather than image. */
@media (max-width: 720px) {
  .vhero__bg { --hero-scrim: .92; --hero-scrim-base: .95; }
}

/* Masthead index — the magazine contents strip */
.masthead {
  display: flex; gap: clamp(1rem, 3vw, 2.6rem); flex-wrap: wrap;
  padding-bottom: 1.1rem; margin-bottom: clamp(1.6rem, 5vh, 3.2rem);
  border-bottom: 1px solid var(--hairline);
}
.masthead a, .masthead span {
  font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .13em; text-transform: uppercase;
  color: var(--muted); display: flex; gap: .5rem; align-items: baseline;
}
.masthead a { transition: color var(--dur-fast) var(--ease); }
.masthead a:hover { color: var(--ink); }
.masthead b { font-weight: 400; color: var(--ink); }
.masthead__end { margin-left: auto; }

/* The type stack */
.vhero__type { flex: 1; display: flex; flex-direction: column; justify-content: center; }
.vline {
  /* One face, one weight, no italic, no outline. The hero used to alternate
     solid sans → serif italic → outlined stroke → solid across four lines;
     four treatments in four lines reads as a sampler rather than a voice.

     Two things follow from dropping the outlined line, and both are wins:
     the `-webkit-text-stroke` line was the single hardest thing on the page to
     keep legible over any background (it is why the hero scrim is tuned to
     4.5:1 rather than the 3:1 large text is entitled to), and the serif at 400
     needs no synthetic weight matching between two families. */
  font-family: var(--serif);
  font-weight: 400;
  /* min() against vh as well as vw — four lines at 8.6vw overflow a 100svh
     hero on a short laptop screen. */
  font-size: clamp(2.6rem, min(8.6vw, 13vh), 8.4rem);
  /* Serif at 400 needs air that a 600-weight sans does not: 0.88 was set for
     a condensed display sans and collides the ascenders and descenders here. */
  line-height: 1.02;
  letter-spacing: -.022em;
  display: block;
  overflow: hidden;
  /* Descenders (g, y, p) would clip against the mask edge without this */
  padding-bottom: .1em;
  margin-bottom: -.1em;
}
.vline > span {
  display: block;
  transform: translateY(105%);
  animation: lineUp 1.15s var(--ease-out) forwards;
  animation-delay: var(--d, 0ms);
}
@keyframes lineUp { to { transform: translateY(0); } }

/* Rotating discipline word — reserves its own width so nothing reflows */
.rotator {
  display: inline-grid;
  vertical-align: bottom;
  overflow: hidden;
  height: 1.35em;
  align-items: center;
  position: relative;
}
.rotator span {
  grid-area: 1 / 1;
  opacity: 0;
  transform: translateY(70%);
  transition: opacity .45s var(--ease-out), transform .55s var(--ease-out);
  white-space: nowrap;
}
.rotator span.is-live { opacity: 1; transform: none; }
.rotator span.is-out { opacity: 0; transform: translateY(-70%); }

/* Hero base row */
.vhero__base {
  display: grid; grid-template-columns: minmax(0,1.15fr) minmax(0,1fr) auto;
  gap: var(--gut) clamp(1.5rem, 4vw, 4rem);
  align-items: end;
  padding-top: clamp(1.6rem, 4vh, 2.6rem);
  margin-top: clamp(1.6rem, 4vh, 2.6rem);
  border-top: 1px solid var(--hairline);
}
.status { display: inline-flex; align-items: center; gap: .55rem; }
.status__dot {
  width: 7px; height: 7px; border-radius: 50%; background: #3E9B44; flex: none;
  box-shadow: 0 0 0 0 rgba(62,155,68,.55);
  animation: ping 2.6s var(--ease) infinite;
}
@keyframes ping {
  0%   { box-shadow: 0 0 0 0 rgba(62,155,68,.5); }
  70%  { box-shadow: 0 0 0 9px rgba(62,155,68,0); }
  100% { box-shadow: 0 0 0 0 rgba(62,155,68,0); }
}

/* Scroll cue */
.cue { display: flex; align-items: center; gap: .6rem; }
.cue__line { width: 1px; height: 44px; background: var(--hairline); position: relative; overflow: hidden; }
.cue__line::after {
  content: ""; position: absolute; inset: 0; background: var(--ink);
  animation: cueRun 2.1s var(--ease) infinite;
}
@keyframes cueRun {
  0%   { transform: translateY(-100%); }
  55%  { transform: translateY(0); }
  100% { transform: translateY(100%); }
}

/* --- 3. SECTION INDICATOR — fixed side gutter --------------------------- */
.sidx {
  position: fixed; left: max(.75rem, calc((100vw - var(--max)) / 2 - 2.2rem));
  top: 50%; transform: translateY(-50%); z-index: 90;
  display: grid; gap: .75rem;
  opacity: 0; transition: opacity .5s var(--ease);
}
.sidx.is-on { opacity: 1; }
.sidx a {
  display: flex; align-items: center; gap: .55rem;
  font-family: var(--mono); font-size: .58rem; letter-spacing: .13em; color: var(--muted);
}
.sidx i { display: block; width: 14px; height: 1px; background: currentColor; transition: width .35s var(--ease), background .35s var(--ease); }
.sidx a:hover i, .sidx a.is-active i { width: 30px; }
.sidx a.is-active { color: var(--ink); }
.sidx__label { opacity: 0; transform: translateX(-4px); transition: opacity .3s var(--ease), transform .3s var(--ease); }
.sidx a:hover .sidx__label, .sidx a.is-active .sidx__label { opacity: 1; transform: none; }
/* Hidden until there's genuine gutter space — it must never crowd the content */
@media (max-width: 1560px) { .sidx { display: none; } }

/* --- 4. WORK — offset asymmetric grid ---------------------------------- */
/* The .wgrid / .wcard component was deleted with the twelve-card homepage
   grid it existed for. Selected work is now four offset tiles — see
   section 24, .pgal. Nothing else on the site used these classes; the
   Work page archive has always used .pcard, which is untouched. */

/* --- 5. CAPABILITY PREVIEW — hover-linked image panel ------------------ */
/* The capability/preview component MOVED to site.css — it is used on the
   homepage AND on the Trends page now, and home.css loads on one page only.
   Same lesson as .shead: if a class appears in more than one part file it
   belongs in the shared sheet. */

/* --- 6. STATEMENT — giant serif, scroll-masked ------------------------- */
.stmt { padding-block: clamp(5rem, 16vh, 12rem); }
.stmt__t {
  font-family: var(--serif); font-weight: 400; font-style: italic;
  font-size: clamp(2.4rem, 6.8vw, 6.4rem);
  line-height: .98; letter-spacing: -.032em;
  max-width: 24ch;
}
.stmt__t em { font-style: normal; font-family: var(--sans); font-weight: 600; letter-spacing: -.04em; }
/* Word-level reveal */
.w {
  display: inline-block; overflow: hidden; vertical-align: bottom;
  padding-bottom: .12em; margin-bottom: -.12em; /* protect descenders */
}
.w > span { display: inline-block; transform: translateY(102%); transition: transform .9s var(--ease-out); transition-delay: var(--wd, 0ms); }
.is-in .w > span { transform: none; }

/* --- 7. TICKER (Agent Scanning typewriter) ---------------------------- */
.tick { font-family: var(--mono); font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase; }
.tick__c {
  display: inline-block; width: 7px; height: 1em; background: currentColor;
  vertical-align: -.14em; margin-left: .25rem;
  animation: blink 1.05s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* --- 8. SIGNAL ROWS on home ------------------------------------------ */
.hsig {
  display: grid; grid-template-columns: 5.5rem minmax(0,1fr) auto; gap: var(--gut);
  align-items: baseline; padding-block: 1.4rem;
  border-bottom: 1px solid var(--hairline-d);
}
.hsig__h { font-size: clamp(1.05rem,1.75vw,1.45rem); font-weight: 600; letter-spacing: -.028em; line-height: 1.12; }
@media (max-width: 760px) {
  .hsig { grid-template-columns: 1fr; gap: .45rem; padding-block: 1.2rem; }
}

/* --- 8b. DITHER BLOOM — the flat-field halftone section ---------------- */
/* One flat colour field, edge to edge, with a 1-bit dithered flower on it.
   The field colour comes from Danielle's project accents, not the reference's
   mint — same technique, her palette. Swap --dither-field to change it. */
.dsec {
  /* Field and ink are the only two knobs. Alternatives that all read well:
     acid    --dither-field:#CFF24D
     sage    --dither-field:#9DBE86
     invert  --dither-field:#111110; --dither-ink:#CFF24D  */
  --dither-field: #F7FFA2;
  --dither-ink: #111110;
  background: var(--dither-field);
  color: var(--dither-ink);
  position: relative;
  overflow: hidden;
  padding-block: clamp(2.2rem, 4.5vh, 3.4rem) 0;
}

/* Heavier grain than the page default — on a flat field it does real work */
.dsec::after {
  content: ""; position: absolute; inset: 0; z-index: 3;
  pointer-events: none; opacity: .16; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grainShift 7s steps(5) infinite;
}

.dsec .meta { color: color-mix(in srgb, var(--dither-ink) 62%, transparent); }
.dsec__head {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: minmax(0,1fr) auto;
  gap: .6rem var(--gut); align-items: baseline;
  padding-bottom: .9rem;
  border-bottom: 1px solid color-mix(in srgb, var(--dither-ink) 22%, transparent);
}

/* The stage. Canvas is full-bleed; the type sits above it. */
.dsec__stage {
  position: relative; z-index: 1;
  margin-top: clamp(1rem, 2.5vh, 1.6rem);
  cursor: crosshair;   /* signals "this is a surface, not a picture" */
}
.dsec__hint {
  position: absolute; z-index: 2; left: var(--gut); bottom: .9rem;
  pointer-events: none;
  transition: opacity .6s var(--ease);
}
.dsec__stage:hover .dsec__hint { opacity: 0; }

/* On touch there is no hover, so tell the truth about the gesture */
@media (hover: none), (pointer: coarse) {
  .dsec__stage { cursor: default; }
  .dsec__hint::after { content: " · drag on the field"; }
}
@media (prefers-reduced-motion: reduce) {
  .dsec__hint { display: none; }
  .dsec__stage { cursor: default; }
}
.dsec canvas {
  display: block;
  width: 100%;
  /* A field needs a bit more room than a single stem did, but still a band
     rather than a full screen */
  height: clamp(340px, 56vh, 560px);
  /* Chunky cells: never let the browser interpolate them */
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
}
.dsec__foot {
  position: relative; z-index: 2;
  display: flex; flex-wrap: wrap; gap: .6rem 1.6rem;
  padding-bottom: clamp(1.1rem, 2.5vh, 1.8rem);
}
.dsec__foot span:last-child { margin-left: auto; }

@media (max-width: 760px) {
  .dsec { padding-top: 1.8rem; }
  .dsec__head { grid-template-columns: 1fr; gap: .4rem; align-items: start; }
  .dsec canvas { height: clamp(260px, 40vh, 340px); }
  .dsec__foot span:last-child { margin-left: 0; }
  .dsec::after { opacity: .12; }
}

@media (prefers-reduced-motion: reduce) {
  .dsec::after { animation: none; }
}
@media (prefers-contrast: more) {
  .dsec::after { display: none; }
}

/* --- 9. NUMBERED SECTION HEAD ---------------------------------------- */
/* MOVED to site.css. It was never homepage art direction — Services has used
   it since it was written and the Liminal case study uses it now, and home.css
   is the only stylesheet loaded on exactly one page. The symptom was quiet:
   .shead fell back to plain block layout, so the number ran into the title
   ("02VOICE") and the right-hand column dropped under the left instead of
   sitting opposite it. On Services that had been shipping unnoticed. */

/* --- 10. MOBILE — rebuild, don't stack ------------------------------- */
@media (max-width: 760px) {
  .vhero { min-height: auto; padding-top: 6.2rem; padding-bottom: 3rem; }
  .vline { font-size: clamp(2.5rem, 13vw, 4rem); letter-spacing: -.04em; }
  .vhero__base { grid-template-columns: 1fr; gap: 1.4rem; }
  .cue { display: none; }
  .masthead { gap: .55rem 1.1rem; }
  .masthead__end { margin-left: 0; }
  .grain { opacity: .028; }
  .stmt { padding-block: 4rem; }
  .stmt__t { max-width: 100%; }
}
@media (max-width: 460px) {
}

/* --- 11. REDUCED MOTION — everything above goes quiet ---------------- */
@media (prefers-reduced-motion: reduce) {
  .grain { animation: none; opacity: .03; }
  .cursor { display: none !important; }
  .vline > span { transform: none !important; animation: none !important; }
  .rotator { height: auto; overflow: visible; }
  .rotator span { position: static; opacity: 1; transform: none; transition: none; }
  .rotator span:not(:first-child) { display: none; }
  .cue__line::after { animation: none; transform: none; }
  .status__dot { animation: none; box-shadow: none; }
  .tick__c { animation: none; opacity: 1; }
  .w > span { transform: none !important; transition: none; }
  .crow__d { grid-template-rows: 1fr !important; opacity: 1 !important; padding-top: .8rem; }
    .cpx__list:hover .crow:not(:hover) .crow__h { opacity: 1; }
  .sidx { opacity: 1; }
}

/* Users who prefer high contrast get the outline line filled in */
@media (prefers-contrast: more) {
    .grain { display: none; }
}

/* --- 15. PARTNERS — names as typography -----------------------------------
   A logo wall flattens every client to the same visual weight and needs an
   asset per name. Set as type they keep the page's voice and the list grows
   for free. Each name lifts to full ink on hover, so it rewards reading
   without becoming a link that goes nowhere. */
/* Real clients are full ink; the unfilled placeholders stay grey so they read
   as gaps to fill rather than as quieter clients. */
/* The names no longer change colour on hover — with a black base there is
   nowhere darker to go, and the preview card that follows the pointer is
   already the feedback. */

/* --- 16. APPROACH — three marks ------------------------------------------- */
.appr {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(2.4rem, 5vw, 4.5rem) var(--gut);
}
.appr__fig {
  color: var(--ink);
  margin-bottom: clamp(1.4rem, 3vh, 2.2rem);
  /* The marks are decorative; keep them from ever out-shouting the copy */
  opacity: .9;
}
.appr__fig svg { width: 100%; height: auto; display: block; max-width: 300px; }
.appr__n { display: block; margin-bottom: .5rem; }
.appr__h {
  font-size: clamp(1.5rem, 3vw, 2.3rem);
  letter-spacing: -.03em; font-weight: 600; line-height: 1;
  margin-bottom: .7rem;
}
.appr__d { color: var(--ink-2); max-width: 34ch; line-height: 1.5; }
@media (max-width: 900px) {
  .appr { grid-template-columns: 1fr; gap: clamp(2rem, 6vh, 3rem); }
  .appr__fig svg { max-width: 180px; }
}

/* --- 17. TREND INTELLIGENCE — split with linked preview -------------------
   Same mechanic as the old capabilities panel, moved here because this is the
   section that earns it: an image genuinely helps you choose which signal to
   read. Capabilities lost its panel so the page doesn't repeat the trick. */
.tsplit {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.tsplit__stack {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--ink-2);
}
.tsplit__stack img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.tsplit__stack img.is-live { opacity: 1; }
.tsplit__cap {
  display: flex; flex-wrap: wrap; gap: .3rem 1rem;
  align-items: baseline; justify-content: space-between;
  padding-top: .8rem;
}
.tsplit__caph { color: var(--on-dark); font-size: 1.02rem; letter-spacing: -.01em; }

.tsplit__list { list-style: none; display: grid; }
.titem {
  display: block;
  padding: clamp(1rem, 2.4vh, 1.7rem) 0;
  border-bottom: 1px solid rgb(255 255 255 / .14);
  transition: opacity var(--dur-fast) var(--ease);
}
.tsplit__list li:first-child .titem { border-top: 1px solid rgb(255 255 255 / .14); }
.titem__cat { display: block; margin-bottom: .45rem; }
.titem__h {
  display: block;
  color: var(--on-dark);
  font-size: clamp(1.25rem, 2.5vw, 2rem);
  line-height: 1.08; letter-spacing: -.028em; font-weight: 500;
}
/* The active item gets a bright rule, and the inactive ones are left alone.

   Dimming the siblings was the first instinct and it cannot be made
   accessible here: the category label is small text at --on-dark-mut, so it
   needs 4.5:1, and it only clears that above 0.82 opacity — by which point
   the dim is invisible anyway. Fading the title alone fails too, because at
   its clamp minimum (20px / weight 500) it is not "large text" and is held to
   4.5:1 as well. The reference doesn't dim either; it rules the active item. */
.titem { border-bottom-color: rgb(255 255 255 / .14); }
.titem.is-on { border-bottom-color: var(--on-dark); }
.titem.is-on .titem__cat { color: var(--on-dark); }
.titem:focus-visible { outline: 2px solid var(--hl); outline-offset: 4px; }

.tsplit__foot {
  display: flex; flex-wrap: wrap; gap: .6rem var(--gut);
  align-items: baseline; justify-content: space-between;
  margin-top: clamp(1.6rem, 4vh, 2.6rem);
}
.alink--dark { color: var(--on-dark); }

@media (max-width: 900px) {
  .tsplit { grid-template-columns: 1fr; }
  /* On touch there is no hover, so the image is decoration with no mechanism
     behind it — show it once, above the list, and let the list do the work. */
  .tsplit__view { margin-bottom: 1.4rem; }
}
@media (prefers-reduced-motion: reduce) {
  .tsplit__stack img { transition: none; }
}

/* --- 18. PARTNER HOVER PREVIEW -------------------------------------------
   A small card that follows the pointer while a partner name is hovered.

   It sits at z-index 130 — above the page, below the custom cursor (140) and
   the grain (150), so the cursor bubble never ends up behind it.

   pointer-events: none is load-bearing: without it the card would sit under
   the pointer, steal the hover from the name underneath, and flicker as the
   two fought each other. */

/* The hovered name goes to full ink and its siblings stay put — same reasoning
   as the signals list: dimming small grey type fails contrast long before the
   effect is visible. */

/* No pointer, no mechanism — and a card that chases the cursor is exactly the
   kind of motion reduced-motion users are asking not to see. */

/* --- 19. CHROME MARKS -----------------------------------------------------
   The inline SVG marks are the base state, not a fallback bolted on after:
   they render with no JavaScript, no WebGL, on save-data, on a weak device
   and under reduced motion — every path where chrome.js declines to load.

   When it does load it adds .is-chrome, which hides the SVGs and reveals one
   canvas stretched across the whole row. Each mark is drawn into its own
   scissored viewport inside that single canvas, so there is one WebGL context
   for the section rather than three. */
.appr { position: relative; }
.chrome-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;      /* the row is not interactive; hover is read from pointer position */
  z-index: 1;
}
.appr.is-chrome .appr__fig svg { visibility: hidden; }
/* The figure keeps its box so the grid does not reflow when chrome takes over */
.appr.is-chrome .appr__fig { opacity: 1; }
.appr__col { position: relative; z-index: 2; }
.appr__fig { position: relative; z-index: 0; }

/* --- 20. HERO CHROME OBJECT ----------------------------------------------
   Type keeps the left, the object takes the right. Beside the headline rather
   than behind it, so no scrim is needed and both stay at full strength — the
   grass-loop test showed that a hero with type at the top AND text at the
   bottom leaves a full-bleed background nowhere to live.

   The column has a width but no content until chrome.js succeeds, so the
   headline measure is identical either way and nothing reflows on load. */
.vhero__stack {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .58fr);
  gap: clamp(1rem, 4vw, 3.5rem);
  align-items: center;
}
.vhero__type { flex: initial; }
.vhero__obj {
  position: relative;
  aspect-ratio: 1;
  align-self: center;
  min-height: 0;
}
.vhero__obj canvas { display: block; width: 100%; height: 100%; }

/* Below this the headline needs the whole measure, and a 141k-triangle model
   on a phone GPU is a poor trade for decoration. */
@media (max-width: 900px) {
  .vhero__stack { grid-template-columns: 1fr; }
  .vhero__obj { display: none; }
}

/* --- 21. NO SELECTION FLASH ON THE 3D ------------------------------------
   Dragging across a canvas makes the browser start a text selection in the
   surrounding content, which paints the default blue highlight straight over
   the object. The canvases are decorative and the hover is read from global
   pointer position rather than from the element, so they can opt out of
   pointer interaction entirely — which removes the flash at the source rather
   than recolouring it. */
.vhero__obj, .appr {
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.vhero__obj canvas, .chrome-canvas {
  pointer-events: none;
  -webkit-user-drag: none;
}

/* --- 22. OPENER FILM ------------------------------------------------------
   The page now opens on the film rather than the headline. It gets the top
   padding the hero used to carry, so it clears the fixed nav. */
.opener {
  padding-top: clamp(5.5rem, 12vh, 8rem);
  padding-bottom: var(--sec-sm);
}
.opener__film {
  width: 100%;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  display: block;
  background: var(--ink);   /* the poster decodes into this, never a white flash */
}
/* The hero no longer needs a full viewport — the film above it already owns
   the first screen, and 100svh here would push the headline entirely below
   the fold on a laptop. */
.vhero { min-height: auto; padding-top: clamp(2rem, 5vh, 4rem); }
@media (min-width: 900px) {
  .vhero { min-height: 78svh; }
}

/* --- TREND INTELLIGENCE ON WHITE -----------------------------------------
   This section was authored against the ink surface, so its components hard-code
   on-dark colours and 14%-white hairlines rather than inheriting them. Dropping
   `.dark` from the section therefore did NOT make it readable — it left white
   type on a white page (measured 1.12:1) and hairlines that vanished entirely.

   Everything below is the same design re-pointed at the light surface. Kept as
   an override block rather than edited in place, so the components still work on
   a dark surface anywhere else they are used. */
#trends .meta { color: var(--muted); }
#trends .meta--ink { color: var(--ink); }
#trends .body-lg,
#trends .u-mut { color: var(--ink-2); }
#trends .tsplit__caph { color: var(--ink); }
#trends .titem__h { color: var(--ink); }
#trends .titem { border-bottom-color: var(--hairline); }
#trends .tsplit__list li:first-child .titem { border-top-color: var(--hairline); }
#trends .titem.is-on { border-bottom-color: var(--ink); }
#trends .titem.is-on .titem__cat { color: var(--ink); }
#trends .alink--dark { color: var(--ink); }
#trends .alink--dark::after { background: var(--ink); }
#trends .ph {
  background: rgba(27,59,232,.09); color: var(--accent);
  border-color: rgba(27,59,232,.45);
}
#trends .tick { color: var(--muted); }
#trends .scanning { color: var(--muted); }
#trends .shead__l .meta:last-child { color: var(--ink); }

/* Secondary type on the sky steps one shade darker. `--muted` is tuned for the
   white page and measured 3.71–4.29:1 once the tint reaches #C9D6E7 — the same
   grey that is fine at the top of the section fails at the bottom of it, which
   is the hazard of putting type on any gradient. This clears 5.3:1 at the
   darkest point type reaches and 9.3:1 up on the white, while staying visibly
   lighter than the ink so the hierarchy survives.

   Written with #trends, not .sky: the earlier trends-on-white overrides in this
   file already use the ID, and an ID beats two classes — a `.sky .meta` rule
   was silently losing to `#trends .meta` a few hundred lines above. */
#trends { --muted-sky: #4A4740; }
#trends .meta,
#trends .u-mut,
#trends .tick,
#trends .scanning,
#trends .body-lg,
#trends .titem__cat { color: var(--muted-sky); }
#trends .meta--ink,
#trends .shead__l .meta:last-child { color: var(--ink); }



/* --- THE OPENER FILM, EDGE TO EDGE ---------------------------------------
   Full bleed by STRUCTURE — the film sits outside .wrap and simply fills its
   section — rather than by the `100vw; margin-left: 50%; translateX(-50%)`
   trick. That trick is wrong here on two counts: 100vw includes the scrollbar
   on desktop, so it overflows by ~15px and gives the page a horizontal scroll;
   and it needs the parent NOT to clip, while this element's own reveal wraps
   it in `overflow: hidden`.

   The caption stays on the grid — a full-bleed image with its label pinned to
   the page's left margin is the point of the layout. */
.opener__bleed { width: 100%; }

/* The film starts at y=0 — no clearance for the header, which floats over it.
   That is the whole point of a fixed pill nav: it is designed to sit ON the
   artwork, and reserving a band of white above the frame to avoid it wastes
   the one moment the page gets to open on an image. */
.opener { padding-top: 0; }

/* Sized against the VIEWPORT, not by ratio. An aspect-ratio makes the height a
   function of the window's width, so the same page opens on a 640px band at
   1512px wide and a 400px one on a laptop — the opening frame changes size for
   reasons that have nothing to do with the design. A height leaves the crop to
   object-fit, which is what it is for.

   88svh rather than 100: the sliver of the caption and the headline below the
   fold is what tells you the page continues. A frame that exactly fills the
   screen reads as a splash screen, and people wait on it instead of scrolling.

   svh, not vh — on mobile `vh` is the LARGEST viewport height, so with the
   browser chrome showing, a 100vh element is taller than the screen and the
   bottom of the frame is cut off until you scroll. */
.opener__film {
  height: 88svh;
  min-height: 420px;
  aspect-ratio: auto;
  max-height: none;
}
@media (max-width: 760px) {
  .opener__film { height: 72svh; min-height: 340px; }
}

/* The caption sat tight under the frame when the frame was a band in the middle
   of white space. Now that the film owns the screen, the caption is the first
   thing on the page proper and needs room to be that. */
.opener .wrap > div { padding-top: 1.1rem; }

/* --- 23. FULL-SCREEN OPENER + MICRO TYPE ---------------------------------
   The film now owns the whole first screen. It was 88svh, deliberately, so a
   sliver of the next thing showed and told you the page continued — with the
   caption deleted there is nothing left to peek, so the cue moved onto the
   film itself as a word. That is what `.omicro__cue` is for; without it a
   100svh film reads as a splash screen and people wait on it.

   svh, not vh: on mobile `vh` is the LARGEST viewport height, so with browser
   chrome showing a 100vh element is taller than the screen and its bottom is
   cut off until you scroll. */
.opener__film {
  height: 100svh;
  min-height: 460px;
}
@media (max-width: 760px) {
  .opener__film { height: 92svh; min-height: 380px; }
}

/* Type on moving footage cannot rely on the footage. The scrim is the only
   thing that makes this legible on a frame that happens to cut to white —
   measured against the opener's own brightest frame, not assumed. */
/* NO SCRIM, NO SHADOW — Dani's call, made with the numbers in front of her.

   Both existed to solve a real measured problem, and removing them brings it
   back, so the cost is recorded here rather than lost. The rule ("Selected
   work" / the year range) sits high in this box, and the banner runs bright
   underneath it: sampled frame by frame, the film peaks at luminance .677
   behind the left end at t=31s — the Liminal shot — which is **1.3:1** against
   white type. For roughly a quarter of the 34 seconds one end of the rule is
   below 3:1.

   It is decorative and aria-hidden, and every word of it appears as real text
   elsewhere on the page, so nothing is lost to a screen reader or to anyone who
   cannot read it — which is what makes this a defensible design choice rather
   than a bug. It is still worth knowing that the words disappear on the bright
   frames, and they do.

   If it ever needs solving without a slab of black: the header's own answer is
   a glass pill behind each end, and the sampler in site.js already knows how to
   measure the frame underneath. */
.omicro {
  position: absolute; inset: auto 0 0 0;
  padding: 0 var(--gut) clamp(1.1rem, 3vh, 2rem);
  color: var(--on-dark);
  font-family: var(--mono);
  font-size: var(--t-meta);
  letter-spacing: .14em;
  text-transform: uppercase;
  pointer-events: none;
}
.omicro__rule,
.omicro__foot { display: flex; align-items: center; gap: 1rem; }
.omicro__rule { padding-bottom: .85rem; }
.omicro__line { flex: 1; height: 1px; background: rgb(255 255 255 / .38); }
.omicro__foot { justify-content: space-between; opacity: .78; }
.omicro__r { margin-left: auto; }
.omicro__rule .omicro__r { margin-left: 0; }

/* The cue blinks slowly rather than bouncing. A moving arrow on top of moving
   footage is two animations fighting; a fade reads as an instruction. */
.omicro__cue::after {
  content: "";
  display: inline-block; vertical-align: middle;
  width: 1px; height: .8em; margin-left: .5em;
  background: currentColor;
  animation: omicroCue 2.4s var(--ease) infinite;
}
@keyframes omicroCue {
  0%, 100% { opacity: .25; transform: translateY(0); }
  50%      { opacity: 1;   transform: translateY(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .omicro__cue::after { animation: none; opacity: .7; }
}

/* --- 24. SELECTED WORK — OFFSET ROWS -------------------------------------
   Four projects. Image and type SIDE BY SIDE, alternating which side each sits
   on. A caption underneath reads as a caption; a column alongside reads as a
   label, and these are labels.

   SIZED AGAINST THE VIEWPORT, NOT JUST THE GRID. The first version set every
   row to 88% of the wrap and let the aspect ratio decide the height — which
   made the two portrait rows **1106px tall on a 772px screen, 1.43x the
   viewport**. You could not see one of them in a single screen, which is a
   strange thing to do to a picture.

   Two fixes, both needed. The portrait rows are narrower than the landscape
   ones, so their natural height comes down; and every frame carries a
   `max-height` so no window can produce a tile taller than itself. The cap is
   the safety net — on a short laptop window the width alone is not enough. */
.pgal {
  display: grid;
  gap: clamp(4.5rem, 12vh, 9rem);
  margin-top: clamp(3rem, 8vh, 6rem);
}
.pgal__item {
  position: relative;
  display: grid;
  gap: clamp(1.4rem, 3vw, 3rem);
  align-items: end;
  text-decoration: none;
  color: inherit;
}
@media (min-width: 900px) {
  /* The type column has a MINIMUM as well as a percentage. At 24% of a narrow
     row it would be a 130px measure, which is four words a line. */
  .pgal__item--l {
    grid-template-columns: minmax(0, 1fr) minmax(190px, 24%);
    width: 76%; margin-right: auto;
  }
  /* Same width and same shape as the left rows. Dani asked for all four covers
     to look like Liminal and Paninoteca, which were the landscape ones — so the
     portrait/landscape alternation is gone and only the SIDE alternates. That
     also retires the sizing problem it caused: one shape means one height, and
     no row can be twice as tall as its neighbour by accident. */
  .pgal__item--r {
    grid-template-columns: minmax(190px, 24%) minmax(0, 1fr);
    width: 76%; margin-left: auto;
  }
  /* On the right-hand rows the type column comes first in the GRID but must
     still read second in the DOM — the heading has to precede the description
     for a screen reader, and `order` would break that. Placing by column keeps
     source order intact. */
  .pgal__item--r .pgal__frame { grid-column: 2; grid-row: 1; }
  .pgal__item--r .pgal__meta  { grid-column: 1; grid-row: 1; text-align: right; }
  .pgal__item--r .pgal__meta > * { margin-left: auto; }
}
.pgal__frame {
  position: relative;
  overflow: hidden;
  background: var(--paper-2);
  aspect-ratio: 5 / 4;
  /* `width: 100%` is load-bearing and was the bug. `aspect-ratio` plus
     `max-height` gives the element a smaller INTRINSIC size, and a flexible
     grid column (`minmax(0, 1fr)`) sizes itself to that intrinsic width — so
     the cap was not merely cropping the height, it was dragging the width down
     with it. Measured: the portrait frames wanted 715x893 and the cap turned
     them into 531x664. That is why they came out looking like thumbnails, and
     why raising the cap alone never fixed it.

     Filling the column explicitly takes the frame's intrinsic size out of the
     grid's arithmetic. The height is then decided by the ratio, the cap only
     ever crops, and the row's size is controlled by the one thing that should
     control it: the item's width.

     The cap is now a safety net for short windows, not the sizing mechanism. */
  width: 100%;
  max-height: 88svh;
}
/* `>` and not a descendant selector. It matters now that two of the covers
   are <video> rather than <img>: a descendant rule would also catch anything
   else ever nested in the frame. It mattered more when the hover previews
   lived in here — they were <img> too, and `.pgal__frame img` at (0,1,1)
   outranked their own class at (0,1,0), so every preview rendered at the full
   size of the cover. Kept as a direct-child selector deliberately. */
.pgal__frame > img,
.pgal__frame > video {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 1.1s var(--ease);
}
.pgal__item:hover .pgal__frame > img,
.pgal__item:hover .pgal__frame > video,
.pgal__item:focus-visible .pgal__frame > img,
.pgal__item:focus-visible .pgal__frame > video { transform: scale(1.035); }

/* The type column. A rule above it rather than a box around it — the same
   hairline vocabulary as every other section head on the site. */
.pgal__meta {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: .5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--hairline);
  transition: border-color var(--dur-fast) var(--ease);
}
.pgal__n {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .1em; color: var(--muted);
}
.pgal__t {
  font-size: clamp(1.3rem, 2.2vw, 1.9rem);
  letter-spacing: -.025em; font-weight: 600; line-height: 1.05;
}
.pgal__m {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
}
.pgal__d {
  font-size: .93rem; line-height: 1.5; color: var(--ink-2);
  max-width: 30ch; margin-top: .35rem;
}
.pgal__y {
  font-family: var(--mono); font-size: var(--t-meta);
  letter-spacing: .1em; color: var(--muted); margin-top: .35rem;
}
.pgal__item:hover .pgal__meta,
.pgal__item:focus-visible .pgal__meta { border-top-color: var(--pa, var(--ink)); }

@media (max-width: 899px) {
  .pgal { gap: clamp(3rem, 9vh, 6rem); }
  .pgal__item, .pgal__item--l, .pgal__item--r { width: 100%; }
  .pgal__frame { aspect-ratio: 4 / 5; max-height: 68svh; }   /* portrait on a phone, where width is the scarce axis */
}
@media (prefers-reduced-motion: reduce) {
  .pgal__frame > img,
  .pgal__frame > video { transition: none; }
  .pgal__item:hover .pgal__frame > img,
  .pgal__item:hover .pgal__frame > video { transform: none; }
}


/* --- MOBILE: THE 3D FACE STAYS (Dani, 9 Oct) ------------------------------
   It was hidden below 900px. Now it sits under the headline, centred, at a
   size a phone GPU handles comfortably. chrome.js still declines on Save-Data,
   2g, reduced motion and two-core devices, and the empty box collapses then. */
@media (max-width: 900px) {
  .vhero__obj {
    display: block;
    width: min(74vw, 340px);
    justify-self: center;
    margin-top: clamp(.5rem, 3vw, 1.5rem);
  }
  html[data-chrome-skipped] .vhero__obj,
  html[data-chrome-face-error] .vhero__obj { display: none; }
}
