/* ═══════════════════════════════════════════════════════════════════════════
   THE SECTIONS — styles for what assets/js/sections/*.js renders.

   Kept apart from site.css because these belong to the section registry: a
   type's markup and a type's styles change together, and burying them in a
   3000-line stylesheet is how the last set of rules ended up describing a page
   that no longer existed.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────────────────────────────────────── the host and its frame */

.s-sec { position: relative; }

/* UNCLIPPED BY DEFAULT — see the note in sections-host.js. A section that
   genuinely needs containment asks for it; everything else is free to break
   its own bounds, which is what the card stack and the hover lifts need. */
.s-sec[data-clip] { overflow: hidden; }

.s-lbl {
  font-size: .78rem; font-weight: 700; letter-spacing: .14em;
  color: var(--g-muted); text-transform: uppercase; margin-bottom: .6rem;
}
.s-h2 {
  font-size: clamp(1.6rem, 4vw, 2.6rem); font-weight: 800; line-height: 1.25;
  color: var(--g-title); letter-spacing: -.02em; margin-bottom: clamp(1rem, 2.5vw, 1.8rem);
  text-wrap: balance;
}

/* ════════════════════════════════════════════════════════════ the work grid
 *
 * NOTHING IS CROPPED. That is the whole rule, and it is a change of principle
 * rather than of numbers.
 *
 * The old grid gave every tile a near-square cell and used `object-fit: cover`
 * to fill it, so a 4:5 portrait and a 2:1 banner both lost whatever did not
 * fit — measured at 33% of a square image gone at 1100px, because the column
 * came from `repeat(4, 1fr)` and the row from `clamp(…15vw…)`, two independent
 * quantities that diverge the moment the wrap hits its max width. A comment
 * above it asserted "the row height goes up with it so a square stays square",
 * which was measurably false.
 *
 * Here the TILE TAKES THE IMAGE'S OWN RATIO. `aspect-ratio: var(--sw-r)` comes
 * from the real measurement in shape.json, so the box IS the picture's shape:
 * cover and contain would produce identical output, and there is nothing left
 * to crop. A wide piece earns two columns so it is not shrunk to a stamp; a
 * portrait keeps one and stands tall.
 *
 * The row is `masonry`-ish by way of dense packing rather than a real masonry
 * algorithm — `grid-auto-flow: dense` lets a one-column tile fill a gap a
 * two-column tile left, which is what stops a wall of holes without needing
 * JavaScript to measure anything at runtime.
 */

/* TIGHTLY PACKED — COLUMNS, NOT A ROW GRID.
 *
 * Three attempts, and the reasoning is worth keeping because the first two
 * both look right in a screenshot of the CSS and wrong on the page.
 *
 * A ratio grid makes every ROW as tall as its tallest tile, so a 9:16 poster
 * beside three squares opens three holes: correct shapes, ragged wall, air
 * everywhere. Row-span masonry (grid-auto-rows: 8px, each tile spanning its
 * own height) fixed most of it — 9,615px down to 4,977 — but `dense` can only
 * fill a hole when some later tile happens to fit it exactly, and with spans
 * of 20-90 rows that is rare: 10 gaps over 40px survived, the worst 429px.
 *
 * CSS columns have no such problem. The browser flows tiles down a column and
 * starts the next, so a tile always begins exactly where the one above ended
 * and there is no hole to fill by luck. Every tile keeps its own
 * aspect-ratio, so the MIX of shapes is what gives the wall its variety —
 * which is the point — and nothing is cropped, which is the rule.
 *
 * THE TRADE, stated: reading order becomes down-then-across rather than
 * across-then-down. For a wall of work that is no loss — there is no order to
 * read — and it is what every mosaic of this kind does.
 */
.sw-grid {
  --sw-gap: clamp(.45rem, 1vw, .8rem);
  columns: 4 14rem;            /* four at full width, never narrower than 14rem */
  column-gap: var(--sw-gap);
}
@media (max-width: 900px) { .sw-grid { columns: 3 11rem; } }
@media (max-width: 620px) { .sw-grid { columns: 2 9rem; } }

.sw-tile {
  /* DECLARED HERE, OVERRIDDEN INLINE. work.js writes the measured ratio and
     column span onto each tile as a style attribute, so without a declaration
     in the stylesheet the tokens exist nowhere a reader — or the [VAR] scanner
     — can find them. A token only ever set inline is a token nobody can look
     up, which is the same complaint that scanner exists to make. */
  --sw-r: 1;
  --sw-cols: 1;
  /* The measured ratio. 1 is the fallback for an image the build has not seen
     yet — a fresh panel upload — and `auto` would be better still, but only
     once the image has loaded, which costs a reflow per tile. */
  /* The tile IS the picture's shape. cover and contain would produce
     identical output on a box that already has the right ratio, so there is
     nothing left to crop. */
  aspect-ratio: var(--sw-r, 1);
  /* A tile must not be split across a column break — half a poster at the
     bottom of one column and half at the top of the next is the one way a
     column layout can crop, and it would undo the whole point. */
  break-inside: avoid; -webkit-column-break-inside: avoid;
  display: block; width: 100%;
  /* The gap BELOW, set after the shorthand rather than before it — a bare
     `margin: 0` further down was resetting it, which is the whole reason a
     column layout was still showing tiles flush against each other. */
  position: relative; overflow: hidden;
  margin: 0 0 var(--sw-gap, .6rem);
  border-radius: var(--g-r-panel, 18px);
  background: var(--g-surface);
  box-shadow: var(--g-edge), 0 10px 30px -18px rgba(0, 0, 0, .5);
  transition: transform var(--g-t, 180ms) var(--g-ease, ease),
              box-shadow var(--g-t, 180ms) var(--g-ease, ease);
}

/* `cover` on a box that is already the image's shape is a no-op — it is here
   so a stale or unmeasured ratio degrades to a tidy fill rather than to a
   letterboxed tile with bars, which looks like a bug rather than a fallback. */
.sw-tile img,
.sw-tile picture { display: block; width: 100%; height: 100%; object-fit: cover; }

.sw-tile:hover { transform: translateY(-3px); box-shadow: var(--g-edge), 0 18px 44px -18px rgba(0, 0, 0, .62); }

.sw-who {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  padding: 1.6rem .8rem .6rem; font-size: .76rem; font-weight: 700; color: #fff;
  background: linear-gradient(transparent, rgba(5, 7, 15, .78));
  opacity: 0; transition: opacity var(--g-t, 180ms) var(--g-ease, ease);
}
.sw-tile:hover .sw-who, .sw-tile:focus-within .sw-who { opacity: 1; }

/* A filtered-out tile leaves the flow entirely rather than going transparent,
   so `dense` can re-pack the gap it left. */
.sw-tile[hidden] { display: none; }

.sw-tabs { margin-bottom: clamp(.8rem, 2vw, 1.2rem); }

@media (prefers-reduced-motion: reduce) {
  .sw-tile, .sw-who { transition: none; }
  .sw-tile:hover { transform: none; }
}

/* ═══════════════════════════════════════════════════════════ the reel frame
 *
 * Shape by what it IS. A reel is 9:16 and a video is 16:9, declared by the
 * panel, so the frame is right before a byte has loaded — rather than a
 * portrait reel sitting letterboxed inside a landscape box while the browser
 * finds out what it received.
 */
.sv-frame {
  aspect-ratio: 9 / 16; max-width: min(24rem, 88vw); margin-inline: auto;
  position: relative; overflow: hidden;
  border-radius: var(--g-r-card, 26px); background: #05070f;
  box-shadow: var(--g-float, 0 24px 60px -24px rgba(0, 0, 0, .65));
}
.sv-frame[data-format="video"] { aspect-ratio: 16 / 9; max-width: min(60rem, 94vw); }
.sv-frame video, .sv-frame img, .sv-frame picture,
.sv-frame .sv-face { display: block; width: 100%; height: 100%; object-fit: cover; }
.sv-cap { text-align: center; font-size: .8rem; color: var(--g-muted); margin-top: .7rem; }

/* ══════════════════════════════════════════════════════════════ the steps
   A real <ol>, because the order IS the content — a screen reader should say
   "1 of 6". The printed number is aria-hidden so it is not read twice, and the
   browser's own marker is suppressed. That one line is the difference between
   one number and two; it is not decoration. */
.sp-list { list-style: none; padding: 0; margin: 0;
           display: grid; gap: clamp(.6rem, 1.4vw, 1rem); }

.sp { display: grid; gap: clamp(.9rem, 2.4vw, 1.5rem); }
.sp-step { display: flex; align-items: baseline; gap: var(--g-s3); }
.sp-n {
  flex: 0 0 auto; width: 1.85rem; height: 1.85rem;
  display: grid; place-items: center; border-radius: var(--g-r-pill);
  background: var(--g-grad); color: #fff;
  font-weight: 800; font-size: .8rem; font-variant-numeric: tabular-nums;
}
.sp-t { margin: 0; color: var(--g-ink); line-height: 1.85; }

/* ─────────────────────────────────── the card that answers, not the example
 *
 * It used to be one fixed month. It is now a question with an answer under it,
 * so it needs the shape of a control panel rather than of a caption: the ask,
 * a row of goals, and the month that goal costs.
 */
.sp-card {
  margin-top: clamp(.4rem, 1.6vw, 1rem);
  padding: clamp(1rem, 3vw, 1.7rem);
  border-radius: var(--g-r-card); border: 1px solid var(--g-line);
  background: var(--g-surface); box-shadow: var(--g-edge);
  display: grid; gap: var(--g-s4);
  /* the hidden live region below is positioned against this */
  position: relative;
}
.sp-card-t {
  margin: 0; color: var(--g-muted);
  font-size: .84rem; font-weight: 700; letter-spacing: .06em;
}

/* A fieldset carries the group name a screen reader reads. It is stripped of
   the browser's own frame — the legend is styled as the question it is — and
   given `min-width: 0`, without which a fieldset refuses to shrink below the
   width of its contents and a long row of goals pushes the card off a phone. */
.sp-pick { border: 0; margin: 0; padding: 0; min-width: 0; }
.sp-ask {
  padding: 0; margin-bottom: var(--g-s3); color: var(--g-title);
  font-size: clamp(1rem, 2.4vw, 1.2rem); font-weight: 800; line-height: 1.45;
}
.sp-chips { display: flex; flex-wrap: wrap; gap: var(--g-s2); }

.sp-chip { position: relative; display: inline-block; cursor: pointer; }
/* THE RADIO IS THE CONTROL — moved out of sight, NOT out of the accessibility
   tree. `display: none`, `visibility: hidden` and `appearance: none` on a
   zero-sized box all take it off the tab order, and with it the arrow-key
   navigation a radio group gets for free. Laid over its own label so a tap
   anywhere on the pill lands on the input itself. */
.sp-chip input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.sp-chip span {
  display: block; padding: .5rem .95rem;
  border-radius: var(--g-r-pill); border: 1px solid var(--g-line);
  background: var(--g-sunk); color: var(--g-ink);
  font-size: .9rem; font-weight: 700; line-height: 1.4;
  transition: background var(--g-t) var(--g-ease),
              border-color var(--g-t) var(--g-ease),
              color var(--g-t) var(--g-ease);
}
.sp-chip:hover span { border-color: var(--g-line-up); color: var(--g-title); }
.sp-chip.on span { background: var(--g-grad); border-color: transparent; color: #fff; }
/* The focus ring is drawn on the label, because the thing being focused is
   invisible. Without this the keyboard has no idea where it is. */
.sp-chip input:focus-visible + span {
  outline: 2px solid var(--g-orange); outline-offset: 2px;
}

.sp-panel { display: grid; gap: var(--g-s3); }
/* `hidden` is an attribute the UA styles as display:none — which the rule
   above would otherwise beat, leaving every goal's answer on screen at once. */
.sp-panel[hidden] { display: none; }

.sp-figs {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: var(--g-s2);
}
.sp-fig {
  padding: var(--g-s3); text-align: center;
  border-radius: var(--g-r-el); border: 1px solid var(--g-line);
  background: var(--g-sunk);
  display: grid; gap: .1rem;
}
.sp-fig b {
  font-size: clamp(1.4rem, 4.2vw, 2rem); font-weight: 800; line-height: 1.15;
  background: var(--g-grad); -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.sp-fig-l { font-size: .76rem; color: var(--g-muted); line-height: 1.5; }
.sp-why { margin: 0; color: var(--g-ink); font-size: .93rem; line-height: 1.85; }

/* Announced, never seen. Clipped rather than hidden: a screen reader ignores
   anything with display:none or visibility:hidden, including a live region. */
.sp-say {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.sp-in { animation: sp-rise var(--g-t-slow) var(--g-ease) both; }
@keyframes sp-rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* The movement goes, the numbers stay. gds.js writes every figure out in full
   before it animates anything, so under this query the card still answers —
   it simply answers without travelling. */
@media (prefers-reduced-motion: reduce) {
  .sp-in { animation: none; }
  .sp-chip span { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════ the logos */
.sl-wall {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: clamp(.5rem, 1.2vw, .9rem);
}
.sl-cell {
  aspect-ratio: 16 / 10; display: grid; place-items: center; padding: .9rem;
  border-radius: var(--g-r-el, 12px); background: var(--g-sunk);
  border: 1px solid var(--g-line);
}
/* `contain`, deliberately, and it is the opposite call from the work grid.
   A logo has a shape somebody designed and a cell it must share with fifteen
   others; cropping a logo to fill a box is a brand mistake, where cropping a
   photograph is only a composition one. */
.sl-cell img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

/* ═════════════════════════════════════════════════════════════════ the close
 *
 * The closing card keeps the full gradient — it holds nothing but type, and
 * type on the sweep is exactly what the sweep is good at. `.g-accent` in the
 * design system paints it and sets its own text white, including the bloom in
 * the corner that gives the flood a light source instead of being a flat fill.
 * Centred, because a closing line addressed to the reader is not a column of
 * copy — it is a sign.
 *
 * `s-close`, NOT `s-cta`, AND THE RENAME HAS TO HAPPEN IN BOTH FILES.
 * site.css:673 has defined `.s-cta` since long before this section existed,
 * for a different component entirely — the small linked tile landing.js still
 * emits as `.s-tile.s-cta.big` with an `.s-cta-in` inside it. Its rules ran on
 * this band and dressed it as that tile: `.s-cta span { font-size: .82rem }`
 * shrank the closing line to 13px, and sections.css's OWN second `.s-cta-go`
 * (down in the free-text block, `justify-self: start`) threw the button into
 * the corner. Renaming the markup alone would have left the band with no rules
 * at all, which renders as a column — so the renderer and these rules move
 * together.
 *
 * WHAT IS DELIBERATELY NOT REPEATED HERE. site.css:387-390 already styles
 * `.s-close`: centred text, the same padding, and `.s-close h2` at
 * clamp(1.8rem, 4.6vw, 3rem)/800/#fff — which is exactly what this band wants
 * and is where the type size now comes from. site.css:607 already carries
 * `body.space .s-close`'s shadow. Only the parts site.css has no opinion about
 * are stated below; stating the rest again is how the two drift.
 */
.s-close {
  display: grid; justify-items: center; gap: clamp(1rem, 2.5vw, 1.6rem);
}

/* `.s-close .s-close-h` and not a bare `.s-close-h`: site.css's `.s-close h2`
   is a class AND a type, so it outranks a single class and its
   `margin-bottom: .8rem` would sit on top of the grid gap above. */
.s-close .s-close-h {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: .1em .25em;
  /* WHITE, not gradient-clipped. The band underneath is already the
     gradient; painting one through the letters as well made the second line
     all but invisible against it. Size and weight come from site.css's
     `.s-close h2`. */
  color: #fff; margin: 0; text-wrap: balance;
}

/* The mark that joins the two halves. Sized to the type it sits between —
   1em, not 18rem — so it reads as punctuation rather than as art. */
.s-close-amp {
  display: inline-flex; align-items: center; font-style: normal;
  height: 1em; opacity: .82; margin-inline: .1em;
}
.s-close-amp img, .s-close-amp picture {
  display: block; height: 1.15em; width: auto; max-width: none;
}

/* Centred beneath, and it has to say so: `justify-items` on the grid is what
   centres the children, and any later `justify-self` on the button wins over
   it — which is precisely what the dead `.s-cta-go` further down this file was
   doing, until it was deleted. */
.s-close-go { justify-self: center; margin-top: .2rem; }

/* ═════════════════════════════════════════════════════ a broken section
   Shown, not hidden. A gap where a section should be reads as a layout bug
   and sends somebody to the CSS; a marked one sends them to the console. */
.s-sec-failed, .s-sec-missing { min-height: 0; }

/* ═══════════════════════════════════════════════════════ THE CARD STACK
 *
 * A PILE OF PAGES YOU TURN. Written against the brief phrase by phrase, because
 * the version this replaces answered none of it: it was a 4:5 portrait tile
 * that span about its own centre inside a clipped panel, which is a card being
 * shuffled, not a page being turned.
 *
 *   SQUARE      `aspect-ratio: 1` on the card, and the stage takes its height
 *               from the card rather than the card fitting a stage. Every
 *               square poster in the portfolio was losing its top and bottom
 *               to a 4:5 box.
 *   BIGGER      sized off the viewport — `min(78vmin, 34rem)`. It is the
 *               subject of its section, not an illustration beside a heading.
 *   PAGE-FLIP   the hinge is ONE EDGE. `transform-origin` sits on the binding
 *               side and the leaf sweeps through 180° about it, which is what
 *               makes it read as paper. Spinning about the centre is the thing
 *               it must not do.
 *   OUT OF
 *   BOUNDS      the stage does not clip, and the section is mounted unclipped
 *               by default (see sections-host.js). A turning leaf passes over
 *               the panel edge instead of being sheared by it.
 *   THEN A CTA  the invitation is UNDERNEATH the pile the whole time, not
 *               appended when the pile empties. The leaves leaving is what
 *               reveals it. Nothing to time, nothing to insert, and no frame
 *               where the section is empty.
 *
 * THE HINGE SIDE IS DIRECTION-AWARE, and it is not decoration. An Arabic book
 * is bound on the right and its pages turn to the right; a Latin one is bound
 * on the left. `transform-origin` takes no logical keyword — `right` is right
 * in both directions — so the side and the sweep both come from a variable the
 * [dir] rule swaps. Getting this wrong turns the pages the wrong way for the
 * language the site opens in.
 */

.sc { --sc-size: min(78vmin, 34rem); }

.sc-stage {
  position: relative;
  width: var(--sc-size);
  aspect-ratio: 1;                 /* SQUARE — the stage follows the card */
  margin-inline: auto;
  overflow: visible;               /* the leaves leave; see the brief above */
  perspective: 1600px;             /* on the STAGE, so all leaves share one
                                      vanishing point. Per-card perspective
                                      gives each leaf its own camera and the
                                      pile stops looking like one object. */
}

.sc-stack {
  --hinge: 0%;                     /* LTR: bound on the left, turns leftward */
  --turn: -180deg;
  position: absolute; inset: 0;
  transform-style: preserve-3d;
}
[dir="rtl"] .sc-stack { --hinge: 100%; --turn: 180deg; }

.sc-card {
  position: absolute; inset: 0; margin: 0;
  aspect-ratio: 1;
  border-radius: var(--g-r-card, 26px);
  overflow: hidden;
  background: var(--g-surface);
  box-shadow: 0 24px 60px -24px rgba(0, 0, 0, .7), var(--g-edge);
  transform-origin: var(--hinge) 50%;
  transform-style: preserve-3d;
  cursor: pointer;
  /* THE PILE. --i is depth from the top, written by cards.js. Each leaf further
     back is nudged and shrunk so the stack has thickness — 6 leaves of visible
     depth, after which they sit flush, because a pile of forty must not march
     off the screen. */
  --d: min(var(--i, 0), 6);
  transform:
    translate3d(calc(var(--d) * .9%), calc(var(--d) * -1.1%), calc(var(--d) * -14px))
    rotate(calc(var(--d) * -.9deg));
  /* The PILE settles with a transition; the TURN is a keyframe animation
     (below), because a transition can only describe the two ends of a
     movement and everything that makes this read as paper happens in the
     middle. Kept short so a card promoted to the top of the pile slides up
     rather than jumping. */
  transition: transform 420ms cubic-bezier(.22, .61, .36, 1);
}

/* THE <img> INSIDE THE <picture> IS THE THING THAT PAINTS.
   `.sc-card > picture` sized the WRAPPER and nothing sized what it wraps, so a
   <picture> stretched to the full 719px card while the <img> inside it sat at
   its intrinsic 288 × 288 in the corner — measured on the live page. A card
   that is nine tenths empty surface with a small poster in one corner, which
   is exactly what it looked like, and no rule anywhere was wrong.
   `picture` is a wrapper element with no rendering of its own; sizing it is
   always sizing the wrong box. Every stack on the page goes through
   ctx.picture(), so this is the normal case, not the exception. */
.sc-card > picture, .sc-card > img,
.sc-card > picture > img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Only the leaf on top takes the click. Without this every card in the pile is
   a target and a click near the edge turns one from the middle. */
.sc-card { pointer-events: none; }
.sc-card[data-top] { pointer-events: auto; }

/* THE BACK OF THE PAGE. Past 90° you are looking at the reverse of a sheet, and
   a sheet has a reverse — without one the card either vanishes at the halfway
   point (backface-visibility: hidden, which reads as a glitch) or shows its own
   photograph mirrored, which reads as a bug. Paper stock, faintly ruled. */
.sc-card > picture, .sc-card > img, .sc-card > figcaption,
.sc-card > picture > img { backface-visibility: hidden; }
.sc-card::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  transform: rotateY(180deg) translateZ(1px);
  backface-visibility: hidden;
  background:
    linear-gradient(115deg, rgba(255,255,255,.06), rgba(255,255,255,0) 42%),
    var(--g-sunk, #12162a);
  border: 1px solid var(--g-line);
}

/* ══════════════════════ TURNED — AND IT HAS TO MOVE LIKE PAPER, NOT LIKE A DOOR
 *
 * A single `transform` transition from 0° to 180° is a rigid plane pivoting
 * about an axis. That is a door, or a card in a plastic sleeve — the one
 * thing it never reads as is a sheet of paper, because paper does three
 * things a rigid plane does not:
 *
 *   IT LIFTS BEFORE IT TURNS. A sheet peels off the pile — the free corner
 *     comes up first and the hinge stays down — so the motion starts as a
 *     rise, not a rotation. That is the `translateZ` and the small `rotateZ`
 *     in the first third.
 *   IT IS NOT FLAT WHILE IT MOVES. A turning page bows. There is no CSS for
 *     bending a box, so the bow is IMPLIED: a slight `scaleY` squeeze at the
 *     halfway point, which is what a curved sheet's silhouette actually does
 *     when you look at it edge-on, plus a brightness sweep standing in for
 *     the light running down the curve.
 *   IT IS THIN. Thin things move FAST through the middle and settle slowly:
 *     the weight is in the ends, not the arc. A single ease-out spreads the
 *     motion evenly and makes the sheet feel like board.
 *
 * Keyframes rather than a transition, because all three of those live in the
 * MIDDLE of the movement and a transition can only describe its ends.
 * `forwards` holds the final frame, so `data-gone` stays the state and the
 * behaviour file still owns it.
 */
@keyframes sc-turn {
  0% {
    transform: translate3d(0, 0, 0) rotateY(0deg) rotateZ(0deg) scaleY(1);
    filter: brightness(1);
    opacity: 1;
  }
  /* the peel: the free edge lifts, the hinge stays down */
  18% {
    transform: translate3d(0, -1.5%, 34px) rotateY(calc(var(--turn) * .07))
               rotateZ(calc(var(--turn) * .012)) scaleY(1.008);
    filter: brightness(1.06);
    opacity: 1;
  }
  /* through the middle, fast, bowed, catching the light */
  52% {
    transform: translate3d(0, -.6%, 58px) rotateY(calc(var(--turn) * .52))
               rotateZ(calc(var(--turn) * .006)) scaleY(.982);
    filter: brightness(1.16);
    opacity: .92;
  }
  /* laid over, flattening out again as it goes */
  82% {
    transform: translate3d(0, 0, 22px) rotateY(calc(var(--turn) * .88))
               rotateZ(0deg) scaleY(.995);
    filter: brightness(.9);
    opacity: .35;
  }
  100% {
    transform: translate3d(0, 0, 0) rotateY(var(--turn)) rotateZ(0deg) scaleY(1);
    filter: brightness(.82);
    opacity: 0;
  }
}

.sc-card[data-gone] {
  /* Slow in, quick through, soft out — the curve is the weight of the sheet. */
  animation: sc-turn 780ms cubic-bezier(.36, .02, .22, 1) forwards;
  pointer-events: none;
}

.sc-who {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  padding: 2.4rem 1rem .9rem;
  font-size: .82rem; font-weight: 700; color: #fff;
  background: linear-gradient(transparent, rgba(5, 7, 15, .82));
}

/* THE INVITATION, waiting underneath from the first frame. It is dimmed and
   unclickable while leaves still cover it — not hidden, because `display: none`
   would mean the browser has nothing laid out to reveal and the reveal would
   cost a reflow at the exact moment the last leaf is moving. */
.sc-invite {
  position: absolute; inset: 0;
  display: grid; place-content: center; gap: 1rem; text-align: center;
  padding: clamp(1.2rem, 4vw, 2.5rem);
  border-radius: var(--g-r-card, 26px);
  background: var(--g-surface); border: 1px solid var(--g-line);
  opacity: 0; transform: scale(.96);
  transition: opacity 420ms ease, transform 420ms ease;
  pointer-events: none;
}
.sc[data-done] .sc-invite { opacity: 1; transform: none; }

/* THE EMPTY STACK WAS STILL COVERING THE INVITATION.
   `.sc-stack` is `inset: 0` and comes AFTER the invite in document order — the
   invite is laid UNDER the leaves on purpose — so it painted and hit-tested
   above it. Measured: with every leaf gone, elementFromPoint at the centre of
   the call's own button returned `DIV.sc-stack`. The call was revealed, the
   opacity was right, the button was there, and it was not clickable at all.
   Once every leaf has gone the stack has nothing left to take a click for.

   AND ONLY THE BUTTON TAKES IT, not the panel behind it. `.sc-invite` fills
   the stage, so a panel that swallowed clicks would swallow the stage's reset
   with it — clicking the pile for a second look is what that is for. So the
   surface stays transparent to the pointer and the one control on it does not. */
.sc[data-done] .sc-stack { pointer-events: none; }
.sc[data-done] .sc-invite a { pointer-events: auto; }

/* AND IT BELONGS TO THE DESIGNS TAB ONLY — «cta shouldnt be visible, only the
   designs tab after going through the designs».
   The pane's own hide is `opacity: 0` plus `visibility: hidden` transitioned
   `0s linear 420ms`, which deliberately keeps the outgoing pane VISIBLE for the
   length of the cross-fade so it can fade rather than snap. Everything else in
   a pane is small, transparent and dark, so it ghosts out unnoticed. The
   invitation is not: it is a full-bleed opaque panel with the page's only
   gradient headline on it, so for those 420ms it paints straight over whichever
   tab you just chose — measured overlapping the paid-ads figures.
   So the call is scoped to a pane that is ON, the same rule as the film frame
   at line 999. It is the pile's ending, not the stage's. */
.s-pane:not(.on) .sc-invite { opacity: 0; }
.sc-invite-t {
  font-size: clamp(1.4rem, 4.5vw, 2.4rem); font-weight: 800; line-height: 1.2;
  background: var(--g-grad); -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

.sc-hint {
  text-align: center; font-size: .8rem; color: var(--g-muted);
  margin-top: clamp(.9rem, 2vw, 1.4rem);
  transition: opacity 300ms ease;
}
.sc[data-done] .sc-hint { opacity: 0; }

/* A hinge is a movement across most of the viewport. Collapsed to a cross-fade
   rather than dropped, so the section still works — the leaf still leaves, it
   just does not sweep to get there. */
@media (prefers-reduced-motion: reduce) {
  .sc-card { transition: opacity 200ms ease; transform: none; }
  /* The leaf still leaves — it just does not sweep to get there. Somebody
     who asked not to be moved is asking for less animation, not less page. */
  .sc-card[data-gone] { animation: none; transform: none; opacity: 0; }
  .sc-invite, .sc-hint { transition: none; }
}

/* The embed that replaces the cover when the showreel is clicked. It fills the
   frame the poster was sized into, so the swap does not resize the section. */
.sv-embed { display: block; width: 100%; height: 100%; border: 0; }
.sv-face { display: block; position: relative; width: 100%; height: 100%; }

/* ═══════════════════════════════════════════════════════ THE PRIZE WHEEL
 *
 * activation.js emits nine `.av-*` elements and NOTHING IN THE REPO STYLED
 * ANY OF THEM. Every layer was `position: static`, so instead of stacking on
 * top of each other they stacked one BELOW the other: measured on the live
 * site, `.av-stage` was 1005 × 3421px around a wheel 1005 × 1005 — roughly
 * 2,400px of empty column, with the rim, the gloss, the bulbs, the hub and the
 * pin strung out underneath the face like a list.
 *
 * Same failure as the card stack and as sections.css itself: a renderer
 * shipped, deployed and serving correct markup that no stylesheet had ever
 * been written for. Markup alone renders; it just renders as a column.
 *
 * THE STAGE IS A SQUARE AND EVERY LAYER FILLS IT. `aspect-ratio: 1` plus
 * absolutely positioned children is what makes the layers a stack rather than
 * a sequence, and it means the wheel is round before the SVG has laid out.
 *
 * THE PIN IS CENTRED ON THE AXIS, NOT ON THE BOX. It sits at the top, half its
 * own width to the left of the centre line — `translateX(-50%)` on an element
 * whose `inset-inline-start: 50%` would be wrong in RTL, so it is `left: 50%`
 * deliberately: the wheel's geometry is not direction-aware and must not be.
 */

.av-wrap { display: grid; justify-items: center; gap: clamp(.8rem, 2vw, 1.2rem); }

.av-stage {
  position: relative;
  /* 26rem, down from 30. It was taking the full column width; 30rem then
     over-corrected — at desktop it stood 480px across in a 1140px section and
     read as the subject of the page rather than as one demonstration among
     nine. 86vw stays as the phone floor, where the wheel IS the screen. */
  width: min(26rem, 86vw);
  aspect-ratio: 1;
  display: grid; place-items: center;
  /* THE LEAN, WHICH IS WHAT MAKES IT A WHEEL AND NOT A PIE CHART.
     Thirty degrees about X, under a 760px perspective — a wheel you are
     standing in front of rather than a disc printed on the page. Rebuilding
     this section flattened it, and flat is the one thing it must not be:
     every physical prize wheel is seen at an angle, so a face-on circle
     reads as a diagram of a wheel.
     No roll about Z, deliberately. Measured before: rolled four degrees the
     pin swings eleven pixels off the top and reads as broken rather than as
     leaning. The depth is in the perspective and the X tilt; the roll only
     ever added a tell. */
  transform: perspective(760px) rotateX(30deg);
  transform-style: preserve-3d;
}
/* Less lean on a phone — the labels are already small, and thirty degrees of
   foreshortening on a 300px wheel closes the far wedges up. */
@media (max-width: 620px) {
  .av-stage { transform: perspective(620px) rotateX(18deg); }
}
@media (prefers-reduced-motion: reduce) {
  .av-face { transition: none; }
}

/* The burst, over everything, and never in the way of a click. */
.av-confetti { position: fixed; inset: 0; z-index: 9999; pointer-events: none; }

/* Every decorative layer is the stage, exactly. Declared together because the
   thing they have in common IS the point — nine layers, one box. */
.av-bloom, .av-shadow, .av-rim, .av-lip,
.av-face, .av-gloss, .av-bulbs {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
}

.av-bloom  { inset: -12%; background: radial-gradient(circle, rgba(239,143,32,.28), transparent 62%); filter: blur(18px); }
.av-shadow { transform: translateY(3%); box-shadow: 0 30px 60px -20px rgba(0,0,0,.75); }
.av-rim    { background: conic-gradient(#f6c86a, #b4700c 25%, #f6c86a 50%, #b4700c 75%, #f6c86a); }
.av-lip    { inset: 3.2%; background: #0b0d1c; }

/* The face carries the SVG and is what spins. The transition lives here so the
   behaviour only has to set a rotation. */
.av-face   { inset: 4.6%; overflow: hidden; }
.av-face > svg { display: block; width: 100%; height: 100%; }

.av-gloss  { inset: 4.6%;
  background: linear-gradient(150deg, rgba(255,255,255,.20), rgba(255,255,255,0) 46%); }

.av-hub {
  position: absolute; width: 17%; aspect-ratio: 1; z-index: 3;
  display: grid; place-items: center;
  border-radius: 50%; background: #12162a;
  box-shadow: 0 0 0 3px #b4700c, 0 6px 18px -6px rgba(0,0,0,.8);
}
.av-hub svg { width: 58%; height: 58%; }

/* THE PIN. Above the rim, pointing down into the wedge that wins. */
.av-pin {
  position: absolute; z-index: 4;
  top: -2.5%; left: 50%; transform: translateX(-50%);
  width: 8%; height: auto;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,.55));
}

.av-lbl { font-weight: 800; font-family: inherit; }

.av-go { min-width: 9rem; }
.av-out { min-height: 1.6rem; text-align: center; font-weight: 700; color: var(--g-title); }

@media (prefers-reduced-motion: reduce) {
  .av-face { transition: none !important; }
}

/* ═══════════════════════════════ THE CLASSES THE RENDERERS EMIT AND NOBODY STYLED
 *
 * Twenty class names across eight renderers had no rule anywhere. site.css was
 * written for the OLD landing page's vocabulary — `.s-card`, `.s-serv`,
 * `.s-month-card`, `.s-glass` — and the rebuild's renderers invented new names
 * without carrying the styles across. Every one of those sections rendered as
 * unstyled flow: correct text, correct order, no design.
 *
 * The look here is not new. It is the one already described in site.css and
 * gravity-ds.css, re-attached to the names the renderers actually use.
 */

/* ─────────────────────────────────────────────────────────────── the hero copy
   The panel is `.g-glass`; this only positions the type inside it and keeps the
   copy clear of the helmet, which is absolutely positioned over the far end. */
.s-hero-copy { position: relative; z-index: 1; max-width: min(46rem, 100%); }
.s-hero-copy h1 {
  font-size: clamp(2rem, 5.4vw, 3.6rem); font-weight: 800; line-height: 1.14;
  letter-spacing: -.02em; color: var(--g-title); text-wrap: balance;
}
.s-hero-en {
  font-size: clamp(.9rem, 1.6vw, 1.05rem); font-weight: 600; letter-spacing: .04em;
  color: var(--g-muted); margin-top: .35rem;
}
.s-hero-lede {
  font-size: clamp(.95rem, 1.7vw, 1.08rem); line-height: 1.9;
  color: var(--g-ink); margin-top: clamp(.8rem, 2vw, 1.2rem);
  max-width: 42rem;
}
/* THREE ACROSS, NOT A COLUMN. `.g-hero-figs` stacks them, which is right beside
   a headline in a two-column hero and wrong underneath one — they read as a
   list of three unrelated facts rather than as a row of proof. */
.s-hero-figs {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  text-align: start; margin-top: clamp(1rem, 2.5vw, 1.6rem);
  gap: clamp(.8rem, 2vw, 1.6rem);
}
@media (max-width: 560px) { .s-hero-figs { grid-template-columns: repeat(3, 1fr); gap: .6rem; } }

/* ───────────────────────────────────────────────────────────────── the services
   BACK TO CARDS. This was rendering as a bare <dl>: a bold name, a grey line,
   repeat — no card, no border, no grid. The look is `.s-card`'s, which site.css
   still defines and which nothing had pointed at since the rebuild. */
.ss-list {
  display: grid; gap: 1rem; margin: 0; padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
}
.ss-row {
  position: relative; padding: 1.6rem 1.5rem; margin: 0;
  border-radius: var(--g-r-card, 26px);
  background: var(--g-surface); border: 1px solid var(--g-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 18px 40px rgba(0, 0, 0, .38);
  transition: transform var(--g-t, 180ms) var(--g-ease, ease),
              border-color var(--g-t, 180ms) var(--g-ease, ease);
}
.ss-row:hover { transform: translateY(-4px); border-color: var(--g-line-up); }
.ss-name { display: block; font-size: 1.02rem; font-weight: 700; color: var(--g-title); margin-bottom: .45rem; }
.ss-desc { display: block; font-size: .88rem; line-height: 1.85; color: var(--g-muted); margin: 0; }
/* `.ss-proof` holds a PICTURE — the piece of work that proves the service —
   not a caption. It sits above the words, edge to edge inside the card, so
   the card reads as a sample with a label rather than a label with a
   thumbnail. Cards without one simply have no image and do not reserve
   space for the absence. */
.ss-proof { margin: -1.6rem -1.5rem .9rem; border-radius: var(--g-r-card, 26px) var(--g-r-card, 26px) 0 0;
            overflow: hidden; }
.ss-proof img, .ss-proof picture { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; }
/* ONE accent card, and it is the one nobody else in the city offers. The rule
   that there is exactly one gradient object on screen is the whole hierarchy. */
.ss-row.is-lead { background: var(--g-grad); border-color: transparent; }
.ss-row.is-lead .ss-name, .ss-row.is-lead .ss-desc, .ss-row.is-lead .ss-proof { color: #fff; }
.ss-row.is-lead .ss-desc { color: rgba(255, 255, 255, .88); }

.ss-said { font-size: .88rem; line-height: 1.9; color: var(--g-muted); }
.s-lede { font-size: clamp(.92rem, 1.6vw, 1.02rem); line-height: 1.9; color: var(--g-muted);
          max-width: 46rem; margin-bottom: clamp(1rem, 2.4vw, 1.6rem); }

/* ────────────────────────────────────────────────────────────────── the logos */
.sl-name { font-size: .74rem; color: var(--g-muted); text-align: center; }

/* ─────────────────────────────────────────────────────────────────── the reel */
.sv-play {
  position: absolute; inset: 0; margin: auto;
  width: 4.4rem; height: 4.4rem; border-radius: 50%;
  background: var(--g-grad); display: block;
  box-shadow: 0 12px 34px -10px rgba(211, 29, 83, .7);
}
.sv-play::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 0; height: 0; border-style: solid;
  border-width: .62rem 0 .62rem 1.02rem;
  border-color: transparent transparent transparent #fff;
}
/* NO RTL MIRROR. This carried `[dir="rtl"] .sv-play::after { transform:
   scaleX(-1) }`, which turned the arrow to point LEFT on the Arabic page — and
   the site opens in Arabic, so that was the only version anybody saw.
   Mirroring is right for anything that follows the reading direction: a back
   arrow, a next chevron, a progress bar. A PLAY triangle is not one of those.
   It points the way the tape runs, and the tape runs one way in every language
   — the same reason nobody mirrors ▶ on a remote control sold in Tripoli.
   Deleted rather than overridden, so it cannot be re-derived by the next
   person doing a pass for RTL correctness. */
.sv-vid { background: #000; }

/* ──────────────────────────────────────────────────────────── the card stack */
.sc-head { text-align: center; }

/* ─────────────────────────────────────────────────────────────────── free text */
.st-p { font-size: 1rem; line-height: 1.95; color: var(--g-ink); max-width: 46rem; }

/* TWO RULES DELETED HERE, AND THEY MUST NOT COME BACK UNDER THIS NAME.
 *
 * `.s-cta-copy` and `.s-cta-go { justify-self: start }` sat here, left behind
 * by a renderer that no longer exists — nothing in assets/js/sections/ emits
 * either class, text.js emits `.st` and `.st-p`. Dead, and harmless while the
 * closing band was called something else.
 *
 * Then the band was renamed `.s-cta` → `.s-close` to get it out from under
 * site.css's `.s-cta`, and these were swept along by the same rename. That put
 * a second `.s-close-go` in this file, LATER in the cascade than the real one,
 * and `justify-self: start` threw the closing button back into the corner it
 * had just been rescued from — the same bug, one name further on.
 *
 * The lesson is the rename's, not this block's: a class that no renderer emits
 * has no business being renamed, because the only thing it can still do is
 * collide. Deleted rather than renamed again. */

/* ══════════════════════════════════════ THE MONTH CARD, BACK ON THE PINK GLASS
 *
 * site.css has always carried this treatment as `body.space .s-month-card`: a
 * warm-to-magenta-to-violet wash at low alpha, a magenta edge, and the bar
 * dropped into its own dark well so the segment colours read against a neutral
 * instead of against the gradient they sit on. The rebuild renamed the element
 * to `.sp-card` and the rule was left addressing a class that no longer
 * appears, so the card came back as flat surface grey.
 *
 * Re-attached rather than re-invented — the values are the ones already in
 * site.css, so the two cannot drift into two different pinks.
 */
body.space .sp-card {
  background: linear-gradient(160deg, rgba(239, 143, 32, .16), rgba(211, 29, 83, .16) 52%, rgba(70, 45, 147, .22));
  border: 1px solid rgba(211, 29, 83, .38);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .04), 0 24px 70px rgba(211, 29, 83, .16);
}
/* The bar sits in its own well. Without it the ramp competes with the wash
   underneath and every segment reads as a slightly different pink. */
body.space .sp-card .g-seg {
  background: rgba(5, 6, 15, .55);
  padding: .35rem; border-radius: var(--g-r-pill, 999px);
  height: 42px;
}

.sp-panel .g-seg { margin-top: var(--g-s3, .8rem); }

/* The figures above the bar. A row, because they are one month split four
   ways rather than four separate facts. */
.sp-figs {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--g-s2, .5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(7rem, 45%), 1fr));
}
.sp-fig {
  display: grid; gap: .1rem; justify-items: center; text-align: center;
  padding: .7rem .5rem; border-radius: var(--g-r-el, 12px);
  background: rgba(5, 6, 15, .35); border: 1px solid rgba(255, 255, 255, .07);
}
.sp-fig b { font-size: clamp(1.3rem, 3.4vw, 1.9rem); font-weight: 800; color: #fff; line-height: 1.1; }
.sp-fig-l { font-size: .72rem; color: rgba(255, 255, 255, .68); }
.sp-why { font-size: .84rem; line-height: 1.8; color: var(--g-ink); margin: 0; }

/* ─────────────────────────────────────── the steps, numbered as the old page had
   A real <ol> whose printed number is aria-hidden, so a screen reader says
   "1 of 4" once rather than "one one". */
.sp-list { list-style: none; padding: 0; margin: clamp(1rem, 2.4vw, 1.5rem) 0 0;
           display: grid; gap: .9rem; }
.sp-step { display: grid; grid-template-columns: auto 1fr; gap: .8rem; align-items: baseline; }
.sp-n {
  width: 1.7rem; height: 1.7rem; display: grid; place-items: center; flex: none;
  border-radius: 50%; background: var(--g-sunk); border: 1px solid var(--g-line);
  font-size: .76rem; font-weight: 700; color: var(--g-muted);
}
.sp-t { font-size: .92rem; line-height: 1.8; color: var(--g-ink); margin: 0; }

/* TWO COLUMNS, the way the old month section read: the process on one side and
   what it costs on the other, so the plan and its price are one thought. */
.sp { display: grid; }
@media (min-width: 900px) {
  .sp-body {
    display: grid; gap: clamp(1.2rem, 3vw, 2rem);
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: start;
  }
  .sp-body > .sp-list { margin-top: 0; }
}

/* ══════════════════════════════════════════ THE CARD STACK, IN A PANEL
 *
 * It sat on the page ground with nothing around it — a square picture floating
 * in the middle of a dark column, which reads as an image that failed to load
 * its container rather than as a deliberate object. It belongs with the
 * services above it, in a box of the same family, so the pair reads as "here
 * is what we do, and here is what that looks like".
 *
 * THE PANEL DOES NOT CLIP. The leaves turn past its edge — that is the whole
 * gesture — so this sets a background and a border and deliberately leaves
 * `overflow` alone. See the note in sections-host.js about clipping being
 * opt-in; the section is mounted unclipped and this must not undo that.
 */
.sc {
  padding: clamp(1.2rem, 3.5vw, 2.4rem);
  border-radius: var(--g-r-card, 26px);
  background: var(--g-surface);
  border: 1px solid var(--g-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 18px 40px rgba(0, 0, 0, .38);
}

/* ═══════════════════════════════════════════ THE SERVICE PANES (.ss-pane)
 *
 * Six cards describing six services is the definition of telling. Each service
 * can now carry a LIVE DEMONSTRATION built from the site's own content — the
 * calendar, the cut, the pile, the typed copy, the figures, the wheel — and
 * this is what they look like inside a card.
 *
 * THREE OF THE SIX BORROW MARKUP THAT IS ALREADY STYLED. `stack` emits `.sc`
 * and `wheel` emits `.av-wrap`, so the pile and the wheel arrive fully dressed
 * and all that is needed here is a SIZE: both are sized off the viewport for
 * their own sections, which is right when they are the subject and far too
 * large inside a card. `film` emits `.sv-frame` when there is a video, which
 * is likewise already styled.
 *
 * THE CARD GRID GOT WIDER. `minmax(17rem, …)` was measured for a name and two
 * lines of text; a calendar of thirty cells inside 17rem is a smear.
 */
.ss-list { grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr)); }

.ss-row.has-pane { display: flex; flex-direction: column; }
.ss-pane {
  display: grid; place-items: center; justify-items: stretch;
  margin-bottom: clamp(.9rem, 2.2vw, 1.2rem);
  min-width: 0;
}
/* The proof image and a live pane are two answers to the same question. A card
   carrying both puts the still first and the demonstration under it, which
   reads as a caption above a stage; the demonstration wins the top. */
.ss-row.has-pane .ss-proof { order: 2; margin: .9rem -1.5rem -1.6rem;
  border-radius: 0 0 var(--g-r-card, 26px) var(--g-r-card, 26px); }
.ss-row.has-pane .ss-said { order: 1; }

/* ─────────────────────────────────────────── month: the content calendar */
.ssp-month { width: 100%; }
.ssp-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: .28rem; margin-bottom: .35rem; }
.ssp-days b { text-align: center; font-size: .64rem; font-weight: 600; color: var(--g-muted); }
/* ─────────────────────────────────────── the calendar: ONE WEEK, walking
 *
 * EVERY WEEK OCCUPIES THE SAME GRID CELL. `grid-area: 1 / 1` rather than
 * `position: absolute`, deliberately: absolute children give the container
 * nothing to take a height from, and it would need a hard-coded height that
 * stops being right the moment the tile size changes. Stacked grid items keep
 * the container sized by its tallest child — one row of seven squares — with
 * no number written anywhere.
 *
 * `visibility: inherit` on the visible one, NOT `visible`. Visibility is
 * inherited, so `visible` on a descendant climbs straight back out of a hidden
 * ancestor — which is precisely the bug that made the film frame paint over
 * the calendar from a pane that was switched off. `inherit` follows the pane:
 * shown when the pane is shown, gone when it is not.
 */
/* THE ROW IS NOT BOUND BY THE GLASS — "i dont think we should use glass as
   borders, some could go beyond". The panel is a surface the work sits ON, not
   a box it is kept IN, and a week of designs that stops politely at the panel
   edge is the same undersized grid with rounder corners. So the row is allowed
   to be wider than its pane and to run out past both edges.
   Nothing clips it: `.s-pane` is already `overflow: visible` (site.css:1389,
   where it was opened for exactly this kind of thing) and `.s-stage` never sets
   overflow at all. Stated here so the next person to add `overflow: hidden` to
   either one knows what it costs. */
/* THE ROW CROSSES THE GLASS, IT DOES NOT STOP AT IT — "make it cross the glass
   by min 10 to 15 px max. not just before it". Landing flush on the edge reads
   as a row that ran out of room; crossing it says the work is on the panel
   rather than in it.

   ONE OWNER FOR THE PADDING. site.css sets `.s-stage { padding: clamp(1.4rem,
   3vw, 2.6rem) }` and the bleed has to be exactly that plus the crossing, so
   the two must agree at every width. Copying the clamp here would be two
   numbers to keep in step — the failure this whole file is full of notes about
   — so the variable is declared here and the padding is set FROM it. Identical
   value, so nothing moves; sections.css loads after site.css and wins. Change
   the stage's padding here and the bleed follows on its own. */
.s-stage { --s-glass-pad: clamp(1.4rem, 3vw, 2.6rem); padding: var(--s-glass-pad); }
.s-stage { --s-cross: 24px; }     /* «cross some more» — was 12 */

.ssp-weeks {
  display: grid; overflow: visible; padding-bottom: 1.5rem;
  /* OUTWARD ONLY. A symmetric bleed crosses the glass on the outer side and
     pushes the same distance the OTHER way, into the rail — measured in LTR,
     the row starting 24px inside the rail's edge and covering its labels. The
     rail is at inline-START of the stage in both directions (right in Arabic,
     left in English) and the pane fills the rest, so the edge to cross is
     inline-END and the other side must not move at all. */
  margin-inline-start: 0;
  margin-inline-end: calc(-1 * (var(--s-glass-pad) + var(--s-cross)));
}
.ssp-wk {
  grid-area: 1 / 1;
  display: flex; align-items: center; justify-content: center;
  gap: clamp(.4rem, 1.4vw, .9rem);
  visibility: hidden; opacity: 0;
  transition: opacity 380ms var(--g-ease), visibility 0s linear 380ms;
}
.ssp-wk.on { visibility: inherit; opacity: 1; transition-delay: 0s; }

/* SIZED OFF THE STAGE'S HEIGHT, NOT A SEVENTH OF ITS WIDTH. Seven equal
   columns in a 710px pane is a 93px tile no matter how the rest is styled —
   measured at 83. Height is the dimension the stage actually has to spare, so
   the square is measured against that and the row takes whatever width that
   implies, which is the width that runs past the glass. */
/* THE TILES FILL THE ROW RATHER THAN BEING A FIXED SIZE INSIDE IT. A fixed
   basis plus `justify-content: center` leaves the content narrower than its
   box, so widening the box to cross the glass just added empty space either
   side and the tiles stayed exactly where they were. Growing from a zero basis
   makes the row's content edge BE the box's edge, which is the thing being
   positioned. The cap stops a short frame — the last one holds two, not four —
   from inflating them to fill the same span. */
.ssp-wk i.on {
  flex: 1 1 0; max-width: 17rem;
  position: relative; display: block; aspect-ratio: 1;
  border-radius: var(--g-r-card, 18px); overflow: hidden;
  background: var(--g-sunk);
  border: 1px solid color-mix(in srgb, var(--c) 55%, transparent);
  box-shadow: 0 16px 40px -18px rgba(0, 0, 0, .75);
  transition: transform 260ms var(--g-ease), box-shadow 260ms var(--g-ease);
}
/* A DAY WITH NOTHING ON IT IS A REST, NOT A HOLE. Drawing it at tile size was
   what made the old grid read as a third empty; drawn as a thin mark it still
   says «nothing shipped that day», which is true and worth saying, without
   spending a whole square to say it. */
.ssp-wk i.pad,
.ssp-wk i.off,
.ssp-wk i.empty {
  flex: 0 0 clamp(.45rem, 1.1vw, .7rem);
  position: relative; display: block;
  height: clamp(2rem, 5vw, 3.4rem);
  border-radius: 999px;
  background: none; border: 1px dashed var(--g-line);
}
.ssp-wk i.pad { border-color: transparent; }
/* `picture` is a wrapper with no rendering of its own — the <img> INSIDE it is
   what paints, and sizing only the wrapper is what left the card stack showing
   a 288px poster in a 719px card. Same rule, stated once for both. */
.ssp-wk img, .ssp-wk picture,
.ssp-wk picture > img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* THE CLIENT, ON HOVER. Every tile is real work for a real client and at the
   old 90px that was unreadable, so pointing at one now says whose it is. The
   lift needs somewhere to go: `overflow` on the row would shear it, so the
   raised tile takes a z-index and the row is left unclipped. */
.ssp-wk i b {
  position: absolute; inset-inline: 0; bottom: 0; z-index: 2;
  padding: .45rem .5rem;
  font-size: .72rem; font-weight: 600; line-height: 1.25;
  color: #fff; text-align: center;
  background: linear-gradient(to top, rgba(0, 0, 0, .82), transparent);
  opacity: 0; transition: opacity 220ms var(--g-ease);
}
/* The day letter. On a tile it sits under the picture; on a rest mark it is
   the only thing there, so it hangs below the sliver instead. */
/* THE DAY, ON THE TILE — "add the calendar view thing".
   Four big pictures in a row is a carousel; what makes it a CALENDAR is that
   each one is pinned to a day. Hung under the tile it was a 10px grey mark
   nobody reads, so it sits ON the design as a chip: always visible, legible at
   a glance, and it travels with the tile through every frame. */
.ssp-wk i u {
  position: absolute; inset-inline-start: .45rem; top: .45rem; z-index: 2;
  text-decoration: none; text-align: center;
  min-width: 1.35rem; padding: .1rem .3rem;
  border-radius: 999px;
  font-size: .68rem; font-weight: 600; line-height: 1.45;
  color: #fff; background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(4px);
}
/* A rest mark has no picture to sit on, so its letter hangs beneath it. */
.ssp-wk i.off u, .ssp-wk i.empty u, .ssp-wk i.pad u {
  inset-inline: 0; inset-inline-start: auto; top: auto; bottom: -1.2rem;
  background: none; backdrop-filter: none; color: var(--g-muted); font-weight: 400;
}
@media (hover: hover) {
  .ssp-wk i.on:hover { transform: scale(1.28); z-index: 3;
                       box-shadow: 0 18px 40px -14px rgba(0, 0, 0, .8); }
  .ssp-wk i.on:hover b { opacity: 1; }
}

/* Which week you are on. Not a control — the walk is automatic — so it carries
   no pointer affordance and is hidden from the accessibility tree upstream. */
.ssp-dots { display: flex; justify-content: center; gap: .34rem; margin-top: .7rem; }
.ssp-dots i {
  width: .34rem; height: .34rem; border-radius: 50%;
  background: var(--g-line-up); transition: background 260ms var(--g-ease),
                                            transform 260ms var(--g-ease);
}
.ssp-dots i.on { background: var(--g-grad-flat); transform: scale(1.35); }

/* THE POSTS LAND, one after another, which is the whole point — you watch a
   week get made rather than arriving after it was. Gated on no-preference so
   somebody who asked for stillness gets the finished week immediately. */
@media (prefers-reduced-motion: no-preference) {
  .ssp-wk.on i { animation: ssp-land 460ms var(--g-ease) backwards;
                 animation-delay: calc(var(--d, 0) * 70ms); }
}
@keyframes ssp-land {
  from { opacity: 0; transform: translateY(-10px) scale(.86); }
  to   { opacity: 1; transform: none; }
}
.ssp-cap { margin: .7rem 0 0; text-align: center; font-size: .74rem; color: var(--g-muted); }

/* ──────────────────────────────────────────────────── film: one cut, held */
.ssp-cut { width: 100%; }
.ssp-cut-stage {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: var(--g-r-el, 14px); overflow: hidden; background: var(--g-sunk);
}
.ssp-cut-stage figure {
  position: absolute; inset: 0; margin: 0;
  opacity: 0; visibility: hidden;
  transition: opacity 420ms var(--g-ease), visibility 0s linear 420ms;
}
.ssp-cut-stage figure.on { opacity: 1; visibility: visible; transition-delay: 0s; }

/* ─── THE TAB THAT WOULD NOT SWITCH, and it was this line above.
 *
 * `visibility` is INHERITED, which means hiding something by setting
 * `visibility: hidden` on its ancestor is a hiding any descendant can undo for
 * itself. The rail's off panes are hidden exactly that way — `.s-pane` is
 * `position: absolute; inset: 0` and the one that is off goes `visibility:
 * hidden` — so `figure.on` above, sitting inside the *film* pane, climbed
 * straight back out of it and kept painting: measured on the live site, a
 * 714 × 401 frame from «Video production» still visible while «Social
 * management» was the selected tab, and stacked later in the document, so it
 * covered the calendar completely. Clicking the first tab looked like a tab
 * that does nothing. It was also still hit-testable, so it ate the clicks.
 *
 * `inherit` and NOT `hidden`: the pane's own `visibility` is transitioned
 * `0s linear 420ms` so that it stays visible for the length of the cross-fade.
 * Following the pane rather than overriding it keeps that fade; a flat
 * `hidden` here would snap the outgoing frame away on the first frame.
 *
 * The general rule this is an instance of: anything inside a pane that sets
 * its own `visibility: visible` has to be scoped to a pane that is on. */
.s-pane:not(.on) .ssp-cut-stage figure.on { visibility: inherit; }

.ssp-cut-stage img, .ssp-cut-stage picture { display: block; width: 100%; height: 100%; object-fit: cover; }
.ssp-who {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  padding: 1.4rem .7rem .5rem; font-size: .7rem; font-weight: 700; color: #fff;
  background: linear-gradient(transparent, rgba(5, 7, 15, .8));
}
.ssp-cut-bar { display: flex; gap: .25rem; margin-top: .5rem; }
.ssp-cut-bar i { flex: 1; height: 2px; border-radius: 2px; background: var(--g-line); }
.ssp-cut-bar i.on { background: var(--g-grad-flat); }
/* A gallery video plays in the showreel's own frame, at card width. */
.ssp-play { max-width: 100%; }

/* ─────────────────────────────── stack: the card pile, at card size */
.ssp-stack {
  --sc-size: min(100%, 15rem);
  padding: 0; background: none; border: 0; box-shadow: none;
}
.ssp-stack .sc-hint { margin-top: .7rem; }

/* ──────────────────────────────────── copy: written at reading speed */
.ssp-copy { width: 100%; }
.ssp-who-row { display: flex; align-items: center; gap: .55rem; margin-bottom: .7rem; }
.ssp-logo { display: block; width: 2rem; height: 2rem; flex: none; }
.ssp-logo img, .ssp-logo picture { display: block; width: 100%; height: 100%; object-fit: contain; }
.ssp-who-row b { font-size: .88rem; color: var(--g-title); }
.ssp-who-row span { font-size: .7rem; color: var(--g-muted); }
.ssp-type {
  margin: 0; min-height: 8.5rem; font-size: .84rem; line-height: 1.95; color: var(--g-ink);
  border-inline-start: 3px solid var(--c, var(--g-pink)); padding-inline-start: .8rem;
}
/* The caret only exists while it is writing. */
.ssp-type.go::after {
  content: ""; display: inline-block; width: .5em; height: 1em;
  vertical-align: -.14em; margin-inline-start: .1em;
  background: var(--c, var(--g-pink)); animation: ssp-caret .9s steps(1) infinite;
}
@keyframes ssp-caret { 50% { opacity: 0; } }
.ssp-alts { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .8rem; }
.ssp-alt {
  padding: .3rem .65rem; border-radius: var(--g-r-pill, 999px); cursor: pointer;
  font: inherit; font-size: .72rem; background: var(--g-sunk);
  border: 1px solid var(--g-line); color: var(--g-muted);
  transition: color var(--g-t) var(--g-ease), border-color var(--g-t) var(--g-ease);
}
.ssp-alt:hover { color: var(--g-title); border-color: var(--g-line-up); }
.ssp-alt:focus-visible { outline: 2px solid var(--g-pink); outline-offset: 2px; }

/* ───────────────────────────────────────── figs: one leads, the rest support */
.ssp-figs { width: 100%; text-align: center; }
.ssp-figs b {
  display: block; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums;
  background: var(--g-grad-flat); -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.ssp-figs span { display: block; margin-top: .35rem; font-size: .72rem; color: var(--g-muted); }
.ssp-lead b { font-size: clamp(2.4rem, 9vw, 3.4rem); }
.ssp-lead span { font-size: .8rem; }
.ssp-figs-rest {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .8rem; margin-top: 1.2rem;
}
.ssp-figs-rest b { font-size: clamp(1.1rem, 4vw, 1.5rem); }

/* ─────────────────────────────────── the wheel, at card size, and awake
 *
 * THE FACE HAD NO TRANSITION. The comment above `.av-face` says the transition
 * lives there so the behaviour only has to set a rotation — and no such
 * declaration was ever written, so the wheel would have jumped straight to its
 * result with no spin at all. Declared now, where the comment already
 * promised it.
 *
 * AND THE BULBS HAD NO RULE. The behaviour places twenty `<i>` around the rim
 * by transform; without a size, a colour and a radius they were twenty
 * zero-height inline elements and the rim was dark. */
.av-face { transition: transform 4.2s cubic-bezier(.13, .78, .24, 1); }
.av-bulbs i {
  position: absolute; inset-inline-start: calc(50% - 4px); top: calc(50% - 4px);
  width: 8px; height: 8px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #fff8e2, #ffc44d 55%, #b4700c);
  box-shadow: 0 0 6px rgba(255, 196, 77, .85);
  animation: ssp-bulb 1.6s ease-in-out infinite;
  animation-delay: calc(var(--stp-i, 0) * 80ms);
}
@keyframes ssp-bulb { 50% { opacity: .35; } }
.av-out { opacity: 0; transition: opacity var(--g-t-slow, 320ms) var(--g-ease); }
.av-out.show { opacity: 1; }
.av-out b {
  display: block; font-size: 1.15rem;
  background: var(--g-grad-flat); -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.ss-pane .av-stage { width: min(15rem, 100%); }
.ss-pane .av-out { font-size: .74rem; font-weight: 400; color: var(--g-muted); }
.ss-pane .av-out b { font-size: 1rem; }
.ss-pane .av-go { min-width: 7rem; }

/* ═══════════════════════════════ THE SAME PANES, ON THE RAIL'S STAGE (.s-pane)
 *
 * TWO HOSTS, THREE SIZES. These demonstrations were written for their own
 * sections, where each is the subject and sizes itself off the viewport; the
 * block above shrinks them to fit inside a service CARD (`.ss-pane`). The rail
 * is the third host and nothing addressed it, so each pane fell back to
 * whichever of the other two it happened to match — which is why the stage
 * came out mostly empty.
 *
 * THE PILE WAS 68 PIXELS WIDE. `.ssp-stack` asks for `--sc-size: min(100%,
 * 15rem)`, and site.css gives `.s-pane` `justify-items: center`, so the pile is
 * a shrink-to-fit grid item — and a percentage measured against a parent that
 * is measuring itself against the percentage resolves to nothing. Measured on
 * the live site: a 68 × 68 stage in a 714 × 512 pane, which is the whole of
 * «all small». Giving it a width first breaks the circle; the percentage then
 * has a real number to be a percentage of, and `.sc-stage`'s `margin-inline:
 * auto` re-centres it.
 *
 * STATED FOR EVERY CHILD, NOT JUST THE PILE. Four of the six panes already say
 * `width: 100%` for themselves, and the two that do not — the pile and the
 * wheel — are the two that collapsed. That is not a coincidence to fix twice:
 * a pane's content fills its pane, so the pane says so once, here, and the
 * next demonstration added to the rail inherits it instead of rediscovering
 * this bug. Each pane centres its own contents from the inside.
 *
 * The two SIZES that follow from this are set once, at the foot of this file,
 * against the stage's own height — see «THE STAGE IS THE SUBJECT».
 */
/* ═══════════════════════════ THE STAGE IS DARK, SO ITS TEXT TOKENS ARE DARK'S
 *
 * "text not visible" — and it was, for a reason worth writing down.
 *
 * site.css commits this stage to white text:
 *
 *     .s-stage, .s-stage .sv-cap, .s-stage .sv-w-out,
 *     .s-stage .sv-days b, .s-stage .sv-who span,
 *     .s-stage .sv-alts button { color: #fff; }
 *
 * Every selector after the first names a class the rebuild RENAMED. The panes
 * went from `sv-` to `ssp-` and site.css was never touched: measured on the
 * deployed file, 181 `.sv-` selectors and 0 `.ssp-`. So `.s-stage` itself still
 * gets `color: #fff` — which is why the rail is bright and looks fine — while
 * every pane element that sets its OWN colour from a theme token fell out of
 * the white and back onto the palette:
 *
 *     .ssp-type      --g-ink    #2a2f52  the paragraph, unreadable
 *     .ssp-who-row b --g-title  #11142e  the client's NAME, gone entirely
 *     .ssp-cap …     --g-muted  #6b719c  the caption, barely there
 *
 * THE FIX IS NOT SIX MORE SELECTORS. Re-pointing that list at `ssp-` names
 * would work today and break again on the next rename — it already broke once
 * exactly this way, which is the evidence that a maintained list of class names
 * is the wrong mechanism. So the TOKENS are re-declared for this subtree
 * instead. Anything inside the stage that asks for ink, title or muted gets the
 * value that is right on a dark ground, whatever the reader's theme says and
 * whatever the element is called. A pane added next year inherits it without
 * knowing this note exists.
 *
 * Scoped to `.s-stage` and nothing wider: the section's own heading sits
 * OUTSIDE the glass, on the page's ground, and must keep following the theme. */
.s-stage {
  --g-ink: #d9deff;
  --g-title: #ffffff;
  --g-muted: rgba(255, 255, 255, .72);
}

/* ═════════════════════════════════════════ THE SECTIONS RISE AS YOU REACH THEM
 *
 * "we had scroll motion, it fully disappeared and i want it".
 *
 * ITS OWN CLASS, AND NOT style.css's `.reveal`, which is the same effect and
 * would have been the obvious reuse. `.reveal` is driven by an
 * IntersectionObserver in main.js that is shared with five other pages, and
 * sharing it made the behaviour impossible to pin down: marked through
 * `.reveal`, every section reported as risen the moment the page loaded, while
 * the identical code re-mounted by hand against the same document marked six
 * and revealed exactly the one in view. An observer's callback is the browser's
 * to schedule and its answers depend on whether the tab is compositing at all.
 *
 * `.s-rise` is driven only by the geometry sweep in sections-behaviours.js —
 * positions read directly, on scroll — so what it does is decidable by reading
 * it, and it cannot be quietly changed by an observer belonging to another
 * page. The visual result is `.reveal`'s, deliberately: this is the motion the
 * page already had, restored, not a new one.
 *
 * `visibility: hidden`, NOT opacity alone. A block waiting to rise is still in
 * the document, and a transparent one is still tabbable and still read out —
 * so a keyboard user tabs into text nobody can see, and a click on apparently
 * empty space lands on it. That was learned here once already.
 *
 * NOTHING EVER HIDES AGAIN, so `visibility` is left out of the transition: it
 * flips to visible the moment `.in` lands and the fade plays over it. */
.s-rise { opacity: 0; visibility: hidden; transform: translateY(34px); }
.s-rise.in {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .8s cubic-bezier(.2, .65, .3, 1),
              transform .8s cubic-bezier(.2, .65, .3, 1);
}
/* Somebody who asked not to be moved is asking for less animation, not less
   page — and on paper nothing ever enters a viewport, so a printed copy of
   this site would otherwise be a header and six blanks. */
@media (prefers-reduced-motion: reduce) {
  .s-rise { opacity: 1; visibility: visible; transform: none; }
}
@media print {
  .s-rise { opacity: 1 !important; visibility: visible !important; transform: none !important; }
}

.s-pane > * { width: 100%; }

/* ─────────────────────────────────────────────────── arrival, and stillness
 *
 * Each pane plays itself in — the month fills day by day, the figures rise —
 * because a demonstration that is simply PRESENT when you arrive is a picture.
 * Gated on `no-preference` rather than switched off in a reduce block, so the
 * default for somebody who has asked for stillness is that it never starts. */
@media (prefers-reduced-motion: no-preference) {
  .ssp-grid i { animation: ssp-pop 420ms var(--g-ease) backwards;
                animation-delay: calc(var(--d, 0) * 26ms); }
  .ssp-figs > div, .ssp-figs-rest > div {
    animation: ssp-rise 480ms var(--g-ease) backwards;
    animation-delay: calc(var(--d, 0) * 90ms);
  }
}
@keyframes ssp-pop  { from { opacity: 0; transform: scale(.82); } to { opacity: 1; transform: none; } }
@keyframes ssp-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* NOTHING MOVES, AND NOTHING IS MISSING. The cut is the one that needs care:
   with no timer running, a stage of stacked frames would show one and hide
   five, so it lays out as a strip instead — no movement, every frame still
   visible, which is the information the cut was carrying. */
@media (prefers-reduced-motion: reduce) {
  .av-face { transition: none; }
  .av-bulbs i { animation: none; }
  .ssp-type.go::after { animation: none; }
  .ssp-cut-stage {
    aspect-ratio: auto; display: grid; gap: .25rem;
    grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
  }
  .ssp-cut-stage figure { position: static; opacity: 1; visibility: visible; transition: none; }
  .ssp-cut-stage img, .ssp-cut-stage picture { aspect-ratio: 16 / 9; height: auto; }
  .ssp-cut-bar { display: none; }
}

/* ─────────────────────────────── a pane on the accent card, still legible
 *
 * The lead card is the one gradient object on the page and the hierarchy rests
 * on there being exactly one. A pane inside it is drawn from the same surface
 * tokens as every other card — `--g-sunk` tiles, `--g-line` hairlines, muted
 * grey captions — all of which are chosen against the deep ground and none of
 * which read on magenta-to-orange. So the pane's surfaces are re-tinted from
 * the card's own white rather than the card being stood down.
 */
.ss-row.is-lead .ssp-grid i,
.ss-row.is-lead .ssp-cut-stage,
.ss-row.is-lead .ssp-alt {
  background: rgba(255, 255, 255, .16); border-color: rgba(255, 255, 255, .26);
}
.ss-row.is-lead .ssp-grid i.pad,
.ss-row.is-lead .ssp-grid i.off { background: none; }
.ss-row.is-lead .ssp-days b,
.ss-row.is-lead .ssp-cap,
.ss-row.is-lead .ssp-figs span,
.ss-row.is-lead .ssp-who-row span,
.ss-row.is-lead .ssp-alt,
.ss-row.is-lead .sc-hint { color: rgba(255, 255, 255, .82); }
.ss-row.is-lead .ssp-who-row b { color: #fff; }
.ss-row.is-lead .ssp-type { color: #fff; border-inline-start-color: #fff; }
.ss-row.is-lead .ssp-alt:hover { color: #fff; border-color: #fff; }
/* The figures are painted by clipping the brand gradient to the glyphs, which
   on the brand gradient is invisible. Flat white instead — and the fill has to
   be cleared explicitly, since `-webkit-text-fill-color` wins over `color`. */
.ss-row.is-lead .ssp-figs b {
  background: none; color: #fff; -webkit-text-fill-color: #fff;
}
.ss-row.is-lead .ssp-cut-bar i { background: rgba(255, 255, 255, .34); }
.ss-row.is-lead .ssp-cut-bar i.on { background: #fff; }

/* ═══════════════════════════════ THE STAGE IS THE SUBJECT, SO GIVE IT THE ROOM
 *
 * "too fucking small" — and it was, twice over, for two different reasons.
 *
 * First the pile collapsed to 68px (fixed above). Then it was capped at 22rem
 * because `.s-show`'s floor is 24rem, which is the right cap for a 24rem stage
 * and the wrong one for the stage this actually is. On a desktop the rail takes
 * ~17rem and the stage takes everything left — around 45rem — while the panel
 * inside it was told to stay under 22. Less than half of it was being used.
 *
 * `.s-show` is declared FIVE TIMES in site.css (997, 1130, 1150, 1343, 1376),
 * the last one winning at clamp(24rem, 44vw, 32rem). That is what a stylesheet
 * looks like after four redesigns, and it is why raising the floor there would
 * be guesswork — the value here is stated once, last, and says why.
 *
 * THE CAP IS THE HEIGHT, because the stage is wider than it is tall and the
 * demonstrations are square. Height is what runs out, so height is what the
 * square has to be measured against.
 *
 * `height: min(var(--sc-size), 100%)` was the first attempt at that and it
 * does not work — measured, 719 × 719 spilling 113px out of a 640px pane. A
 * percentage height only resolves against a parent with a DEFINITE height, and
 * `.ssp-stack` is a plain block whose height is its content's. So the `100%`
 * came back indefinite, the whole `min()` fell to `auto`, and `width: auto`
 * plus `aspect-ratio: 1` did what those two always do with nothing to push
 * back: filled the width and made the height match it.
 *
 * SO THE HEIGHT IS NAMED INSTEAD OF MEASURED. `--s-stage-h` holds the same
 * expression `min-height` is set from — declared here, on `.s-show`, once, and
 * inherited down — so a square can be capped by the stage's height while still
 * being sized through `width`, which needs no definite parent. The stack and
 * the wheel then each subtract what sits UNDER them: a line of hint for the
 * pile, a result line and a button for the wheel.
 */
/* AND THE STAGE FOLLOWS WHICHEVER PANE IS SHOWING — «too much open spaces».
 *
 * One floor sized for the TALLEST pane is a floor the short ones rattle around
 * in. Measured at 1920, in a 710 × 640 stage:
 *
 *     calendar 561   the cut 441   the pile 546   the wheel 532
 *     copy     223  ← 417px of nothing
 *     figures  151  ← 489px of nothing, and it is the paid-ads tab
 *
 * Three quarters of the panel empty under six words and four numbers. The
 * screenshot that came back was that pane.
 *
 * `--s-fit` is written by armPane() in sections-behaviours.js: the height the
 * pane that is coming ON actually needs. It falls back to the old fixed floor,
 * so with no JavaScript the stage is exactly what it was rather than collapsed.
 *
 * IT IS A SECOND VARIABLE ON PURPOSE, and this is the part worth not undoing.
 * `--s-stage-h` is what the pile and the wheel measure THEMSELVES against, so
 * driving it from the measured content would be a loop: the pile sized from the
 * stage, the stage sized from the pile, settling somewhere neither asked for or
 * oscillating. `--s-stage-h` stays a static clamp and remains the CAP; `--s-fit`
 * only ever sizes the container. Nothing reads both.
 *
 * `min-height`, not `height`, so a pane that turns out taller than measured —
 * a longer headline wrapping to three lines — still grows rather than clipping. */
.s-show {
  --s-stage-h: clamp(26rem, 52vw, 40rem);
  /* BOTH BOUNDS LIVE HERE, not in the script that writes `--s-fit`. The script
     reports one number — what the pane measured — and this decides what to do
     with it. `--s-stage-h` is the cap, so the stage can never ask for more room
     than the pile and the wheel were told they could have; 15rem is the floor,
     because a stage that shrinks to four numbers reads as a section that failed
     to load rather than one being economical. The fallback is the cap, so with
     no JavaScript the stage is exactly the fixed floor it always was. */
  min-height: clamp(15rem, var(--s-fit, var(--s-stage-h)), var(--s-stage-h));
  transition: min-height 420ms var(--g-ease);
}
/* A stage that resizes under somebody who asked for stillness is the movement
   they asked not to have. The height still changes; it just arrives. */
@media (prefers-reduced-motion: reduce) { .s-show { transition: none; } }

/* The pile: a square plus its hint. */
.s-pane > .ssp-stack { --sc-size: min(100%, 32rem, calc(var(--s-stage-h) - 2.5rem)); }

/* ─────────────────────────────────────── THE FIGURES, AT THE SIZE THEY CLAIM
 *
 * "numbers should get bigger.. al of them", then "numbers still not bigger".
 *
 * They were sized for the version of this pane that lives inside a service
 * CARD — 54px for the lead and 24px for the other three — and then reused
 * unchanged on the rail's stage, which is three times the area. Four small
 * numbers floating in the top third of a 435px panel: measured, this pane used
 * 175px of content and left 260px empty, the worst of the six.
 *
 * Scoped to `.s-pane` and not to `.ssp-figs`, because the card-sized version is
 * still correct where it is and both hosts read the same class. Same reason the
 * pile and the wheel are sized here rather than at their own declarations.
 *
 * The claim is «الأرقام، لا الوعود». A number that has to be looked for is not
 * making that claim. */
/* ─── THE DEGREE GOES ROUND THE 360.
 *
 * The claim is «360°» — all the way round — so the one glyph that can act it
 * out does. It travels a circle centred on the number rather than spinning in
 * place: `rotate(θ) translate(R)` walks it round the rim, and the `rotate(-θ)`
 * after it cancels the carry so the symbol itself stays upright the whole way.
 * A ° that tumbles as it goes reads as a loading spinner.
 *
 * THE RADIUS IS IN `em`, so it is measured against the number it orbits and
 * follows it through every step of the clamp above — a pixel radius would be
 * correct at exactly one viewport width and wrong either side of it.
 *
 * Slow on purpose. This sits behind a tab somebody may leave open; twelve
 * seconds a lap is movement you notice once and then stop seeing, which is the
 * difference between a detail and a distraction. */
.s-pane .ssp-lead { position: relative; }
.s-pane .ssp-lead .ssp-deg {
  position: absolute; top: 2.2rem; left: 50%;
  font-size: clamp(2.2rem, 5.5vw, 4rem); font-weight: 800; font-style: normal;
  line-height: 1; color: var(--g-orange, #ff8a3d);
  background: var(--g-grad-flat); -webkit-background-clip: text;
  background-clip: text; color: transparent;
  animation: ssp-deg-orbit 12s linear infinite;
  pointer-events: none;
}
@keyframes ssp-deg-orbit {
  from { transform: rotate(0deg)   translateX(3.1rem) rotate(0deg); }
  to   { transform: rotate(360deg) translateX(3.1rem) rotate(-360deg); }
}
/* Somebody who asked for stillness gets the symbol where it has always been:
   beside the number, top-right, not travelling. */
@media (prefers-reduced-motion: reduce) {
  .s-pane .ssp-lead .ssp-deg { animation: none; transform: translateX(2.6rem); }
}

.s-pane .ssp-lead b       { font-size: clamp(3.6rem, 9vw, 6.5rem); }
.s-pane .ssp-lead span    { font-size: clamp(.85rem, 1.5vw, 1.02rem); margin-top: .5rem; }
.s-pane .ssp-figs-rest    { gap: clamp(1rem, 3vw, 2rem);
                            margin-top: clamp(1.8rem, 4vw, 3rem); }
.s-pane .ssp-figs-rest b  { font-size: clamp(1.9rem, 4.4vw, 3.2rem); }
.s-pane .ssp-figs-rest span { font-size: clamp(.72rem, 1.2vw, .88rem); margin-top: .45rem; }

/* The wheel arrives at its own section's size and is a touch large for a stage
   it now shares with a rail — "the wheel is a tad too big". Targeted at
   `.av-stage` rather than at a wrapper class, because `.av-stage` is what
   activation.js actually emits inside both of its hosts. */
.s-pane .av-stage { width: min(100%, 26rem, calc(var(--s-stage-h) - 9rem)); }

@media (max-width: 900px) {
  .s-show { --s-stage-h: clamp(22rem, 78vw, 30rem); }
}

/* ═════════════════════════════════════════ ON THE MARGIN — the other numbers
 *
 * ONE AT A TIME, AND BOTH HALVES BIG.
 *
 * This was six small cells across a strip, and it was wrong twice. The pictures
 * are studio shots of things coming apart in mid-air — a cup shedding its lid,
 * a keyboard losing its keycaps — and at icon size none of that survives; it is
 * six smudges. And six numbers in a row is a table, which is the one shape a
 * joke must not take.
 *
 * So the panel holds one pairing at a time: the picture at the size it was made
 * for, the number at display size beside it. It changes on its own, and it is
 * the last thing before the closing call, so the reader leaves on a human note
 * rather than on another claim.
 */
.st-head { font-size: clamp(1.2rem, 3vw, 1.7rem); }

.st-stage {
  position: relative;
  margin-top: clamp(1rem, 2.5vw, 1.6rem);
  border-radius: var(--g-r-card, 26px);
  /* WHITE, AND IT IS THE CUT-OUTS THAT DECIDE IT.
     Every one of these objects was rendered on a white seamless ground and
     lifted off it, and no cut-out is perfect — a pale fringe survives at
     the edge of a soft shadow or a blurred bean. On a dark panel that
     fringe is a halo around every piece. On white it is the background it
     came from, and it disappears completely.
     It also earns its place as a deliberate light band on a page that is
     otherwise deep space: the aside looks like an aside. */
  background: #f4f4f7;
  border: 1px solid rgba(0, 0, 0, .06);
  padding: clamp(1.2rem, 3.5vw, 2.4rem);
  /* THE STAGE HOLDS ITS OWN HEIGHT. Slides are stacked absolutely so they can
     cross-fade, which takes them out of flow — without a floor here the panel
     would collapse to nothing and the page would jump every time it changed. */
  /* Grows with the frame — the picture is the tall element and a stage that
     did not follow it would clip the pieces the moment they overshoot. */
  min-height: clamp(21rem, 42vw, 32rem);
  display: grid;
  /* THE QUERY CONTAINER IS THE PANEL, NOT THE COLUMN, and it has to be:
     the figure is sized from cqw, and the column it sits in is now sized
     from the figure. A container cannot be measured against its own
     contents, so the measurement is taken from the one box either side
     agrees on — this one, whose width the section gives it. */
  container-type: inline-size;
  /* Declared here because THREE rules have to agree about them: the frame
     takes the width, the slide takes the gap, and the number subtracts both
     to find out how much room is left for it. Three literals would drift. */
  --st-art: clamp(19rem, 38vw, 32rem);
  --st-gap: clamp(1rem, 3vw, 2.5rem);
}

/* BOTH COLUMNS SHRINK TO THEIR CONTENTS AND THE PAIR IS CENTRED.
   On halves, the number sat at the inner edge of its half and the leftover
   collected in one lump against the outer wall of the panel — 52px of it
   behind «6,639K» and 281px behind «17K». The picture and the figure were
   always the same 40px apart; it was the void beside them that made the
   short rows read as shoved to one side. Sized to content and centred, that
   space splits evenly and every row sits in the middle of the panel with the
   picture the same distance away. */
.st-slide {
  grid-area: 1 / 1;
  display: grid;
  grid-template-columns: auto auto;
  justify-content: center;
  align-items: center; gap: var(--st-gap);
  transition: opacity 420ms ease, transform 420ms cubic-bezier(.22,.61,.36,1);
}
/* `hidden` rather than opacity alone: a transparent slide is still focusable
   and still read aloud, which is how a page announces six answers at once. */
.st-slide[hidden] { display: none; }
.st-slide:not(.on) { opacity: 0; transform: translateY(10px); }
.st-slide.on       { opacity: 1; transform: none; }

/* ── the picture, and it comes apart as it arrives ──────────────────────────
 *
 * The frame is a SQUARE the pieces are positioned inside by percentage, which
 * is why the split recorded percentages rather than pixels: the same numbers
 * lay the object out at any size, on any screen.
 *
 * Each piece knows which way it is travelling. --dx/--dy is the vector from
 * the frame's centre to the piece's own centre, so "start gathered" is
 * simply that vector run backwards — the lid begins ON the cup and rises off
 * it, the keycaps begin in their sockets and leave. Nothing is hand-placed;
 * the direction is read off where the piece actually sits.
 *
 * The stagger runs biggest-first because the split sorted them that way: the
 * body settles, then the parts leave it. Reversed, the pieces would arrive
 * before the thing they came off.
 */
/* The tilt is declared on the ART and read on the FRAME inside it, because
   that is where the renderer can set it per row — a custom property set on
   an ancestor inherits, and declaring it again on the frame would shadow
   the row's own value with the default. */
.st-art { display: grid; place-items: center; --st-tip: 0deg; }
.st-frame {
  /* A DEFINITE WIDTH, AND THE CAP IS cqw RATHER THAN A PERCENTAGE.
     Its column is content-sized now, so `100%` asks the column how wide it is
     while the column is asking this element the same question. That measured
     0 — the pieces inside are absolutely positioned and contribute no
     intrinsic width, so the frame collapsed and the picture vanished from
     every row. cqw is a percentage of the PANEL, which knows its width
     without asking anybody. */
  position: relative; width: min(var(--st-art, 32rem), 100cqw);
  aspect-ratio: 1;
  /* A QUERY CONTAINER, AND THAT IS WHAT FIXES THE MOTION.
     --stp-dx is measured in FRAME percentages — the lid sits 23% right of
     centre — but a percentage inside translate() resolves against the
     ELEMENT'S OWN box. A coffee bean is 2% of the frame, so translating it
     by 10%% moved it two pixels. The big pieces shifted slightly, the small
     ones not at all, and what was left was the opacity: it read as a plain
     fade because everything except the fade was measured against the wrong
     box.
     `cqw` is a percentage of THIS element, whatever its size, so one unit
     means the same distance for every piece and the whole thing stays
     proportional as the frame grows. */
  container-type: inline-size;
  /* Declared here and written by the renderer per row — a token set only
     inline is one nobody can look up, and check.php fails on it. */

  /* The art was rendered floating with no contact shadow, so the drop shadow
     is added here — it is what puts the object above the panel rather than
     printed on it. On the pieces individually, so each carries its own. */
  /* Softer, because the ground is white now — a shadow tuned for a dark
     panel reads as dirt on a light one. */
  filter: drop-shadow(0 16px 26px rgba(20, 26, 58, .22));
}
.st-base, .st-p, .st-whole { position: absolute; display: block; }
.st-base, .st-whole { inset: 0; width: 100%; height: 100%; object-fit: contain; }
.st-p {
  /* DECLARED HERE, WRITTEN INLINE. The renderer sets these per piece from
     the measured split, so without a declaration the tokens exist nowhere a
     reader — or check.php's scanner — can find them. Namespaced `stp-`
     because `--x` on a page this many components share is a collision
     waiting to happen. */
  --stp-x: 0%; --stp-y: 0%; --stp-w: 20%;
  --stp-dx: 0; --stp-dy: 0; --stp-i: 0;
  left: var(--stp-x); top: var(--stp-y); width: var(--stp-w); height: auto;
}
/* RTL does not mirror a photograph. `left` is deliberate and must stay left:
   the split measured from the frame's left edge, and `inset-inline-start`
   would flip every piece to the wrong side of an object that has no reading
   direction. */
.st-emoji { font-size: clamp(3.5rem, 11vw, 6rem); line-height: 1; }

@media (prefers-reduced-motion: no-preference) {
  .st-slide:not(.on) .st-p, .st-slide:not(.on) .st-base { opacity: 0; }
  .st-slide.on .st-base {
    animation: st-base-in 620ms both ease-out; animation-delay: 260ms;
  }
  .st-slide.on .st-p {
    /* TWO ANIMATIONS, IN SEQUENCE. The first throws the piece out to where
       it belongs; the second never stops. A one-shot assembly is a trick
       that happens once and then sits there being a photograph — and this
       is a section on a site called Gravity, where the objects should keep
       behaving as though nothing is holding them. The float's delay is the
       drift's duration plus its own stagger, so it takes over exactly as
       the piece lands rather than fighting it. */
    animation:
      st-drift 1050ms both cubic-bezier(.16, .84, .28, 1.06),
      st-float calc(5200ms + var(--stp-i, 0) * 730ms) ease-in-out infinite alternate;
    animation-delay:
      calc(var(--stp-i, 0) * 95ms),
      calc(1050ms + var(--stp-i, 0) * 95ms);
  }
  @keyframes st-base-in { from { opacity: 0 } to { opacity: 1 } }

  /* THE OBJECT TIPS INTO PLACE.
     The cup was drawn already leaning, so starting the frame at the opposite
     angle stands it upright — and rotating to zero IS the tip. The pieces
     leave it while it goes over, which is the order the eye expects: the cup
     moves first and the lid comes off because it moved.
     On the FRAME rather than each piece, so the whole object turns as one
     body and the pieces' own drift happens on top of it. Rotating them
     individually would make six things turning near each other, which is
     not a cup tipping over.
     `--st-tip` is 0 for every row that should not do this — a keyboard that
     rights itself is a keyboard falling over. */
  .st-slide.on .st-frame {
    animation: st-tip 1150ms both cubic-bezier(.34, .72, .28, 1.02);
  }
  @keyframes st-tip {
    from { transform: rotate(calc(var(--st-tip, 0deg) * -1)) translateY(2%); }
    to   { transform: none; }
  }
  /* GATHERED, THEN THROWN OUT PAST THE MARK, THEN SETTLED.
     The overshoot at 78%% is what makes it read as a physical thing rather
     than a tween: a piece with momentum passes where it is going and comes
     back. Without it the motion arrives and stops dead, which the eye reads
     as a slide rather than a throw.
     `1cqw` is one percent of the FRAME, so `--stp-dx * -0.78cqw` is a real
     distance across the picture — the lid starts on the cup and travels the
     full width of the gap. The old `%%` version was measured against each
     piece's own box and moved the small ones two pixels. */
  @keyframes st-drift {
    from {
      opacity: 0;
      transform:
        translate(calc(var(--stp-dx, 0) * -0.78cqw), calc(var(--stp-dy, 0) * -0.78cqw))
        scale(.72) rotate(calc(var(--stp-dx, 0) * -0.55deg));
    }
    38% { opacity: 1; }
    78% {
      transform:
        translate(calc(var(--stp-dx, 0) * 0.07cqw), calc(var(--stp-dy, 0) * 0.07cqw))
        scale(1.03) rotate(calc(var(--stp-dx, 0) * 0.05deg));
    }
    to { opacity: 1; transform: none; }
  }

  /* THE IDLE, and it is the half that makes the section believable.
     Every piece keeps drifting after it lands, on its own period, so the
     object never fully settles — which is what weightless looks like, and
     the reason this works for an agency called Gravity. The distances are
     deliberately tiny: it should be felt rather than watched, and anything
     larger turns a joke about coffee into a screensaver.
     The drift direction seeds the float direction, so pieces continue the
     way they were already going instead of all breathing in unison. */
  @keyframes st-float {
    from {
      transform: none;
    }
    to {
      transform:
        translate(calc(var(--stp-dx, 0) * 0.045cqw), calc(var(--stp-dy, 0) * 0.045cqw - 0.5cqw))
        rotate(calc(var(--stp-dx, 0) * 0.035deg));
    }
  }
}

/* ── the number, at display size ────────────────────────────────────────── */
/* SIZED AGAINST THE ROOM THAT IS ACTUALLY LEFT, not against the window.
   `14vw` is a guess about how much space there is, and the guess was wrong:
   on a wide screen the viewport asked for 200px of type where the panel could
   afford about 90, and nine glyphs of it ran out of the white band and off the
   section entirely.
   What is left for the figure is the panel minus the picture minus the gap —
   all three declared together on .st-stage so they cannot disagree. --st-em is
   how many ems wide this particular figure is, counted glyph by glyph by the
   renderer, so the division gives the largest size that still fits and the
   number cannot leave the panel whatever it grows to.
   NOT a container query on .st-say itself: that box is now sized by its own
   contents, and an element cannot be the container it is measured against. */
.st-say { display: grid; gap: .3rem; justify-items: start; }
.st-say b {
  font-size: clamp(2.4rem,
    min(14vw, calc((100cqw - var(--st-art) - var(--st-gap)) / var(--st-em, 5))),
    8.5rem);
  font-weight: 800; line-height: .95;
  letter-spacing: -.03em;
  /* THE SWEEP, and this is the one place on the page it can be painted
     THROUGH type without disappearing: the band behind it is white. The
     closing CTA had to give this up precisely because its own ground is
     the same gradient, and two gradients in one place is none. */
  background: var(--g-grad); -webkit-background-clip: text;
  background-clip: text; color: transparent;
  /* Digits hold their column while they count instead of shuffling the width. */
  font-variant-numeric: tabular-nums;
}
.st-lbl {
  font-size: clamp(1rem, 2.4vw, 1.3rem); line-height: 1.65;
  /* on white, not on the page ground */
  color: #5b6079; font-weight: 500; max-width: 22ch;
}

/* ── the position readout ───────────────────────────────────────────────── */
.st-dots {
  display: flex; justify-content: center; gap: .45rem;
  margin-top: clamp(.8rem, 2vw, 1.1rem);
}
.st-dot {
  width: 7px; height: 7px; padding: 0; border-radius: 50%; cursor: pointer;
  background: var(--g-line-up); border: 0;
  transition: background 200ms ease, transform 200ms ease;
}
.st-dot:hover { transform: scale(1.25); }
.st-dot.on { background: var(--g-grad-flat, #e2255e); transform: scale(1.35); }
.st-dot:focus-visible { outline: 2px solid var(--g-line-up); outline-offset: 3px; }

@media (max-width: 700px) {
  .st-slide { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .st-say { justify-items: center; }
  .st-lbl { max-width: 26ch; }
  /* STACKED, so nothing stands beside the number and the whole panel is its
     to use. Keeping the subtraction here would reserve room for a picture
     that is now above it, and on a phone the picture is nearly the whole
     width — the figure would have been sized against what little was left. */
  .st-say b { font-size: clamp(2.4rem, min(15vw, calc(100cqw / var(--st-em, 5))), 8.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  .st-slide { transition: none; }
  .st-slide:not(.on) { transform: none; }
}
