/* ============================================================
   ALVARA — homepage
   Built from "Alvara Homepage - Hi-Fi.dc.html" + BUILD-SPEC.md (v2A)
   Two-tone, plus one brass accent in six measured places — see the
   "accent: brass" token block below.
   ============================================================ */

:root{
  /* ---- page ground ---- */
  --bg:      #F4F3F0;
  --fg:      #0A0A0A;
  --fg-13: rgba(10,10,10,0.13);
  --fg-15: rgba(10,10,10,0.15);
  --fg-18: rgba(10,10,10,0.18);
  --fg-25: rgba(10,10,10,0.25);
  --fg-30: rgba(10,10,10,0.30);
  --fg-55: rgba(10,10,10,0.55);
  --fg-60: rgba(10,10,10,0.60);
  --fg-65: rgba(10,10,10,0.65);
  --fg-75: rgba(10,10,10,0.75);

  /* ---- feature blocks: capabilities, CTA + footer, dark work card ----
     These stay light-on-dark in both themes; only the ground shifts. */
  --block-bg:  #0A0A0A;
  --block-fg:  #F4F3F0;
  --block-fg-18: rgba(244,243,240,0.18);
  --block-fg-20: rgba(244,243,240,0.20);
  --block-fg-30: rgba(244,243,240,0.30);
  --block-fg-50: rgba(244,243,240,0.50);
  --block-fg-60: rgba(244,243,240,0.60);
  --block-fg-65: rgba(244,243,240,0.65);
  --block-fg-70: rgba(244,243,240,0.70);

  /* ---- work cards ---- */
  --card-light-border: rgba(10,10,10,0.18);


  /* the mark always sits on a photograph, so it stays light in both themes */
  --card-light-bg:     #EAE9E4;

  /* Hover shifts the card one tonal step only — darker in light mode, lighter
     in dark — and the type does not change with it. The old hover flipped the
     whole card to near-black, a 14.9:1 jump, because cream type needed that
     much ground to sit on. Holding the type still is what allows the step to
     be small. Both directions are tuned to the same perceptual size: 1.19 in
     light, 1.18 in dark, so the effect reads identically in either theme. */
  --card-hover-bg: #D8D7D2;
  --logo-on-photo: #F4F3F0;
  --logo-scrim:    rgba(10,10,10,0.42);


  /* ---- buttons ---- */
  --btn-bg:       #0A0A0A;
  --btn-fg:       #F4F3F0;
  --btn-bg-hover: #2A2A2A;

  /* ---- accent: brass ----
     One hue, 43.6deg, in three exposures. It is the same hue the footer
     photography already carries: footer-09.jpg means 43.1deg. See
     docs/superpowers/specs/2026-08-22-accent-colour-design.md before
     touching any of these — every value is solved, not picked.

     --ac       the constant. Only ever lands on ink, where it measures
                8.51:1 and needs no per-theme adjustment.
     --ac-ink   the ground-aware value, for anything that must stay legible
                on whatever surface it sits on. On cream --ac is 2.10:1 and
                fails small type, so light drops to L 0.32 of the same hue:
                4.96:1. Dark goes back to the constant.
     --ac-on    what reads on top of --ac-ink. It inverts with the theme
                because --ac-ink does.
     --ac-mark  the marquee asterisk only. Decoration, aria-hidden, so it is
                tuned by optical weight rather than by AA: 2.10:1 light and
                2.09:1 dark, so the mark reads the same on both grounds.
     --ac-art   the dithered waw in the capabilities rail, also decoration.
                It first shipped at the grey's exact weight, 2.37:1 light and
                2.51:1 dark, so that only the hue moved; that read as too
                faint on a screen and it was raised one step to 3.11:1 in
                both themes on 2026-08-22. Two values because the two themes
                put it on different blocks. The ceiling is 3.3:1, where the
                field starts competing with the lead above it rather than
                sitting behind it, so there is very little room left. */
  --ac:      #C4A75A;
  --ac-ink:  #7E6625;
  --ac-on:   #F4F3F0;
  --ac-mark: #C4A75A;
  --ac-art:  #715D28;

  /* ---- chrome ---- */
  /* The nav pill has no border, so a shadow is the only thing separating it
     from the hero. Light gets a soft cast; dark gets a wider, deeper one
     because a dark shadow has almost nothing to darken on an ink page. */
  --nav-shadow:     0 10px 30px rgba(10,10,10,0.10), 0 2px 8px rgba(10,10,10,0.06);
  /* The ink blocks lift off the page rather than being cut into it. Offset
     upward, because the only edge that shows is the rounded top one: both
     blocks run full bleed, so the sides are off-screen, and .cta runs to the
     bottom of the document. Tuned per theme for the same reason --nav-shadow
     is: on an ink page a dark shadow has almost nothing to darken. */
  --block-shadow:   0 -14px 34px rgba(10,10,10,0.09), 0 -3px 10px rgba(10,10,10,0.05);
  --glass-bg:       rgba(244,243,240,0.25);
  --glass-bg-solid: rgba(244,243,240,0.82);
  --panel-bg:       rgba(244,243,240,0.55);
  --panel-bg-solid: rgba(244,243,240,0.96);
  --panel-hover:    rgba(10,10,10,0.10);
  --ticker-bg:      rgba(244,243,240,0.46);
  --scrim:
    linear-gradient(to bottom, rgba(244,243,240,0.40) 0%, rgba(244,243,240,0.16) 30%, rgba(244,243,240,0) 56%),
    linear-gradient(to top, rgba(244,243,240,0.16) 0%, rgba(244,243,240,0) 10%);

  /* A hint of sky put back into the top of the frame. The warm-white scrim
     above is what flattens the footage's own sky to grey; this rides on top
     of it and returns some saturation. It is not chrome — it only ever sits
     over photography and is fully out by 55%, well above the horizon. It is
     unrelated to the brass accent and must stay that way: this is the sky in
     the footage, not a signal. Dial the first stop's alpha to taste;
     everything else follows from it. */
  --hero-tint:
    linear-gradient(to bottom,
      rgba(96,164,224,0.40) 0%,
      rgba(96,164,224,0.19) 32%,
      rgba(96,164,224,0)    55%);

  /* ---- stand-in wash behind any image that fails to load ---- */
  --wash-color: #E4E2DE;
  --wash-image:
    radial-gradient(120% 90% at 72% 22%, rgba(255,255,255,.85) 0%, rgba(255,255,255,0) 55%),
    radial-gradient(100% 80% at 18% 88%, rgba(10,10,10,.16) 0%, rgba(10,10,10,0) 60%),
    linear-gradient(160deg, #EDEBE7 0%, #D9D7D2 48%, #C7C5C0 100%);

  /* shape + rhythm */
  --gutter:  clamp(24px, 4.45vw, 64px);
  --card-gap: clamp(16px, 1.7vw, 24px);
  --r-card:  16px;
  --r-block: 56px;
  --r-pill:  999px;

  /* motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* type */
  --sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Dark theme: everything sits on ink, and the two feature blocks lift to a
   raised tone so they still read as distinct blocks rather than merging. */
[data-theme="dark"]{
  --bg: #0A0A0A;
  --fg: #F4F3F0;
  --fg-13: rgba(244,243,240,0.13);
  --fg-15: rgba(244,243,240,0.15);
  --fg-18: rgba(244,243,240,0.18);
  --fg-25: rgba(244,243,240,0.25);
  --fg-30: rgba(244,243,240,0.30);
  --fg-55: rgba(244,243,240,0.55);
  --fg-60: rgba(244,243,240,0.60);
  --fg-65: rgba(244,243,240,0.65);
  --fg-75: rgba(244,243,240,0.75);

  --block-bg: #161614;

  --card-light-border: rgba(244,243,240,0.14);


  /* the mark always sits on a photograph, so it stays light in both themes */
  --card-light-bg:     #161614;

  --card-hover-bg: #252522;
  --logo-on-photo: #F4F3F0;
  --logo-scrim:    rgba(10,10,10,0.42);


  --btn-bg:       #F4F3F0;
  --btn-fg:       #0A0A0A;
  --btn-bg-hover: #FFFFFF;

  --ac-ink:  #C4A75A;
  --ac-on:   #0A0A0A;
  --ac-mark: #53441E;
  --ac-art:  #77632B;

  --nav-shadow:     0 16px 40px rgba(0,0,0,0.45), 0 3px 12px rgba(0,0,0,0.35);
  /* Tighter and much stronger than the light value, and it still does less.
     The dark page ground is #0A0A0A, ten levels above pure black, so that is
     the entire budget a black shadow has to work with. Measured at the seam:
     light drops 11 levels over 25px, dark 8 over 15px, which is as close to
     matched as the ground allows. Widening the blur here only spreads the
     same eight levels thinner. */
  --block-shadow:   0 -16px 28px rgba(0,0,0,0.80), 0 -4px 10px rgba(0,0,0,0.70);
  --glass-bg:       rgba(10,10,10,0.35);
  --glass-bg-solid: rgba(14,14,13,0.86);
  --panel-bg:       rgba(18,18,17,0.55);
  --panel-bg-solid: rgba(18,18,17,0.96);
  --panel-hover:    rgba(244,243,240,0.12);
  --ticker-bg:      rgba(10,10,10,0.34);
  --scrim:
    linear-gradient(to bottom, rgba(10,10,10,0.50) 0%, rgba(10,10,10,0.22) 30%, rgba(10,10,10,0) 56%),
    linear-gradient(to top, rgba(10,10,10,0.22) 0%, rgba(10,10,10,0) 10%);

  --hero-tint:
    linear-gradient(to bottom,
      rgba(96,140,190,0.10) 0%,
      rgba(96,140,190,0.05) 32%,
      rgba(96,140,190,0)    55%);

  --wash-color: #17171A;
  --wash-image:
    radial-gradient(120% 90% at 72% 22%, rgba(244,243,240,.16) 0%, rgba(244,243,240,0) 55%),
    linear-gradient(160deg, #232323 0%, #131313 60%, #0A0A0A 100%);
}

/* Only while the theme is mid-switch, so the page cross-fades instead of
   snapping, without leaving transitions on every element permanently. */
.theme-anim, .theme-anim *, .theme-anim *::before, .theme-anim *::after{
  transition: background-color .55s var(--ease), color .55s var(--ease),
              border-color .55s var(--ease), box-shadow .55s var(--ease) !important;
}

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

/* The gutter is reserved permanently so the scrollbar's arrival never changes
   the layout width. The intro gate sets overflow:hidden on html and body; when
   it lifts, the scrollbar appears and the page narrows by its width — 1440 to
   1425 here. The hero video is object-fit:cover, so that re-crops the footage
   and the frame visibly jumps. It only ever showed in light mode: dark opens
   on the night frame, which has no detail for a 15px crop shift to reveal. */
html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  scrollbar-gutter:stable;
  /* iOS paints a translucent grey box over whatever is being tapped. On the
     round theme toggle and the round card controls that box is a square, so a
     tap flashed a hard-edged rectangle around a circle. The states worth
     seeing are already drawn by :hover and :active. It inherits, so this
     covers every control on the page. */
  -webkit-tap-highlight-color:transparent;
}

body{
  margin:0;
  background:var(--bg);
  color:var(--fg);
  font-family:var(--sans);
  font-size:16px;
  line-height:1.5;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

h1,h2,h3,p,figure,blockquote{ margin:0; }
ul{ margin:0; padding:0; list-style:none; }

a{ color:inherit; text-decoration:none; transition:opacity .25s var(--ease); }
a:hover{ opacity:.65; }

img,video{ display:block; max-width:100%; }

/* The focus ring is the strongest possible reading of "this element is
   active", so it is the one piece of chrome the accent is allowed to own
   outright. --ac-ink keeps it above the 3:1 non-text threshold on every
   surface: 4.96:1 on cream, 8.51:1 on ink. */
:focus-visible{ outline:2px solid var(--ac-ink); outline-offset:3px; border-radius:2px; }

/* Selection is brass in both themes with an ink glyph, 8.51:1 either way —
   one pair of values, because the highlight paints its own ground and does
   not care what is behind it. */
::selection{ background:var(--ac); color:#0A0A0A; }

.mono{
  font-family:var(--mono);
  font-weight:400;
  letter-spacing:.14em;
  text-transform:uppercase;
}

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

.skip{
  position:absolute; left:-9999px; top:0; z-index:200;
  background:var(--fg); color:var(--bg);
  padding:14px 20px; border-radius:0 0 var(--r-card) 0;
  font-size:14px;
}
.skip:focus{ left:0; opacity:1; }

/* ---------- buttons ---------- */

.btn{
  display:inline-block;
  border-radius:var(--r-pill);
  font-size:14px;
  font-weight:500;
  padding:12px 24px;
  white-space:nowrap;
  transition:background-color .25s var(--ease), color .25s var(--ease), transform .25s var(--ease);
}
.btn:hover{ opacity:1; }
.btn:active{ transform:scale(.98); }

.btn-lg{ font-size:clamp(15px,1.1vw,17px); padding:clamp(15px,1.3vw,20px) clamp(28px,3vw,44px); }

/* Parallax box for the hero CTA. The transform has to live on a wrapper:
   put it on the .btn itself and it would overwrite :active's scale and
   inherit the button's own 250ms transition, which would drag the pill
   along behind the scroll instead of tracking it. */
.hero-cta{ display:inline-block; }

.btn-dark{ background:var(--btn-bg); color:var(--btn-fg); }
.btn-dark:hover{ background:var(--btn-bg-hover); }

.btn-light{ background:#F4F3F0; color:#0A0A0A; }
.btn-light:hover{ background:#FFFFFF; }

/* Secondary rank. The nav pill is filled and sits directly above the hero
   button, so a second filled pill would read as an equal claim on the eye.
   The hairline holds the same footprint at a lower weight and fills in on
   hover. box-shadow rather than border so the 1px does not change the
   button's box, and stays 1px at every density. */
.btn-ghost{
  background:transparent;
  color:var(--fg);
  box-shadow:inset 0 0 0 1px var(--fg-30);
  transition:background-color .25s var(--ease), color .25s var(--ease),
             box-shadow .25s var(--ease), transform .25s var(--ease);
}
.btn-ghost:hover{
  background:var(--btn-bg);
  color:var(--btn-fg);
  box-shadow:inset 0 0 0 1px var(--btn-bg);
}

/* ---------- image slots ---------- */
/* Every <img class="slot-img"> sits on a tonal fallback. If the file is
   missing the image hides itself (js/site.js) and the fallback stands in
   without any layout shift. */

/* The matching still sits under the video, so the correct end of the
   transition is on screen before a single frame has decoded. It is also
   what stands in when motion is reduced or the file fails. */
.hero-bg{
  background-color:#0A0A0A;
  background-image:url('/assets/media/hero/hero-light.jpg');
  background-size:cover;
  background-position:center;
}
[data-theme="dark"] .hero-bg{ background-image:url('/assets/media/hero/hero-dark.jpg'); }
/* the still under the gate must match the frame its arc opens on:
   light climbs from night, dark falls from day */
.intro-gate .hero-bg{ background-image:url('/assets/media/hero/hero-dark.jpg'); }
[data-theme="dark"].intro-gate .hero-bg{ background-image:url('/assets/media/hero/hero-light.jpg'); }
.hero-bg{ background-position:center; }

/* The whole frame, uncropped vertically. The foreground depth is the best
   thing in the footage, so nothing sits on it and nothing trims it: the copy
   moved to the top of the hero instead. */
.hero-video{
  width:100%; height:100%;
  object-fit:cover;
  object-position:center;
  display:block;
}

.slot-img,.slot-video{
  width:100%; height:100%;
  object-fit:cover;
  transition:transform .6s var(--ease);
}
/* slight residual colour rather than a flat 100%: a fully desaturated frame
   next to a warm page reads as broken, not as a choice */
.card .slot-img{
  filter:grayscale(0.92) contrast(1.04);
  transition:transform .6s var(--ease), filter .55s var(--ease);
}
.card:hover .slot-img{ filter:grayscale(0) contrast(1); }

/* Product mark centred over the photograph, present in both states. It is a
   sibling of the image rather than a child, so the hover zoom scales the
   photograph underneath while the mark stays put — scaling it too would read
   as the logo lurching forward. The radial scrim is what makes it legible:
   these are busy documentary frames with no reliably quiet centre. */
.card-logo{
  position:absolute;
  inset:0;
  z-index:1;
  display:flex;
  align-items:center;
  justify-content:center;
  pointer-events:none;
  color:var(--logo-on-photo);
}
.card-logo::before{
  content:'';
  position:absolute;
  inset:0;
  background:radial-gradient(52% 62% at 50% 50%, var(--logo-scrim) 0%, transparent 72%);
}
/* Kosta: the real mark, outlined from the Kosta Design System — stem + forward
   chevron, and KOSTA in Barlow Condensed 700 converted to vector paths. Paths,
   not live type, so the "two typefaces only" rule still holds: this is artwork.
   Sized by height, because in the source lockup the mark's height IS the
   wordmark's font size; the caps come out at 0.716 of it. Width follows the
   4.006 aspect. Fill is currentColor, so it inherits --logo-on-photo. */
/* The chevron is Kosta's one brand colour (#FF8A00). It arrives with the
   photograph's colour on hover rather than sitting on the greyscale rest state,
   so the card still reads as one monochrome object until you touch it. Both are
   sanctioned versions of the mark — mono and full colour — not a recolour.
   To make it permanent, move the stroke onto .kl-chevron and drop this rule. */
.kl-chevron{ transition:stroke .55s var(--ease); }
.card:hover .kl-chevron{ stroke:#FF8A00; }

/* Sahly: the real lockup, outlined from the Sahly Design System — the seen
   (س) drawn as one monoline stroke, and "sahly" in Gabarito 800 converted to
   vector paths. Same reasoning as Kosta: paths, not a third webfont. The
   viewBox is cropped to the ink, so height sizing is predictable.
   Both marks are sized by height on the same clamp so the two cards register
   as a pair; Sahly's ascender height is 0.728 of its box against Kosta's
   0.716, close enough that one clamp serves both. */
.kosta-lockup,
.sahly-lockup{
  position:relative;      /* above the scrim */
  display:block;
  height:clamp(34px,3.6vw,56px);
  width:auto;
}
.slot-img[data-missing]{ display:none; }

.card-media,.hero-bg{
  background-color:var(--wash-color);
  background-image:var(--wash-image);
}

/* ============================================================
   NAV
   ============================================================ */

.nav{
  position:fixed;
  top:24px; left:24px; right:24px;
  z-index:100;
  transition:transform .35s var(--ease);
}
.nav[data-hidden]{ transform:translateY(calc(-100% - 24px)); }

/* the pill hugs its contents and centres, rather than spanning the window */
.nav-pill{
  position:relative;
  width:100%;
  max-width:960px;
  margin:0 auto;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:24px;
  /* No border. The pill is defined by its tint and blur alone — a hairline
     around a glass shape reads as a drawn outline rather than as an edge.
     .nav-links keeps its border: that panel floats over blurred page content
     and does need a defined edge. */
  border-radius:var(--r-pill);
  padding:10px 10px 10px 28px;
  background:var(--glass-bg);
  box-shadow:var(--nav-shadow);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .nav-pill{ background:var(--glass-bg-solid); }
}

/* Opening the menu blurs the page behind it.

   This was first built as `filter:blur()` on #main. A filter leaves a soft,
   half-transparent edge at the element's own border, so it needed scale(1.02)
   to push that edge out of frame — and scaling a 4858px-tall element about its
   centre lifts its top edge by half of 2%, ~49px. The whole page visibly
   jumped upward when the menu opened.

   A fixed layer blurring its own backdrop has neither problem: the page keeps
   its exact geometry (no transform anywhere), and the layer covers the
   viewport edge to edge, so there is no soft border to hide. It sits below
   .nav's z-index:100, so the bar and the panel stay sharp above it, and above
   the unpositioned #main, so the page blurs beneath it.

   It lives outside <header> so the nav's outside-click handler counts a tap on
   it as outside, and pointer-events:auto stops that tap reaching links on the
   blurred page. */
.nav-scrim{
  position:fixed; inset:0;
  z-index:90;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  backdrop-filter:blur(9px);
  -webkit-backdrop-filter:blur(9px);
  transition:opacity .4s var(--ease), visibility .4s;
}
.nav.is-open ~ .nav-scrim{
  opacity:1;
  visibility:visible;
  pointer-events:auto;
}

.nav-brand{ display:flex; align-items:center; }
.nav-brand img{ display:block; height:34px; width:auto; }
/* the mark ships in two colourways; the charcoal one vanishes on ink.
   Scoped through .nav-brand so these outrank the `.nav-brand img` rule. */
.nav-brand .brand-on-dark{ display:none; }
[data-theme="dark"] .nav-brand .brand-on-light{ display:none; }
[data-theme="dark"] .nav-brand .brand-on-dark{ display:block; }

.nav-links{ display:flex; align-items:center; gap:30px; font-size:14px; }
.nav-panel-cta{ display:none; }   /* only ever shown inside the mobile panel */

.nav-actions{ display:flex; align-items:center; gap:14px; }

/* ---------- roll-up hover on menu links ---------- */
/* the label slides out upward while its duplicate arrives from below */

/* The label and its duplicate each move by 105% of THEIR OWN height, which
   only clears the clipping window when that window is one line tall. Below
   768px the touch-target rule gives .footer-col .roll a 44px min-height, so a
   17.5px line moved 18.4px inside a 44px box: the duplicate stopped in plain
   sight under the label and read as struck-through text, and the label itself
   never fully left either.

   Both layers now fill the box — the pseudo-element through inset:0, the span
   through align-items:stretch on a flex .roll — so 105% is always just over
   the box height and both clear it at any size. Keep them stretched: reverting
   either to its own content height brings the bug straight back at whatever
   breakpoint pads the link next. */
.roll{
  position:relative;
  display:inline-flex;
  align-items:stretch;
  overflow:hidden;
  line-height:1.25;
  vertical-align:top;
}
.roll span{ display:flex; align-items:center; }
.roll::after{
  content:attr(data-label);
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  white-space:nowrap;
}
/* the roll replaces the global link dim, but only where it actually plays;
   without motion the links fall back to that dim so hover still reads */
.js-motion .roll:hover,
.js-motion .roll:focus-visible{ opacity:1; }

.js-motion .roll span,
.js-motion .roll::after{ transition:transform .45s var(--ease); }
.js-motion .roll::after{ transform:translateY(105%); }
.js-motion .roll:hover span,
.js-motion .roll:focus-visible span{ transform:translateY(-105%); }
.js-motion .roll:hover::after,
.js-motion .roll:focus-visible::after{ transform:translateY(0); }

/* without motion the duplicate must not sit on top of the label */
.roll::after{ transform:translateY(105%); }

/* ---------- theme switch ---------- */
/* shows the mode it will move to: a moon while light, a sun while dark */

.theme-toggle{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
  width:40px; height:40px;
  padding:0;
  border:1px solid var(--fg-18);
  border-radius:50%;
  background:none;
  color:inherit;
  cursor:pointer;
  transition:border-color .3s var(--ease), background-color .3s var(--ease);
}
/* A hover-only affordance, held behind a hover query. Without it a tap leaves
   :hover stuck until the next tap elsewhere, so the toggle's border darkened
   and simply stayed dark — a state that arrived and never left. A pointer that
   cannot hover should never see a hover state. */
@media (hover:hover){
  .theme-toggle:hover{ border-color:var(--fg-55); }
}

.theme-toggle .icon{
  position:absolute;
  width:18px; height:18px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.7;
  stroke-linecap:round;
}
.icon-moon{ fill:currentColor; stroke:none; }
.js-motion .theme-toggle .icon{ transition:opacity .3s var(--ease), transform .45s var(--ease); }

.icon-sun{ opacity:0; transform:rotate(-60deg) scale(.6); }
.icon-moon{ opacity:1; transform:none; }
[data-theme="dark"] .icon-sun{ opacity:1; transform:none; }
[data-theme="dark"] .icon-moon{ opacity:0; transform:rotate(60deg) scale(.6); }

/* ---------- menu toggle: two bars that fold into a cross ---------- */

.nav-toggle{
  display:none;
  width:44px; height:44px;
  padding:0;
  border:0;
  background:none;
  color:inherit;
  cursor:pointer;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:5px;
  border-radius:50%;
}
.nav-toggle .bar{
  width:19px; height:1.5px;
  border-radius:2px;
  background:var(--fg);
  transition:transform .4s var(--ease), width .3s var(--ease);
}
/* hover: the bars draw toward each other and the top one retracts */
.nav-toggle:hover .bar:nth-child(1){ width:13px; transform:translateY(1.5px); }
.nav-toggle:hover .bar:nth-child(2){ transform:translateY(-1.5px); }

.nav.is-open .nav-toggle .bar{ width:19px; }
.nav.is-open .nav-toggle .bar:nth-child(1){ transform:translateY(3.25px) rotate(45deg); }
.nav.is-open .nav-toggle .bar:nth-child(2){ transform:translateY(-3.25px) rotate(-45deg); }

/* ============================================================
   HERO
   ============================================================ */

.hero{
  position:relative;
  min-height:max(720px, 100svh);
  display:flex;
  flex-direction:column;
  overflow:hidden;
}

.hero-bg{
  position:absolute; inset:0;
}

.hero-scrim{
  position:absolute; inset:0;
  background:var(--hero-tint), var(--scrim);
  pointer-events:none;
}

.hero-body{
  position:relative; z-index:1;
  flex:1;
  display:flex;
  flex-direction:column;
  justify-content:flex-start;
  padding:calc(96px + var(--gutter)) var(--gutter) 0;
}


/* two lines: "Some problems are" / "products in disguise".
   The cap is sized so the longer line clears 1000px at the 80px top end. */
.hero-h1{
  font-size:clamp(32px, 5.15vw, 74px);
  font-weight:700;
  letter-spacing:-.045em;
  line-height:1;
  max-width:min(100%, 1000px);
}
.hero-h1 .line{ display:block; }
.hero-h1 .line-in{ display:block; }

.hero-row{
  display:flex;
  flex-direction:column;
  align-items:flex-start;
  gap:clamp(20px,2.4vw,30px);
}

/* sits on one line at desktop widths; the string length is its own cap.
   The top margin lives here rather than on the row so the gap under the
   headline is exact, instead of inheriting the button's extra height.
   Kept small enough that the line ends well before the monitor. */
.hero-sub{
  font-size:clamp(15px,1.25vw,18px);
  line-height:1.5;
  color:var(--fg-75);
  margin-top:clamp(20px,2.5vw,36px);
}

/* ---------- logo ticker ---------- */

.ticker{
  position:relative; z-index:1;
  margin-top:clamp(18px,2.2vw,32px);
  border-top:1px solid var(--fg-15);
  padding:clamp(18px,2vw,28px) 0;
  overflow:hidden;
  background:var(--ticker-bg);
  backdrop-filter:blur(26px) saturate(1.2);
  -webkit-backdrop-filter:blur(26px) saturate(1.2);
}
.ticker-track{
  display:flex;
  width:max-content;
  animation:ticker 32s linear infinite;
  color:var(--fg-75);
}
.ticker:hover .ticker-track{ animation-play-state:paused; }


.ticker-group{
  display:flex;
  align-items:center;
  gap:clamp(44px,6.1vw,88px);
  padding:0 clamp(22px,3.05vw,44px);
  white-space:nowrap;
}

@keyframes ticker{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }

/* the logo set is styled text until real marks land, see README */
.lg-kosta{ font-size:22px; font-weight:700; letter-spacing:-.02em; }
.lg-sahly{ font-size:23px; font-weight:800; letter-spacing:-.04em; }
.lg-meridian{ font-size:17px; font-weight:500; letter-spacing:.22em; }
.lg-aldar{ font-size:21px; font-weight:600; letter-spacing:-.02em; }
.lg-halcyon{ font-family:var(--mono); font-size:16px; letter-spacing:.1em; }
.lg-vantage{ font-size:20px; font-weight:700; letter-spacing:-.02em; }
.lg-obeid{ font-family:Georgia,'Times New Roman',serif; font-size:21px; font-style:italic; }
.lg-tarmac{ font-size:17px; font-weight:500; letter-spacing:.22em; }

/* ============================================================
   STATS
   ============================================================ */

.stats{
  display:grid;
  /* minmax(0,1fr), not 1fr. A bare 1fr track is min-content-floored, so a
     cell whose figure is wider than its share widens that one track and the
     dividers stop lining up. That was tolerable while the row ran full
     width and every track had about 3% of slack; pulling the row in to the
     spine below costs 16px a track and spends that slack, and "100%"
     immediately pushed its column 10px wider than the other three.
     Capping the minimum at 0 pins all four equal, and the one figure that
     no longer fits simply runs into its own padding instead of moving a
     rule: 284.1px of digits in a 277.5px content box at 1440, so it sits
     6.6px closer to the last divider than the nominal 31.3px, leaving
     24.8px. That is the right way round — a placeholder figure should not
     get to move the grid, and the stats copy is flagged for replacement. */
  grid-template-columns:repeat(4,minmax(0,1fr));
  border-top:1px solid var(--fg-15);
  border-bottom:1px solid var(--fg-15);
}
/* The horizontal padding has to leave room for the widest figure, or that
   cell's 1fr track expands to fit it and the dividers stop lining up. "100%"
   runs about 2.55x the numeral's font-size, i.e. ~0.199vw against a 0.25vw
   track, so the padding coefficient must stay well below ~2.5vw. At 2.8vw the
   last column was 14px wider than the other three; 2.4vw left only a 1.3%
   margin, which subpixel rounding still ate at some widths. 2.2vw gives ~3%. */
/* The cell padding is hoisted to a variable so the row can cancel it on its
   two outer edges: without that the first figure sat at 32px while every
   other block on the page starts at --gutter, 64px, and "6" read as falling
   off the left edge. Padding rather than margin, and on .stats rather than
   the cells, so the top and bottom hairlines still run the full width while
   the content lines up with the spine. All four tracks stay equal, so the
   dividers are unaffected. */
.stats{ --stat-pad:clamp(20px,2.2vw,34px); padding-inline:calc(var(--gutter) - var(--stat-pad)); }
.stat{ padding:clamp(32px,3.9vw,56px) var(--stat-pad); }
.stat + .stat{ border-left:1px solid var(--fg-15); }

/* Tracking is set off the "100%" cell: at -.05em the two zeros had a measured
   ink gap of exactly 0px — they touched and read as a single blob. -.028em is
   the lightest value that opens a real gap (1.4px at the 112px size) while
   keeping the numerals tight. Re-measure if the face or the figures change. */
.stat-num{
  font-size:clamp(56px,7.8vw,112px);
  font-weight:700;
  letter-spacing:-.028em;
  line-height:1;
  font-variant-numeric:tabular-nums;
}
/* Sized against the numeral above it, not against the other mono labels.
   At 11px the label was a tenth the height of a 112px figure and read as a
   footnote; the pair has to look like one object. Tracking eases back as the
   size grows — .14em is tuned for 11px and gets loose above 12px. */
.stat-label{
  font-size:clamp(12px,1vw,14px);
  letter-spacing:.11em;
  margin-top:clamp(14px,1.5vw,22px);
  color:var(--fg-75);
}

/* ============================================================
   MARQUEE HEADINGS
   ============================================================ */

.marquee{ overflow:hidden; }
.marquee-track{
  display:flex;
  width:max-content;
  white-space:nowrap;
  font-size:clamp(48px, calc(19.2vw - 76.5px), 200px);
  font-weight:700;
  letter-spacing:-.05em;
  line-height:.9;
}
/* The asterisk is the accent's primary carrier, and the only place on the
   page where it is large. It was --fg-13, a 1.22:1 ghost; --ac-mark holds
   the same optical weight at 2.10:1 light / 2.09:1 dark. Same glyph, same
   size, same position — only the ink changed. */
.marquee-track i{ font-style:normal; font-weight:400; color:var(--ac-mark); }

/* The asterisk rolls along with the line rather than spinning at a rate
   picked by feel. Both marquees travel at 75px/s — that is what their two
   durations were solved for — and the glyph's ink is a perfect square of
   exactly 0.700em, so at the 200px cap it is a 140px wheel: circumference
   pi*140 = 439.8px, one turn 439.8/75 = 5.86s. One value serves both
   marquees precisely because they share the speed; re-derive it if either
   duration moves.

   Anticlockwise, because the track travels left and a wheel rolling left
   turns that way.

   The loop is seamless without the rotation having to divide into the track
   duration: all four glyphs carry the same animation with the same start
   time, so they are always at the same angle, and the glyph that scrolls in
   at the -50% teleport matches the one that just left.

   transform-origin is the ink centre, not the box centre, and it was solved
   from screenshots rather than from font metrics: at 50%/51.7% the ink
   centroid moved 1.8px between 0 and 22.5 degrees, which puts the true
   centre 4.7px away, and 53.3%/52.3% brings that drift to under a fifth of a
   pixel. The box is 137.8 x 177.4 against 138 x 137 of ink, so the offset is
   mostly the glyph's side bearing. Re-solve the same way if the face changes;
   a wrong origin reads as a bob, not as a spin. */
.marquee-track i{
  display:inline-block;
  /* Now that the mark is drawn rather than typed it is a symmetric box, so
     the origin is simply its centre. It used to be 53.3%/52.3%, solved from
     screenshots to find the ink inside a glyph's side bearings. */
  transform-origin:50% 50%;
}
/* Sized to the ink of the glyph it replaces — measured 0.695em square — so the
   marquee's group width and therefore its 36.5s/34.4s durations are unchanged.
   An inline-block's baseline is its bottom edge, which puts the centre 0.35em
   up, matching where the old glyph's ink centre sat. */
.marquee-track .ast{
  display:block;
  width:0.695em;
  height:0.695em;
  fill:currentColor;
}
.js-motion .marquee-track i{ animation:cartwheel 5.86s linear infinite; }

@keyframes cartwheel{ from{ transform:rotate(0deg); } to{ transform:rotate(-360deg); } }

/* Durations are derived from group width, not chosen by feel: the animation
   travels exactly one group per cycle, so an identical duration on two tracks
   of different widths gives two different speeds. Both are solved for 75px/s
   at 1440 — work 2741px/75 = 36.5s, process 2583px/75 = 34.4s — so the two
   marquees run at the same pace. They were 49 and 47px/s originally.
   Re-derive both if either phrase is edited. */
.marquee-work .marquee-track{ animation:marquee 36.5s linear infinite; }
.marquee-process .marquee-track{ animation:marquee 34.4s linear infinite; }

@keyframes marquee{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }

/* ============================================================
   SELECTED WORK
   ============================================================ */

.work{ padding-top:clamp(72px,8.3vw,120px); }
.work .marquee{ padding-bottom:clamp(32px,3.9vw,56px); }

/* Capped, and on the gutter rather than a bare 24px.

   The media box is a fixed 340px tall with fluid width, so without a cap the
   crop ran from 1.72:1 at 1237 to 3.66:1 at 2560 — the same 2:1 master read
   as a letterbox strip on a wide monitor. 1512 is solved, not chosen: two
   680px cards (2.00:1 against the 340px media) plus the 24px gap and two 64px
   gutters. Above the cap the block centres; at or below it the gutter holds.

   The padding was 24px while the headline, stats and capabilities h2 all sat
   on --gutter, so the cards were 31px left of the page's spine at 1237 and
   40px at 2560. Moving to --gutter puts them back on it. Measured: card and
   h1 both at 64.0 at 1440, both at 55.0 at 1237.

   Below 768 the 1781 rule takes over with a single column and the same
   gutter, so the cap never applies there. */
.cards{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--card-gap);
  padding:0 var(--gutter);
  max-width:1512px;
  margin-inline:auto;
}

/* Every colour inside the card is read from a local property, so hovering
   swaps one set of values and the title, copy, arrow and tags all follow.
   Without this the hover state would need a parallel rule per element. */
.card{
  --c-bg:    var(--card-light-bg);
  --c-fg:    var(--fg);
  --c-fg-25: var(--fg-25);
  --c-fg-30: var(--fg-30);
  --c-fg-65: var(--fg-65);
  --c-fg-75: var(--fg-75);

  border-radius:var(--r-card);
  /* No background and no overflow here. Both moved onto the faces when the
     card learned to turn: a clipping box around a rotating child cuts it to
     a flat rectangle instead of letting it turn in space, and a painted box
     behind two backface-hidden faces is exactly what shows through as a
     "skeleton" at the moment neither face is facing you. The card is now
     only the perspective and the hover lift. */
  color:var(--c-fg);
  /* the whole card is the target, not just the arrow */
  cursor:pointer;
  transition:transform .4s var(--ease),
             color .4s var(--ease);
}

.card:hover{
  --c-bg: var(--card-hover-bg);
  transform:translateY(-6px);
}
.card:hover .slot-img{ transform:scale(1.04); }

/* One image per card, not two. The resting state is desaturated in CSS and
   colour returns on hover, so both states come from the same file — no second
   download, and no risk of two frames sitting a pixel out of register, which
   would read as a jitter mid-transition. */
.card-media{ position:relative; height:340px; overflow:hidden; flex:0 0 auto; }
.card-media .slot-img{ position:absolute; inset:0; }

/* The two cards carry copy of different lengths, and the grid already
   stretches them to a common height. Flexing the body and pushing the tags
   with margin-top:auto lands both tag rows on the same baseline, so the
   difference is absorbed as one gap under the shorter card's copy instead of
   as two rows of pills sitting out of register. */
/* ---------- the film behind the card ----------
   The card turns on click to show the product film. Everything here is inert
   until then: .card-back carries no src, so a visitor who never opens a card
   downloads nothing, and nothing on the resting page moves.

   The flip is the only 3D transform on the page, and it is gated behind a
   deliberate click. That is what keeps the page at rest as restrained as it
   was. Do not reach for perspective anywhere else. */
.card{ perspective:1600px; }

/* The inner box is what turns. The front face stays in normal flow and so
   still sets the card's height — that is what keeps the two cards on a
   common height and stops anything below shifting when one opens. */
.card-inner{
  position:relative;
  flex:1 1 auto;
  display:flex;
  transform-style:preserve-3d;
  transition:transform .7s var(--ease);
}
.card.is-flipped .card-inner{ transform:rotateY(180deg); }

/* Nothing on the resting card says it turns, so it tilts once on hover and
   comes back. The hint is the gesture itself: a rotation on the same axis the
   click will use, which reads as "this is a card and it has a back" in a way
   a glow or a caption would not.

   24 degrees, and the ceiling is vertical rather than horizontal. Rotating
   about the centre, the near edge comes toward the viewer and the projection
   both widens and heightens it — but the widening peaks at about 13 degrees
   (+8.1px) and falls away after, so at 24 the card is no wider than flat and
   the 24px grid gap to its neighbour is never in play. The height is what
   grows: +30px top and bottom at 24 degrees, against the 56px of air under
   the marquee. Past about 30 it starts to crowd that.

   An earlier version sat at 13 on the mistaken belief that the neighbouring
   card was the limit. It is not; 13 was simply too quiet to notice.

   Fires on every hover — see js/site.js. */
@keyframes card-nudge{
  0%   { transform:rotateY(0deg); }
  40%  { transform:rotateY(-24deg); }
  100% { transform:rotateY(0deg); }
}
.card.is-nudging .card-inner{ animation:card-nudge .75s var(--ease); }

/* Each face carries its own ground and its own rounded clip, so there is
   never an unrotated painted box behind them. backface-visibility is the
   only thing deciding which face you see, and it hands over cleanly at 90
   degrees. Do not add `visibility` here: it switches the instant the class
   changes rather than at the halfway point, which leaves a window where
   neither face is painted. That window was the "skeleton". Keyboard and
   screen-reader access is handled with `inert` from js/site.js instead,
   which costs nothing visually. */
/* iOS Safari drops backface culling when a face inside a preserve-3d parent
   also clips, which both of these do — so on iPhone the front face stayed
   visible through the turned card and its product lockup showed mirrored on
   top of the film, scrim and all.

   visibility is the belt to that brace, but it cannot switch when the class
   does: that is what left a window where neither face painted, the "skeleton"
   fixed earlier. It switches on a transition delay instead, timed to the
   moment the card passes 90 degrees, which is where backface-visibility would
   have handed over. The two then agree and the gap never reopens.

   That moment is NOT half the duration. --ease is cubic-bezier(0.22,1,0.36,1),
   which is heavily front-loaded: solved numerically, the turn reaches 90
   degrees at 13.1% of its length, i.e. 92ms of the 700ms. A 350ms delay was
   tried first and left the front face showing for 250ms too long on iOS.
   Re-derive this if either the duration or the curve changes. */
.card-face{
  backface-visibility:hidden;
  -webkit-backface-visibility:hidden;
  border-radius:var(--r-card);
  overflow:hidden;
}
.card-front{
  flex:1 1 auto;
  display:flex; flex-direction:column;
  min-width:0;
  background:var(--c-bg);
  visibility:visible;
  transition:background-color .4s var(--ease), visibility 0s linear .092s;
}

.card-back{
  position:absolute; inset:0;
  transform:rotateY(180deg);
  background:var(--demo-ground);
  display:flex; align-items:center; justify-content:center;
  visibility:hidden;
  transition:visibility 0s linear .092s;
}
.card.is-flipped .card-front{ visibility:hidden; }
.card.is-flipped .card-back{ visibility:visible; }

/* Fitted to width, centred. The band above and below is painted in the
   footage's own ground — #141414 for Kosta, #FCF3E8 for Sahly, both sampled
   off all four edges of four frames — so there is no visible letterbox and
   the film reads as bleeding edge to edge. Sahly is 4:3 and Kosta 16:9, so
   the two bands are different depths; that is why the colour is set per
   card in the markup rather than as one token here. */
.demo-video{
  width:100%; height:auto;
  max-height:100%;
  object-fit:contain;
  background:var(--demo-ground);
}

/* Brass, because closing is the one genuinely active control on the face.
   Which exposure of the brass depends on the ground, and the ground here is
   the footage's, not the theme's — so --demo-fg is set per card beside
   --demo-ground rather than read from --ac-ink. A flat #C4A75A would be
   7.91:1 on Kosta's ink and 2.12:1 on Sahly's cream, where it disappears.

     Kosta  #C4A75A on #141414   7.91:1
     Sahly  #7E6625 on #FCF3E8   5.01:1

   The disc is filled with the ground itself so the control stays legible
   wherever the film happens to be bright, and hover swaps ink for ground,
   which measures the same either way. */
.card-back{ --ac-ink:var(--demo-fg); }   /* so :focus-visible follows too */

.demo-close{
  position:absolute; top:16px; right:16px;
  z-index:1;
  width:40px; height:40px; border-radius:50%;
  border:1px solid var(--demo-fg);
  background:var(--demo-ground); color:var(--demo-fg);
  display:flex; align-items:center; justify-content:center;
  cursor:pointer; padding:0;
  transition:background-color .3s var(--ease), color .3s var(--ease);
}
.demo-close svg{ width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; }
@media (hover:hover){
  .demo-close:hover{ background:var(--demo-fg); color:var(--demo-ground); }
}

.card{ display:flex; flex-direction:column; }
.card-body{ padding:clamp(24px,2.2vw,32px); display:flex; flex-direction:column; flex:1 1 auto; }
.card-body{ border-top:0; }

.card-head{ display:flex; justify-content:space-between; align-items:flex-start; gap:24px; }

.card-title{ font-size:clamp(22px,2.1vw,30px); font-weight:600; letter-spacing:-.02em; line-height:1.1; }
.card-copy{ margin-top:10px; font-size:15px; line-height:1.5; max-width:420px; }
/* Both cards run four to five lines now, so a one-word last line is a real
   risk. `pretty` pulls a word down rather than leaving an orphan, and falls
   back to normal wrapping where it is unsupported. */
.card-copy{ text-wrap:pretty; }
.card-copy{ color:var(--c-fg-65); }

/* A real <button> since 2026-08-22: it opens the product film, and it is the
   card's only focusable control. Before that the card had cursor:pointer and
   nothing reachable by keyboard at all. The reset is explicit because a
   button inherits neither the page's font nor its colour. border-color joins
   the transition list because the hover now moves it to brass. */
.arrow{
  flex:0 0 auto;
  width:44px; height:44px;
  border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-size:18px;
  font-family:inherit;
  background:transparent;
  color:inherit;
  padding:0;
  cursor:pointer;
  transition:background-color .3s var(--ease), color .3s var(--ease),
             border-color .3s var(--ease), transform .3s var(--ease);
}
.arrow{ border:1px solid var(--c-fg-30); }
/* The action control fills brass rather than ink, arriving at the same
   moment the photograph gets its colour back. 44px of it, and only while
   the card is under the pointer: brass detailing, not a gold surface.
   Measured on the hovered card — disc 3.82:1 light / 6.60:1 dark, glyph on
   the disc 4.96:1 light / 8.51:1 dark. */
/* Same reasoning as .theme-toggle: on a touch screen this would latch on
   after a tap and hold. The card turns on tap anyway, so the front face is
   not the thing being looked at. */
@media (hover:hover){
  .card:hover .arrow{ background:var(--ac-ink); color:var(--ac-on); border-color:var(--ac-ink); transform:translateX(4px); }
}

/* The tag row and the case study link share one baseline. `margin-top:auto`
   and the 28px gap moved up here from `.tags` when the link was added: they
   are what collect the slack under the shorter card's copy so both cards'
   tag rows land on the same line, and that job now belongs to the row rather
   than to the pills inside it. See "Card tag alignment".

   The link was first placed after `.tags` as a sibling and read as an
   afterthought hanging off the bottom of the card. On the same line, right
   against the gutter, it is a counterweight to the pills instead. */
.card-foot{
  margin-top:auto;
  padding-top:28px;
  display:flex;
  align-items:center;
  gap:14px 20px;
  flex-wrap:wrap;
}
.tags{ display:flex; flex-wrap:wrap; gap:10px; font-size:10px; letter-spacing:.12em; }
.tags li{ border-radius:var(--r-pill); padding:6px 12px; }
.tags li{ border:1px solid var(--c-fg-25); color:var(--c-fg-75); }

.gap{ height:clamp(80px,9.7vw,140px); }

/* ============================================================
   CAPABILITIES
   ============================================================ */

/* Inside an ink block the ground is ink in both themes, so the ground-aware
   token can go back to the constant — the same move --block-fg makes for
   the foreground. Without this the light theme would put the 4.00:1 dark
   brass on ink where the 8.51:1 one belongs. */
.caps, .cta, .footer{ --ac-ink:var(--ac); --ac-on:#0A0A0A; }

.caps{
  background:var(--block-bg);
  color:var(--block-fg);
  border-radius:var(--r-block) var(--r-block) 0 0;
  padding:clamp(72px,7.2vw,104px) var(--gutter);
  box-shadow:var(--block-shadow);
}

/* Two columns: the heading and lead hold a left rail, the discipline rows run
   down the right. A single full-width column left ~450px of nothing to the
   right of every hairline, which read as a half-used rule rather than as air.
   This also shortens the section from 1177px to about 830px. */
.caps-inner{
  display:grid;
  grid-template-columns:minmax(0,30%) minmax(0,1fr);
  column-gap:clamp(40px,4vw,64px);
  align-items:start;
}

/* The rail is a plain grid column. It was sticky and that was removed: the
   section is only ~45px taller than a desktop viewport, so it pinned for a
   moment and released, which read as a scroll hitch rather than as an effect.
   Do not reinstate it without a much taller section. */

/* The waw mark, ordered-dithered into the mono grid. The source is the alpha
   channel of assets/brand/alvara-mark.svg — the same 3A mask that fades the
   logo's tail — thresholded against a Bayer 8x8 matrix, which is what a dither
   shader does. So the mark dissolves here for the same reason it dissolves in
   the nav, by the same ramp.
   Aceternity's dither-shader was the reference but none of it is used: it is
   React behind `npx shadcn add`, which cannot run in this repo. Ordered
   dithering needs no dependency.
   The grid is 38x28 and a mono cell is 0.6 wide by 1.0 tall, so the art was
   sampled at that ratio — squashing the source is what restores the mark's
   proportions once it is set in type. Do not add letter-spacing or change
   line-height: either one shears the mark. */
.caps-art{
  /* Solved so the rail stays shorter than the rows: at 1512 the heading, gap
     and lead take 263px of a 609px column, leaving 346 for this margin plus
     28 rows of art. 44 + 280 fits; 56 + 327 did not, and grew the section by
     36px. Re-derive both if the heading or lead ever changes length. */
  /* max-content shrinks the <pre> to its longest line so it can be centred;
     as a full-width block the art sat hard left against centred-looking copy
     above it. The ink fills all 38 columns, so the box is the mark's true
     extent and centring the box centres the mark. */
  width:max-content;
  margin:clamp(28px,2.9vw,44px) auto 0;
  font-size:clamp(7px,0.67vw,10px);
  line-height:1;
  letter-spacing:0;
  /* Brass at 3.11:1 in both themes. It was first set to the exact weight of
     the grey it replaced — 2.37 light, 2.51 dark, against --block-fg-30's
     2.37 and 2.50 — on the reasoning that a thousand glyphs should gain no
     presence from the hue change. On a real screen that read as too faint,
     so it went up one step of lightness on the same hue. 3.3:1 is the
     ceiling, where the field stops sitting behind the lead and starts
     competing with it, so this is close to the top of the window. Below
     1.7:1 it is too faint to occupy the space it exists to fill. It is
     decoration and aria-hidden, so AA does not apply. */
  color:var(--ac-art);
  white-space:pre;
  user-select:none;
}

.caps-h2{
  font-size:clamp(32px,3vw,44px);
  font-weight:600;
  letter-spacing:-.035em;
  line-height:1.08;
  margin-bottom:20px;
  /* Balance needs a box wide enough to have a choice. In the rail the heading
     sets three lines and balance keeps them even; do not pair it with a narrow
     max-width, which is what made this read as cramped at 820px full-width. */
  text-wrap:balance;
}

/* The lead closes the rail, so its old bottom margin is gone — the gap below
   it is the rail's, not the paragraph's. */
.caps-lead{
  font-size:16px;
  line-height:1.55;
  color:var(--block-fg-65);
  margin-bottom:0;
  text-wrap:pretty;
}

/* Two columns, not three. The old 120px column held an 01-04 counter; the rows
   are a set of disciplines now, not a sequence, so the numbers are gone and the
   titles fall back onto the same left edge as the h2. */
.caps-row{
  display:grid;
  /* 300px, because at 260 or less three of the five titles wrap to two lines
     and the rhythm falls apart. Measured at 1512. */
  grid-template-columns:minmax(0,300px) minmax(0,1fr);
  gap:clamp(20px,2.2vw,32px);
  padding:clamp(26px,2.5vw,36px) 0;
  border-top:1px solid var(--block-fg-18);
  align-items:baseline;
}
/* The first row starts the right column, so it has no rule above it and no
   top padding — that is what lines "Design" up with the headline beside it. */
.caps-row:first-child{ border-top:0; padding-top:0; }
.caps-row-last{ border-bottom:1px solid var(--block-fg-18); }

.caps-title{ font-size:clamp(22px,1.95vw,28px); font-weight:600; letter-spacing:-.02em; line-height:1.15; text-wrap:balance; }

/* The copy column now sizes the measure, but the cap stays as a guard so the
   line does not run away on a very wide window. */
.caps-copy{ font-size:15px; line-height:1.55; color:var(--block-fg-65); max-width:38em; text-wrap:pretty; }

/* ============================================================
   PROCESS
   ============================================================ */

.process{ padding-top:clamp(72px,7.6vw,110px); }
.process .marquee{ padding-bottom:clamp(40px,4.4vw,64px); }

.steps{
  display:grid;
  grid-template-columns:repeat(4,1fr);
  gap:var(--card-gap);
  padding:0 var(--gutter) clamp(80px,8.3vw,120px);
}
.step{ border-top:1px solid var(--fg-30); padding-top:24px; }
/* The index is the only coloured thing in the row: titles and copy stay
   black. This is a real sequence, so the colour is doing hierarchy rather
   than decoration — read the numbers first, then the names. 4.96:1 light,
   8.51:1 dark. */
.step-num{ font-size:11px; letter-spacing:.14em; color:var(--ac-ink); }
.step-title{ font-size:clamp(20px,1.7vw,24px); font-weight:600; letter-spacing:-.02em; margin-top:16px; line-height:1.15; }
.step-copy{ margin-top:12px; font-size:14px; line-height:1.55; color:var(--fg-65); }

/* ============================================================
   TESTIMONIAL
   ============================================================ */

.quote{
  border-top:1px solid var(--fg-15);
  padding:clamp(80px,9.7vw,140px) var(--gutter);
  text-align:center;
}
.quote-eyebrow{ font-size:11px; letter-spacing:.16em; color:var(--fg-55); margin-bottom:clamp(28px,2.8vw,40px); }
/* Sized for a real testimonial rather than a pull quote. The placeholder it
   replaced was 128 characters and carried 42px comfortably; the approved Kosta
   quote is 380, which at that size ran eight lines and turned the section into
   the tallest thing on the page. Dropping the cap to 30px and widening the
   measure to 1040 brings it to four lines at 1440.

   It reads better as well as fitting: a long, specific, awkwardly honest quote
   at a modest size is more convincing than a short one set huge, which is the
   register of a slogan. Do not push this back up without re-measuring the line
   count. */
.quote-text{
  margin:0 auto;
  font-size:clamp(19px,2.1vw,30px);
  font-weight:500;
  letter-spacing:-.02em;
  line-height:1.34;
  max-width:1040px;
}
.quote-by{ font-size:12px; letter-spacing:.14em; margin-top:clamp(28px,3vw,44px); color:var(--fg-65); }

/* ============================================================
   CTA + FOOTER  (one continuous ink block)
   ============================================================ */

.cta{
  background:var(--block-bg);
  color:var(--block-fg);
  border-radius:var(--r-block) var(--r-block) 0 0;
  padding:clamp(88px,9vw,130px) var(--gutter) 0;
  text-align:center;
  box-shadow:var(--block-shadow);
}
.cta-h2{
  font-size:clamp(40px,6.7vw,96px);
  font-weight:700;
  letter-spacing:-.045em;
  line-height:1;
}
.cta .btn{ margin-top:clamp(32px,3.9vw,56px); }

.footer{
  background:var(--block-bg);
  color:var(--block-fg);
  /* the wordmark sizes off this element's content box */
  container-type:inline-size;
  /* padding, not margin, so it cannot collapse out of the ink block */
  padding:clamp(80px,9vw,130px) var(--gutter) 0;
}
.footer-row{
  display:flex;
  justify-content:space-between;
  align-items:center;
  gap:24px;
  flex-wrap:wrap;
  padding:28px 0;
  border-top:1px solid var(--block-fg-18);
  font-size:14px;
}
.footer-meta{ font-size:11px; letter-spacing:.1em; color:var(--block-fg-50); }

/* ---------- the contact / legal / follow columns ----------
   Four even tracks, not the uneven 30/70 the capabilities section uses:
   these are four lists of the same kind, where caps is one argument beside
   its evidence. An earlier pass led with a wide "Studio" column carrying the
   postal address; that content was cut, and what was left of it duplicated
   the meta line word for word, so the column went with it.

   Site, Contact, Legal, Follow. The three section links MOVED here from the
   hairline row, they were not copied: listing them in both places is what
   turns a footer into a sitemap, and it was the reason an earlier draft of
   this block was rejected. The row below now carries the copyright alone.
   The email moved for the same reason, out of that row and into Contact,
   because printing the same address twice in one footer is worse than
   re-measuring a row. */
/* The tracks are content-sized and the leftover is distributed BETWEEN them,
   rather than four equal tracks each holding its content at the left. The
   page has no max-width, so at 2000px an even grid gives each column a 468px
   cell for about 100px of links: three of them hug a left edge and the fourth
   leaves 216px of dead air before the right gutter, and the whole block reads
   as left aligned inside a very wide box. Content-sized tracks put the last
   column flush to the right gutter, so the block is bracketed by the same two
   edges as every other row on the page, and space-between keeps the three
   gaps identical whatever the leftover turns out to be. */
.footer-cols{
  display:grid;
  grid-template-columns:repeat(4,max-content);
  justify-content:space-between;
  column-gap:48px;
  padding-bottom:48px;
}
.footer-col-h{
  font-size:10px;
  letter-spacing:.16em;
  color:var(--block-fg-50);
  margin:0 0 16px;
}
.footer-col ul{ display:grid; gap:9px; font-size:14px; }

/* The glyph is 0.86em, so it tracks the 14px beside it: a solid 12px square
   against a 10.0px cap height, which puts it 1px proud top and bottom. Sized
   at a full 1em the filled bug reads about a weight heavier than the word it
   labels and pulls the eye down the column. */
.footer-social{ display:inline-flex; align-items:center; gap:9px; }
.soc{ width:.86em; height:.86em; flex:0 0 auto; }

/* The separator in the meta line. One 11px glyph — the smallest the accent
   gets, and the last thing anyone notices. It is wrapped in <em> purely to
   have something to colour; the copy is unchanged. */
.footer-meta em{ font-style:normal; color:var(--ac); }

/* A 1px brass rule wipes in under the address on hover. The link's own text
   never changes colour, so the resting footer is what it always was. The
   nav and footer .roll links deliberately get nothing: they already carry
   the label flip, and a second hover signal on the same component would be
   two answers to one question. */
/* The rule hangs below the box rather than being made room for with padding,
   which now keeps the 9px gap between the two Contact items even rather than
   9px above and 12px below. It was originally written for the hairline row,
   where a 3px taller link would have shifted the address off the baseline it
   shared with the nav and the meta line. */
.footer-mail{ position:relative; display:inline-block; }
.footer-mail::before{
  content:'';
  position:absolute; left:0; right:0; bottom:-4px; height:1px;
  background:var(--ac);
  transform:scaleX(0); transform-origin:left;
}
.js-motion .footer-mail::before{ transition:transform .35s var(--ease); }
.footer-mail:hover{ opacity:1; }
.footer-mail:hover::before,
.footer-mail:focus-visible::before{ transform:scaleX(1); }

/* The wordmark is set in real Outfit Bold rather than the Light artwork,
   which only ships as outlines. Numbers are measured off the live face:
   "alvara" is 2.983em wide and 0.726em tall to the ascender, so 33.5cqw
   fills the footer's content box and 0.52em crops it at 72% of the
   letterform, cutting through the bowls well above the baseline.

   Outfit declares a tall hhea ascent, so at line-height 1 Chrome puts the
   baseline a full 1em below the box top. The ascender therefore starts
   0.274em down, and that is what the word's negative margin cancels. */
.wordmark-crop{
  position:relative;
  font-size:30vw;            /* fallback where container units are missing */
  font-size:33.5cqw;
  height:.52em;
  overflow:hidden;
  margin-top:16px;
}

/* Each layer fills the crop box exactly, so a supplied photograph maps 1:1
   onto the band that is actually visible. It used to be painted across the
   full 1em line box, of which only the middle 52% ever showed, which is why
   crops kept losing their subject. Two layers cross-fade, and only the
   hidden one is ever repainted, so a swap is never visible mid-fade. */
.wm-layer{
  position:absolute;
  inset:0;
  background-position:center;
  background-size:cover;
  background-repeat:no-repeat;
  color:var(--block-fg);
}
.wm-word{
  display:block;
  font-family:'Outfit', var(--sans);
  font-weight:700;
  font-size:1em;
  line-height:1;
  margin-top:-.275em;        /* seats the ascender flush to the crop */
  letter-spacing:0;
  text-align:center;
  color:inherit;
}
.wm-ready .wm-layer{
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  opacity:0;
}
.wm-ready .wm-layer.is-on{ opacity:1; }
.js-motion .wm-ready .wm-layer{ transition:opacity .55s var(--ease); }

/* ============================================================
   FIRST-LOAD GATE
   The footage runs its whole arc alone before the page arrives.
   Everything is held back, and the scroll is locked so nothing can
   be revealed early by scrolling past it.
   ============================================================ */

.intro-gate,
.intro-gate body{ overflow:hidden; height:100%; }

.intro-gate .nav,
.intro-gate .hero-body,
.intro-gate .ticker,
.intro-gate .hero-scrim{ opacity:0; }

.intro-gate .nav{ pointer-events:none; }

.intro-gate main > :not(.hero),
.intro-gate .footer{ visibility:hidden; }

/* the nav and scrim ease in with the rest once the gate lifts */
.js-motion .nav,
.js-motion .hero-scrim{ transition:opacity .55s var(--ease); }

/* ============================================================
   REVEALS
   js-reveal  = JS present, opacity fades allowed
   js-motion  = JS present AND motion allowed (adds transforms)
   Neither class present (no JS) means everything renders final.
   ============================================================ */

.js-reveal .reveal{
  opacity:0;
  transition:opacity .7s var(--ease);
  transition-delay:var(--reveal-delay, 0ms);
}
.js-motion .reveal{
  transform:translateY(24px);
  transition:opacity .7s var(--ease), transform .7s var(--ease);
  transition-delay:var(--reveal-delay, 0ms);
}
.js-reveal .reveal.is-in{ opacity:1; transform:none; }

/* Hero load: a straight fade, everything together, triggered on the frame
   the monitor cuts out. No movement and no stagger. The original was a
   1.5s line-by-line mask that read as its own event rather than as a
   consequence of the light going out. */
.js-reveal .hero-h1,
.js-reveal .hero-row,
.js-reveal .ticker{ opacity:0; }

.js-reveal.hero-ready .hero-h1,
.js-reveal.hero-ready .hero-row,
.js-reveal.hero-ready .ticker{
  opacity:1;
  transition:opacity .55s var(--ease);
}

/* footer wordmark rises out of its crop */
.js-motion .hero-h1,
.js-motion .hero-sub,
.js-motion .hero-cta{ will-change:transform; }

.js-motion .wm-word{ transform:translateY(60px); transition:transform 1s var(--ease); }
.js-motion .wordmark-crop.is-in .wm-word{ transform:none; }

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

/* ============================================================
   DOCUMENT PAGES — /privacy.html, /terms.html

   These sit on the page ground rather than in an ink block: a legal
   document is read, not scanned, and cream-on-ink for two thousand
   words is a different job from a 40px section heading. The ink
   arrives at the footer, so the page still lands where every other
   page lands.

   The grid is the capabilities grid, 30/70, for the same reason it
   was chosen there: a narrow rail beside its evidence. Here the rail
   is sticky, which caps deliberately is not. That was removed there
   because the section was only ~45px taller than the viewport, so it
   pinned for a moment and released, reading as a hitch. A policy runs
   several screens, which is the case sticky was made for.
   ============================================================ */
.doc{
  padding:calc(96px + var(--gutter)) var(--gutter) clamp(72px,8vw,112px);
}
.doc-eyebrow{ font-size:11px; color:var(--fg-55); margin-bottom:20px; }
.doc-h1{
  font-size:clamp(40px,5.6vw,76px);
  font-weight:700;
  letter-spacing:-.042em;
  line-height:1.02;
  text-wrap:balance;
  max-width:14ch;
}
.doc-standfirst{
  margin-top:22px;
  font-size:clamp(16px,1.35vw,19px);
  line-height:1.55;
  color:var(--fg-65);
  max-width:56ch;
  text-wrap:pretty;
}
.doc-grid{
  display:grid;
  grid-template-columns:minmax(0,30%) 1fr;
  gap:clamp(32px,4.4vw,64px);
  margin-top:clamp(48px,5.5vw,80px);
  padding-top:clamp(32px,3.4vw,48px);
  border-top:1px solid var(--fg-18);
}

/* ---- the rail ---- */
.doc-rail{ position:sticky; top:104px; align-self:start; }
.doc-rail-h{ font-size:10px; color:var(--fg-55); margin-bottom:16px; }
/* the numbers are a CSS counter in both the rail and the body, so the two
   can never drift apart when a section is added or moved */
.doc-toc{ counter-reset:toc; display:grid; gap:9px; font-size:14px; }
.doc-toc li{ counter-increment:toc; display:flex; gap:12px; }
.doc-toc li::before{
  content:counter(toc,decimal-leading-zero);
  font-family:var(--mono);
  font-size:11px;
  letter-spacing:.1em;
  color:var(--fg-30);
  flex:0 0 auto;
  padding-top:2px;
}
.doc-updated{
  margin-top:28px;
  padding-top:20px;
  border-top:1px solid var(--fg-18);
  font-size:11px;
  color:var(--fg-55);
}

/* ---- the text ---- */
.doc-body{ counter-reset:sec; max-width:66ch; }
.doc-body h2{
  counter-increment:sec;
  font-size:clamp(22px,2.1vw,28px);
  font-weight:700;
  letter-spacing:-.026em;
  line-height:1.18;
  margin:0 0 18px;
  text-wrap:balance;
}
.doc-body h2::before{
  content:counter(sec,decimal-leading-zero);
  display:block;
  font-family:var(--mono);
  font-weight:400;
  font-size:11px;
  letter-spacing:.14em;
  color:var(--fg-55);
  margin-bottom:10px;
  /* deliberately not the accent. The process index 01-04 is brass because
     it is one of the six sanctioned placements; a seventh would be a
     seventh, and these are numbers on a legal document, not a device. */
}
.doc-sec + .doc-sec{
  margin-top:clamp(40px,4vw,56px);
  padding-top:clamp(40px,4vw,56px);
  border-top:1px solid var(--fg-15);
}
.doc-body p,
.doc-body li{
  font-size:16.5px;
  line-height:1.68;
  color:var(--fg-75);
  text-wrap:pretty;
}
.doc-body p + p{ margin-top:16px; }
.doc-body ul{ margin-top:16px; display:grid; gap:12px; }
.doc-body ul li{ position:relative; padding-left:20px; }
.doc-body ul li::before{
  content:'';
  position:absolute;
  left:0; top:.72em;
  width:7px; height:1px;
  background:var(--fg-30);
}
.doc-body strong{ color:var(--fg); font-weight:500; }
/* <code> would otherwise fall back to the browser's default monospace, which
   is a third typeface. It is IBM Plex Mono, like every other mono on the
   page, one step down so it sits inside the line rather than on top of it. */
.doc-body code{
  font-family:var(--mono);
  font-size:.86em;
  letter-spacing:.02em;
  color:var(--fg);
}
.doc-body a{ text-decoration:underline; text-underline-offset:3px; text-decoration-thickness:1px; }
.doc-body a{ text-decoration-color:var(--fg-30); }
.doc-body a:hover{ text-decoration-color:var(--ac-ink); }

@media (max-width:1024px){
  /* The rail stops being viable well before the row does: at 1024 it is only
     ~290px, too narrow for a 32px heading. Both collapse together. */
  .caps-inner{ grid-template-columns:1fr; }
  /* Stacked, the art would sit between the lead and the rows and push the
     whole list down for no gain — it only exists to occupy the rail. */
  .caps-art{ display:none; }
  .caps-lead{ margin-bottom:clamp(44px,5vw,72px); }
  .caps-row:first-child{ border-top:1px solid var(--block-fg-18); padding-top:clamp(26px,2.5vw,36px); }
  .caps-row{ grid-template-columns:1fr; row-gap:14px; }
  .stats{ grid-template-columns:1fr 1fr; }
  .stat:nth-child(-n+2){ border-bottom:1px solid var(--fg-15); }
  .stat + .stat{ border-left:none; }
  .stat:nth-child(even){ border-left:1px solid var(--fg-15); }
  .steps{ grid-template-columns:1fr 1fr; row-gap:40px; }
  /* stacked, the rail is a header rather than a column, so it must not pin */
  .doc-grid{ grid-template-columns:1fr; gap:36px; }
  .doc-rail{ position:static; }

}

/* Below this width the compact pill can no longer hold the link row, so the
   links become a panel under the pill and the toggle takes over. */
@media (max-width:860px){
  .nav-pill{ width:100%; max-width:none; gap:16px; }
  .nav-toggle{ display:flex; }

  /* Burger | wordmark | theme toggle. Left-aligning the brand left a 123px
     hole in the middle of the bar at 390px — 36% of its width — and empty
     space inside a bounded shape reads as a mistake, where the same space
     split either side of a centred mark reads as composition.
     A grid, not space-between: the two controls are different widths, so
     distributing free space would leave the wordmark visibly off-centre.
     `display:contents` lifts the controls out of .nav-actions so they can be
     placed in their own columns without restructuring the markup. */
  .nav-pill{
    display:grid;
    grid-template-columns:1fr auto 1fr;
    align-items:center;
    padding:10px 12px;
  }
  .nav-actions{ display:contents; }
  .nav-toggle{ order:-1; justify-self:start; }
  .nav-brand{ justify-self:center; }
  .theme-toggle{ justify-self:end; }

  .nav-links{
    position:absolute;
    top:calc(100% + 10px);
    left:0; right:auto;
    width:max-content;
    min-width:190px;
    max-width:min(72vw, 300px);
    flex-direction:column;
    align-items:stretch;
    gap:2px;
    padding:8px;
    border-radius:24px;
    background:var(--panel-bg);
    /* NOTE: this blur is very nearly a no-op and the opacity above is doing
       the real work. .nav-links is a child of .nav-pill, and an element with
       backdrop-filter creates a Backdrop Root for its descendants — so this
       filter can only sample what is painted inside the pill, and the panel
       hangs below the pill's box entirely. Raising the blur will not make the
       hero copy behind the menu any softer; only the alpha will. Kept because
       it costs nothing and becomes real the moment the panel is moved out of
       the pill. */
    backdrop-filter:blur(28px) saturate(1.2);
    -webkit-backdrop-filter:blur(28px) saturate(1.2);
    opacity:0;
    visibility:hidden;
    transform:translateY(-8px);
    transition:opacity .3s var(--ease), transform .3s var(--ease), visibility .3s;
  }
  .nav.is-open .nav-links{ opacity:1; visibility:visible; transform:none; }

  /* What actually collides with the menu is the hero copy sitting directly
     behind it, not the footage — and the panel's own blur cannot soften it
     (see the note on .nav-links: a backdrop-filtered ancestor means this
     panel's backdrop root excludes the page). So the copy steps back while
     the menu is open. That is what lets the panel stay properly translucent:
     the landscape still reads through the glass, only the competing text
     recedes. */
  /* the page blur lives on .nav-scrim, outside this breakpoint — see below */

  /* in the panel the links are rows, so the roll effect stands down */
  .nav-links .roll{ overflow:visible; padding:14px 18px; border-radius:16px; font-size:16px; }
  .nav-links .roll::after{ display:none; }
  .nav-links .roll span{ transform:none !important; }
  .nav-links .roll:hover,
  .nav-links .roll:focus-visible{ background:var(--panel-hover); }

  /* one primary action, and it lives with the links rather than beside them.
     Scoped through .nav-actions so the later `.btn{display:inline-flex}`
     touch-target rule cannot bring it back. */
  .nav-actions .nav-cta{ display:none; }
  .nav-panel-cta{
    display:flex;
    justify-content:center;
    margin:8px 4px 4px;
    padding:15px 24px;
    font-size:15px;
  }
}

@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .nav-links{ background:var(--panel-bg-solid); }
}

/* On a tall, narrow hero the type occupies far more of the frame, so the
   corner scrim alone leaves the eyebrow sitting on bright grass. */
@media (max-width:768px){
  :root{
    --scrim:
      linear-gradient(to bottom, rgba(244,243,240,0.62) 0%, rgba(244,243,240,0.3) 34%, rgba(244,243,240,0) 62%),
      linear-gradient(to top, rgba(244,243,240,0.18) 0%, rgba(244,243,240,0) 10%);
  }
  [data-theme="dark"]{
    --scrim:
      linear-gradient(to bottom, rgba(10,10,10,0.66) 0%, rgba(10,10,10,0.34) 34%, rgba(10,10,10,0) 62%),
      linear-gradient(to top, rgba(10,10,10,0.26) 0%, rgba(10,10,10,0) 10%);
  }
}

@media (max-width:768px){
  :root{ --r-block:40px; }
  .nav{ top:16px; left:16px; right:16px; }
  .nav-pill{ padding:8px 10px; }
  .nav-brand img{ height:30px; }
  .nav-cta{ padding:12px 18px; }

  .hero{ min-height:max(640px, 100svh); }
  .hero-row{ flex-direction:column; align-items:flex-start; gap:24px; }

  .cards{ grid-template-columns:1fr; padding:0 var(--gutter); }
  .card-media{ height:clamp(220px,52vw,340px); }
  .card-head{ gap:16px; }

  .stat-num{ font-size:clamp(64px,17vw,96px); }

  .steps{ grid-template-columns:1fr; row-gap:32px; }

  .footer-cols{ grid-template-columns:repeat(2,minmax(0,1fr)); justify-content:start; gap:32px 40px; padding-bottom:36px; }
  .footer-row{ flex-direction:column; align-items:flex-start; gap:14px; }

  .btn{ min-height:44px; display:inline-flex; align-items:center; justify-content:center; }
  /* The 44px touch target, and for .roll the min-height ONLY: it is already
     inline-flex and must keep align-items:stretch, or its two layers stop
     filling the clipping box and the label reads as struck through. The
     other column links are ordinary inline boxes, so they need a display
     before a min-height can do anything at all. */
  .footer-col .roll{ min-height:44px; }
  .footer-col a:not(.roll){ min-height:44px; display:inline-flex; align-items:center; }
  /* the 44px boxes already separate the items; keeping the 9px gap as well
     spaces a four-item list out to 53px a row and it reads as loose */
  .footer-col ul{ gap:0; }
}

@media (max-width:375px){
  .ticker-group{ gap:36px; padding:0 18px; }
  .footer-cols{ grid-template-columns:1fr; gap:28px; }
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */

@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  .ticker-track,
  .marquee-track,
  .marquee-track i{ animation:none; transform:none; }
  .marquee-track{ width:100%; }
  .marquee-group:nth-child(2){ display:none; }
  .hero-bg{ transform:none !important; inset:0; }
  /* the parallax writes inline transforms; JS reads the preference once at
     load, so clear them here in case it is turned on mid-session */
  .hero-h1,
  .hero-sub,
  .hero-cta{ transform:none !important; will-change:auto; }
  .card:hover{ transform:none; }
  .card:hover .slot-img{ transform:none; }
  .card:hover .arrow{ transform:none; }
  /* the film still opens, it just cross-fades instead of turning in space */
  .card{ perspective:none; }
  .card-inner{ transform:none !important; transition:none; }
  /* the back simply fades in over the front, so both stay visible throughout
     and the delayed visibility swap has no place here */
  .card-back{ transform:none; opacity:0; transition:opacity .3s linear; visibility:visible; }
  .card.is-flipped .card-back{ opacity:1; }
  .card.is-flipped .card-front{ visibility:visible; }
  .card-face{ backface-visibility:visible; -webkit-backface-visibility:visible; }
  .card-front{ transition:none; }
  /* the hint is a transform, so it goes; the arrow's accessible name is what
     carries the affordance here */
  .card.is-nudging .card-inner{ animation:none; }
  .btn:active{ transform:none; }
  /* the stills under the hero carry both states without any playback */
  .hero-video{ display:none; }
  .nav-toggle .bar{ transition:width .2s linear; }
  .nav-toggle:hover .bar:nth-child(1),
  .nav-toggle:hover .bar:nth-child(2){ transform:none; }
  .nav-links{ transition:opacity .2s linear, visibility .2s; transform:none !important; }
}

/* ============================================================
   CASE STUDIES  (/work/kosta, /work/sahly)

   Four additions to the document template the legal pages and the FAQ
   already share. Everything else on those pages is reused as-is.
   ============================================================ */

/* The tag row under the standfirst, echoing the product card's own tags so
   the page reads as the same object opened up. `.tags` already carries the
   pill styling; this only positions the row. */
.cs-tags{ margin:22px 0 0; }

/* Embed / Shape / Build. A real sequence with real durations, which is the
   one place on this site a numbered row is honest rather than decorative. */
.cs-phases{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:14px;
  margin:0 0 22px;
}
.cs-phase{ background:var(--panel-bg,rgba(10,10,10,0.045)); border-radius:3px; padding:16px 18px; }
.cs-phase .n{
  display:block; font-family:var(--mono); font-size:10.5px; letter-spacing:.14em;
  text-transform:uppercase; color:var(--ac-ink); margin-bottom:6px;
}
.cs-phase b{ display:block; font-size:22px; letter-spacing:-.02em; margin-bottom:2px; }
.cs-phase .d{ display:block; font-size:13.5px; line-height:1.45; color:var(--fg-65); }
@media (max-width:620px){ .cs-phases{ grid-template-columns:1fr; } }

/* The client quote. Deliberately quieter than the homepage testimonial: there
   it is the whole section, here it is evidence at the end of an argument. */
.cs-quote{
  margin:0; padding:0 0 0 22px; border-left:2px solid var(--ac-ink);
}
.cs-quote p{ font-size:19px; line-height:1.45; margin:0 0 12px; }
.cs-quote footer{ font-size:12px; letter-spacing:.14em; color:var(--fg-55); }

.cs-next{ margin-top:8px; }
.cs-next a{ color:var(--ac-ink); }

/* Sends the reader to the case study without flipping the card. `js/site.js`
   already returns early for any click on an `a` inside a card, so this needs
   no JavaScript. Sits in `.card-foot`, on the tag row's baseline.

   Colour comes from the card's own local properties, not the page tokens, so
   it follows the one-tonal-step hover with everything else in the card. */
.card-cs{
  flex:0 0 auto;
  /* `margin-left:auto` rather than `justify-content:space-between` on the row.
     Below about 430 the Kosta card's three pills fill the line and the link
     wraps; space-between then left-aligns it, because it is alone on the second
     line, so one card had the link right and the other had it left. An auto
     margin pins it to the right edge whether it wrapped or not. */
  margin-left:auto;
  font-family:var(--mono);
  font-size:10px;
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--c-fg-75);
  text-decoration:none;
  border-bottom:1px solid var(--c-fg-25);
  padding-bottom:3px;
  white-space:nowrap;
}
@media (hover:hover){
  .card-cs:hover{ border-bottom-color:var(--ac-ink); }
}

/* ============================================================
   CONSENT BANNER  (direction A, "the ink block")

   Injected by js/analytics.js rather than written into six pages.

   It is the same object as `.caps` and `.cta`: full bleed, ink, rounded on
   the top corners only, with the upward `--block-shadow`. That is the page's
   existing way of saying "a different kind of thing starts here", which is
   exactly what a consent request is. An earlier version was a rounded
   lozenge floating bottom centre, which put a SECOND floating pill on a page
   that already has one and centred it against a layout built on a hard left
   spine. Do not go back to a floating card.

   The copy sits on `--gutter`, the same 64px spine as the hero headline, the
   stats row and the capabilities h2.

   THE HEIGHT IS DELIBERATE. This is a request for permission and it is
   supposed to be answered rather than ignored, so it is a substantial block
   rather than a strip. Do not trim the vertical padding to make it discreet.

   ACCEPT AND DECLINE ARE IDENTICAL. Same size, same border, same weight, one
   click each. Under GDPR refusing has to be as easy as agreeing, and a quiet
   decline beside a loud accept is the single most-fined dark pattern in
   Europe. Do not make Accept the primary button.
   ============================================================ */

.consent{
  position:fixed;
  left:0; right:0; bottom:0;
  z-index:200;                        /* above .nav's 100 */
  background:var(--block-bg);
  color:var(--block-fg);
  border-radius:var(--r-block) var(--r-block) 0 0;
  box-shadow:var(--block-shadow);
  padding:clamp(28px,3.2vw,42px) var(--gutter) clamp(30px,3.4vw,46px);
  display:flex;
  align-items:flex-end;
  gap:clamp(24px,4vw,64px);
  opacity:0;
  transform:translateY(16px);
  transition:opacity .5s var(--ease), transform .5s var(--ease);
}
.consent.is-in{ opacity:1; transform:none; }

.consent-lbl{
  display:block;
  font-family:var(--mono);
  font-size:11px;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--ac);
  margin-bottom:10px;
}

/* No max-width. The line is written to fit on one line at 1440 and the
   banner is full bleed, so constraining the measure would only reintroduce
   the wrapping and the dead space beside it. */
.consent-copy{
  margin:0;
  font-size:clamp(14px,1.05vw,15.5px);
  line-height:1.5;
  color:var(--block-fg-70);
  text-wrap:pretty;
}
.consent-copy a{ color:var(--ac); text-underline-offset:3px; }

.consent-acts{
  margin-left:auto;
  display:flex;
  gap:10px;
  flex:0 0 auto;
}

.consent-btn{
  font-family:inherit;
  font-size:14px;
  font-weight:500;
  letter-spacing:-0.01em;
  padding:11px 26px;
  border-radius:var(--r-pill);
  border:1px solid var(--block-fg-30);
  background:transparent;
  color:var(--block-fg);
  cursor:pointer;
  white-space:nowrap;
  transition:border-color .2s var(--ease), background .2s var(--ease);
}
@media (hover:hover){
  .consent-btn:hover{ border-color:var(--block-fg); background:rgba(244,243,240,0.07); }
}
.consent-btn:focus-visible{ outline:2px solid var(--ac); outline-offset:3px; }

/* Below 860 the controls take their own row at full width, so neither is
   reachable by thumb before the other. The block keeps its height. */
@media (max-width:860px){
  .consent{
    flex-wrap:wrap;
    align-items:stretch;
    border-radius:32px 32px 0 0;
    gap:22px;
  }
  .consent-acts{ margin-left:0; width:100%; }
  .consent-btn{ flex:1 1 0; padding:13px 0; }
}

@media (prefers-reduced-motion:reduce){
  .consent{ transition:opacity .01ms; transform:none; }
}

/* An inline control inside document prose. Used once, for reopening the
   cookie choice from privacy.html: withdrawal has to be as easy as consent,
   and a link that does nothing without JavaScript would be worse than a
   button that says what it is. Styled as the surrounding link so it does not
   read as a form control dropped into a sentence. */
.doc-inline-btn{
  font:inherit;
  color:var(--ac-ink);
  background:none;
  border:0;
  padding:0;
  cursor:pointer;
  text-decoration:underline;
  text-underline-offset:3px;
}
.doc-inline-btn:focus-visible{ outline:2px solid var(--ac-ink); outline-offset:3px; }
