/* ===========================================================================
   OYNK — quiet confidence
   Flat, typographic, calm. Type gets lighter as it gets bigger.
   =========================================================================== */

@font-face {
  font-family: "Jakarta";
  src: url("/assets/fonts/plus-jakarta-sans-latin.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* surfaces — neutral dark, never blue-black, never pure black */
  --ground:    #141414;
  --raised:    #1c1c1c;
  --raised-2:  #242424;
  --line:      #2a2a2a;
  --line-hi:   #3a3a3a;

  /* Text — most copy is grey; off-white is emphasis, not the default.
     The reference site's greys (#8a8a8a / #5f5f5f) are AA at best — 5.34:1
     and 2.89:1 on this ground. OYNK advertises WCAG 2.2 AAA, so these are
     lifted to clear 7:1 on both surfaces. Hierarchy comes from size and
     weight instead of from contrast. */
  --ink:       #dedede;   /* 13.69:1 */
  --ink-dim:   #b4b4b4;   /*  8.89:1 */
  --ink-faint: #a8a8a8;   /*  7.75:1 */

  /* two flat pastels. No gradients, no glow. */
  --butter:    #ffef93;
  --lilac:     #c2a8ff;
  --mint:      #a8ffc2;   /* --lilac, channels rotated */
  --peach:     #ffc2a8;   /* rotated again */
  --on-accent: #0b0812;

  --sans: "Jakarta", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --r-pill: 9999px;
  --r-card: 20px;
  --r-sm:   12px;

  --shell:  82rem;
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --tap:    48px;
  --dot-gap: 14px;   /* pitch of the hero lattice */
  --ease:   cubic-bezier(.22, .61, .36, 1);
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; background: var(--ground); }

/* No-JS fallback. When scripting is on, data-motion is set before first
   paint and the MOTION PREFERENCE layer at the end of this file decides
   instead — including letting a visitor turn motion back on. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-motion]) { scroll-behavior: auto; }
  :root:not([data-motion]) *,
  :root:not([data-motion]) *::before,
  :root:not([data-motion]) *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 400 1rem/1.7 var(--sans);
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: hidden makes body a scroll container, and a sticky
     element cannot stick inside one that is not the thing being scrolled.
     clip stops the sideways overflow without creating the container. */
  overflow-x: clip;
}

img, svg { max-width: 100%; height: auto; display: block; }
::selection { background: var(--lilac); color: var(--on-accent); }

:where(a, button, input, summary):focus-visible {
  outline: 2px solid var(--lilac);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

.shell { width: 100%; max-width: var(--shell); margin-inline: auto; padding-inline: var(--gutter); }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: var(--ground);
  padding: 1rem 1.5rem; border-radius: 0 0 var(--r-sm) 0; font-weight: 600;
}
.skip:focus { left: 0; }

/* ---------------------------------------------------------------- TYPE --- */

h1, h2, h3, h4 { margin: 0; text-wrap: balance; }

/* The rule: the bigger it is, the lighter it gets. */
.display {
  font-size: clamp(2.5rem, 6.4vw, 5.75rem);
  font-weight: 500;
  letter-spacing: -0.05em;
  line-height: .9;
}
.statement {
  font-size: clamp(1.875rem, 4.2vw, 3.75rem);
  font-weight: 400;
  letter-spacing: -0.05em;
  line-height: 1.05;
}
.title {
  font-size: clamp(1.25rem, 1.8vw, 1.5rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.25;
}

/* Uppercase is reserved for 12px labels. Nothing else shouts. */
/* The marker runs a four-colour cycle down the page rather than the same
   lilac ten times. Three of the four are one hue rotation of the same
   value, so they carry equal weight on the ground and cannot fight each
   other. Every one is a light tint under the same near-black text, so the
   contrast holds whichever lands where: 9.8:1 at worst against a 7:1 bar. */
/* Nine steps rather than four, in no particular order. On a four-cycle the
   run reads as lilac-butter-mint-peach over and over and you see the rhythm
   before you have read the words. Nine is long enough that a page runs out of
   sections before the sequence comes round, and the order inside it is
   deliberately uneven — peach lands three times, the rest twice. The only
   rule kept is that no two touching sections share a colour, wrap included. */
main > section { --mark: var(--lilac); }
main > section:nth-of-type(9n+2) { --mark: var(--mint); }
main > section:nth-of-type(9n+3) { --mark: var(--butter); }
main > section:nth-of-type(9n+4) { --mark: var(--peach); }
main > section:nth-of-type(9n+5) { --mark: var(--lilac); }
main > section:nth-of-type(9n+6) { --mark: var(--peach); }
main > section:nth-of-type(9n+7) { --mark: var(--mint); }
main > section:nth-of-type(9n+8) { --mark: var(--butter); }
main > section:nth-of-type(9n+9) { --mark: var(--peach); }

.label {
  display: inline-block;
  font-size: .75rem; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase;
  margin: 0 0 1.5rem;
  color: var(--ink);
  padding: .45em .6em .4em;
}

/* On the highlight everything is one colour — the two accents were doing
   the distinguishing before, and they cannot on a lilac ground. */
.label .a, .label .b { color: inherit; }
.label .b { font-weight: 800; }

p { margin: 0 0 1.15em; color: var(--ink-dim); max-width: 60ch; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }
.lede {
  font-size: clamp(1.0625rem, 1.55vw, 1.5rem);
  line-height: 1.65; color: var(--ink-dim); max-width: 34ch;
}
.ink { color: var(--ink); }

a { color: var(--ink); text-decoration-color: var(--line-hi); text-underline-offset: .2em; }
a:hover { text-decoration-color: currentColor; }

/* Key phrase inside a statement, marked the way you would with a highlighter.
   Same gesture as the section label above it and the same colour, so a section
   reads as one idea rather than two decorations.

   Padding is in em so it tracks the display size, and box-decoration-break
   keeps the block intact if the phrase wraps a line. The wipe reuses the label
   keyframes: the stroke draws left to right and the dark text appears as it
   passes. */
.hl { color: inherit; padding: .04em .1em; }

/* ------------------------------------------------------------- MARKER --- */
/* The words are never missing. The element itself is ordinary light text on the
   ground; a copy of it sits on top carrying the marker and the dark text, and
   that copy is clipped to nothing until it is scrolled to. Opening the clip
   sweeps stroke and dark text across together, so at every frame the phrase is
   readable — light where the marker has not reached, dark where it has.

   The earlier version faded the real text out and back in, which meant a
   heading on screen with a hole in it while it waited its turn.

   The copy comes from data-text so there is only ever one readable string; the
   overlay is aria-hidden by being generated content, so it is never announced
   twice. */
.label, .hl { position: relative; display: inline-block; }

/* Fail visible, not blank. The stroke covers the element's own text and the
   dark copy is drawn from data-text — so a label added without that attribute
   paints a solid block of colour with nothing in it. Rather than rely on
   remembering, drop the whole treatment when the attribute is absent: the
   label just renders as plain light text. */
.label:not([data-text])::before, .label:not([data-text])::after,
.hl:not([data-text])::before,    .hl:not([data-text])::after { content: none; }
/* The stroke is its own layer so it can sit lower than the text box. With a
   tight line-height the ink overflows the content box — measured at 0.063em on
   the statements — so a background bounded by that box leaves the tail of a g
   sticking out below it in the light colour. The label needs none of this: its
   ink lands 7.9px inside its own box. */
.label::before, .hl::before {
  content: "";
  position: absolute; inset: 0;
  background-color: var(--mark);
  clip-path: inset(0 100% 0 0);
  pointer-events: none;
}
.hl::before { inset: 0 0 -0.09em 0; }

/* and the dark text over it, clipped in step */
.label::after, .hl::after {
  content: attr(data-text) / "";
  position: absolute; left: 0; top: 0;
  padding: inherit;
  color: var(--on-accent);
  white-space: nowrap;
  /* Negative at the bottom so the clip never cuts a descender. The stroke
     below already extends past the box; without the same allowance here the
     dark copy of the text clips its own tail flat against the box edge. */
  clip-path: inset(0 100% -.14em 0);
  pointer-events: none;
}
@supports (animation-timeline: view()) {
  .label::before, .hl::before {
    animation: mark-sweep linear both;
    animation-timeline: view();
  }
  .label::after, .hl::after {
    animation: mark-sweep-text linear both;
    animation-timeline: view();
  }
  /* stroke and text share a range, so they travel as one edge */
  .label::before, .label::after { animation-range: cover 4% cover 27%; }
  .hl::before, .hl::after       { animation-range: cover 9% cover 33%; }
  /* two sweeps, identical timing — the text one keeps the bottom allowance */
  @keyframes mark-sweep {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0 0 0); }
  }
  @keyframes mark-sweep-text {
    from { clip-path: inset(0 100% -.14em 0); }
    to   { clip-path: inset(0 0 -.14em 0); }
  }
}
/* No scroll timeline, or reduced motion: show the finished marker. */
@supports not (animation-timeline: view()) {
  .label::before, .hl::before { clip-path: inset(0 0 0 0); }
  .label::after, .hl::after   { clip-path: inset(0 0 -.14em 0); }
}
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion]) .label::before, :root:not([data-motion]) .hl::before { clip-path: inset(0 0 0 0); }
  :root:not([data-motion]) .label::after, :root:not([data-motion]) .hl::after   { clip-path: inset(0 0 -.14em 0); }
}

/* -------------------------------------------------------------- PILLS --- */

.pill {
  display: inline-flex; align-items: center;
  padding: .375rem .75rem;
  border: 1px solid var(--line-hi); border-radius: var(--r-pill);
  font-size: .6875rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-dim); white-space: nowrap;
}
.pills { display: flex; flex-wrap: wrap; gap: .5rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .625rem;
  min-height: var(--tap); min-width: var(--tap);
  padding: .8125rem 1.5rem;
  border: 1px solid var(--ink); border-radius: var(--r-pill);
  background: var(--ink); color: var(--ground);
  font: 600 .9375rem/1 var(--sans); letter-spacing: -0.01em;
  text-decoration: none; cursor: pointer;
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
.btn:hover { background: var(--butter); border-color: var(--butter); color: var(--on-accent); }
.btn .arrow { transition: transform .3s var(--ease); }
.btn:hover .arrow { transform: translateX(4px); }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--line-hi); }
.btn--ghost:hover { background: transparent; border-color: var(--ink); color: var(--ink); }

.btn-row { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }

/* Text link that rolls on hover — the small craft detail. */
.roll {
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: var(--tap);
  text-decoration: none; color: var(--ink);
  font-weight: 500; font-size: .9375rem;
}
.roll__m { display: block; overflow: hidden; height: 1.4em; }
.roll__m span { display: block; height: 1.4em; line-height: 1.4em; transition: transform .35s var(--ease); }
/* The second copy exists only so the label can roll up into it. It is
   hidden from assistive tech already; this also keeps it out of the
   clipboard, so copying a link does not yield the words twice. */
.roll__m span[aria-hidden="true"] { user-select: none; -webkit-user-select: none; }
.roll:hover .roll__m span { transform: translateY(-1.4em); }
.roll .arrow { transition: transform .35s var(--ease); color: var(--ink-dim); }
.roll:hover .arrow { transform: translateX(4px); color: var(--butter); }

/* -------------------------------------------------------------- HEADER --- */

.masthead { position: sticky; top: 0; z-index: 50; background: var(--ground); }
.masthead__in { display: flex; align-items: center; gap: 1.5rem; min-height: 80px; }

.wordmark {
  display: inline-flex; align-items: center;
  min-height: var(--tap); color: var(--ink); text-decoration: none;
}
/* The mark as it works on oynk.digital: at rest the window is cropped to the
   snout alone, and hovering widens it so the wordmark unfurls to the right.
   Y, N and K are painted before the snout in the markup, so they read as
   coming out from behind it rather than sliding in alongside.

   preserveAspectRatio "slice" is what does the cropping — the svg fills the
   window and the viewBox is cover-scaled from its left edge, so the snout
   stays put and pinned while the window grows. Widths are derived from the
   viewBox rather than typed in: 1822.5/371.1 for the full mark, and the
   snout ends at x=502 of 1822.5. */
.wordmark {
  --mark-h: 30px;
  --mark-full:  calc(var(--mark-h) * 4.9111);
  /* 502.7/371.1 — the snout's real right edge in the viewBox. 1.3524 left it
     0.85 units short, which is only 0.07px at this size, but the rightmost
     point of the curve has a vertical tangent: a sub-pixel cut there flattens
     a tall run of the edge and the snout reads as sliced off. The spare pixel
     keeps the antialiased edge inside the window too, and costs nothing —
     the letters are hidden by opacity and transform, not by the crop. */
  --mark-snout: calc(var(--mark-h) * 1.35462 + 1px);
  min-width: var(--tap);   /* the hit area holds at 48px even cropped to the snout */
}
.wordmark__mark {
  display: block; overflow: hidden;
  height: var(--mark-h);
  width: var(--mark-full);
}
.wordmark__svg { display: block; height: var(--mark-h); width: 100%; }
.wordmark path { fill: currentColor; transform-box: view-box; }
.wordmark:hover { color: var(--butter); }

/* Only crop where there is a pointer to uncrop it with. On touch there is no
   hover, so the name would otherwise never appear at all. */
@media (hover: hover) {
  .wordmark__mark { width: var(--mark-snout); }

  /* Cropping alone cannot hide the letters: the Y's left arm begins at x=439,
     inside the snout's own 12.8–502 span, so a sliver of it survives any crop
     that still shows the whole snout. They are tucked 500 units left and faded
     out instead, which is also what makes them read as coming from behind the
     snout rather than being uncovered by the window. */
  .wordmark .lg-y,
  .wordmark .lg-n,
  .wordmark .lg-k { opacity: 0; transform: translateX(-500px); }

  .wordmark:hover .wordmark__mark,
  .wordmark:focus-visible .wordmark__mark { width: var(--mark-full); }
  .wordmark:hover .lg-y, .wordmark:focus-visible .lg-y,
  .wordmark:hover .lg-n, .wordmark:focus-visible .lg-n,
  .wordmark:hover .lg-k, .wordmark:focus-visible .lg-k { opacity: 1; transform: none; }

  .wordmark__mark { transition: width .42s cubic-bezier(.45, 0, .3, 1); }
  .wordmark path {
    transition: opacity .42s cubic-bezier(.45, 0, .3, 1),
                transform .42s cubic-bezier(.45, 0, .3, 1);
  }
}

/* Hidden geometry source for the header logo and the particle sampler. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.nav { margin-left: auto; display: flex; align-items: center; gap: .25rem; }
.nav a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); min-width: var(--tap); padding-inline: 1rem;
  font-size: .9375rem; font-weight: 500; color: var(--ink); text-decoration: none;
}
/* The menu carried the roll markup but never the .roll class, so
   `.roll:hover .roll__m span` could never match it — the hover did nothing at
   all, and every label sat in the DOM twice for no reason. It highlights now,
   with the same marker the statements use. --mark is set per section inside
   main and the header sits outside it, so the colour is named here. */
/* The fallback lives on the nav, not on the marker: set here it would beat
   the colour inherited from the link and every item would sweep the same. */
.nav { --mark: var(--lilac); }

.nav .navmark {
  position: relative; display: inline-block;
  padding: .1em .32em; margin-inline: -.32em;   /* air, without moving anything */
}
.nav .navmark::before {
  content: "";
  position: absolute; inset: 0;
  background-color: var(--mark);
  border-radius: 2px;
  clip-path: inset(0 100% 0 0);
  transition: clip-path .34s var(--ease);
  pointer-events: none;
}
/* The dark copy rides over the sweep, clipped in step. Alternative text is
   empty so it is never announced twice. The negative bottom keeps the clip
   off a descender — the y in Pricing and Insights. */
.nav .navmark::after {
  content: attr(data-text) / "";
  position: absolute; left: 0; top: 0;
  padding: inherit;
  color: var(--on-accent);
  white-space: nowrap;
  clip-path: inset(0 100% -.14em 0);
  transition: clip-path .34s var(--ease);
  pointer-events: none;
}
.nav a:hover .navmark::before, .nav a:focus-visible .navmark::before,
.nav__top:hover .navmark::before, .nav__top:focus-visible .navmark::before {
  clip-path: inset(0 0 0 0);
}
.nav a:hover .navmark::after, .nav a:focus-visible .navmark::after,
.nav__top:hover .navmark::after, .nav__top:focus-visible .navmark::after {
  clip-path: inset(0 0 -.14em 0);
}
/* The page you are on shows in that item's own colour, the one its
   marker sweeps in — not a single yellow for everything. */
.nav a[aria-current="page"] { color: var(--mark); }
.nav .btn { margin-left: 1rem; color: var(--ground); }
.nav .btn:hover { color: var(--on-accent); }

/* ------------------------------------------------- NAV SUBMENU --- */
/* A disclosure. The button carries the same marker as the links either side
   of it, so the row still reads as one set of items rather than one odd one. */

.nav__has { position: relative; }
.nav__top {
  display: inline-flex; align-items: center; gap: .45rem;
  min-height: var(--tap); padding-inline: 1rem;
  background: none; border: 0; cursor: pointer;
  font: 500 .9375rem/1 var(--sans); color: var(--ink);
}
/* The parent is not the current page, but something under it is. */
.nav__top[data-within="true"] { color: var(--mark); }

.chev {
  width: .4rem; height: .4rem; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
  transition: transform .2s var(--ease);
  opacity: .7;
}
.nav__top[aria-expanded="true"] .chev { transform: translateY(2px) rotate(-135deg); }

.nav__sub {
  position: absolute; top: 100%; left: .35rem; z-index: 20;
  min-width: 12rem; margin: .35rem 0 0; padding: .4rem;
  list-style: none;
  background: var(--raised); border: 1px solid var(--line-hi);
  border-radius: var(--r-card);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
}
.nav__sub a {
  display: flex; align-items: center;
  min-height: var(--tap); padding-inline: .75rem;
  border-radius: var(--r-sm);
  font-size: .9375rem; color: var(--ink); text-decoration: none; white-space: nowrap;
}
.nav__sub a[aria-current="page"] { color: var(--mark); }

@media (max-width: 960px) {
  /* The nav is already a column here, so the submenu is just part of it —
     absolute positioning would hang it off the side of the screen. */
  .nav__has { width: 100%; }
  .nav__top { width: 100%; justify-content: flex-start; padding-inline: 0;
              min-height: 56px; font-size: 1.125rem;
              border-bottom: 1px solid var(--line); }
  .nav__sub {
    position: static; min-width: 0; margin: 0; padding: 0 0 0 1rem;
    background: none; border: 0; box-shadow: none;
  }
  .nav__sub a { min-height: 52px; padding-inline: 0; border-bottom: 1px solid var(--line); }
}

.nav-toggle { display: none; }

@media (max-width: 960px) {
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center; margin-left: auto;
    min-height: var(--tap); min-width: var(--tap); padding-inline: 1.125rem;
    background: transparent; border: 1px solid var(--line-hi); border-radius: var(--r-pill);
    color: var(--ink); font: 600 .875rem/1 var(--sans); cursor: pointer;
  }
  .nav {
    display: none; position: absolute; inset-inline: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--ground); border-bottom: 1px solid var(--line);
    padding: .5rem var(--gutter) 1.5rem;
  }
  .nav[data-open="true"] { display: flex; }
  .nav a { justify-content: flex-start; min-height: 56px; padding-inline: 0; font-size: 1.125rem; border-bottom: 1px solid var(--line); }
  .nav .btn { margin: 1.25rem 0 0; }
}

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

/* Centred and long, after Maria João Abrantes: the copy sits in its own column
   with real air either side rather than hugging the left of a wide shell, and
   the headline is allowed to run the full width of it instead of being folded
   into a narrow measure. The section fills the viewport and centres its content
   vertically, so the type sits low on the screen with space above it. */
.hero {
  min-height: calc(100vh - 80px);
  min-height: calc(100svh - 80px);
  display: grid; align-content: center;
  padding-block: clamp(3rem, 12vw, 10.5rem) clamp(3rem, 7vw, 5.5rem);
}
.hero__copy { max-width: 60rem; margin-inline: auto; }

/* A lattice behind the hero. One repeating radial-gradient — a 1px dot in the
   middle of each 14px cell — so the whole texture is a single background
   declaration: no image, no request, no canvas, and it resamples itself at
   any zoom or pixel density.

   The mask is the part that matters. Without it a full-bleed dot grid sits
   behind the headline and makes it harder to read, which is a lot to pay for
   a texture. This one is clear through the middle and fades the dots in
   towards the edges, so the grid is only ever where the words are not —
   present at the corners of your eye, gone where you are actually reading.

   It sits under .hero's own content but over .field--back, so the particles
   still pass in front of it. Both layers are transparent except where they
   draw, so neither hides the other.

   Homepage only. An inner hero is a little over half the height and its copy
   fills 19% to 88% of it, so the same ellipse would put the grid straight
   back under the words. Drop :not(.hero--inner) to run it everywhere, and
   retune the mask if you do. */
.hero:not(.hero--inner)::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background-image: radial-gradient(
    color-mix(in srgb, var(--ink-faint) 24%, transparent) 1px,
    transparent 1.2px);
  background-size: var(--dot-gap, 14px) var(--dot-gap, 14px);
  -webkit-mask-image: radial-gradient(ellipse 65% 62% at 50% 53%, transparent 15%, #000 85%);
          mask-image: radial-gradient(ellipse 65% 62% at 50% 53%, transparent 15%, #000 85%);
}

/* Once the field is live it draws this grid itself, from the same pitch and
   the same mask, so that the cursor can push it and the headline word can be
   assembled out of it. This copy is the fallback and stands down. */
.field-live .hero:not(.hero--inner)::before { display: none; }

/* Cheap to state, easy to get wrong: without this the grid is drawn but the
   mask is not, and the dots run straight under the headline. */
@supports not ((mask-image: radial-gradient(#000, transparent)) or (-webkit-mask-image: radial-gradient(#000, transparent))) {
  .hero:not(.hero--inner)::before { display: none; }
}
.hero .lede { margin-top: clamp(1.75rem, 3vw, 2.5rem); max-width: 39ch; }

/* One word in the lede will not sit still. A slow wave travels through it,
   letter by letter, on a loop — "extraordinary" being the one word in the
   sentence that refuses to behave like the others.

   Deliberately tiny: a tenth of an em, about two pixels here. Negative
   delays phase each letter into a cycle that is already running, so there
   is no startup and no visible loop seam. The spread is a little over half
   a cycle, which reads as one travelling wave rather than thirteen letters
   bobbing at random.

   Letters are inline-block so the transforms cannot reflow the paragraph, and
   they are decorative — the readable word sits in .vh alongside them. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
.xo { color: var(--lilac); font-weight: 500; }
.xo__l { display: inline-block; }

.xo__l {
  animation: xo-wave 2.4s ease-in-out infinite;
  animation-delay: calc(var(--i) * -110ms);
}
/* Transform-only, so it composites. It still costs something to run a loop
   nobody is looking at, so it parks when the hero leaves the viewport. */
.xo.is-parked .xo__l { animation-play-state: paused; }
@keyframes xo-wave {
  0%, 100% { transform: none; }
  50%      { transform: translateY(-.1em); }
}
.hero .btn-row { margin-top: 2.5rem; }

/* ------------------------------------------------------------- MARQUEE --- */

.marquee {
  border-block: 1px solid var(--line);
  overflow: hidden; padding-block: 1.375rem;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee__track { display: flex; width: max-content; animation: slide 46s linear infinite; }
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee__group { display: flex; align-items: center; flex: none; }
.marquee__group span {
  display: inline-flex; align-items: center; gap: 1.75rem;
  padding-inline: 1.75rem;
  font-size: .8125rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-dim); white-space: nowrap;
}
.marquee__group span::after {
  content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--line-hi);
}
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion]) .marquee__track { animation: none; }
  :root:not([data-motion]) .marquee { -webkit-mask-image: none; mask-image: none; }
}

/* --------------------------------------------------------------- BANDS --- */

.band { padding-block: clamp(3.5rem, 8vw, 7rem); }
.band__head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.band__head .statement { margin: 0; color: var(--ink); }
.band__head p:not(.statement):not(.label) { margin-top: 1.5rem; }

/* Reveal — native scroll timeline, no JS. */
@supports (animation-timeline: view()) {
  .rise { animation: rise linear both; animation-timeline: view(); animation-range: entry 0% entry 55%; }
  @keyframes rise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
}

/* --------------------------------------------------------------- CARDS --- */

.card {
  background: var(--raised); border-radius: var(--r-card);
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.grid { display: grid; gap: 1rem; }
/* minmax(0, 1fr) rather than 1fr: a plain 1fr track will not shrink below its
   content's min-content width, so at a breakpoint the columns hold their size
   and push the row wider than the page. The media query sees the viewport
   including the scrollbar while the layout gets the space without it, which is
   exactly where that bites — the testimonials ran 35px past the edge at 900. */
@media (min-width: 760px)  { .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px)  { .grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 620px)  { .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1080px) { .grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* work cards */
/* No background on hover. It filled the text column only, so it read as a
   panel that stopped short of the image rather than as one card lighting up,
   and it competed with the shot lifting. The title going butter is the whole
   hover state. */
.work { display: flex; flex-direction: column; gap: 1.5rem; text-decoration: none; }
.work .title { color: var(--ink); }
.work__foot { margin-top: auto; padding-top: .5rem; }

/* ------------------------------------------------------- REVEAL, STAGGERED --- */
/* .rise already fades each block up on a view() timeline. What it could not do
   is arrive a piece at a time — a four-row list and a five-badge row both came
   in as one slab, which is what made the lower page feel like it had stopped
   trying.

   There is no animation-delay to lean on with a scroll timeline: progress is
   position, not time. So the stagger has to be distance, each child given its
   own slightly later range. Reusing the existing rise keyframes rather than
   defining a second set.

   Both ends stay inside the entry phase deliberately. A range that closes in
   "cover" needs the element to travel well up the screen to finish, and the
   last section on a page never gets that far — you reach the end of the
   document first and it stays part-faded for good. */

@supports (animation-timeline: view()) {
  /* Cover, not entry, for the same reason as the markers: the entry phase
     is only as long as the element is tall, so on a 92px badge the whole
     reveal happened inside 37 pixels of scroll. */
  .who-list .who-row,
  .awards__list .award {
    animation: rise linear both;
    animation-timeline: view();
  }

  /* The rows are stacked, so they already arrive in sequence on their own
     and need no offset — one range for all four. */
  .who-list .who-row { animation-range: cover 4% cover 28%; }

  /* The badges sit side by side at the same height, so without an offset all
     five would land at once. Here the stagger has to be explicit. */
  .awards__list .award:nth-child(1) { animation-range: cover 2% cover 24%; }
  .awards__list .award:nth-child(2) { animation-range: cover 6% cover 28%; }
  .awards__list .award:nth-child(3) { animation-range: cover 10% cover 32%; }
  .awards__list .award:nth-child(4) { animation-range: cover 14% cover 36%; }
  .awards__list .award:nth-child(5) { animation-range: cover 18% cover 40%; }

  /* the children carry it now, so the container must not fade them again */
  .who-list, .awards__list { animation: none; }

  /* Each row draws its own rule as it arrives, left to right. Same gesture as
     the marker on the section labels, so the page has one idea about how
     things appear rather than several. */
  .who-row { border-top-color: transparent; position: relative; }
  .who-row::before {
    content: ""; position: absolute; inset: -1px 0 auto 0; height: 1px;
    background: var(--line); transform-origin: left;
    animation: rule-draw linear both;
    animation-timeline: view();
    animation-range: cover 4% cover 30%;
  }
  @keyframes rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
}

/* ------------------------------------------------------- WHAT WE BUILD --- */
/* Three plain lists. A visitor deciding whether to call needs to find their own
   job on the page, and prose does not let them scan for it. */

.cols { display: grid; gap: clamp(2rem, 4vw, 3rem); }
@media (min-width: 760px) { .cols { grid-template-columns: repeat(3, 1fr); } }
.col__h {
  margin: 0 0 1rem; padding-bottom: .75rem;
  border-bottom: 1px solid var(--line);
  font-size: 1.125rem; font-weight: 600; letter-spacing: -.01em;
  color: var(--butter);
}
.col__list { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }
.col__list li { color: var(--ink-dim); line-height: 1.45; }

/* A worked example under the service lists. Naming what actually shipped does
   more than any adjective — it is the difference between claiming you build
   software and showing the thing you built. */
.worked {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(1.5rem, 3vw, 2rem);
  border-top: 1px solid var(--line);
  display: grid; gap: .75rem;
}
@media (min-width: 820px) { .worked { grid-template-columns: 8rem minmax(0, 1fr); gap: 2rem; } }
.worked__k {
  margin: 0; font-size: .75rem; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase; color: var(--butter);
}
.worked__b { margin: 0; max-width: 62ch; }
.worked__b b { color: var(--ink); font-weight: 600; }

/* ----------------------------------------------------- WHO WE BUILD FOR --- */
/* Rows, not cards. Standards above this is already a four-up grid and Proof
   below it is four boxes; a third rectangle grid in a row is where the page
   stops having ideas. Full-width rows separated by hairlines read as a list you
   scan rather than a set of tiles you weigh up. */

.who-list { display: grid; }
.who-row {
  display: grid; gap: .5rem clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: 1fr;
  padding-block: clamp(1.75rem, 3.5vw, 2.75rem);
  border-top: 1px solid var(--line);
}
.who-row:last-child { border-bottom: 1px solid var(--line); }
.who-row__n {
  margin: 0; font-size: .75rem; font-weight: 700;
  letter-spacing: .14em; color: var(--butter);
}
.who-row__t {
  margin: 0; font-size: clamp(1.375rem, 2.6vw, 2rem);
  font-weight: 500; letter-spacing: -.03em; line-height: 1.1;
  text-wrap: balance; transition: color .3s var(--ease);
}
.who-row__b { margin: 0; max-width: 46ch; }

@media (min-width: 820px) {
  .who-row { grid-template-columns: 3.5rem minmax(0, 1fr) minmax(0, 1.15fr); align-items: start; }
  .who-row__n { padding-top: .55rem; }
}
@media (hover: hover) {
  .who-row:hover .who-row__t { color: var(--butter); }
}

/* ----------------------------------------------------------------- WHY --- */
/* Prose, not a grid. Every section around this one is cards or columns, so the
   argument lands harder set as something to read. The lead paragraph runs
   larger and full width; the two that follow sit in columns beside it. */

.why { display: grid; gap: clamp(1.25rem, 3vw, 2rem); }
.why__lead {
  font-size: clamp(1.25rem, 2.1vw, 1.75rem);
  line-height: 1.5; color: var(--ink); max-width: 34ch;
}
@media (min-width: 900px) {
  .why { grid-template-columns: 1fr 1fr; column-gap: clamp(2rem, 5vw, 4.5rem); }
  .why__lead { grid-column: 1 / -1; max-width: 30ch; }
}
.why p { max-width: 46ch; }

/* --------------------------------------------------------------- STATS --- */

/* No boxes. Four tiles here would be the third rectangle grid in a row, and a
   number in a card reads as a tile to weigh up rather than a figure to take in.
   Hairline rules between them instead, and the numerals set large enough to
   carry the section on their own. */
.stats {
  display: grid; gap: 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  border-top: 1px solid var(--line);
}
.stat {
  padding: clamp(1.5rem, 3vw, 2.25rem) clamp(1rem, 2vw, 1.75rem) clamp(1.5rem, 3vw, 2.25rem) 0;
  border-bottom: 1px solid var(--line);
}
@media (min-width: 640px) {
  .stat + .stat { border-left: 1px solid var(--line); padding-left: clamp(1.25rem, 2.5vw, 2rem); }
}
.stat__n {
  display: block; font-size: clamp(2.75rem, 6vw, 4.75rem); font-weight: 400;
  letter-spacing: -0.055em; line-height: .95; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.stat:nth-child(2n) .stat__n { color: var(--butter); }
.stat:nth-child(3n) .stat__n { color: var(--lilac); }
.stat__l { margin: 1rem 0 0; font-size: .875rem; color: var(--ink-dim); }

/* The second break in the axis, and deliberately not the board's.

   The board took the page sideways. Repeating that further down would read as
   a tic rather than a route, so this one keeps the direction and breaks the
   plane instead: the ruled grid holds absolutely still while the four figures
   ride up through it at different rates and land level.

   The rules are what make it legible. Shear a bordered cell and you get a
   zigzag where a straight line should be, so nothing here touches .stat —
   only its contents move, against a frame that does not. You can read each
   figure's offset off the rule beside it.

   They settle rather than keep moving, and they settle early: the numbers are
   level well before you are reading them. Motion that makes a figure harder
   to read is a bad trade at any speed.

   One named timeline on the row, not one per element. The number and its
   label sit a line apart, so their own view() timelines would be a line out
   of step and the pair would visibly come unstuck. */

@supports (animation-timeline: view()) {
  @media (min-width: 900px) {          /* all four in a single row */
    .stats { view-timeline-name: --stats; }
    /* the shear replaces the block reveal rather than compounding with it */
    .stats.rise { animation: none; }

    .stat__n, .stat__l {
      animation: stat-shear linear both;
      animation-timeline: --stats;
      animation-range: entry 0% contain 35%;
    }
    .stat:nth-child(1) { --shear:  3.25rem; }
    .stat:nth-child(2) { --shear: -2.50rem; }
    .stat:nth-child(3) { --shear:  4.00rem; }
    .stat:nth-child(4) { --shear: -3.25rem; }
  }
}
@keyframes stat-shear {
  from { transform: translateY(var(--shear, 0)); }
  to   { transform: translateY(0); }
}

/* -------------------------------------------------------------- QUOTES --- */

.quote { background: var(--raised); border-radius: var(--r-card); padding: clamp(1.75rem, 3vw, 2.5rem); display: flex; flex-direction: column; gap: 1.75rem; }
.quote blockquote { margin: 0; font-size: clamp(1.0625rem, 1.4vw, 1.1875rem); line-height: 1.6; color: var(--ink); letter-spacing: -0.015em; }
.quote figcaption { margin-top: auto; }
.quote cite { font-style: normal; font-weight: 600; color: var(--ink); display: block; }
.quote figcaption span { display: block; margin-top: .375rem; font-size: .875rem; color: var(--ink-dim); }

/* --------------------------------------------------------------- TOOLS --- */

.tool { background: var(--raised); border-radius: var(--r-card); padding: clamp(1.5rem, 2.5vw, 2rem); }
.tool h3 { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; margin-bottom: .875rem; color: var(--ink); }
.tool p { font-size: .9375rem; }

/* ------------------------------------------------------------- SCANNER --- */

.scanner { background: var(--raised); border-radius: var(--r-card); padding: clamp(1.75rem, 4vw, 3rem); }
.scanner__form { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1.75rem; }
.scanner__form input {
  flex: 1 1 18rem; min-height: var(--tap);
  padding: .8125rem 1.25rem;
  background: var(--ground); color: var(--ink);
  border: 1px solid var(--line-hi); border-radius: var(--r-pill);
  font: 400 1rem/1 var(--sans);
}
.scanner__form input::placeholder { color: var(--ink-faint); }
.scanner__form input:hover { border-color: var(--ink-faint); }
.scanner__note { margin-top: 1.25rem; font-size: .875rem; }
.scanner__note a { display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); }

/* --------------------------------------------------------------- TABLE --- */

.table-wrap { overflow-x: auto; background: var(--raised); border-radius: var(--r-card); }
table { border-collapse: collapse; width: 100%; min-width: 36rem; }
caption { text-align: left; padding: 1.5rem 1.75rem 0; font-weight: 600; color: var(--ink); }
th, td { text-align: left; padding: 1.125rem 1.75rem; border-bottom: 1px solid var(--line); color: var(--ink-dim); font-size: .9375rem; font-weight: 400; vertical-align: top; }
thead th { font-size: .6875rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
tbody th { color: var(--ink); font-weight: 500; }
tbody tr:last-child > * { border-bottom: 0; }
td.na { color: var(--ink-faint); }

/* ----------------------------------------------------------- CHECKLIST --- */

.checklist { list-style: none; margin: 0; padding: 0; display: grid; gap: .625rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.checklist li {
  background: var(--raised); border-radius: var(--r-pill);
  padding: .875rem 1.25rem; font-size: .9375rem; color: var(--ink-dim);
  display: flex; gap: .75rem; align-items: center;
}
.checklist li::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--butter); }

/* ----------------------------------------------------------------- FAQ --- */

.faq details { border-top: 1px solid var(--line); }
.faq details:last-of-type { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; justify-content: space-between; gap: 2rem;
  min-height: 72px; padding-block: 1.25rem;
  font-size: clamp(1rem, 1.4vw, 1.1875rem); font-weight: 500; letter-spacing: -0.02em; color: var(--ink);
}
.faq summary::-webkit-details-marker { display: none; }
/* Drawn, not typed. A "+" glyph is centred on the font's maths axis, which
   is above the middle of its line box, so centring the box leaves the ink
   sitting high — and the "-" it swaps for is off by a different amount, so
   the two do not even agree with each other. Two gradients put the strokes on
   the circle's actual centre, and the open state closes the vertical one
   rather than rotating a glyph that looks the same either way. */
.faq summary::after {
  content: ""; flex: none;
  width: 32px; height: 32px;
  border: 1px solid var(--line-hi); border-radius: 50%;
  color: var(--ink-dim);
  background:
    linear-gradient(currentColor, currentColor) center / 10px 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 10px no-repeat;
  transition: background-size .3s var(--ease), border-color .3s var(--ease), color .3s var(--ease);
}
.faq summary:hover::after { border-color: var(--butter); color: var(--butter); }
.faq details[open] summary::after { background-size: 10px 1.5px, 1.5px 0; }
.faq details p { padding-bottom: 1.75rem; max-width: 68ch; }

/* -------------------------------------------------------------- CLOSER --- */

.closer { padding-block: clamp(3.5rem, 8vw, 7rem); }
/* Paint in water, behind the last thing anyone reads on the page. The canvas
   is decorative and inert — no pointer events, no accessible name — and the
   card is exactly what it was without it. See ink.js: the type is a keep-out
   region that the flow parts around, so the effect and the legibility are the
   same mechanism rather than a compromise between them. */
.closer__card {
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--raised); border-radius: var(--r-card);
  padding: clamp(2rem, 5vw, 4.5rem);
}
.closer__ink {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%;
  pointer-events: none; display: block;
}
.closer__card > *:not(.closer__ink) { position: relative; }
.closer .statement { max-width: 18ch; }
.closer p { margin-top: 1.75rem; }
.closer .btn-row { margin-top: 2.5rem; }
.closer p a { display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); }

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

.foot { border-top: 1px solid var(--line); padding-block: clamp(3rem, 6vw, 4.5rem) 2rem; }
.foot__cols { display: grid; gap: 2.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); }
.foot h2 { font-size: .75rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 1rem; }
.foot ul { list-style: none; margin: 0; padding: 0; }
.foot li a { display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); color: var(--ink-dim); text-decoration: none; font-size: .9375rem; }
.foot li a:hover { color: var(--ink); }
.foot address { font-style: normal; color: var(--ink-dim); line-height: 1.9; font-size: .9375rem; }
.foot address a { color: var(--ink-dim); display: inline-flex; align-items: center; min-height: var(--tap); }

.badges { display: grid; gap: 1rem; margin-top: 3rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
.badge { background: var(--raised); border-radius: var(--r-card); padding: 1.375rem 1.5rem; }
.badge dt { font-size: .6875rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.badge dd { margin: .625rem 0 0; font-size: 1.125rem; font-weight: 500; letter-spacing: -0.02em; color: var(--ink); }
.badge p { margin: .625rem 0 0; font-size: .8125rem; }

.colophon {
  margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: .5rem 1.75rem;
  font-size: .8125rem; color: var(--ink-faint);
}

/* --------------------------------------------------------------- CRUMB --- */

.crumb { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem; padding-block: .5rem; font-size: .8125rem; color: var(--ink-faint); }
.crumb a { color: var(--ink-dim); text-decoration: none; display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); justify-content: center; }
.crumb a:hover { color: var(--ink); }


/* ========================================================== THE FIELD --- */
/* Two canvases, one particle buffer, opposite halves of the depth range.
   Page content sits between them, so some particles pass in front of the
   type and some behind it. Both are inert to the pointer. */

.field {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  display: block;
}
.field--back  { z-index: 0; }
.field--front { z-index: 3; }

/* Lift content above the back field so the sandwich works. */
.hero, .band, .closer, .marquee, .foot { position: relative; z-index: 1; }

/* Words drawn in particles. The text stays in the DOM for screen readers,
   search engines and any browser without WebGL, and it stays *visible* until
   the field is actually live — so the word arrives as type and then becomes
   particles, rather than starting as dots you have to decode. */
.pword { transition: color .55s cubic-bezier(.4,0,.2,1); }
/* Only the word the particles have actually reached hands over. The other
   one stays as readable type — otherwise the sentence has a hole in it. */
.has-field.field-live .pword.is-particles { color: transparent; }

@media (prefers-reduced-motion: reduce) {
  /* Reduced motion means no movement, not no graphics. The particle word
     still renders — it is simply static, and the type hands over instantly
     instead of dissolving. Leaving the type visible would double-render it. */
  :root:not([data-motion]) .pword { transition: none; }
}



/* ==================================================== EDITORIAL WRAP --- */
/* Text flows around the figure instead of sitting in a rigid block beneath
   it. shape-outside only applies to floats, so wrapped cards are block, not
   flex — and they need a clearfix, which is the tax for using floats in 2026. */

.card--wrap { display: block; container-type: inline-size; }
.card--wrap::after { content: ""; display: block; clear: both; }

/* A float only earns its place if the column is wide enough to wrap around
   it. Below that the text shreds into three-word lines, so the figure just
   sits above instead. Container query, not viewport — what matters is how
   wide this card is, not how wide the screen is. */
.wrapfig {
  display: grid; place-items: center;
  width: 4.5rem; aspect-ratio: 1;
  margin: 0 0 1.1rem;
  background: var(--raised-2);
  overflow: hidden;
  border-radius: 50%;
}

@container (min-width: 21rem) {
  .wrapfig {
    float: right;
    shape-outside: circle(50%);
    clip-path: circle(50%);
    width: clamp(5rem, 24%, 8.5rem);
    margin: .2rem 0 .9rem 1.35rem;
  }
  .wrapfig--left {
    float: left;
    shape-outside: circle(50%);
    margin: .2rem 1.35rem .9rem 0;
  }
}
/* Drops straight in when real photography arrives. */
.wrapfig img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wrapfig svg { width: 58%; height: auto; color: var(--butter); }
.wrapfig__n {
  font-size: clamp(1.25rem, 2.4vw, 1.75rem); font-weight: 500;
  letter-spacing: -.03em; color: var(--butter);
}
.wrapfig__q {
  font-size: clamp(2rem, 4vw, 3rem); line-height: 1; color: var(--butter);
  transform: translateY(.12em);
}





/* ========================================================== USED BY --- */
/* Named clients, set to be read rather than scrolled past. Two councils
   carry more weight here than every credential on the page, so they lead
   and the list is static — a marquee would send them by too fast. */

.clients { position: relative; z-index: 1; padding-block: clamp(2rem, 4vw, 3.25rem) 0; }
.clients__list { margin-bottom: clamp(1.5rem, 3vw, 2.25rem) !important; }
.clients__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .75rem 0;
}
.clients__list li {
  font-size: clamp(.9375rem, 1.15vw, 1.0625rem);
  font-weight: 500; letter-spacing: -.015em; color: var(--ink);
  padding-inline: clamp(.85rem, 1.6vw, 1.35rem);
  border-left: 1px solid var(--line);
}
.clients__list li:first-child { padding-left: 0; border-left: 0; }
@media (max-width: 620px) {
  .clients__list { flex-direction: column; gap: .5rem; }
  .clients__list li { padding-left: 0; border-left: 0; }
}


/* ================================================== SELECTED WORK --- */
/* Full width, image first. One column, because a case study that has to
   share a row with another one is not being presented, it is being listed. */

.work-list { display: flex; flex-direction: column; gap: clamp(3rem, 6vw, 4rem); }
.work { display: flex; flex-direction: column; }

.work__shot {
  aspect-ratio: 16 / 9;
  background: var(--raised);
  border: 1px dashed var(--line-hi);
  display: grid; place-items: center; overflow: hidden;
}
.work__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.work__vid { width: 100%; height: 100%; object-fit: cover; display: block; }
.work__shot--video { position: relative; }

/* Brand mark on a shot that is a photograph rather than a product screen. The
   other three carry their own identity on screen; this one is a park path, so
   without it you cannot tell whose app it is.

   Overlaid rather than burned into the image: it stays sharp at any size and
   costs no re-encode. It sits outside .work__media on purpose, so the hover
   scale moves the photo underneath it and leaves the mark still. */
.work__shot:has(.work__brand) { position: relative; }
.work__brand {
  position: absolute; left: 4.2%; bottom: 7%;
  width: clamp(4.5rem, 15%, 11rem); height: auto;
  fill: #fff;
  filter: drop-shadow(0 1px 10px rgba(0, 0, 0, .55));
  pointer-events: none;
}

/* The clip runs 13 seconds on a loop, so it needs a way to stop it —
   WCAG 2.2 2.2.2 covers any motion over five seconds that starts on its own.
   The button is hidden until the script that drives the video is running,
   so it never appears as a control that does nothing. */
.vid-toggle {
  position: absolute; right: .75rem; bottom: .75rem;
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,.28);
  border-radius: 50%;
  background: rgba(10,10,10,.62);
  backdrop-filter: blur(6px);
  color: #fff; cursor: pointer;
  opacity: 0; transition: opacity .25s var(--ease), background .25s var(--ease);
}
.vid-toggle svg { width: 16px; height: 16px; }
.work__shot--video:hover .vid-toggle,
.vid-toggle:focus-visible { opacity: 1; }
.vid-toggle:hover { background: rgba(10,10,10,.85); }
/* once paused, the control stays visible — otherwise the only way back is to
   find an invisible button */
.vid-toggle[data-paused] { opacity: 1; }
@media (hover: none) { .vid-toggle { opacity: 1; } }
.work__shot:has(img) { border-style: none; }
.work__todo {
  margin: 0; text-align: center;
  font: 600 .75rem/1.9 var(--mono, ui-monospace, monospace);
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-faint);
}
.work__todo span { display: block; font-weight: 400; letter-spacing: .1em; }

/* Tags are set as plain uppercase text, not pills — a row of chips under a
   full-bleed image reads as chrome and competes with it. */
.work__tags {
  list-style: none; margin: 0; padding: 1.5rem 0 0;
  display: flex; flex-wrap: wrap; gap: .3rem .75rem;
  font-size: .75rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-faint);
}
.work__tags .work__n { color: var(--butter); }
.work__tags .work__name { color: var(--ink); }

.work__title {
  margin: .55rem 0 0;
  font-size: clamp(1.25rem, 1.9vw, 1.5rem); font-weight: 500;
  letter-spacing: -.02em; line-height: 1.35; color: var(--ink);
  max-width: 46ch; text-wrap: balance;
}
.work__body { margin: .75rem 0 0; font-size: .9375rem; max-width: 62ch; }

/* ================================================== THE WORK BOARD --- */
/* Four case studies on a board that travels sideways while the page is held
   still. The point is the axis change, not the sideways-ness: every other
   section here answers a downward scroll by moving up, and this one does
   not. One break in the axis reads as a route. Four would read as a tic,
   which is why this is the only section that does it.

   No library. The hold is position: sticky, the travel is a scroll-driven
   animation, and the frame is a clip-path opening from a rounded box to
   full bleed.

   Not the house squircle. A superellipse written in percentages is stretched
   by whatever box it is put on, and a stretched squircle is not a squircle —
   the corners lose the one property that makes it one. It survives only
   where the box is square, which is why .bio__face still carries it and this
   does not. A corner radius is a length, so it holds at any aspect ratio:
   the same corner on a 16:9 board as on a phone.

   (CSS now has corner-shape: squircle, which would give a true continuous
   corner at any aspect. It applies to border-radius only — measured rather
   than assumed: round, squircle and notch all hit-test identically through a
   clip-path. If this frame ever stops needing to animate its own inset, it
   can move to border-radius and take the house shape back.)

   Below 900px, and wherever scroll-driven animations are missing, the same
   markup is a horizontal strip you swipe. That is not a lesser version — it
   is how most people will read it, so it is the base and the pin is the
   enhancement.

   The pin is gated on data-motion="full", which the head script sets before
   first paint. That means it never runs unless the motion switch is live to
   turn it off: no scripting, no pin. A hijacked scroll with no way out is a
   trap, and shipping one behind a switch that isn't loaded is the same
   thing with better intentions. */

/* Apple publishes no general radius scale. What it does publish is the corner
   radius of its own surfaces, in the Human Interface Guidelines and in the
   Apple Design Resources files — these are those, points read as pixels:

     10   macOS window, and the top corners of an iOS sheet
     18   iPad Pro display
     55   iPhone 15 Pro display

   The board is a display-shaped surface, so it takes a display radius. */
:root {
  --r-mac:    10px;
  --r-ipad:   18px;
  --r-iphone: 55px;
}

.pan { margin-top: clamp(2rem, 4vw, 3rem); }

/* A grid, not a row of cards. Four rows — shot, tags, title, body — and
   every panel is a subgrid spanning all four, so the images share a bottom
   edge, the numbers share a line and the body copy starts level across the
   whole board however differently the titles wrap. A board is aligned; that
   is what makes it a board rather than four things near each other.

   The row gap has to be set apart from the column gap. Both come from the
   rail because a subgrid takes its gaps from its parent, and the column gap
   here is 4cqw — fine between panels, absurd between an image and its
   caption. */
.pan__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  grid-template-rows: repeat(4, auto);
  column-gap: var(--pan-gap, clamp(1.25rem, 3vw, 2rem));
  row-gap: 1.5rem;
  margin: 0; padding: .5rem var(--gutter) 1.5rem;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
}
.pan__panel, .pan__tail { grid-row: 1 / -1; scroll-snap-align: start; }
.pan__panel { display: grid; grid-template-rows: subgrid; gap: 0; }
/* The end card has no rows to share; it sits against the middle of the four
   the panels occupy. */
.pan__tail { align-self: center; }
.pan__panel { width: min(82vw, 27rem); }
.pan__tail  { width: min(74vw, 17rem); }

.pan__k {
  margin: 0; font-size: .75rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--butter);
}
.pan__tail p:last-child { margin-top: 1.5rem; }

/* Progress. A section that takes the axis away owes the reader a note on how
   far through it they are; without one the only way to find out is to keep
   scrolling and hope. Hidden in strip mode, where the scrollbar says it. */
.pan__prog { display: none; }

@supports (animation-timeline: view()) {
  /* min-height as well as min-width. The frame is exactly one viewport tall
     and the panels have to fit inside it; below about 700px the tallest one
     does not, and a board that clips its own case studies is worse than the
     strip. Short windows get the strip, and it is no loss. */
  @media (min-width: 900px) and (min-height: 700px) {
    :root[data-motion="full"] .pan {
      /* The one tuning knob: how many screens of scroll the board costs.
         Raise it to slow the travel, lower it to speed it up. */
      --pan-screens: 4;

      /* Everything below is in cqw against the frame, so the rail total is
         exact — and stays exact when a fifth project lands. Bump --pan-n on
         the element and nothing else has to change. */
      --pan-pad:   7cqw;
      --pan-panel: 44cqw;
      --pan-tail: 38cqw;
      --pan-gap:   4cqw;

      height: calc(var(--pan-screens) * 100svh);
      margin-top: 0;
      view-timeline: --pan block;
    }

    :root[data-motion="full"] .pan__pin { position: sticky; top: 0; height: 100svh; }

    :root[data-motion="full"] .pan__frame {
      position: absolute; inset: 0;
      container: pan / inline-size;
      display: flex; align-items: center;
      background: var(--raised);
      /* clip, not hidden: hidden would make this a scroll container, and the
         browser would then scroll it sideways to reveal a focused link —
         fighting the transform that is already moving the rail. */
      overflow: clip;
      /* One even margin on all four sides. A percentage would not give that:
         inside inset() the vertical pair resolves against height and the
         horizontal pair against width, so a landscape box gets two different
         margins — the same trap the percentage squircle fell into. */
      --pan-inset: clamp(1.5rem, 3vw, 3.5rem);
      --pan-radius: var(--r-iphone);
      clip-path: inset(var(--pan-inset) round var(--pan-radius));
      animation: pan-frame linear both;
      animation-timeline: --pan;
      animation-range: contain 0% contain 100%;
    }

    :root[data-motion="full"] .pan__rail {
      --pan-rail: calc(2 * var(--pan-pad) + var(--pan-tail)
                       + var(--pan-n) * var(--pan-panel)
                       + var(--pan-n) * var(--pan-gap));
      --pan-end: calc(100cqw - var(--pan-rail));
      --pan-from: 0px;
      --pan-to: var(--pan-end);

      /* flex: none or the frame shrinks it. The rail is a flex item wider
         than its container by design, and a shrunk rail is a wrong travel
         distance — the pan would stop short of the last panel. */
      flex: none; width: var(--pan-rail);
      /* The bottom padding is the progress bar's lane. Reserving it here
         lifts the panels off the bar instead of leaving the two to overlap
         on a short window, where the margin is thinnest. */
      padding: 0 var(--pan-pad) clamp(2rem, 5vh, 3.5rem);
      overflow: visible; scroll-snap-type: none;
      animation: pan-x linear both;
      animation-timeline: --pan;
      animation-range: contain 0% contain 100%;
    }

    :root[data-motion="full"] .pan__panel { width: var(--pan-panel); }
    :root[data-motion="full"] .pan__tail  { width: var(--pan-tail); }

    :root[data-motion="full"] .pan__prog {
      display: block; position: absolute; z-index: 2;
      left: var(--pan-pad); right: var(--pan-pad);
      bottom: clamp(.85rem, 2.6vh, 1.75rem);
      height: 2px; background: var(--line-hi);
      opacity: 0;
      animation: pan-fade linear both;
      animation-timeline: --pan;
      animation-range: contain 0% contain 100%;
    }
    :root[data-motion="full"] .pan__bar {
      display: block; height: 100%; background: var(--lilac);
      transform-origin: left center; transform: scaleX(0);
      animation: pan-bar linear both;
      animation-timeline: --pan;
      animation-range: contain 0% contain 100%;
    }
  }
}

/* The testimonials, on the same machinery running the other way.

   The work board travels left; this comes back right, so the two sections
   move against each other rather than one repeating the other. It also gets
   rid of a scrollbar: a scroll-driven pan has no scroll container, so there
   is nothing to drag and nothing to render.

   Wider cards and no subgrid — a quote is a self-contained card, not four
   panels that have to agree about where their captions start — and a shorter
   runway, because three quotes do not earn four screens the way four case
   studies do. */
@supports (animation-timeline: view()) {
  @media (min-width: 900px) and (min-height: 700px) {
    :root[data-motion="full"] .pan--quotes {
      --pan-panel: 52cqw;
      --pan-tail: 0cqw;
      --pan-gap: 5cqw;
      /* Half the leftover, so the first quote is centred at rest and the last
         one is centred when the travel ends. The board runs its panels flush
         to a small pad because there is always a next one coming; with three
         quotes the two ends are most of what you see, and a quote sitting
         off-centre against empty frame reads as the pan having stopped early. */
      --pan-pad: calc((100cqw - var(--pan-panel)) / 2);
      /* Centring the ends adds a panel's width of travel, so the runway grows
         with it — measured back to 1.03 across per down. */
      --pan-screens: 3.4;
    }
    :root[data-motion="full"] .pan--quotes .pan__rail {
      --pan-rail: calc(2 * var(--pan-pad) + var(--pan-n) * var(--pan-panel)
                       + (var(--pan-n) - 1) * var(--pan-gap));
      --pan-from: var(--pan-end);
      --pan-to: 0px;
    }
    /* It starts at the far end, so the frame has to open there too. */
    :root[data-motion="full"] .pan--quotes .pan__rail > .quote { width: var(--pan-panel); }
    /* Size the tracks, do not infer them. The rail asks for max-content
       columns, and a quote's max-content contribution comes out 80px wider
       than the width set on it — its own 2x40px of padding, added on top
       rather than counted inside. The card then sits centred in an oversized
       track and every centring calculation is 40px out. The case-study panels
       hide this by having no padding of their own. */
    :root[data-motion="full"] .pan--quotes .pan__rail { grid-auto-columns: var(--pan-panel); }
    :root[data-motion="full"] .pan--quotes .pan__bar { transform-origin: right center; }
  }
}
/* The rail is a four-row grid built for the case-study panels, and a quote is
   not one of those — without a span it auto-places down the rows and the
   three of them stack instead of sitting side by side. Spanning all four is
   the same as saying "one row", and stretching makes the three cards equal
   height. */
.pan--quotes .pan__rail > .quote {
  /* A figure carries 40px of inline margin from the browser's own stylesheet,
     which is exactly the 40px every centring calculation here was out by. The
     three-up grid stretched it away and it never showed. */
  margin: 0;
  grid-row: 1 / -1; align-self: stretch; scroll-snap-align: center;
  /* The rail sizes its columns to max-content, which for a quote is the whole
     thing on one line — 2708px of it. The pinned mode sets a width and hid
     that; the swipe fallback has to state its own. */
  width: min(86vw, 30rem);
}

/* The open and close take 12% of the runway each; the middle 76% is the
   travel, at a constant rate, so the board never appears to accelerate. */
@keyframes pan-frame {
  0%   { clip-path: inset(var(--pan-inset) round var(--pan-radius)); animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  12%  { clip-path: inset(0 round 0);                                animation-timing-function: linear; }
  88%  { clip-path: inset(0 round 0);                                animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  100% { clip-path: inset(var(--pan-inset) round var(--pan-radius)); }
}
/* The endpoints are properties, so one keyframe set drives a board that
   travels left and a board that travels right. */
@keyframes pan-x {
  0%,  12%  { translate: var(--pan-from, 0px) 0; }
  88%, 100% { translate: var(--pan-to, 0px) 0; }
}
@keyframes pan-bar {
  0%,  12%  { transform: scaleX(0); }
  88%, 100% { transform: scaleX(1); }
}
@keyframes pan-fade {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ------------------------------------------------------- STANDARDS --- */
.std__tools {
  margin-top: clamp(2rem, 4vw, 3rem); padding-top: 1.5rem;
  border-top: 1px solid var(--line); font-size: .9375rem; max-width: 70ch;
}
.std__tools b { color: var(--ink); font-weight: 600; }


/* ====================================================== RECOGNITION --- */
/* Real badges, at a size where the text inside them is legible and no
   larger. They are third-party proof, so they sit with the client names
   rather than in a section of their own further down. */

.awards { position: relative; z-index: 1; padding-block: clamp(1.75rem, 3.5vw, 2.5rem) 0; }
.awards__lede { margin: 0 0 1.5rem; font-size: .9375rem; max-width: 60ch; }

/* A grid of three shared rows — badge, name, result — with every award a
   subgrid spanning all three, so the names start on one line and the results
   start on another however differently the names wrap. A fixed height on the
   name row was the first attempt and it only worked at the width it was
   measured at: at 1024 the longer names run to three lines and the reserve
   was two, so the second row went out of true.

   grid-auto-rows repeats the same three tracks, so when it drops to two or
   three columns each line of awards is internally aligned the same way. */
.awards__list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-template-rows: 88px auto auto;
  grid-auto-rows: 88px auto auto;
  column-gap: clamp(.75rem, 1.6vw, 1.5rem);
  /* the gap *inside* each award: a subgrid takes its gaps from its parent */
  row-gap: .75rem;
}
@media (max-width: 1023px) { .awards__list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .awards__list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Captioned. Five unexplained logos ask the reader to already know what they
   are; the win in particular was invisible unless you recognised the badge. */
/* Three rows, stated rather than left to the content: badge, name, result.
   The badges are three different heights by design — the win is bigger than
   the finalists and the Midlands banner is a letterbox — so left to itself
   every caption starts at a different line and the row reads as unaligned.
   Fixing the first row and sitting the badges on the bottom of it puts all
   five on one shelf and starts every caption on one line. */
.award {
  display: grid; grid-row: span 3; grid-template-rows: subgrid;
  justify-items: start;
}
.award img { align-self: end; }
/* The newest win has no badge here — see the note in the markup — so its
   caption is placed on the caption lines rather than being allowed to float
   up into the empty badge row. Both rows have to be named: placing only the
   name leaves the result to auto-place into the first free cell, which is the
   empty badge row, and it lands above the name it belongs to. */
.award--text .award__name   { grid-row: 2; }
.award--text .award__result { grid-row: 3; }
.award__name {
  margin: 0; font-size: .8125rem; line-height: 1.35;
  color: var(--ink); max-width: 22ch;
}
.award__result {
  margin: 0; font-size: .75rem; line-height: 1.35;
  letter-spacing: .04em; color: var(--ink-faint);
}
.award--win .award__result { color: var(--butter); font-weight: 600; }
.award img {
  height: 68px; width: auto; display: block;
  /* The badges are printed for light backgrounds; on this ground they need
     lifting rather than recolouring, so the artwork stays untouched. */
  filter: brightness(1.06) contrast(1.02);
}
.award--wide img { height: 34px; }

/* The win leads, and is allowed to be larger than the four finalists. */
.award--win img { height: 88px; }

@media (max-width: 620px) {
  .awards__list { gap: 1.25rem; }
  .award img { height: 54px; }
  .award--win img { height: 68px; }
  .award--wide img { height: 28px; }
}


/* ----------------------------------------------------------- ALSO --- */
/* Everything else we have built, named but not given a case study. Set as
   small caps so it reads as a credential list rather than competing with
   the two projects above it. */

.also {
  display: flex; flex-wrap: wrap; gap: .5rem .9rem;
  margin: clamp(2.5rem, 5vw, 3.5rem) 0 0;
  padding-top: 1.5rem; border-top: 1px solid var(--line);
  max-width: none;
}
.also__k {
  font: 600 .75rem/1.7 var(--mono, ui-monospace, monospace);
  letter-spacing: .18em; text-transform: uppercase; color: var(--butter);
}
.also__list {
  display: flex; flex-wrap: wrap; gap: .3rem .85rem;
  font: 600 .75rem/1.7 var(--mono, ui-monospace, monospace);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint);
}
.also__list > .also__i + .also__i::before {
  content: "·"; margin-right: .85rem; color: var(--line-hi);
}

/* Each name rolls up to what the thing actually is. Same mechanic as the
   nav, but the second face is new information rather than a copy of the
   first, so it stays in the accessibility tree — with a dash in front of it
   so it reads as "Beanz Barn — custom plugin" rather than running together. */
.also__i { display: inline-flex; align-items: center; }
.also__r { display: block; overflow: hidden; height: 1.7em; }
.also__f {
  display: block; height: 1.7em; line-height: 1.7em;
  transition: transform .35s var(--ease);
}
.also__f + .also__f { color: var(--butter); }
.also__i { white-space: nowrap; }
/* The same roll the pointer does, played by reveal.js on a timer. Stated
   outside the hover query on purpose: a touch screen has no hover, and is
   exactly where an automatic reveal earns its keep — without it, nobody on a
   phone ever learns what any of these are. */
.also__i.is-flip .also__f { transform: translateY(-1.7em); }
@media (hover: hover) {
  .also__i:hover .also__f { transform: translateY(-1.7em); }
}

/* Once there is room for one line, the row spans the measure and the names
   space themselves across it. The interpuncts go with it — the spacing is
   doing their job, and keeping both gives you a separator inside a gap wide
   enough not to need one. Below that it wraps, and they come back. */
@media (min-width: 760px) {
  .also { flex-wrap: nowrap; align-items: baseline; }
  .also__list { flex: 1; flex-wrap: nowrap; justify-content: space-between; gap: 0; }
  .also__list > .also__i + .also__i::before { content: none; }
}


/* ------------------------------------------------- WORK, IN MOTION --- */
/* The shot settles into its frame as you reach it, and lifts under the
   pointer. Enough that the image reads as an object rather than a paste-in,
   not so much that it performs. */

.work__shot { overflow: hidden; }

/* The hover, after Maria João Abrantes: two transforms at different speeds.
   The whole shot lifts and grows a little, quickly; the media inside grows
   considerably more, slowly, and is clipped by the shot. The gap between the
   two is what stops it reading as a flat scale-up.

   Her numbers, taken from her stylesheet rather than guessed: -4px and 1.015
   over .3s ease-in-out for the card, 1.08 over .5s ease-out-expo for the media.

   They are on separate elements for a reason. The image also carries a
   scroll-driven settle, and an animation beats a transition on the same
   property — with both on the image, the old hover scale silently never fired. */
.work__shot,
.work__media { transition: transform .3s cubic-bezier(.4, 0, .2, 1); }
.work__media { transition-duration: .5s; transition-timing-function: cubic-bezier(.16, 1, .3, 1); }
.work__media { display: block; width: 100%; height: 100%; will-change: transform; }
.work__media img,
.work__media video { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (hover: hover) {
  .work:hover .work__shot  { transform: translateY(-4px) scale(1.015); }
  .work:hover .work__media { transform: scale(1.08); }
}

@supports (animation-timeline: view()) {
  .work__shot img {
    animation: settle linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 55%;
  }
  @keyframes settle {
    from { transform: scale(1.10); }
    to   { transform: scale(1); }
  }
}

@media (hover: hover) {

  .work__title { transition: color .3s var(--ease); }
  .work__title a { color: inherit; text-decoration: none; }
  .work:hover .work__title a { color: var(--butter); }
  .work:hover .work__title { color: var(--butter); }
}

.foot__what { max-width: 34ch; margin-bottom: 1rem; }

/* ------------------------------------------------------- MATRIX --- */
/* Who is on your project, and when. A real table, so a screen reader gets the
   same answer the eye does — every cell says on your project or not involved.
   The filled row running the whole width is the argument. */

.matrix { overflow-x: auto; }
.matrix table { width: 100%; border-collapse: collapse; min-width: 34rem; }
.matrix thead th {
  padding: 0 0 1rem; text-align: center; vertical-align: bottom;
  font-size: .6875rem; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint);
}
.matrix tbody th {
  text-align: left; font-weight: 400;
  padding: 1.1rem 1.5rem 1.1rem 0;
  border-top: 1px solid var(--line);
  display: flex; align-items: center; gap: .875rem;
}
.matrix__nm { display: grid; color: var(--ink); font-weight: 600; line-height: 1.25; }
.matrix__nm span { font-weight: 400; font-size: .8125rem; color: var(--ink-faint); }
.matrix tbody td {
  border-top: 1px solid var(--line);
  text-align: center; vertical-align: middle;
}
.matrix .dot {
  display: inline-block; width: .625rem; height: .625rem; border-radius: 50%;
  background: var(--line-hi);
}
.matrix .onit .dot { background: var(--butter); box-shadow: 0 0 0 4px rgba(255, 239, 147, .12); }
.matrix__note { margin: 1.5rem 0 0; max-width: 46ch; }

/* --------------------------------------------------------- TEAM --- */
.team__face {
  width: 3.25rem; height: 3.25rem; border-radius: 50%;
  object-fit: cover; display: inline-block; vertical-align: middle;
  margin-right: .75rem; border: 1px solid var(--line-hi);
}
.who-row__t:has(.team__face) { display: flex; align-items: center; gap: 0; }

/* A face beside a quote is worth more than the same quote without one — it is
   the difference between a claim and a person who said it. */
.quote__who { display: flex; align-items: center; gap: .875rem; margin-top: 1.25rem; }
.quote__face {
  width: 3rem; height: 3rem; border-radius: 50%;
  object-fit: cover; flex: none; border: 1px solid var(--line-hi);
}

/* Initials hold the slot until a photograph arrives, so the attribution row
   sits at the same height in every card instead of one being short. */
/* Three quotes side by side means the names have to line up across all
   three, so the card is a column and the caption is pushed to the bottom.
   The quote mark moved inside the blockquote to get here — a float is
   ignored on a flex item, and flow-root keeps it contained. Beats
   .card--wrap's display:block on specificity, not source order. */
.quote.card--wrap { display: flex; flex-direction: column; }
.quote.card--wrap::after { content: none; }
.quote blockquote { display: flow-root; }

/* Same threshold the float uses: below it the mark stacks instead of being
   wrapped around, and 19px type in a column this narrow gives a 28-character
   measure. Drop both rather than let a three-up run a full screen tall. */
@container (max-width: 20.99rem) {
  .quote blockquote { font-size: 1rem; line-height: 1.55; }
  .quote .wrapfig { width: 3rem; margin-bottom: .9rem; }
  .quote .wrapfig__q { font-size: 1.875rem; }
  /* no padding rule here: .quote is the container, and a container query
     never matches the element that declares it */
}

/* One at a time, on the pan machinery — see .pan--quotes. Three side by side
   gave each quote a 28-character measure, and a whole apparatus existed to
   stop the cramming from showing: shared subgrid rows to line the names up, a
   container query to shrink the type, the quote mark moved inside the
   blockquote so a float would survive being a flex item. None of it is needed
   once a quote has the width to be read at. */
.pan--quotes .quote { container-type: normal; }
.pan--quotes .quote figcaption { margin-top: auto; }

/* ------------------------------------------------------- AWARDS --- */
/* Badge beside the claim, so a row is readable without decoding the artwork. */

.awards-grid { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.awards-grid__i {
  display: grid; gap: clamp(1rem, 3vw, 2rem);
  grid-template-columns: 1fr;
  align-items: center;
  padding-block: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--line);
}
.awards-grid__i:last-child { border-bottom: 1px solid var(--line); }
@media (min-width: 720px) { .awards-grid__i { grid-template-columns: 6rem minmax(0, 1fr); } }
.awards-grid__i img { height: 64px; width: auto; filter: brightness(1.06) contrast(1.02); }
.awards-grid__i--win img { height: 84px; }
.awards-grid__nobadge { font-size: 2.25rem; line-height: 1; color: var(--butter); }
/* black-on-white artwork, so it is flipped to white to sit on this ground */
.awards-grid__mono { filter: brightness(0) invert(1); opacity: .85; }
.awards-grid__n { margin: 0; color: var(--ink); font-weight: 600; }
/* A win nobody can look up is just a sentence, so where the organisers
   publish a listing it is linked from the entry. */
.awards-grid__c { margin: .5rem 0 0; font-size: .8125rem; }
.awards-grid__c a { color: var(--ink-dim); text-underline-offset: 3px; }
.awards-grid__c a:hover, .awards-grid__c a:focus-visible { color: var(--butter); }
.awards-grid__r { margin: .15rem 0 0; font-size: .875rem; letter-spacing: .04em; color: var(--ink-faint); }
.awards-grid__i--win .awards-grid__r { color: var(--butter); font-weight: 600; }
.awards-grid__b { margin: .6rem 0 0; max-width: 54ch; }

/* -------------------------------------------------------- LOGOS --- */
/* A wall of marks that were never designed to sit together. They are trimmed
   and normalised to one height when they are encoded; here they are forced to
   a single white, so a dark brown illustration and a white wordmark read as a
   set rather than an accident.

   Four across, and the last row centres rather than hanging left when the count
   does not divide by four. */

.logos {
  list-style: none; margin: 0; padding: 0;
  /* Fixed cells rather than a free-flowing row: on hover each logo is
     replaced in place by a shot of the thing, and that can only happen
     without the row jumping if the cell was already the right size. 15rem
     clears the widest logo (210px) at its encoded size, so the ink-area
     normalisation across the four is left exactly as it was. */
  display: grid; justify-content: center; align-items: center;
  grid-template-columns: repeat(4, minmax(0, 15rem));
  gap: clamp(1rem, 2.5vw, 1.75rem);
  /* the wall is its own beat, not a footnote to the project above it */
  margin-top: clamp(3.5rem, 7vw, 6rem);
  border-top: 1px solid var(--line);
  padding-top: clamp(3rem, 6vw, 4.5rem);
  padding-bottom: clamp(1rem, 2vw, 1.5rem);
}
.logos__i {
  position: relative;
  display: grid; place-items: center;
  aspect-ratio: 16 / 9;
  min-height: var(--tap);
}
.logos__i > a,
.logos__i > .logos__mark { grid-area: 1 / 1; }
.logos__i > a {
  display: grid; place-items: center;
  width: 100%; height: 100%;
  border-radius: var(--r-card);
}
.logos__mark {
  position: relative; z-index: 1;
  /* already sized by ink area when encoded — only guard the small screen */
  max-width: 92%; height: auto;
  filter: brightness(0) invert(1);
  opacity: .5;
  transition: opacity .3s var(--ease);
}
.logos__i a {
  display: grid; place-items: center;
  min-height: var(--tap); min-width: var(--tap);
}
@media (hover: hover) {
  .logos__i:hover .logos__mark { opacity: 1; }
}
.logos__i:focus-within .logos__mark { opacity: 1; }

/* ------------------------------------------------------ LOGO SWAP --- */
/* On hover the logo hands its cell over to a shot of the product. The shot is
   a background-image set only under :hover and :focus-within, which is the
   whole lazy-loading strategy: a background is not fetched until a rule using
   it applies to a rendered box. No <img>, no JS, no preload, and nothing on
   the wire for visitors who never hover one.

   Swynx has no shot yet. It has no .logos__shot either, so it simply brightens
   like before rather than swapping to an empty frame. */

.logos__shot {
  position: absolute; inset: 0;
  border-radius: var(--r-card);
  background: var(--raised-2) center / cover no-repeat;
  opacity: 0; transform: scale(.97);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
  pointer-events: none;
}

.logos__i:focus-within .logos__shot { background-image: var(--peek); opacity: 1; transform: none; }
.logos__i:focus-within .logos__mark { opacity: 0; }

/* The same reveal the pointer does, played by logos.js on a timer. Stated
   alongside :focus-within rather than inside the hover query, because a
   touch screen has no hover and is exactly where an automatic reveal earns
   its keep — it is the only way anyone there sees these at all. */
.logos__i.is-peek .logos__shot { background-image: var(--peek); opacity: 1; transform: none; }
.logos__i.is-peek .logos__mark { opacity: 0; }

@media (hover: hover) {
  .logos__i:hover .logos__shot { background-image: var(--peek); opacity: 1; transform: none; }
  /* The logo has to leave, not just dim — it would otherwise sit on top of
     the screenshot it is being replaced by. */
  .logos__i:hover .logos__mark { opacity: 0; }
}

@media (max-width: 900px) {
  .logos { grid-template-columns: repeat(2, minmax(0, 15rem)); }
}

/* -------------------------------------------------- INNER PAGES --- */
/* The homepage hero fills the viewport because it has the particle field in it
   and nothing above the fold to compete with. An inner page has a job to get on
   with, so it opens shorter and the content starts sooner. */

.hero--inner {
  min-height: 0;
  display: block;
  padding-block: clamp(2.5rem, 7vw, 5.5rem) clamp(2rem, 4vw, 3.5rem);
}
/* The homepage centres its hero column because the whole page is built around
   that one block. An inner page has sections under it, so the hero has to start
   on the same left edge as everything else or the page looks broken. */
.hero--inner .hero__copy { max-width: 52rem; margin-inline: 0; }
.hero--inner .display { font-size: clamp(2.125rem, 5.2vw, 4.25rem); }
.hero--inner .lede { max-width: 46ch; }
/* Inner pages have no particle field, so the word keeps its own colour rather
   than waiting for particles that never arrive. */
.hero--inner .pword { color: var(--butter); opacity: 1; }

/* Long-form: legal pages and articles. One column, generous measure, and the
   headings carry the same weight as a card title so the page holds together. */
/* 39rem, not "68ch". The ch unit is the width of a zero, and Jakarta's figures
   are much wider than its average letter — 68ch measured out at 95 characters a
   line, which is well past comfortable for long-form reading. This is ~74. */
.legal__block, .art { display: grid; gap: 1.1rem; max-width: 39rem; }
.legal__block + .legal__block { margin-top: clamp(2rem, 4vw, 3rem); }
.legal__block .title, .art .title {
  margin: 0; font-size: clamp(1.125rem, 2vw, 1.375rem);
}
.legal__block .title:not(:first-child), .art .title:not(:first-child) { margin-top: 1rem; }
.art p, .legal__block p { margin: 0; max-width: none; }
.art__cat {
  display: inline-block; margin-right: .5rem;
  font-size: .6875rem; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--butter);
}
.art__list { margin: 0; padding-left: 1.25rem; display: grid; gap: .5rem; color: var(--ink-dim); }
.art__list li { line-height: 1.6; }
.art blockquote {
  margin: .5rem 0; padding-left: 1.25rem;
  border-left: 2px solid var(--lilac); color: var(--ink);
}

/* ------------------------------------------------------- BOOKING --- */
/* The gate is the default state; the calendar only exists after a click. */

.cal { margin-top: clamp(2.5rem, 5vw, 4rem); }
.cal__gate {
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  display: grid; justify-items: start; gap: 1.25rem;
}
.cal__note { margin: 0; max-width: 52ch; }
.cal__alt { margin: 0; font-size: .9375rem; color: var(--ink-faint); }
.cal__gate[data-failed] .cal__note::after {
  content: " It did not load — your browser or an extension may be blocking it.";
  color: var(--butter);
}
#cal-inline { min-height: 0; }
#cal-inline[data-loading] { min-height: 34rem; }
#cal-inline > * { border-radius: var(--r-card); overflow: hidden; }

/* ----------------------------------------------------- STAT COUNTERS --- */
.stat__n { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------- CARRIED-OVER INDEX --- */
/* The list on a hub that was ported from the old site. Not a card grid: these
   are long documents, and a title with a line of what it is under it tells you
   more about whether to open one than a box would. */
.index-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.index-list li { border-top: 1px solid var(--line); padding: 1.5rem 0; }
.index-list li:last-child { border-bottom: 1px solid var(--line); }
.index-list a {
  display: block; color: var(--ink); text-decoration: none;
  font-size: clamp(1.0625rem, 1.6vw, 1.25rem); font-weight: 500; letter-spacing: -.02em;
  min-height: var(--tap); display: flex; align-items: center;
}
.index-list a:hover { color: var(--butter); }
.index-list span { display: block; margin-top: .25rem; font-size: .9375rem; color: var(--ink-dim); max-width: 68ch; }

/* ------------------------------------------------------------ BIOS --- */
/* Portrait and words side by side, alternating down the page so the eye has
   somewhere to go. The source files are 320px square, so the portrait is
   capped there rather than blown up soft — bigger than it was, and still its
   own resolution. */

.bios { display: grid; gap: clamp(2.5rem, 5vw, 4rem); }
.bio {
  display: grid; gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: center;
}
/* The house shape. A superellipse rather than a rounded rectangle: the
   curvature is continuous into the straight edge instead of meeting it at a
   join, which is the whole difference between a squircle and a big
   border-radius. Held as a token so anything else that wants the shape can
   take it without carrying 56 coordinates around.

   A clip removes the border with everything else outside the curve, so the
   shape does its own edge. */
:root { --squircle: polygon(100.00% 50.00%, 99.87% 70.83%, 99.49% 77.41%, 98.86% 82.10%, 97.96% 85.80%, 96.78% 88.85%, 95.31% 91.39%, 93.53% 93.53%, 91.39% 95.31%, 88.85% 96.78%, 85.80% 97.96%, 82.10% 98.86%, 77.41% 99.49%, 70.83% 99.87%, 50.00% 100.00%, 29.17% 99.87%, 22.59% 99.49%, 17.90% 98.86%, 14.20% 97.96%, 11.15% 96.78%, 8.61% 95.31%, 6.47% 93.53%, 4.69% 91.39%, 3.22% 88.85%, 2.04% 85.80%, 1.14% 82.10%, 0.51% 77.41%, 0.13% 70.83%, 0.00% 50.00%, 0.13% 29.17%, 0.51% 22.59%, 1.14% 17.90%, 2.04% 14.20%, 3.22% 11.15%, 4.69% 8.61%, 6.47% 6.47%, 8.61% 4.69%, 11.15% 3.22%, 14.20% 2.04%, 17.90% 1.14%, 22.59% 0.51%, 29.17% 0.13%, 50.00% 0.00%, 70.83% 0.13%, 77.41% 0.51%, 82.10% 1.14%, 85.80% 2.04%, 88.85% 3.22%, 91.39% 4.69%, 93.53% 6.47%, 95.31% 8.61%, 96.78% 11.15%, 97.96% 14.20%, 98.86% 17.90%, 99.49% 22.59%, 99.87% 29.17%); }

.bio__face {
  width: 100%; max-width: 320px; height: auto;
  aspect-ratio: 1; object-fit: cover;
  clip-path: var(--squircle);
  background: var(--raised-2);
}
.bio__nm { margin: 0; font-size: clamp(1.375rem, 2.4vw, 1.875rem); letter-spacing: -.02em; }
.bio__role {
  margin: .3rem 0 0;
  font: 600 .6875rem/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .12em; text-transform: uppercase; color: var(--butter);
}
.bio__x { margin: 1.1rem 0 0; max-width: 46ch; }

@media (min-width: 760px) {
  .bio { grid-template-columns: 320px 1fr; }
  /* Every other one turns round. The portrait keeps its own column either
     way, so the text column does not change width down the page. */
  .bio:nth-child(even) { grid-template-columns: 1fr 320px; }
  .bio:nth-child(even) .bio__face { grid-column: 2; grid-row: 1; }
  .bio:nth-child(even) .bio__t { grid-column: 1; grid-row: 1; }
}

/* ---------------------------------------------------- SCAN RESULTS --- */
/* The mini P.E.E.R. scan renders into the scanner card. Everything below is
   painted by scan.js, so the markup lives in one place — the renderer — and
   this file only has to say what it looks like.

   Bands are the three pastels, never a red: the palette has no red in it, and
   inventing one for a bad score would be the only saturated thing on the site.
   Colour is never the only signal — the number is always beside it. */

.scan { margin-top: 2rem; }
.scan:empty { display: none; }

/* The spoken summary. Announced, not drawn: the same sentence is already on
   screen as the score and the grade, so showing it too would just be the
   headline twice. */
.scan__say {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}

.scan__wait { display: flex; align-items: center; gap: .875rem; padding: 1.5rem 0; }
.scan__wait p { margin: 0; color: var(--ink-dim); font-size: .9375rem; }
.scan__wait small { display: block; margin-top: .25rem; color: var(--ink-faint); font-size: .8125rem; }
.scan__spin {
  flex: none; width: 1.25rem; height: 1.25rem; border-radius: 50%;
  border: 2px solid var(--line-hi); border-top-color: var(--ink);
  animation: scan-spin 1s linear infinite;
}
@keyframes scan-spin { to { transform: rotate(360deg); } }
/* With motion off the global rule runs the spin once for 1ms and stops it,
   which leaves a ring frozen part-way round looking stuck. Even it out so it
   reads as a marker beside the text rather than a stalled control — the words
   are what carry "this is working" in that mode. */
:root[data-motion="reduced"] .scan__spin { border-top-color: var(--line-hi); }

.scan__err { padding: 1.25rem 0; }
.scan__err p { margin: 0; color: var(--ink); font-size: .9375rem; }
.scan__err small { display: block; margin-top: .375rem; color: var(--ink-dim); font-size: .8125rem; }

/* --- headline ---------------------------------------------------------- */
.scan__top {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .75rem 1.25rem;
  padding-bottom: 1.5rem; border-bottom: 1px solid var(--line);
}
.scan__score {
  font-size: clamp(3rem, 9vw, 4.5rem); font-weight: 400; line-height: .9;
  letter-spacing: -0.055em; font-variant-numeric: tabular-nums;
}
.scan__of { font-size: .3em; color: var(--ink-dim); letter-spacing: 0; }
.scan__for { flex: 1 1 12rem; margin: 0; color: var(--ink-dim); font-size: .875rem; word-break: break-all; }

/* --- the four dimensions ----------------------------------------------- */
.scan__dims { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  background: var(--line); border-bottom: 1px solid var(--line); }
.scan__dim { background: var(--raised); padding: 1.25rem 1rem; }
.scan__dim-n { margin: 0; font-size: .6875rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); }
.scan__dim-v { margin: .5rem 0 0; font-size: 2rem; font-weight: 400; line-height: 1;
  letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
.scan__dim-d { margin: .5rem 0 0; font-size: .8125rem; line-height: 1.45; color: var(--ink-dim); }

/* --- weight and waste --------------------------------------------------- */
.scan__facts { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1.5rem 0 0; padding: 0; list-style: none; }
/* Looks like .pill but keeps its case. .pill uppercases, which turns "CO₂e"
   into "CO₂E" and "0.03g" into "0.03G" — both wrong, and the first one is a
   unit the rest of the site is careful about. */
.scan__fact {
  display: inline-flex; align-items: center;
  padding: .375rem .75rem;
  border: 1px solid var(--line-hi); border-radius: var(--r-pill);
  font-size: .8125rem; color: var(--ink-dim); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* --- quick wins --------------------------------------------------------- */
.scan__wins { margin: 1.75rem 0 0; padding: 0; list-style: none; }
.scan__win { padding: 1rem 0; border-top: 1px solid var(--line); }
.scan__win:first-child { border-top: 0; padding-top: 0; }
.scan__win h3 { margin: 0; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; }
.scan__win p { margin: .375rem 0 0; font-size: .875rem; line-height: 1.6; color: var(--ink-dim); }
.scan__save { color: var(--ink-faint); }

/* --- what to do next ---------------------------------------------------- */
.scan__next { margin-top: 1.75rem; padding-top: 1.5rem; border-top: 1px solid var(--line); }
.scan__next p { margin: 0 0 1.25rem; font-size: .9375rem; line-height: 1.6; color: var(--ink-dim); }
.scan__again {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); min-width: var(--tap); padding: .5rem 0;
  background: none; border: 0; color: var(--ink-dim);
  font: inherit; font-size: .875rem; text-decoration: underline;
  text-underline-offset: .25em; cursor: pointer;
}
.scan__again:hover { color: var(--ink); }
.scan__again[disabled] { opacity: .6; cursor: default; }

/* --- score bands -------------------------------------------------------- */
/* Three, not five: the palette has three pastels and a fourth tone would be a
   new colour on the site rather than a new state in the component. */
[data-band="high"] { color: var(--mint); }
[data-band="mid"]  { color: var(--butter); }
[data-band="low"]  { color: var(--peach); }

/* =============================================== MOTION PREFERENCE === */
/* The operating system setting is the default. A visitor can override it
   from the switch in the footer, and an inline script in the head resolves
   the choice into data-motion on <html> before first paint, so nothing ever
   animates and then stops.

   Everything above is written unconditionally and turned off here, rather
   than each block testing the preference itself. That way one rule governs
   motion, and turning it back *on* against an OS preference costs nothing —
   which the media-query version could not do.

   The scroll-driven timelines need the extra step: animation-duration means
   nothing to a view() animation, because progress comes from the scroll
   position rather than from time. Dropping the timeline back to the document
   one lets the 1ms duration land each animation on its end state — visible,
   in place, and not frozen halfway through a reveal. */

:root[data-motion="reduced"] { scroll-behavior: auto !important; }

:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after {
  animation-timeline: auto !important;
  animation-duration: 1ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: 1ms !important;
  transition-delay: 0s !important;
}

/* The stats shear is stated outright rather than left to the 1ms rule above.
   That rule works by letting the animation run to its end, which needs a
   frame; these four figures have to sit level against the rules they are
   ruled by, and a figure hanging 50px out of its own cell reads as a broken
   layout rather than as reduced motion. Cheap certainty. */
:root[data-motion="reduced"] .stat__n,
:root[data-motion="reduced"] .stat__l { animation: none !important; transform: none !important; }

/* The marquee is the one loop with no end state worth landing on: it would
   stop at translateX(-50%), which is halfway through the duplicated strip.
   Stop it where it starts, and drop the edge mask that only made sense on
   something moving. */
:root[data-motion="reduced"] .marquee__track { animation: none !important; }
:root[data-motion="reduced"] .marquee { -webkit-mask-image: none; mask-image: none; }

/* ------------------------------------------------- MOTION SWITCH --- */
/* Hidden in the markup and revealed by motion.js, so it is never a control
   that does nothing. State is carried by aria-checked, which means the track
   and knob are decoration and the visible label is the accessible name. */

.motion { margin-top: 1.75rem; }
.motion__btn {
  display: inline-flex; align-items: center; gap: .75rem;
  min-height: var(--tap); padding: .5rem 1rem .5rem .5rem;
  background: none; border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--ink-dim); font: inherit; font-size: .8125rem; cursor: pointer;
}
.motion__btn:hover { color: var(--ink); border-color: var(--line-hi); }
.motion__track {
  position: relative; flex: none; width: 2.25rem; height: 1.25rem;
  background: var(--raised-2); border: 1px solid var(--line-hi);
  border-radius: var(--r-pill);
}
.motion__knob {
  position: absolute; top: 50%; left: 2px;
  width: .875rem; height: .875rem; margin-top: -.4375rem;
  border-radius: 50%; background: var(--ink-dim);
  transition: transform .2s cubic-bezier(.45, 0, .3, 1), background-color .2s linear;
}
.motion__btn[aria-checked="true"] { color: var(--ink); }
.motion__btn[aria-checked="true"] .motion__track {
  background: var(--lilac); border-color: var(--lilac);
}
.motion__btn[aria-checked="true"] .motion__knob {
  transform: translateX(1rem); background: var(--on-accent);
}
