/*
 * Self-hosted type, OFL licences next to the files in public/fonts/.
 * Geist (Vercel, from Google Fonts) is the text and display face: one
 * variable file per script with the whole weight axis (100-900), so weights
 * can be tuned in fine steps (the --w-* tokens in tokens.css). Latin is
 * preloaded from index.html. Instrument Serif and Geist Mono load when first
 * used. (Instrument Sans, the previous face, is still in public/fonts.)
 */
@font-face {
  font-family: "Geist";
  src: url("/fonts/geist-latin.woff2") format("woff2");
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
    U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Geist";
  src: url("/fonts/geist-latin-ext.woff2") format("woff2");
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
    U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Instrument Serif";
  src: url("/fonts/instrument-serif-italic-400.woff2") format("woff2");
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0000-017F, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2196-2199, U+2212, U+2215, U+221E, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Geist Mono";
  src: url("/fonts/geist-mono-500.woff2") format("woff2");
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  unicode-range: U+0000-017F, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2196-2199, U+2212, U+2215, U+221E, U+FEFF, U+FFFD;
}

/*
 * Arial scaled to Geist's average advance (English letter frequencies) and
 * vertical metrics, so the swap reflows nothing. Measured with fontTools
 * against Arial and Arial Bold, Geist instanced at 400 and 600. Where Arial
 * is missing, system-ui follows.
 */
@font-face {
  font-family: "Geist Fallback";
  src: local("Arial"), local("ArialMT");
  font-style: normal;
  font-weight: 100 500;
  size-adjust: 105.05%;
  ascent-override: 95.67%;
  descent-override: 28.08%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Geist Fallback";
  src: local("Arial Bold"), local("Arial-BoldMT");
  font-style: normal;
  font-weight: 501 900;
  size-adjust: 101.65%;
  ascent-override: 98.87%;
  descent-override: 29.02%;
  line-gap-override: 0%;
}

/*
 * Tokens. Colour, type and the 3-bar geometry all come from the mark
 * (public/brand/driezie-mark.svg): a "D" of three stacked bars, 12 : 2 : 12 : 2 : 12,
 * each stepping right by one tenth. Values carried over from v3 docs/01.
 */

:root {
  /* One light page; sections invert with [data-invert]. No theme switch. */
  color-scheme: light;

  /* Brand constants (never change between themes) */
  --paper: #f4f1ec;
  --paper-2: #ebe8e1;
  --ink: #18191a;
  --ink-2: #101112;
  --ink-soft: #2c2c34;
  --accent: #df9568;
  --accent-ink: #8a4a20;

  --muted-on-paper: #56544f;
  /* Faint is still text: at least 4.5:1 on paper and paper-2 (WCAG 1.4.3). */
  --faint-on-paper: #696560;
  --muted-on-ink: #a5a09a;
  --faint-on-ink: #8f8b85;
  --rule-on-paper: #d6d2c9;
  --rule-strong-on-paper: #bab5aa;
  --rule-on-ink: #35363a;
  --rule-strong-on-ink: #4a4b50;

  /* Theme channel: resolved where it is used, so a section can invert */
  --bg: light-dark(var(--paper), var(--ink));
  --bg-2: light-dark(var(--paper-2), var(--ink-2));
  --fg: light-dark(var(--ink), var(--paper));
  --fg-muted: light-dark(var(--muted-on-paper), var(--muted-on-ink));
  --fg-faint: light-dark(var(--faint-on-paper), var(--faint-on-ink));
  --hairline: light-dark(var(--rule-on-paper), var(--rule-on-ink));
  --hairline-strong: light-dark(var(--rule-strong-on-paper), var(--rule-strong-on-ink));
  --media-bg: light-dark(var(--paper-2), var(--ink-2));
  --accent-text: light-dark(var(--accent-ink), var(--accent));
  /* The focus ring: at least 3:1 against the ground it sits on (WCAG 1.4.11),
     so the deep accent on paper and the bright one on ink. */
  --focus: light-dark(var(--accent-ink), var(--accent));
  --display-ink: light-dark(var(--ink-soft), var(--paper));

  /* Type */
  --font-display: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
  --font-editorial: "Instrument Serif", ui-serif, Georgia, serif;
  --font-meta: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --t-section: clamp(4.5rem, 15vw, 15rem); /* serif-italic section titles, 1st December style */
  --t-display: clamp(3rem, 8.6vw, 9rem);
  --t-h1: clamp(2.5rem, 6vw, 5.75rem);
  --t-h2: clamp(1.875rem, 3.6vw, 3.25rem);
  --t-h3: clamp(1.375rem, 2.1vw, 2rem);
  --t-statement: clamp(1.5rem, 2.85vw, 2.875rem);
  --t-lead: clamp(1.0625rem, 1.45vw, 1.375rem);
  --t-body-l: clamp(1rem, 1.15vw, 1.1875rem);
  --t-body: clamp(0.9375rem, 1vw, 1.0625rem);
  --t-micro: 0.8125rem;
  --t-label: 0.6875rem;

  --tr-display: -0.045em;
  --tr-h1: -0.04em;
  --tr-h2: -0.035em;
  --tr-h3: -0.025em;
  --tr-body: -0.011em;
  --tr-label: 0.09em;

  --lh-display: 0.95;
  --lh-heading: 1.05;
  --lh-body: 1.55;
  --lh-micro: 1.35;

  /* Space and grid: twelve columns, 8 px baseline */
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2rem;
  --space-5: 3rem;
  --space-6: 5rem;
  --space-7: 8rem;
  --space-8: 13rem;

  --page-x: clamp(1.25rem, 3.6vw, 4rem);
  --gutter: clamp(0.75rem, 1.4vw, 1.25rem);
  --maxw: 1720px;
  --section-y: clamp(5rem, 11vw, 11rem);
  --nav-h: 4rem;
  /* Where .wrap content starts, measured from the viewport edge */
  --edge: max(var(--page-x), calc((100vw - var(--maxw)) / 2 + var(--page-x)));

  /* Motion: three curves, each with one job. GSAP registers the same three as
     "out", "outExpo" and "inOut" (src/core/motion.ts).
     --ease-out       anything that answers you (hover, press, colour): moves at once, lands softly
     --ease-out-expo  anything that arrives (reveals, entrances): a long, settled landing
     --ease-in-out    anything that travels between two states (the menu turn, the word swap):
                      a little weight to get going, then a long settle */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.55, 0, 0.1, 1);
  --d-fast: 0.3s;
  --d-hover: 0.5s;
  --d-base: 0.5s;
  --d-slow: 1s;
  --d-text: 1.1s;

  --radius: 2px;

  /* Weights. Geist is variable, so they can sit between the usual steps. */
  --w-text: 400;
  --w-strong: 600;
}

/* A section (or the nav over one) turns to ink wholesale. The section also
   resets its text and background: color is inherited as a computed value, so
   without this it would keep the page's ink text on its own ink background. */
[data-invert],
.nav.is-inverted {
  color-scheme: dark;
}

[data-invert] {
  background: var(--bg);
  color: var(--fg);
  font-weight: var(--w-text);
}

/* Windows draws type heavier than macOS and iOS (ClearType, and most Windows
   screens have half the pixels of a Retina one), and light type on a dark
   ground looks heavier still. There the weights come down a little, a bit
   more on the dark sections, so the type looks the same everywhere.
   (os-windows is set in index.html before the first paint.) */
html.os-windows {
  --w-text: 370;
  --w-strong: 570;
}

html.os-windows [data-invert] {
  --w-text: 350;
  --w-strong: 550;
}

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

html {
  /* Sideways parallax (titles, contact lines) must never make the page scroll sideways. */
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-padding-top: var(--nav-h);
  interpolate-size: allow-keywords;
  background: var(--bg);
}

body {
  margin: 0;
  min-height: 100svh;
  background: var(--bg);
  color: var(--fg);
  font: var(--w-text) var(--t-body) / var(--lh-body) var(--font-display);
  letter-spacing: var(--tr-body);
  font-kerning: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

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

img {
  height: auto;
}

h1,
h2,
h3,
h4,
p,
figure,
dl,
dd,
ol,
ul,
fieldset,
blockquote {
  margin: 0;
}

h1,
h2,
h3,
h4 {
  font-weight: inherit;
  font-size: inherit;
}

/*
 * No line ever ends with one lonely word. Headings and short display text are
 * balanced (even line lengths); running text is "pretty" (the browser avoids
 * an orphan on the last line). House rule: every new text style gets one.
 */
:where(h1, h2, h3, h4, h5, h6, blockquote, figcaption, .display, .h1, .section-title, .lead, .label, .micro, .parade__title, .btn, .chip, .eyebrow) {
  text-wrap: balance;
}

:where(p, li, dd, dt, td, th, .statement, .body-l) {
  text-wrap: pretty;
}

:where(ul[role="list"], ol[role="list"]) {
  list-style: none;
  padding: 0;
}

fieldset {
  border: 0;
  padding: 0;
  min-width: 0;
}

legend {
  padding: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

/* No grey flash on tap; every control has its own pressed and focus states. */
a,
button,
summary,
label {
  -webkit-tap-highlight-color: transparent;
}

button {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

table {
  border-collapse: collapse;
  width: 100%;
}

th {
  font-weight: inherit;
  text-align: left;
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--radius);
}

::selection {
  background: var(--accent);
  color: var(--ink);
}

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

.skip {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 100;
  padding: 0.75rem 1rem;
  background: var(--fg);
  color: var(--bg);
  translate: 0 -200%;
  transition: translate var(--d-fast) var(--ease-out);
}

.skip:focus-visible {
  translate: 0 0;
}

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--page-x);
}

.section {
  padding-block: var(--section-y);
}

/* Lenis (core/motion.ts): the menu locks the page; areas that scroll on
   their own keep the wheel to themselves (Lenis leaves them alone too). */
html.is-scroll-locked {
  overflow: clip;
}

[data-smoother-prevent] {
  overscroll-behavior: contain;
}

/* Two weights in the whole system: 400 for display and body, 600 for micro text and buttons. */

.display {
  font-size: var(--t-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-display);
  color: var(--display-ink);
}

.h1 {
  font-size: var(--t-h1);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-h1);
  color: var(--display-ink);
}

.section-title {
  font-family: var(--font-editorial);
  font-style: italic;
  font-size: var(--t-section);
  line-height: 0.82;
  letter-spacing: -0.035em;
  color: var(--display-ink);
}

.statement {
  font-size: var(--t-statement);
  line-height: 1.16;
  letter-spacing: -0.028em;
}

.lead {
  font-size: var(--t-lead);
  line-height: 1.4;
  letter-spacing: -0.018em;
}

.body-l {
  font-size: var(--t-body-l);
  line-height: 1.5;
}

.micro {
  font-size: var(--t-micro);
  font-weight: var(--w-strong);
  line-height: var(--lh-micro);
  letter-spacing: 0;
}

.label {
  font: 500 var(--t-label) / 1.3 var(--font-meta);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

.index {
  font: 500 var(--t-micro) / 1 var(--font-meta);
  font-variant-numeric: tabular-nums;
  color: var(--fg-faint);
}

.muted {
  color: var(--fg-muted);
}

/* The one serif-italic word a statement may carry. */
.accent-serif {
  font-family: var(--font-editorial);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.012em;
  padding-right: 0.04em;
}

.eyebrow {
  display: flex;
  gap: 0.5em;
  align-items: baseline;
}

/* Masked lines: the arrival and the scroll reveals rise text out of its own line box. */
.line {
  display: block;
  overflow: clip;
  overflow-clip-margin: 0.08em;
  padding-bottom: 0.06em;
  margin-bottom: -0.06em;
}

.line__inner {
  display: block;
}

.line--step-1 {
  padding-left: 8.333%;
}

.line--step-2 {
  padding-left: 16.666%;
}

/*
 * Text on a six-sided prism, the menu's turn in miniature (src/render/html.ts
 * roll()). The word is the front face and its copy the face below it, 60°
 * round; the faces sit one apothem (√3/2 of a face) from the prism's axis.
 * Hovering turns the prism 60° so the copy comes up from underneath while the
 * word tips up and away. The outer box clips to the line (clip-path, which
 * keeps the baseline where it is), the prism inside stays truly 3D.
 */
.roll {
  position: relative;
  display: inline-block;
  vertical-align: top;
  white-space: nowrap;
  clip-path: inset(0 -0.3em);
}

.roll__prism {
  --apothem: calc(1lh * 0.866);
  --turn: 0deg;
  display: block;
  transform-style: preserve-3d;
  /* Pulled back one apothem, so the front face rests exactly in the text plane. */
  transform: perspective(10em) translateZ(calc(-1 * var(--apothem))) rotateX(var(--turn));
  transition: transform var(--d-hover) var(--ease-out);
}

.roll__face {
  display: block;
  backface-visibility: hidden;
  transform: translateZ(var(--apothem));
}

.roll__face--next {
  position: absolute;
  inset: 0;
  transform: rotateX(-60deg) translateZ(var(--apothem));
}

@media (hover: hover) {
  :is(a, button, label):hover .roll__prism {
    --turn: 60deg;
  }
}

/* Buttons: 2 px radius from the mark's corners; solid is the foreground colour. */
.btn {
  --btn-h: 2.75rem;
  --btn-pad: 1.25rem;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  min-height: var(--btn-h);
  padding: 0 var(--btn-pad);
  border-radius: var(--radius);
  font-size: var(--t-micro);
  font-weight: var(--w-strong);
  line-height: 1.3;
  white-space: nowrap;
  transition:
    background-color var(--d-hover) var(--ease-out),
    color var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out);
}

.btn--sm {
  --btn-h: 2.25rem;
  --btn-pad: 0.95rem;
}

.btn--outline {
  border: 1px solid var(--hairline-strong);
}

@media (hover: hover) {
  .btn--outline:hover {
    border-color: var(--fg);
  }
}

/*
 * Solid buttons turn as a whole, as a real block (solid() in
 * src/render/html.ts): the ink face with the label in front, the accent face
 * underneath. On hover the block rolls a quarter turn and the accent face
 * comes up from below; the label never moves on its own face. Nothing clips
 * it, so mid-turn the block's edge swings out past the button. A cube's
 * neighbouring faces sit edge-on at rest, so at rest it is a flat button.
 * Being 3D at rest too, the faces keep one text rendering whether or not the
 * menu has the page turned.
 */
.btn--solid {
  padding: 0;
  min-height: 0;
}

.block {
  display: block;
}

.block__prism {
  --depth: calc(var(--btn-h) / 2);
  --turn: 0deg;
  display: block;
  transform-style: preserve-3d;
  /* Pulled back half a face, so the front face rests exactly in the page. */
  transform: perspective(20rem) translateZ(calc(-1 * var(--depth))) rotateX(var(--turn));
  transition: transform 0.65s var(--ease-out);
}

.block__face {
  display: flex;
  align-items: center;
  gap: 0.6em;
  min-height: var(--btn-h);
  padding: 0 var(--btn-pad);
  border-radius: var(--radius);
  background: var(--fg);
  color: var(--bg);
  backface-visibility: hidden;
  transform: translateZ(var(--depth));
  transition:
    background-color var(--d-hover) var(--ease-out),
    color var(--d-hover) var(--ease-out);
}

.block__face--next {
  position: absolute;
  inset: 0;
  background: var(--accent);
  color: var(--ink);
  transform: rotateX(-90deg) translateZ(var(--depth));
}

@media (hover: hover) {
  .btn--solid:hover .block__prism {
    --turn: 90deg;
  }
}

/*
 * Stats (tumble() and stat() in src/render/html.ts): the solid button's
 * quarter turn without its faces' colours. The number and its label sit on
 * the block's underside, edge-on below it, until src/modules/enhance.ts adds
 * .is-in as it comes into view; then the block rolls a quarter turn and both
 * come up from below into place, once. --depth is half the block's height
 * (set by enhance.ts), and the perspective keeps the button's proportions, so
 * it turns like a bigger copy of the button. Waits only with JS (html.js, set
 * before first paint) and motion; otherwise it just stands there.
 */
.tumble {
  display: inline-block;
  vertical-align: top;
}

.tumble__prism,
.tumble__face {
  display: block;
}

@media (prefers-reduced-motion: no-preference) {
  html.js .tumble__prism {
    --depth: var(--tumble-depth, 2.5rem);
    --turn: 0deg;
    transform-style: preserve-3d;
    /* Pulled back half a face, so the face lands exactly in the page. The
       button: perspective 20rem for a 1.375rem half-height, so 14.5 times. */
    transform: perspective(calc(var(--depth) * 14.5)) translateZ(calc(-1 * var(--depth))) rotateX(var(--turn));
    transition: transform 0.9s var(--ease-out) calc(var(--i, 0) * 0.12s);
  }

  html.js .tumble__face {
    backface-visibility: hidden;
    transform: rotateX(-90deg) translateZ(var(--depth));
  }

  html.js .tumble.is-in .tumble__prism {
    --turn: 90deg;
  }
}

/* A text link and its arrow. Its hover is the roll of its text (roll()), as
   on every link of the site; no underline. */
.link {
  display: inline-flex;
  gap: 0.4em;
  align-items: baseline;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.chip {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 0.85rem;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  white-space: nowrap;
}

.chip--choice,
.chip--button {
  position: relative;
  min-height: 2.75rem;
  cursor: pointer;
  transition:
    background-color var(--d-fast) var(--ease-out),
    color var(--d-fast) var(--ease-out),
    border-color var(--d-fast) var(--ease-out);
}

.chip--choice input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

@media (hover: hover) {
  .chip--choice:hover,
  .chip--button:hover {
    border-color: var(--fg);
  }
}

.chip--choice:has(input:checked) {
  background: var(--fg);
  border-color: var(--fg);
  color: var(--bg);
}

.chip--choice:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

/* Status: a word and a shape, never colour alone. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  white-space: nowrap;
}

.status::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  background: currentColor;
}

.status--coming-soon::before {
  background: none;
  border: 1px solid currentColor;
  border-radius: 50%;
}

.mark__bar {
  fill: currentColor;
}

/* The work slider (src/render/ribbon.ts, src/modules/ribbon.ts): edge to
   edge; images share the strip's height, each width from its own ratio. The
   page that uses it gives it its height (hero.css, pages.css). */
.ribbon {
  position: relative;
  overflow: clip;
}

/* The pause button (core/pause.ts), in the slider's bottom-right corner:
   paper on the pictures, two bars, a triangle once paused. 2.5rem, well over
   the 24px target size (WCAG 2.5.8). */
.ribbon__pause {
  position: absolute;
  right: var(--page-x);
  bottom: 0.75rem;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 0 0 1px rgb(24 25 26 / 0.12);
  cursor: pointer;
  transition: scale var(--d-hover) var(--ease-out);
}

.ribbon__pause:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 3px;
}

@media (hover: hover) {
  .ribbon__pause:hover {
    scale: 1.08;
  }
}

/* Pause: two bars. */
.ribbon__pause-icon {
  width: 0.7rem;
  height: 0.8rem;
  border-inline: 0.22rem solid currentColor;
}

/* Paused: a triangle (play). */
.ribbon__pause[aria-pressed="true"] .ribbon__pause-icon {
  width: 0;
  height: 0;
  border-block: 0.42rem solid transparent;
  border-inline: 0.72rem solid currentColor;
  border-inline-end: 0;
  margin-left: 0.2rem;
}

/* Nothing moves with reduced motion, so there is nothing to pause. */
@media (prefers-reduced-motion: reduce) {
  .ribbon__pause {
    display: none;
  }
}

/* Two identical sets side by side; src/modules/ribbon.ts moves the row left
   and wraps it by one set's width, so the loop never shows a seam. */
.ribbon__row {
  --gap: 0.5rem;
  position: absolute;
  inset: 0 auto 0 0;
  display: flex;
  gap: var(--gap);
  width: max-content;
  will-change: transform;
}

.ribbon__set {
  display: flex;
  gap: var(--gap);
  height: 100%;
}

.ribbon__shot {
  height: 100%;
  overflow: clip;
  background: var(--media-bg);
}

.ribbon__shot img {
  height: 100%;
  width: auto;
}

/* Room for the pan (src/modules/ribbon.ts moves each image up to 7% each way). */
@media (prefers-reduced-motion: no-preference) {
  .ribbon__shot img {
    scale: 1.16;
  }
}

/* Reduced motion: no auto-slide; the strip scrolls sideways by hand instead. */
@media (prefers-reduced-motion: reduce) {
  .ribbon {
    overflow-x: auto;
  }

  .ribbon__row {
    position: relative;
    height: 100%;
  }
}

/* Generic scroll reveal: rises as it enters (CSS scroll timeline, see scroll.css). */
[data-reveal] {
  --reveal-y: 2rem;
}
/*
 * The nav floats: no bar, three fixed layers (.nav__face). The brand and the
 * Menu button are white with mix-blend-mode: difference, so they turn ink on
 * paper, paper on ink and the negative of a photo, always readable whatever is
 * under them. Contact stays a solid button and swaps to paper over the dark
 * sections (src/modules/nav.ts sets .is-inverted; tokens.css flips it).
 */

.nav {
  /* Only a wrapper; it must not be positioned, or it would isolate the blend. */
  --menu-btn-w: 5.25rem;
}

.nav__face {
  position: fixed;
  top: 0;
  z-index: 40;
  height: var(--nav-h);
  display: flex;
  align-items: center;
}

.nav__brand,
.menu-btn {
  color: #fff;
  mix-blend-mode: difference;
}

/* While the end of the page turns (html.is-folding, scroll.css) the Menu
   button steps aside (opening the menu over a page that leans away would
   glitch) and Contact moves over into its place. */
.nav__contact,
.menu-btn {
  transition:
    opacity 0.35s var(--ease-out),
    visibility 0.35s,
    translate 0.6s var(--ease-out-expo),
    filter var(--menu-time) var(--ease-in-out);
}

html.is-folding:not(.menu-3d) .menu-btn {
  opacity: 0;
  visibility: hidden;
}

html.is-folding:not(.menu-3d) .nav__contact {
  translate: calc(var(--menu-btn-w) + 0.5rem) 0;
}

.nav__brand {
  left: var(--edge);
  gap: 0.6rem;
}

.nav__mark {
  width: 1rem;
  height: 1.25rem;
}

.nav__contact {
  right: calc(var(--edge) + var(--menu-btn-w) + 0.5rem);
}

/* Our own scrollbar, only where the system's takes room and Lenis scrolls
   (html.custom-scrollbar; src/modules/scrollbar.ts). A thin line that blends
   like the brand, lights up while the page moves, and turns with the page. */
html.custom-scrollbar {
  scrollbar-width: none;
}

.scrollbar {
  display: none;
}

html.custom-scrollbar .scrollbar {
  display: block;
  right: 0;
  z-index: 41;
  width: 0.875rem;
  height: 100dvh;
  color: #fff;
  mix-blend-mode: difference;
}

html.custom-scrollbar .scrollbar.is-empty {
  display: none;
}

.scrollbar__thumb {
  position: absolute;
  top: 0;
  right: 3px;
  width: 3px;
  border-radius: 3px;
  background: currentColor;
  opacity: 0.3;
  touch-action: none;
  transition:
    opacity var(--d-hover) var(--ease-out),
    width var(--d-fast) var(--ease-out);
}

.scrollbar.is-active .scrollbar__thumb,
.scrollbar.is-dragging .scrollbar__thumb {
  opacity: 0.65;
}

@media (hover: hover) {
  .scrollbar:hover .scrollbar__thumb {
    width: 6px;
    opacity: 0.75;
  }
}

.scrollbar.is-dragging .scrollbar__thumb {
  width: 6px;
}

/*
 * The menu. The page and the panel are two faces of one prism, joined at an
 * edge: opening turns the prism, so the page folds away into the dark while
 * the panel swings round to face you. The page is several elements (the nav
 * faces, main, the footer; each on its own so the fixed nav stays fixed and
 * can blend); each gets the same transform, turning round the same line in the
 * middle of the screen (--fold-x / --fold-y, set per element by
 * src/modules/menu.ts), so together they move as one face.
 *
 * Both faces carry `translateX · perspective · rotateY`, written by GSAP from
 * one progress value: the shared edge moves from the right of the screen to
 * the left of the panel while the page turns 0 → -fold and the panel +fold → 0,
 * so the angle between them never changes. Closing is the same turn backwards.
 * menu.ts sets .menu-3d on <html> for the whole open-and-close, .menu-open on
 * <html> while it is open (shade, dimming) and .is-open on the dialog from
 * opening until it has closed (the reveal of its content).
 */

:root {
  /* A share of the screen, so the panel holds its own on a large desktop. */
  --menu-w: min(100vw, max(30rem, 40vw));
  --menu-time: 1.1s; /* the turn (src/modules/menu.ts OPEN_S), the shade and dimming coming in */
  --menu-time-close: var(--menu-time); /* the shade and dimming going out: as long as they took to come in */
  --menu-fold: 40deg; /* the angle between the two faces */
  --menu-depth: 1600px; /* perspective: smaller is more dramatic */
  --menu-pad: clamp(1.5rem, 3vw, 2.5rem);
  /* The image behind the turning page (public/media/menu-backdrop.jpg). It covers
     the whole screen behind the page. */
  --menu-backdrop: url("/media/menu-backdrop.jpg");
}

/* ---- The Menu button: a word and three stepped bars, like the bars of the
   mark. No background: it blends with the page under it (nav.css). ---- */

.menu-btn {
  right: var(--edge);
  width: var(--menu-btn-w);
  justify-content: flex-end;
  gap: 0.6rem;
}

.menu-btn__icon {
  display: grid;
  gap: 3px;
  width: 1.1rem;
}

.menu-btn__icon span {
  height: 2px;
  background: currentColor;
  transform-origin: right;
  transition: scale var(--d-hover) var(--ease-out);
}

.menu-btn__icon span:nth-child(2) {
  scale: 0.82 1;
}

.menu-btn__icon span:nth-child(3) {
  scale: 0.64 1;
}

@media (hover: hover) {
  .menu-btn:hover .menu-btn__icon span {
    scale: 1 1;
  }
}

/* ---- The page face ---- */

/* Opaque, so once it turns it reads as a solid face, not loose text. */
main,
.footer {
  background: var(--bg);
}

/* The page dims as it turns away. Darkened, not made see-through, so the
   backdrop image behind the prism never shows through the page. Not with a
   filter on the page: a filter makes the browser render the whole element
   off screen first, and on the long homepage (many screens tall) that froze
   the turn for a third of a second as the dimming came in. Instead a black
   shade at 40% (the same as brightness 0.6) over just the part that is seen,
   inside the face so it turns with it. --shade-top is where the screen sits
   in the face (src/modules/menu.ts setAxis); the extra screen-height above and
   below covers what the perspective shows at the far edge. */
html.menu-3d :is(main, .footer)::after {
  content: "";
  position: absolute;
  z-index: 2147483000;
  inset-inline: 0;
  /* Never past the face's own edges (at the top of the page there is no page above the screen). */
  top: max(0px, calc(var(--shade-top, 0px) - 60lvh));
  bottom: max(0px, calc(100% - var(--shade-top, 0px) - 160lvh));
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--menu-time-close) var(--ease-in-out);
}

html.menu-open :is(main, .footer)::after {
  opacity: 0.4;
  transition-duration: var(--menu-time);
}

/* Contact dims with it (its transition is in nav.css): it is a face of the
   page too, and would look sharper than the page's own buttons. The logo and
   Menu blend with the page, so they dim with it by themselves. */
html.menu-open .nav__contact {
  filter: brightness(0.6);
}

/* The nav on the page (the logo, Contact, Menu, the scrollbar) is not seen
   while the menu is open: it fades as the page turns away, and comes back
   only as the page lands again (closing, or the next page turning in after a
   page change through the menu). Not at the very top of the page
   (.menu-at-top, src/modules/menu.ts): there it turns away with the page. */
html.menu-open:not(.menu-at-top) :is(.nav__brand, .nav__contact, .menu-btn, .scrollbar) {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.3s var(--ease-out),
    visibility 0s 0.3s;
}

html.menu-3d:not(.menu-open, .menu-at-top) :is(.nav__brand, .nav__contact, .menu-btn, .scrollbar) {
  transition:
    opacity 0.45s var(--ease-out) 0.55s,
    visibility 0s 0.55s;
}

/* The page must not scroll under the menu (Lenis is stopped too; this covers
   reduced motion). Held for the whole open-and-close, so the scrollbar cannot
   come back mid-turn and shift the panel's edge. */
html.menu-3d {
  overflow: hidden;
}

/* ---- The panel ---- */

.menu {
  position: fixed;
  inset: 0 0 0 auto;
  width: var(--menu-w);
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  outline: none; /* the dialog takes focus itself on open; its links have their own rings */
}

/* Light falls from the panel's side: the far end of the page face sits deepest in the shade. */
.menu::backdrop {
  background: linear-gradient(to right, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.08) calc(100% - var(--menu-w)));
  opacity: 0;
  transition: opacity var(--menu-time-close) var(--ease-in-out);
}

html.menu-open .menu::backdrop {
  opacity: 1;
  transition-duration: var(--menu-time);
}

.menu__panel {
  container-type: inline-size; /* the links size to the panel */
  display: grid;
  grid-template-rows: var(--nav-h) 1fr auto;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--menu-pad) var(--menu-pad);
}

/* Close sits exactly where Menu was (as far as the panel allows): a word and
   two bars that cross into an ×. */
.menu__close {
  position: absolute;
  top: 0;
  right: min(var(--edge), var(--menu-pad));
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  height: var(--nav-h);
}

.menu__close-icon {
  position: relative;
  width: 1.1rem;
  height: 1.1rem;
  transition: rotate var(--d-hover) var(--ease-out);
}

.menu__close-icon span {
  position: absolute;
  inset: calc(50% - 1px) 0 auto;
  height: 2px;
  background: currentColor;
}

.menu__close-icon span:first-child {
  rotate: 45deg;
}

.menu__close-icon span:last-child {
  rotate: -45deg;
}

@media (hover: hover) {
  .menu__close:hover .menu__close-icon {
    rotate: 90deg;
  }
}

/* The pages: big, tight, low in the panel. */
.menu__nav {
  grid-row: 2;
  align-self: end;
  padding-bottom: clamp(2rem, 7svh, 4.5rem);
}

.menu__link {
  display: block;
  overflow: clip;
  padding-block: 0.02em 0.06em;
  /* Sized to the panel, and never wider than it: the longest word, "Services",
     is 3.5 em wide, so the panel width over 3.8 is the most it can take. */
  font-size: clamp(3rem, min(17cqi, calc((var(--menu-w) - 2 * var(--menu-pad)) / 3.8)), 9rem);
  line-height: 0.98;
  letter-spacing: -0.05em;
  transition:
    opacity var(--d-hover) var(--ease-out),
    translate var(--d-hover) var(--ease-out);
}

.menu__text {
  display: inline-block;
}

.menu__link[aria-current] .menu__text {
  font-family: var(--font-editorial);
  font-style: italic;
  letter-spacing: -0.02em;
}

/* Work carries its number of cases: a square in the accent, as tall as the
   word's capitals and standing on its baseline, with the number large in it.
   (An inline-block with nothing in flow sits on the line with its bottom
   edge, so it stands on the baseline.) The square is the front of a little
   cube, like the solid buttons: hovering Work rolls it a quarter turn and
   the light face below comes up. */
.menu__count {
  position: relative;
  display: inline-block;
  width: 0.72em;
  height: 0.72em;
  margin-left: 0.14em;
  vertical-align: baseline;
  font-style: normal;
  letter-spacing: 0;
}

.menu__cube {
  --depth: 0.36em;
  --turn: 0deg;
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* Pulled back half a side, so the front face rests exactly in the page. */
  transform: perspective(6em) translateZ(calc(-1 * var(--depth))) rotateX(var(--turn));
  transition: transform 0.65s var(--ease-out);
}

/* The sides keep the word's font size, so --depth (in em) means the same on
   them as on the cube; only the number inside is set smaller. */
.menu__side {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--ink);
  backface-visibility: hidden;
  transform: translateZ(var(--depth));
}

.menu__num {
  font: var(--w-strong) 0.46em / 1 var(--font-display);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.menu__side--next {
  background: var(--paper);
  transform: rotateX(-90deg) translateZ(var(--depth));
}

@media (hover: hover) {
  .menu__link:hover .menu__cube {
    --turn: 90deg;
  }
}

/* With a pointer, the page you point at stays; the rest steps back. */
@media (hover: hover) {
  .menu__nav ul:has(.menu__link:hover) .menu__link:not(:hover) {
    opacity: 0.3;
  }

  .menu__link:hover {
    translate: 0.06em 0;
  }
}

.menu__foot {
  grid-row: 3;
  display: grid;
  gap: 1.25rem;
}

/* No underline: the address rolls on hover (roll()), like the other links. */
.menu__mail {
  justify-self: start;
  font-size: clamp(1.125rem, 1.5vw, 1.375rem);
  letter-spacing: -0.02em;
}

.menu__meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem 1.5rem;
}

.menu__social {
  display: flex;
  gap: 1.25rem;
}

.menu__clock time {
  font-variant-numeric: tabular-nums;
}

/* ---- Motion ---- */

@media (prefers-reduced-motion: no-preference) {
  /* Behind the turning page: the inside of the prism, ink with the backdrop
     image (--menu-backdrop) over it, held to the screen. Only while the menu
     is in play; the switch happens while the page still covers the screen. */
  html.menu-3d {
    /* The picture at 1% opacity: an ink layer at 99% over it (a background
       image has no opacity of its own). */
    background:
      linear-gradient(rgb(24 25 26 / 0.99), rgb(24 25 26 / 0.99)) fixed,
      var(--menu-backdrop) center / cover no-repeat fixed var(--ink);
  }

  html.menu-3d body {
    background: transparent;
  }

  /* The turn itself is GSAP (src/modules/menu.ts writes the transforms; no
     transform transition here, it would fight them). CSS gives the axis, and
     the panel's starting position for the frame before the first write. */
  html.menu-3d :is(.nav__face, main, .footer) {
    transform-origin: var(--fold-x, 100%) var(--fold-y, 50vh);
    will-change: transform;
  }

  /* Sections far off screen are not drawn while the page is turned
     (src/modules/menu.ts markOffscreen): the turn of a long page stays smooth. */
  html.menu-3d [data-menu-offscreen] {
    visibility: hidden;
  }

  .menu {
    transform-origin: 0 50%;
    transform: translateX(100%) perspective(var(--menu-depth)) rotateY(var(--menu-fold));
  }

  /* The pages rise out of their line one after another as the panel lands.
     Closing, they stay put and the panel takes them away; .is-open goes only
     once the dialog has closed, so they reset unseen. */
  .menu__text {
    translate: 0 112%;
    transition: none;
  }

  .menu.is-open .menu__text {
    translate: 0 0;
    transition: translate 1.2s var(--ease-out-expo) calc(0.3s + var(--i, 0) * 0.055s);
  }

  .menu__foot,
  .menu__close {
    opacity: 0;
    transition: none;
  }

  .menu.is-open :is(.menu__foot, .menu__close) {
    opacity: 1;
    transition: opacity 0.9s var(--ease-out) 0.6s;
  }

  /* The × crosses as the panel arrives. */
  .menu__close-icon span {
    transition: none;
  }

  .menu:not(.is-open) .menu__close-icon span {
    rotate: 0deg;
  }

  .menu.is-open .menu__close-icon span {
    transition: rotate 0.9s var(--ease-out-expo) 0.55s;
  }
}

/* Taking over a state that is already on screen (src/modules/menu.ts handoff):
   nothing may animate in for that first frame. */
.menu.is-instant,
.menu.is-instant *,
.menu.is-instant::backdrop {
  transition: none !important;
}

/* While the page is turned it is one face: a case page's hero must not morph
   on its own in a page-to-page transition through the menu. */
html.menu-3d .case__hero {
  view-transition-name: none;
}

/* Page to page through the menu, the panel is captured on its own and held:
   both pages show it identically (.is-leaving below, the handoff on the next
   page), so the new one simply replaces the old and only the page under it
   cross-fades. Captured by name, it stays put whether or not a browser
   includes the top layer in the page's own snapshot. */
html.menu-open #menu,
html.menu-handoff #menu,
html.menu-onward #menu {
  view-transition-name: menu-panel;
}

::view-transition-old(menu-panel) {
  animation: none;
  opacity: 0;
}

::view-transition-new(menu-panel) {
  animation: none;
}

/* Page to page through the menu: no cross-fade. The
   picture of the page before stays on screen, cut down to what is left of the
   panel and on top of the new page (which waits out of view on the right of
   the panel, the handoff below). menu.ts turns it, the panel's picture and the
   new page on to the left together, so the page before is seen until the turn
   is done. Until it takes over, a do-nothing animation keeps the transition
   from ending. */
html.menu-onward::view-transition-old(root),
html.menu-onward::view-transition-new(root) {
  mix-blend-mode: normal;
  animation: none;
}

html.menu-onward::view-transition-old(root) {
  z-index: 1;
  clip-path: inset(0 var(--menu-w) 0 0);
  transform-origin: 0 0;
  animation: menu-hold 3s linear both;
}

html.menu-onward::view-transition-group(menu-panel) {
  transform-origin: 0 0;
}

@keyframes menu-hold {
  to {
    opacity: 1;
  }
}

/* Leaving through a menu link (src/modules/menu.ts): the hover look is
   dropped, so this menu matches the next page's menu exactly (which shows
   this page as the current one until it has turned away, index.html). */
.menu.is-leaving .menu__nav .menu__link {
  opacity: 1 !important;
  translate: none !important;
  transition: none;
}

.menu.is-leaving :is(.roll__prism, .menu__cube) {
  --turn: 0deg !important;
  transition: none;
}

/*
 * Arriving through the menu (index.html adds .menu-handoff with .menu-3d,
 * .menu-open and .menu-onward before the first paint): the menu is open,
 * exactly as the page before left it, and the new page waits on the far side
 * of the panel, the next face of the cylinder: joined to its right edge (the
 * right edge of the screen), turned away to the right, out of view. menu.ts
 * then carries the turn on to the left. Each fixed nav face states where the
 * axis (the screen's left edge, at half its height) is from its own box.
 */
@media (prefers-reduced-motion: no-preference) {
  html.menu-handoff :is(.nav__face, main, .footer) {
    transform: translateX(100vw) perspective(var(--menu-depth)) rotateY(var(--menu-fold));
  }

  html.menu-handoff :is(main, .footer, .scrollbar) {
    transform-origin: 0 50vh;
  }

  html.menu-handoff .nav__brand {
    transform-origin: calc(-1 * var(--edge)) 50vh;
  }

  html.menu-handoff .nav__contact {
    transform-origin: calc(100% + var(--edge) + var(--menu-btn-w) + 0.5rem - 100vw) 50vh;
  }

  html.menu-handoff .menu-btn {
    transform-origin: calc(100% + var(--edge) - 100vw) 50vh;
  }

  /* The panel, shown without being opened yet, its content already in place. */
  html.menu-handoff #menu {
    display: block;
    z-index: 60;
    transform: none;
  }

  html.menu-handoff #menu .menu__text {
    translate: none;
  }

  html.menu-handoff #menu :is(.menu__foot, .menu__close) {
    opacity: 1;
  }

  html.menu-handoff #menu .menu__close-icon span:first-child {
    rotate: 45deg;
  }

  html.menu-handoff #menu .menu__close-icon span:last-child {
    rotate: -45deg;
  }

  /* The shade the open dialog's backdrop will give. */
  html.menu-handoff body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 55;
    pointer-events: none;
    background: linear-gradient(to right, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.08) calc(100% - var(--menu-w)));
  }
}

@media (prefers-reduced-motion: reduce) {
  main,
  .footer,
  .menu::backdrop {
    transition: none;
  }
}

/*
 * Hero: the first screen. The statement and a narrow lead under it (from
 * column 8, the button below) sit centred in the space between the nav and a
 * full-bleed slider of the work along the bottom: every case's mockup, as
 * pictures (not links). About four tiles fit across on desktop and laptop,
 * about one and a quarter on phones; the whole slider is on screen on every
 * size. It slides on its own (src/modules/ribbon.ts); text and slider scroll
 * away together and Studio rises over it a little faster than the page
 * (scroll.css).
 */

:root {
  /* Image height: four 4:3 mockups across the screen (three gaps), never taller than 38svh. */
  --hero-strip: min(calc((100vw - 1.5rem) / 4 * 0.75), 38svh);
  /* Desktop and laptop: the whole strip sits on screen, a gap above the bottom edge. */
  --hero-peek: 0px; /* how much of the strip sits below the fold (phones only) */
  --hero-gap: 0.5rem;
}

.hero {
  --air: clamp(1.5rem, 4svh, 3rem);
  display: grid;
  /* minmax(0, 1fr): the strip is far wider than the screen and must not widen the column. */
  grid-template-columns: minmax(0, 1fr);
  /* The slider takes what the text leaves, up to its ideal height (about four
     tiles across) and never under 10rem; the text is centred in the rest. */
  grid-template-rows: 1fr minmax(min(10rem, var(--hero-strip)), var(--hero-strip));
  height: calc(100svh + var(--hero-peek));
  padding-bottom: var(--hero-gap);
}

/* Too short for both: let the hero grow instead of overlapping what follows. */
@media (max-height: 560px) {
  .hero {
    height: auto;
    min-height: calc(100svh + var(--hero-peek));
    grid-template-rows: auto var(--hero-strip);
  }
}

/* Centred in the free space: equal air above (below the nav) and below (above the strip). */
.hero__intro {
  align-self: center;
  padding-block: calc(var(--nav-h) + var(--air)) var(--air);
}

.hero__intro > .eyebrow {
  margin-bottom: clamp(1.25rem, 4svh, 3rem);
  animation: fade-in 0.9s var(--ease-out) 0.1s both;
}

/* The notice above it, when a blog post asks for one (src/render/hero.ts): a
   hairline box with a dot in the accent, the tag, the line and an arrow. The
   line rolls on hover like the other links; on a phone it may drop under the
   tag, and a line too long for the screen ends in an ellipsis. */
.notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.75rem;
  width: fit-content;
  max-width: 100%;
  margin-bottom: clamp(1rem, 3svh, 2rem);
  padding: 0.55rem 0.9rem 0.55rem 0.75rem;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  animation: fade-in 0.9s var(--ease-out) 0.05s both;
  transition: border-color var(--d-hover) var(--ease-out);
}

.notice__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.notice__tag::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--accent);
}

.notice__text {
  min-width: 0;
  max-width: 100%;
}

.notice__text .roll {
  max-width: 100%;
}

.notice__text .roll__face {
  overflow: hidden;
  text-overflow: ellipsis;
}

.notice__arrow {
  transition: translate var(--d-hover) var(--ease-out);
}

@media (hover: hover) {
  .notice:hover {
    border-color: currentColor;
  }

  .notice:hover .notice__arrow {
    translate: 0.25em 0;
  }
}

/* Phones: the notice takes the eyebrow's place (the lead under the statement
   says the same) and stays on one line, the dot standing in for its tag (the
   word is still there for screen readers), so the whole work strip still fits
   on the first screen. */
@media (max-width: 699px) {
  .notice {
    flex-wrap: nowrap;
    padding: 0.5rem 0.75rem;
  }

  .notice__word {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .hero__intro > .notice ~ .eyebrow {
    display: none;
  }
}

.hero__statement {
  font-size: min(8.4vw, 9.5rem, 15svh);
}

.hero__statement .line__inner {
  animation: rise var(--d-text) var(--ease-out-expo) both;
  animation-delay: calc(0.15s + var(--i, 0) * 0.13s);
}

/* The swapping word ("everyone" / "you"). The slot clips its letters so they
   rise in and out of it; the padding (cancelled by negative margins) makes room
   for the italic overhang and the descender of the y. Its width animates, so
   the rest of the line glides instead of jumping.
   It clips with clip-path, not overflow: Safari puts the baseline of an
   inline-block with overflow at its bottom edge, which lifted the word above
   the rest of the line. */
.swap {
  --pad-x: 0.12em;
  --pad-top: 0.1em;
  --pad-bottom: 0.3em;
  position: relative;
  display: inline-block;
  padding: var(--pad-top) var(--pad-x) var(--pad-bottom);
  margin: calc(-1 * var(--pad-top)) calc(-1 * var(--pad-x)) calc(-1 * var(--pad-bottom));
  clip-path: inset(0);
  white-space: nowrap;
}

/* inline-block so src/modules/swap.ts can read its exact layout width. */
.swap__sizer {
  display: inline-block;
  visibility: hidden;
}

.swap__word {
  position: absolute;
  top: var(--pad-top);
  left: var(--pad-x);
  white-space: nowrap;
}

.swap__char {
  display: inline-block;
}

/* The lead: a narrow block under the statement starting at column 8, the button below it. */
.hero__aside {
  display: grid;
  justify-items: start;
  gap: 1.25rem;
  width: min(24rem, 100%);
  margin-top: clamp(1rem, 3svh, 2rem);
  margin-left: calc((100% + var(--gutter)) * 7 / 12);
  animation: fade-in 1s var(--ease-out) 0.5s both;
}

.hero__lead {
  color: var(--fg-muted);
}

/* The strip (.ribbon in components.css) takes its height from its grid row. */
.hero__ribbon {
  min-height: 0;
  animation: fade-in 1.2s var(--ease-out) 0.7s backwards;
}

@keyframes rise {
  from {
    translate: 0 105%;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

@media (max-width: 1099px) {
  .hero__aside {
    margin-left: calc((100% + var(--gutter)) * 6 / 12);
  }
}

@media (max-width: 899px) {
  :root {
    /* Phones: about one and a quarter mockups across, the whole strip on the
       first screen with the same small gap under it as on desktop. */
    --hero-strip: min(calc((100vw - 0.5rem) / 1.25 * 0.75), 34svh);
  }

  /* Short phones: the hero grows rather than squeezing the text into the strip. */
  .hero {
    height: auto;
    min-height: 100svh;
  }

  .hero__statement {
    font-size: min(11vw, 9svh);
  }

  /* Phones: every line on the left edge (no stair step), and the changing
     word on a line of its own, so the words after it never move when it
     swaps ("iedereen" and "jij" are not the same width). */
  .hero__statement .line--step-1,
  .hero__statement .line--step-2 {
    padding-left: 0;
  }

  .hero__statement .swap {
    display: block;
    width: fit-content;
  }

  .hero__aside {
    width: 100%;
    margin-left: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero__statement .line__inner,
  .hero__intro > .eyebrow,
  .hero__aside,
  .hero__ribbon {
    animation: none;
  }
}

.studio__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-5);
}

.studio__label {
  grid-column: 1 / span 3;
  padding-top: 0.6em;
}

/* The introduction: the headline (its first line starting a quarter in, as on
   1st December, inking in word by word), then the two paragraphs and the
   closing line on the right half, with the portrait level with them on the
   left. Its parts sit on the section's own grid. */
.studio__intro {
  display: contents;
}

.studio__statement {
  grid-column: 4 / -1;
  grid-row: 1;
  text-indent: 25%;
  max-width: 16em;
  font-size: clamp(2.25rem, 4.6vw, 4.75rem);
  line-height: 1.02;
  letter-spacing: -0.04em;
}

.studio__body {
  grid-column: 7 / -1;
  grid-row: 2;
  align-self: start;
  display: grid;
  gap: 1.25rem;
  max-width: 34em;
}

.studio__body .body-l {
  color: var(--fg-muted);
}

.studio__closing {
  margin-top: 0.5rem;
  font-size: var(--t-lead);
  font-weight: var(--w-strong);
  letter-spacing: -0.02em;
}

/* The same button as the hero's, under the closing line (not stretched across the column). */
.studio__cta {
  justify-self: start;
  margin-top: 0.5rem;
}

/* The numbers, under the button, between hairlines (.stats, below). */
.studio__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: var(--space-4);
}

/* text-indent inherits: split words (inline-blocks) must not indent themselves. */
.studio__statement .word {
  text-indent: 0;
}

/* The portrait: a column in from the edge, its top level with the paragraphs. */
.studio__portrait {
  grid-column: 2 / span 3;
  grid-row: 2;
  align-self: start;
  display: grid;
  gap: 0.75rem;
}

.studio__portrait-frame {
  overflow: clip;
  aspect-ratio: 3 / 4;
  background: var(--media-bg);
}

.studio__portrait-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 30%;
  scale: 1.16;
}

/* A number and what it counts (About, above, and the stats blocks of the case
   write-ups, blocks.ts; stat() in html.ts). The cell keeps the hairline and
   stands still; the block inside it (number + label) is what rolls in. */
.stat {
  padding: 1.25rem var(--gutter) 0 0;
}

.stat .tumble {
  display: block;
}

.stat .tumble__face {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.stat + .stat {
  padding-left: var(--gutter);
}

/*
 * The hairlines, as lines of their own (not borders) so they can be drawn:
 * one along the top of the row, one before every cell but the first. They
 * draw themselves as the first block starts to roll in (enhance.ts adds
 * .is-in to the .tumble): the top line from left to right, each divider top
 * down with its own cell's roll. Without JS or with reduced motion they are
 * simply there.
 */
.stats {
  position: relative;
}

.stat {
  position: relative;
}

.stats::before,
.stat + .stat::before {
  content: "";
  position: absolute;
  pointer-events: none;
}

.stats::before {
  inset: 0 0 auto;
  height: 1px;
  background: var(--hairline-strong);
  transform-origin: left;
}

.stat + .stat::before {
  inset: 0 auto 0 0;
  width: 1px;
  background: var(--hairline);
  transform-origin: top;
}

@media (prefers-reduced-motion: no-preference) {
  html.js .stats::before {
    scale: 0 1;
    transition: scale 1.2s var(--ease-out);
  }

  html.js .stat + .stat::before {
    scale: 1 0;
    transition: scale 0.9s var(--ease-out) calc(var(--i, 0) * 0.12s);
  }

  html.js .stats:has(.tumble.is-in)::before,
  html.js .stat:has(> .tumble.is-in)::before {
    scale: 1;
  }
}

.stat__value {
  font-size: var(--t-h1);
  line-height: 0.9;
  letter-spacing: var(--tr-h1);
  font-variant-numeric: tabular-nums;
  color: var(--display-ink);
}

@media (max-width: 899px) {
  .studio__label,
  .studio__statement,
  .studio__body {
    grid-column: 1 / -1;
  }

  .studio__statement,
  .studio__body,
  .studio__portrait {
    grid-row: auto;
  }

  .studio__statement {
    text-indent: 0;
  }

  .studio__portrait {
    grid-column: 1 / span 8;
  }
}

.contrast__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-5);
  align-items: start;
}

.contrast__copy {
  grid-column: 1 / span 4;
  position: sticky;
  top: calc(var(--nav-h) + var(--space-4));
  display: grid;
  gap: var(--space-3);
}

.contrast__title {
  margin-top: var(--space-2);
}

.contrast__body {
  color: var(--fg-muted);
  max-width: 30em;
}

.lab {
  grid-column: 6 / -1;
  display: grid;
  gap: var(--space-3);
}

/* The stage: the image, a filtered copy clipped to the lens circle (the
   fallback), and the WebGPU canvas on top when it is available. */
.lab__stage {
  --lx: 50%;
  --ly: 50%;
  --lr: 0px;
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: clip;
  background: var(--media-bg);
  cursor: crosshair;
  touch-action: pan-y;
}

.lab__img,
.lab__sim,
.lab__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.lab__img {
  object-fit: cover;
}

.lab__sim {
  clip-path: circle(var(--lr) at var(--lx) var(--ly));
}

.lab__stage.is-whole .lab__sim {
  clip-path: none;
}

/* Fallback ring; the GPU draws its own. */
.lab__stage::after {
  content: "";
  position: absolute;
  top: var(--ly);
  left: var(--lx);
  width: calc(var(--lr) * 2);
  height: calc(var(--lr) * 2);
  translate: -50% -50%;
  border: 1px solid var(--accent);
  border-radius: 50%;
  pointer-events: none;
}

.lab__stage.is-gpu::after,
.lab__stage.is-whole::after {
  display: none;
}

.lab__canvas {
  opacity: 0;
  transition: opacity 0.5s var(--ease-out);
}

.lab__canvas.is-ready {
  opacity: 1;
}

/* The mode's name rides beside the lens, or sits in the corner for the whole image. */
.lab__lens {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  padding: 0.35rem 0.55rem;
  background: var(--accent);
  color: var(--ink);
  border-radius: var(--radius);
  pointer-events: none;
  opacity: 0;
  will-change: transform;
}

.lab__stage.is-whole .lab__lens {
  top: 0.75rem;
  left: auto;
  right: 0.75rem;
  opacity: 1;
}

.lab__controls {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
}

.lab__group {
  display: grid;
  gap: 0.75rem;
  align-content: start;
}

.lab__whole[aria-pressed="true"] {
  background: var(--fg);
  border-color: var(--fg);
  color: var(--bg);
}

.lab__note {
  max-width: 44em;
}

@media (max-width: 1199px) {
  .lab__controls {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 899px) {
  .contrast__copy,
  .lab {
    grid-column: 1 / -1;
  }

  .contrast__copy {
    position: static;
  }
}

/*
 * What we do (src/render/services.ts, src/modules/deal.ts). Dark. Without the
 * deal (phones, reduced motion, no JavaScript) the line sits above the three
 * cards, face up, in a row (in a column on phones); .is-dealing turns it into
 * a pinned stage with the line in its middle and the cards dealt onto a ring
 * round it, while the ring turns.
 */

.services {
  padding-bottom: var(--section-y);
}

.services__stage {
  display: grid;
  justify-items: center;
  gap: var(--space-5);
  padding-top: var(--section-y);
}

.services__center {
  display: grid;
  justify-items: center;
  gap: 1.25rem;
  text-align: center;
}

/* The italic half gets a line of its own, so the line breaks at its comma
   on every screen (never one word left alone on a phone). */
.services__lead em {
  display: block;
}

.services__lead {
  max-width: 10em;
  font-size: clamp(2.25rem, 4vw, 4rem);
  line-height: 1.02;
  letter-spacing: -0.045em;
}

.services__ring {
  width: 100%;
}

.services__cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--gutter);
}

.deal {
  width: min(15rem, 80vw);
}

/* ---- The card: a playing card, title in two corners, the second upside down ---- */

.svc {
  container-type: inline-size;
  aspect-ratio: 5 / 7;
  perspective: 50rem;
}

.svc__turn {
  position: relative;
  height: 100%;
  transform-style: preserve-3d;
}

.svc__face {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: 7cqi;
  border-radius: 5cqi;
  backface-visibility: hidden;
}

.svc__front {
  background: var(--paper);
  color: var(--ink);
}

.svc__back {
  place-items: center;
  background: var(--accent);
  color: var(--ink);
  transform: rotateY(180deg);
}

/* A thin inset frame on the back, like a printed card. */
.svc__back::before {
  content: "";
  position: absolute;
  inset: 5cqi;
  border: 1px solid rgb(24 25 26 / 0.35);
  border-radius: 3cqi;
}

.svc__mark {
  grid-row: 1 / -1;
  width: 30%;
  height: auto;
}

.svc__corner {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 6cqi;
}

.svc__corner--turned {
  rotate: 180deg;
}

.svc__title {
  max-width: 9em;
  font: var(--w-strong) clamp(0.6875rem, 7.5cqi, 1rem) / 1.2 var(--font-display);
  letter-spacing: -0.01em;
}

.svc__bar {
  flex: none;
  width: 14cqi;
  height: auto;
}

.svc__line {
  align-self: center;
  font-size: clamp(0.75rem, 8.6cqi, 1.125rem);
  line-height: 1.3;
  letter-spacing: -0.015em;
}

/* ---- The deal: the pinned ring ---- */

.services.is-dealing .services__pin {
  height: 500svh; /* long enough that the ring turns slowly, every card upright once (deal.ts) */
  view-timeline: --deal block;
  view-timeline-inset: 0; /* not the page's scroll-padding: entry ends exactly where the stage pins */
}

.services.is-dealing .services__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  place-items: center;
  padding: 0;
  overflow: clip;
}

/* The line stays in the middle of the stage the whole time, on top of the
   cards: first alone, then with the cards round it. White with
   difference blending, like the nav: white on the dark stage, ink where a
   card turns under it. */
.services.is-dealing .services__center {
  position: absolute;
  inset: 50% auto auto 50%;
  z-index: 1;
  translate: -50% -50%;
  width: min(30rem, 80vw);
  color: #fff;
  mix-blend-mode: difference;
}

/* The cards stand on a ring nearly as tall as the screen, with room left
   above and below it; each card's top edge on its rim. */
.services.is-dealing .services__ring {
  position: relative;
  width: min(calc(100svh - 7rem), 92vw);
  aspect-ratio: 1;
}

.services.is-dealing .services__cards {
  position: absolute;
  inset: 0;
  display: block;
}

/* What Draggable turns by hand (deal.ts): an empty layer the size of the ring,
   so it turns round the same middle; the ring follows it. */
.services__handle {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Three cards, evenly round the ring (120° apart), the first upright at the
   top. Each slot turns round the middle of the ring.
   --to-centre is how far a card is from the middle of the ring, in its own
   height (half the ring less half a card, which is 1.4 times its width): the
   cards are dealt out from there (scroll.css). */
.services.is-dealing .deal {
  position: absolute;
  inset: 0;
  width: auto;
  rotate: calc(var(--i) * 120deg);
}

.services.is-dealing .svc {
  --to-centre: 82%;
  width: 27%;
  margin-inline: auto;
}

/* Phones: no ring (deal.ts). The line, then the cards side by side in a row
   you swipe, edge to edge, the next card peeking in from the right so it is
   clear there is more (one card tall, not three under each other). They
   rise in as they come into view (scroll.css). */
@media (max-width: 699px) {
  .services__lead {
    font-size: 2rem;
  }

  .services__ring {
    width: 100vw;
  }

  .services__cards {
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: var(--space-2);
    padding-inline: var(--page-x);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-x);
    scrollbar-width: none;
  }

  .services__cards::-webkit-scrollbar {
    display: none;
  }

  .deal {
    flex: none;
    width: min(15rem, 68vw);
    scroll-snap-align: start;
  }
}
/* Process (src/render/process-variants.ts). Where the screen is big enough, a
   full-screen stage that stays put while you scroll through the section, in
   beats (src/modules/process-words.ts):
     - "From first question" in its middle, then "to launch" rolls up under
       it (.is-launch);
     - the title fades out (.is-out) and the three steps roll in one by one
       (.is-in), corner to corner: left, middle, right;
     - the steps roll back down and the call to action rolls up in their
       place (.is-ready), with pictures of the work coming in round it.
   On phones and short screens: no stage; the line, the steps, the call to
   action and a row of pictures stand in order, each step rolling in as it
   comes into view. With reduced motion or no JavaScript, everything simply
   stands. */

.pv-words {
  padding-block: var(--section-y);
}

.pv-words :is(h2, h3) {
  text-wrap: balance;
}

.pv-words p {
  text-wrap: pretty;
}

/* The opening line, centred. */
.pv-words__line {
  max-width: 14em;
  margin: 0 auto var(--space-6);
  text-align: center;
}

.pv-words__block,
.pv-words__face,
.pv-words__first,
.pv-words__launch {
  display: block;
}

.pv-words__list {
  display: grid;
  gap: clamp(0.5rem, 1.5vw, 1.5rem);
}

/* The three steps run corner to corner across the full width: the first on
   the left, the second in the middle, the third against the right edge.
   Each step is only as wide as its word and line. */
.pv-words__item {
  display: grid;
  grid-template-columns: max-content minmax(0, 19rem);
  column-gap: clamp(1.5rem, 3vw, 3rem);
  align-items: end;
  justify-self: start;
}

.pv-words__item--2 {
  justify-self: center;
}

.pv-words__item--3 {
  justify-self: end;
}

.pv-words__step {
  font-size: var(--t-display);
  line-height: 1.05;
  letter-spacing: var(--tr-display);
  color: var(--display-ink);
}

.pv-words__summary {
  font-size: var(--t-body-l);
  padding-bottom: 0.6em;
}

/* The call to action. */
.pv-cta {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  max-width: 44rem;
  margin: var(--space-7) auto 0;
  text-align: center;
}

.pv-cta__intro {
  max-width: 26em;
}

.pv-cta__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-2);
}

/* The pictures of the work. In flow (phones, short screens): a row that
   scrolls sideways. On the stage they are scattered round the call to
   action (below). No shadows: they sit on the page like the rest of it. */
.pv-shots {
  display: flex;
  gap: var(--gutter);
  margin-top: var(--space-5);
  padding: var(--space-2) var(--page-x) 0;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--page-x);
  scrollbar-width: none;
}

.pv-shot {
  flex: 0 0 min(62vw, 22rem);
  scroll-snap-align: start;
}

/* Square, like the hero's slider and every other picture on the site. */
.pv-shot__link {
  display: block;
}

.pv-shot__img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--media-bg);
}

/* The hero's slider (src/render/ribbon.ts), only on phones (below), where it
   takes the place of the pictures. */
.pv-ribbon {
  display: none;
}

.pv-shot__link:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: no-preference) {
  /* Each word waits on the underside of its block (components.css .tumble)
     until its step is in; then the block rolls it up into place and its
     line follows. Going back, they roll down again. */
  html.js .pv-words__item.is-in .tumble__prism {
    --turn: 90deg;
  }

  html.js .pv-words__summary {
    transition:
      opacity 0.8s var(--ease-out) calc(0.3s + var(--i, 0) * 0.12s),
      translate 0.8s var(--ease-out-expo) calc(0.3s + var(--i, 0) * 0.12s);
  }

  html.js .pv-words__item:not(.is-in) .pv-words__summary {
    opacity: 0;
    translate: 0 1rem;
    transition-duration: 0.3s;
    transition-delay: 0s;
  }

  /* The call to action's title is on a block too, two lines tall (so half a
     block deep is one line), and rolls up the same way. */
  html.js .pv-cta .tumble__prism {
    --depth: 1lh;
  }

  html.js .pv-words.is-ready .pv-cta .tumble__prism {
    --turn: 90deg;
  }

  /* Leaving, the opening line simply fades out, both parts together (only
     "to launch" coming in rolls, below). */
  html.js .pv-words__line {
    transition: opacity 0.6s var(--ease-out);
  }

  html.js .pv-words.is-out .pv-words__line {
    opacity: 0;
  }

  /* Its second part ("to launch") waits under its own block and rolls up
     under the first on its beat. */
  html.js .pv-words.is-launch .pv-words__launch .tumble__prism {
    --turn: 90deg;
  }

  /* The stage ----------------------------------------------------------- */
  @media (min-height: 640px) and (min-width: 769px) {
    html.js .pv-words {
      height: 460svh;
      padding-block: 0;
      view-timeline: --process block;
    }

    html.js .pv-words__stage {
      position: sticky;
      top: 0;
      height: 100svh;
      display: grid;
      align-items: center;
      padding-block: calc(var(--nav-h) + var(--space-3)) var(--space-4);
      overflow: clip;
    }

    /* The line, the steps and the call to action share one cell, in front
       of the pictures. */
    html.js .pv-words__grid {
      position: relative;
      z-index: 1;
      display: grid;
      align-items: center;
    }

    html.js :is(.pv-words__line, .pv-words__list, .pv-cta) {
      grid-area: 1 / 1;
    }

    html.js .pv-words__line {
      margin-bottom: 0;
      pointer-events: none;
    }

    html.js .pv-words__step {
      font-size: min(var(--t-display), 15svh);
    }

    html.js .pv-cta {
      justify-self: center;
      margin: 0;
    }

    /* Until its beat, the call to action waits: its title under its block,
       the rest faded out (still reachable by keyboard: process-words.ts
       brings the stage to it on focus). */
    html.js :is(.pv-cta__intro, .pv-cta__actions) {
      transition:
        opacity 0.8s var(--ease-out) 0.35s,
        translate 0.8s var(--ease-out-expo) 0.35s;
    }

    html.js .pv-words:not(.is-ready) :is(.pv-cta__intro, .pv-cta__actions) {
      opacity: 0;
      translate: 0 1rem;
      pointer-events: none;
      transition-duration: 0.3s;
      transition-delay: 0s;
    }

    /* The pictures, scattered round the call to action over the whole
       stage, each a little tilted. They come in one after the other on its
       beat and drift up slowly while the stage holds. Tilt in `rotate`,
       drift in `translate`, coming in in `scale`. No hover effect. */
    html.js .pv-shots {
      position: absolute;
      inset: calc(var(--nav-h) + var(--space-2)) var(--page-x) var(--space-3);
      display: block;
      margin: 0;
      padding: 0;
      overflow: visible;
    }

    html.js .pv-shot {
      position: absolute;
      left: var(--x);
      top: var(--y);
      width: var(--w);
      rotate: var(--tilt);
      transition:
        opacity 0.7s var(--ease-out) calc(0.2s + var(--n) * 0.07s),
        scale 0.9s var(--ease-out-expo) calc(0.2s + var(--n) * 0.07s);
      animation: pv-drift linear both;
      animation-timeline: --process;
      animation-range: contain 55% contain 100%;
    }

    html.js .pv-words:not(.is-ready) .pv-shot {
      opacity: 0;
      scale: 0.88;
      pointer-events: none;
      transition-duration: 0.3s;
      transition-delay: 0s;
    }

    .pv-shot--1 {
      --x: 1%;
      --y: 6%;
      --w: 19%;
      --tilt: -5deg;
      --drift: 1.5rem;
    }

    .pv-shot--2 {
      --x: 79%;
      --y: 6%;
      --w: 20%;
      --tilt: 4deg;
      --drift: 1.5rem;
    }

    .pv-shot--3 {
      --x: 3%;
      --y: 60%;
      --w: 17%;
      --tilt: 4deg;
      --drift: 4.5rem;
    }

    .pv-shot--4 {
      --x: 80%;
      --y: 58%;
      --w: 18%;
      --tilt: -4deg;
      --drift: 2.5rem;
    }

    .pv-shot--5 {
      --x: 19%;
      --y: 78%;
      --w: 13%;
      --tilt: -3deg;
      --drift: 1.5rem;
    }

    .pv-shot--6 {
      --x: 58%;
      --y: 4%;
      --w: 13%;
      --tilt: 3deg;
      --drift: 1.5rem;
    }

  }
}

@keyframes pv-drift {
  from {
    translate: 0 var(--drift, 0);
  }

  to {
    translate: 0 calc(var(--drift, 0) * -1);
  }
}

/* Narrower screens */

@media (max-width: 1099px) {
  .pv-words__item {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-1);
  }

  .pv-words__summary {
    max-width: 26em;
  }
}

/* Phones (redone 9 Nov 2026, Jelte disliked the stepped, centred version):
   one left edge for everything, and the steps as a timeline. A hairline runs
   down the left with a dot in the accent at each step; each step is its
   week, its name (a heading, not display size) and its line. Every step
   fades up as it comes into view (scroll.css-style view timeline), so the
   section is short and calm to scroll. */
@media (max-width: 768px) {
  .pv-words__line {
    max-width: none;
    margin: 0 0 var(--space-5);
    text-align: left;
  }

  .pv-words__list {
    --rail: 1.5rem; /* room left of the steps for the line and the dots */
    position: relative;
    gap: var(--space-5);
    padding-left: var(--rail);
  }

  /* The line, from the first dot to the last. */
  .pv-words__list::before {
    content: "";
    position: absolute;
    top: 0.9rem;
    bottom: 0.9rem;
    left: 0.3rem;
    width: 1px;
    background: var(--hairline-strong);
  }

  .pv-words__item,
  .pv-words__item--2,
  .pv-words__item--3 {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-1);
    justify-self: stretch;
    padding-left: 0;
  }

  /* The dot on the line, level with the step's name. */
  .pv-words__item::before {
    content: "";
    position: absolute;
    top: 0.75rem;
    left: calc(-1 * var(--rail) + 0.3rem - 0.3rem);
    width: 0.65rem;
    height: 0.65rem;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 4px var(--bg);
  }

  .pv-words__step {
    font-size: clamp(2rem, 9vw, 2.75rem);
    line-height: 1.1;
  }

  /* The week above the line of text, as on the pricing and FAQ pages. */
  .pv-words__when {
    margin-bottom: 0.35em;
  }

  .pv-words__summary {
    max-width: 30em;
    padding-bottom: 0;
  }

  /* The call to action on the same left edge. */
  .pv-cta {
    justify-items: start;
    margin: var(--space-6) 0 0;
    text-align: left;
  }

  .pv-cta__actions {
    justify-content: flex-start;
  }

  /* Under the call to action: the hero's slider, the same size as in the hero
     on a phone (hero.css --hero-strip), full bleed, in place of the pictures. */
  .pv-shots {
    display: none;
  }

  .pv-ribbon {
    display: block;
    height: min(calc((100vw - 0.5rem) / 1.25 * 0.75), 34svh);
    margin-top: var(--space-5);
  }

  /* Each step fades up as it comes into view; no rolling blocks on a phone. */
  html.js .pv-words .tumble__prism {
    --turn: 90deg;
    transition: none;
  }

  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
      .pv-words__item {
        animation: pv-step-in linear both;
        animation-timeline: view();
        animation-range: entry 10% entry 70%;
      }

      html.js .pv-words__item:not(.is-in) .pv-words__summary {
        opacity: 1;
        translate: none;
      }
    }
  }

  /* As much room under the pictures, to the next section, as above them,
     to the buttons (.pv-shots: margin-top space-5 plus padding-top space-2). */
  .pv-words {
    padding-bottom: calc(var(--space-5) + var(--space-2));
  }

  /* The pictures drift from right to left on their own, in a loop
     (process-words.ts doubles them and sets .is-marquee; with reduced motion
     or no JavaScript the row stays a row you swipe). Every picture carries
     its gap as padding, so the doubled row is exactly twice one set wide and
     moving each picture back one set's width (--count of them) loops without
     a seam. Transform only, so it runs on the compositor; it stops while off
     screen (.is-paused), while a finger is on it and while a picture has
     keyboard focus (no :hover: on phones it sticks after a tap). */
  @media (prefers-reduced-motion: no-preference) {
    /* Paused by the visitor (core/pause.ts): the pictures stand still. */
    html.motion-paused .pv-shots.is-marquee .pv-shot {
      animation-play-state: paused;
    }

    html.js .pv-shots.is-marquee {
      gap: 0;
      overflow: hidden;
      scroll-snap-type: none;
    }

    html.js .pv-shots.is-marquee .pv-shot {
      flex-basis: calc(min(62vw, 22rem) + var(--gutter));
      padding-inline-end: var(--gutter);
      animation: pv-marquee var(--marquee-time, 40s) linear infinite;
    }

    html.js .pv-shots.is-marquee:is(.is-paused, :active, :focus-visible, :has(:focus-visible)) .pv-shot {
      animation-play-state: paused;
    }
  }
}

@keyframes pv-marquee {
  to {
    translate: calc(var(--count, 6) * -100%) 0;
  }
}

/* The lab page around it */

.pv-lab__head {
  display: grid;
  gap: var(--space-3);
  padding-top: calc(var(--nav-h) + var(--space-7));
  padding-bottom: var(--space-6);
}

.pv-lab__head > p {
  max-width: 34em;
  text-wrap: pretty;
}

/* A phone's step coming into view (above). */
@keyframes pv-step-in {
  from {
    opacity: 0;
    translate: 0 1.5rem;
  }
}
.contact {
  background: var(--bg);
  color: var(--fg);
  padding-block: var(--section-y) var(--space-6);
}

.contact__head {
  display: flex;
  justify-content: space-between;
  gap: var(--gutter);
  margin-bottom: var(--space-5);
}

.contact__clock time {
  font-variant-numeric: tabular-nums;
}

.contact__statement {
  font-size: min(7.4vw, 13svh);
}

.contact__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

.contact__intro {
  grid-column: 1 / span 4;
  color: var(--fg-muted);
}

.contact__email {
  grid-column: 6 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.25rem;
}

/* The address rolls on hover (roll()), like every other link. */
.contact__address {
  font-size: var(--t-h2);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h2);
}

[data-copy-label] {
  display: inline-block;
}

.contact__actions {
  grid-column: 6 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.75rem;
}

@media (max-width: 899px) {
  .contact__statement {
    font-size: 10vw;
  }

  .contact__intro,
  .contact__email,
  .contact__actions {
    grid-column: 1 / -1;
  }

  .contact__head {
    flex-direction: column;
    gap: 0.5rem;
  }
}

/*
 * The footer: dark and in one piece (src/render/chrome.ts). The invitation to
 * start a project, one quiet row of links, and the studio's name as wide as
 * the content.
 */

.footer {
  overflow: clip;
  padding-top: var(--section-y);
}

/* ---- The invitation: the line on the left at heading size, how to reach
   the studio on the right, their bottoms level ---- */

.footer__invite {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-5);
  align-items: end;
  padding-bottom: var(--section-y);
}

.footer__meta {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  gap: var(--gutter);
}

/* Full white, its italic second line muted. Its words ink in from faint as
   the footer comes up (data-words, scroll.css). */
.footer__statement {
  grid-column: 1 / span 7;
  font-size: var(--t-h2);
  line-height: 1.05;
  letter-spacing: -0.035em;
}

.footer__statement > span {
  display: block;
}

.footer__statement em {
  color: var(--fg-muted);
}

.footer__reach {
  grid-column: 9 / -1;
  display: grid;
  gap: 1.5rem;
}

.footer__intro {
  max-width: 30em;
  color: var(--fg-muted);
}

.footer__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.75rem;
}

.footer__address {
  font-size: var(--t-body-l);
}

/* ---- The links ---- */

.footer__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75rem 2rem;
  padding-block: var(--space-3);
  border-top: 1px solid var(--hairline);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
}

.footer__legal {
  margin-left: auto;
}

/* ---- The name, as wide as the content (the same box as .wrap) ----
   text-fit: grow (Chrome 150+) scales it to fill the line exactly. Elsewhere
   the size is worked out from the font: "Driezie Studio" in Geist at weight
   600 and this tracking is 5.915 em wide, so the width × 0.165 fills about
   98% of the line, and grow takes it the rest of the way. A little air
   between it and the row of links above. */
.footer__name {
  max-width: var(--maxw);
  margin: var(--space-3) auto 0;
  padding-inline: var(--page-x);
  font-size: calc((min(100vw, var(--maxw)) - 2 * var(--page-x)) * 0.165);
  font-weight: 600;
  line-height: 0.78;
  letter-spacing: -0.055em;
  white-space: nowrap;
  text-fit: grow;
  color: var(--fg);
  /* The line ends at the baseline: no empty descender band under the name. */
  margin-bottom: -0.04em;
  /* Only a picture of the name (aria-hidden): its letters reach up over the
     row of links above (the line is shorter than the type), so it must not
     catch clicks or taps there, and it never gets selected or highlighted. */
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

@media (max-width: 899px) {
  .footer__statement,
  .footer__reach {
    grid-column: 1 / -1;
  }

  .footer__legal {
    margin-left: 0;
  }
}

/* Pages beyond the homepage: Work, case pages, About, Contact. */

/* ---- Shared ---- */

[data-words] .word {
  text-indent: 0;
}

.page-head {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-3);
  align-items: end;
  padding-top: calc(var(--nav-h) + var(--space-6));
  padding-bottom: var(--space-6);
}

.page-head .eyebrow {
  grid-column: 1 / -1;
}

.page-head__title {
  grid-column: 1 / span 7;
}

.page-head__intro {
  grid-column: 8 / -1;
  max-width: 26em;
  padding-bottom: 0.6em;
  color: var(--fg-muted);
}

/* ---- Work: the parade of cases (src/render/pages/work.ts, modules/parade.ts) ----
   On the light page. Without the parade (phones, reduced motion, no
   JavaScript) the page head sits above a plain grid of the cases: one column
   on a phone, each case rising in as it comes into view (scroll.css). With
   the parade, the head and the
   parade share one screen (the head on top, the track filling the rest) that
   pins for half a screen of scroll per step from one case to the next; every
   case waits at the middle of the track's left edge, at nothing, for
   parade.ts to move it. */

.parade__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: var(--space-5) var(--gutter);
  padding-inline: var(--page-x);
  padding-bottom: var(--section-y);
}

.parade__link {
  display: grid;
  gap: 0.85rem;
}

.parade__media {
  display: block;
  overflow: clip;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  background: var(--media-bg);
}

.parade__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  scale: 1.08;
  transition: scale 0.9s var(--ease-out-expo);
}

@media (hover: hover) {
  .parade__link:hover .parade__media img {
    scale: 1.16;
  }
}

.parade__caption {
  display: grid;
  gap: 0.2rem;
}

.parade__title {
  font-size: var(--t-lead);
  font-weight: var(--w-strong);
  line-height: 1.2;
  letter-spacing: -0.015em;
}

@media (prefers-reduced-motion: no-preference) and (min-width: 769px) {
  html.js .parade__pin {
    height: calc((var(--count) - 1) * 50svh + 100svh);
  }

  html.js .parade__stage {
    position: sticky;
    top: 0;
    display: flex;
    flex-direction: column;
    height: 100svh;
    overflow: clip;
  }

  html.js .parade__stage > .page-head {
    flex: none;
    padding-top: calc(var(--nav-h) + var(--space-4));
    padding-bottom: var(--space-2);
  }

  /* A size container: the cases are sized to the height it leaves (cqh). */
  html.js .parade__track {
    position: relative;
    flex: 1;
    min-height: 0;
    container-type: size;
  }

  html.js .parade__list {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
  }

  /* As big as the track allows: the 4:3 picture and its caption (about
     3.5rem) fill 90% of its height, and never more than 36% of the width. */
  html.js .parade__item {
    position: absolute;
    top: 50%;
    left: 0;
    width: min(36vw, calc((90cqh - 3.5rem) * 4 / 3));
    transform: scale(0); /* until parade.ts places it (no translate here: GSAP would read it in as pixels) */
    will-change: transform;
  }
}

/* The label that follows the pointer over a case: its name
   (src/modules/pointer.ts). The outer element only moves (GSAP sets its
   transform, centred on the pointer); the pill inside only pops in and out. */
.card-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 30;
  pointer-events: none;
}

.card-cursor__pill {
  display: block;
  padding: 0.55rem 0.7rem;
  background: var(--accent);
  color: var(--ink);
  border-radius: var(--radius);
  white-space: nowrap;
  opacity: 0;
  scale: 0.4;
  transition:
    opacity 0.2s var(--ease-out),
    scale 0.35s var(--ease-out);
}

.card-cursor.is-on .card-cursor__pill {
  opacity: 1;
  scale: 1;
}

@media (prefers-reduced-motion: reduce) {
  .card-cursor__pill {
    scale: 1;
    transition: opacity 0.15s linear;
  }
}

/* ---- Case page (src/render/pages/case.ts) ----
   Light, like Work and Contact: the head (the way back above the name, the
   summary beside it, as on /work/), the cover with the facts beside it, the
   write-up, the next case. */

.case__head {
  padding-top: calc(var(--nav-h) + var(--space-5));
  padding-bottom: var(--space-5);
}

.case__back {
  grid-column: 1 / -1;
  justify-self: start;
  margin-bottom: var(--space-2);
}

.case__intro {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  align-items: start;
}

/* The cover, in a 4:3 frame like the pictures on /work/. The frame is the
   shared element (view-transition-name), not the picture in it: the picture
   you clicked there morphs into it, its clip and corners carried along, and
   as both frames are 4:3 nothing is cropped on the way. Inside, the picture
   sits at the same scale as there, so it lands without a jump. */
.case__hero {
  grid-column: 1 / span 8;
  aspect-ratio: 4 / 3;
  overflow: clip;
  border-radius: var(--radius);
  background: var(--media-bg);
  view-transition-name: case-media;
}

.case__hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  scale: 1.08;
  transition: scale 0.9s var(--ease-out-expo);
}

/* Arriving from a picture that was zoomed in under the pointer (hover on
   /work/): the cover starts at that zoom and settles while the frame moves
   (index.html sets --case-from and takes the class off as the morph starts). */
html.case-morph .case__hero img {
  scale: var(--case-from, 1.08);
}

.case__facts {
  grid-column: 10 / -1;
  display: grid;
  border-top: 1px solid var(--hairline-strong);
}

.case__facts > div {
  display: grid;
  gap: 0.45rem;
  padding-block: 1rem 1.15rem;
  border-bottom: 1px solid var(--hairline);
}

.case__body {
  display: grid;
  gap: var(--space-6);
  padding-block: var(--space-7);
}

/* The write-up, as the homepage's introduction: the lead large across the
   page (inking in word by word, scroll.css), the rest of the text in the
   right half under it; the pictures across. */
.case__body .cb-prose--lead p:first-child {
  font-size: clamp(1.625rem, 2.9vw, 2.875rem);
  line-height: 1.12;
  letter-spacing: -0.028em;
}

@media (min-width: 900px) {
  .case__body .cb-prose--lead p:first-child {
    grid-column: 1 / span 10;
  }

  .case__body .cb-prose p,
  .case__body .cb-list ul {
    grid-column: 7 / -1;
  }
}

/* The next case: its name on the left, its picture (4:3, so it morphs into
   that page's cover as on /work/) on the right. */
.case__next {
  padding-bottom: var(--section-y);
}

.case__next-link {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

.case__next-text {
  grid-column: 1 / span 6;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.case__next-title {
  margin-top: auto;
  font-size: var(--t-h1);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-h1);
  color: var(--display-ink);
}

.case__next-arrow {
  display: inline-block;
  transition: translate var(--d-hover) var(--ease-out);
}

.case__next-media {
  grid-column: 7 / -1;
  display: block;
  aspect-ratio: 4 / 3;
  overflow: clip;
  border-radius: var(--radius);
  background: var(--media-bg);
}

.case__next-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  scale: 1.08;
  transition: scale 0.9s var(--ease-out-expo);
}

@media (hover: hover) {
  .case__next-link:hover .case__next-media img {
    scale: 1.16;
  }

  .case__next-link:hover .case__next-arrow {
    translate: 0.3em 0;
  }
}

/* ---- Case write-up blocks ---- */

.cb-prose {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 1em;
}

.cb-prose p {
  grid-column: 4 / span 6;
  font-size: var(--t-body-l);
  line-height: 1.6;
  color: var(--fg-muted);
}

/* Bold in running text (**words** in a post): the same face, heavier and in
   full ink, e.g. the question that opens each paragraph. */
.cb-prose strong,
.cb-list strong {
  font-weight: var(--w-strong);
  color: var(--fg);
}

/* A link in running text rolls on hover like every other link. At rest it
   keeps a thin underline (on the roll's faces, so it rolls along), so it is
   never told apart from the words around it by colour alone. */
.cb-link {
  color: var(--fg);
}

.cb-link .roll__face {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

.cb-prose--lead p:first-child {
  grid-column: 3 / span 8;
  font-size: var(--t-statement);
  line-height: 1.2;
  letter-spacing: -0.022em;
  color: var(--fg);
}

.cb-media {
  display: grid;
  gap: 0.75rem;
}

.cb-frame {
  overflow: clip;
  background: var(--media-bg);
}

.cb-frame img {
  width: 100%;
}

.cb-frame video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--ratio);
  object-fit: cover;
}

.cb-media--phone {
  width: min(22rem, 100%);
  margin-inline: auto;
}

.cb-two {
  display: grid;
  gap: var(--gutter);
  align-items: start;
}

.cb-two .cb-media--phone {
  width: auto;
  margin: 0;
}

.cb-quote {
  display: grid;
  justify-items: center;
  gap: 1.25rem;
  padding-block: var(--space-5);
  text-align: center;
}

.cb-quote__text {
  margin: 0;
  max-width: 16em;
  font-family: var(--font-editorial);
  font-style: italic;
  font-size: var(--t-h1);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--display-ink);
}

.cb-list {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
}

.cb-list ul {
  grid-column: 4 / span 6;
  display: grid;
  gap: 0.85rem;
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

.cb-list li {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
}

.cb-list li::before {
  content: "→";
  color: var(--fg-faint);
}

/* Its hairlines are drawn by .stats (studio.css). */
.cb-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.cb-chart {
  display: grid;
  gap: var(--space-3);
}

.cb-chart__bars {
  display: grid;
  gap: 0.6rem;
}

.cb-chart__row {
  display: grid;
  grid-template-columns: minmax(10rem, 16rem) 1fr;
  gap: var(--gutter);
  align-items: center;
}

.cb-chart__row dd {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.cb-chart__track {
  position: relative;
  flex: 1;
  height: 0.9rem;
}

.cb-chart__bar {
  display: block;
  width: var(--w);
  height: 100%;
  background: var(--fg);
  transform-origin: 0 50%;
}

.has-threshold .cb-chart__track::after {
  content: "";
  position: absolute;
  inset: -0.3rem auto -0.3rem var(--threshold);
  border-left: 1px dashed var(--accent-text);
}

.cb-chart__value {
  min-width: 5ch;
  color: var(--fg-muted);
}

.cb-chart__legend {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.cb-chart__key {
  width: 0;
  height: 1rem;
  border-left: 1px dashed var(--accent-text);
}

.cb-decisions {
  display: grid;
  gap: var(--space-3);
}

.cb-decisions__head,
.cb-decisions__row {
  display: grid;
  grid-template-columns: 3rem repeat(3, minmax(0, 1fr));
  gap: var(--gutter);
}

.cb-decisions ol {
  border-top: 1px solid var(--hairline-strong);
}

.cb-decisions__row {
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--hairline);
  align-items: baseline;
}

/* ---- Contact page (src/render/pages/contact.ts) ----
   The first screen is at least as tall as the window: the title and its
   lede at the top, the work slider under them edge to edge, then the ways to
   reach the studio and the facts (pushed to the bottom when there is room
   to spare). Then the form beside its tips. */

.cpage {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
  padding-top: calc(var(--nav-h) + var(--space-4));
  padding-bottom: var(--space-4);
}

/* Desktop folds the page away as the footer comes up, and the nav hides Menu
   while it does (nav.css, modules/fold.ts). This page is only a screen
   tall, so the footer came up at the first scroll and Menu was gone almost
   at once. Room under the facts keeps the footer away until the whole
   section has been scrolled through; the first screen stays as it was.
   Phones have no fold. */
@media (min-width: 769px) {
  .cpage {
    padding-bottom: max(var(--section-y), 40svh);
  }
}

/* The homepage's slider at about two thirds of its height: six mockups across
   on desktop, never under 7.5rem (two and a bit across on phones). */
.cpage__ribbon {
  --cpage-strip: min(max(calc((100vw - 3rem) / 6 * 0.75), 7.5rem), 20svh);
  height: var(--cpage-strip);
  margin-top: var(--space-5);
}

.cpage__reach {
  margin-top: auto;
}

.cpage__head {
  display: flex;
  justify-content: space-between;
  gap: var(--gutter);
}

.cpage__top {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  align-items: end;
  margin-top: var(--space-4);
}

/* A step down from display, so the title stays on two lines and the rows
   below still fit on the first screen. */
.cpage__title {
  grid-column: 1 / span 7;
  font-size: var(--t-h1);
}

.cpage__line {
  display: block;
}

/* Write a message (the form below) and Start a project (the onboarding). */
.cpage__buttons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.cpage__lede {
  grid-column: 8 / -1;
  display: grid;
  justify-items: start;
  gap: var(--space-3);
  max-width: 28em;
  padding-bottom: 0.6em;
}

/* The ways: one row each between hairlines. The row is the link (the link's
   ::after covers it); its text rolls and the arrow steps on hover. */
.ways {
  padding-top: var(--space-4);
}

.way {
  position: relative;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 0.75rem;
  align-items: baseline;
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--hairline);
}

.way:first-child {
  border-top: 1px solid var(--hairline-strong);
}

.way__main {
  grid-column: 1 / span 11;
  min-width: 0;
}

.way__link {
  display: inline-block;
  max-width: 100%;
  font-size: min(var(--t-h2), 7.2vw);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h2);
}

.way__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.way__link:focus-visible {
  outline: none;
}

.way:has(.way__link:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.way__arrow {
  grid-column: 12;
  justify-self: end;
  align-self: center;
  font-size: var(--t-h3);
  line-height: 1;
  transition: translate var(--d-hover) var(--ease-out);
}

@media (hover: hover) {
  .way:has(.way__link:hover) .way__arrow {
    translate: 0.3em 0;
  }
}

/* The facts: three columns under the rows. */
.cfacts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3) var(--gutter);
  padding-top: var(--space-4);
}

.cfacts > div {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: 0.5rem;
}

.cfacts dd {
  margin: 0;
  max-width: 22em;
}

.cfacts dt {
  margin-bottom: 0.25rem;
}

/* A field (the onboarding): one rule under the answer that turns to the
   accent while you type. */
.field {
  display: block;
  padding-block: 0.5rem 0.9rem;
  border-bottom: 1px solid var(--hairline-strong);
  transition: border-color var(--d-hover) var(--ease-out);
}

/* Focus: the underline turns the focus colour and doubles to 2px (the shadow
   under it), clear without moving anything (WCAG 2.4.7, 1.4.11). */
.field:focus-within {
  border-bottom-color: var(--focus);
  box-shadow: 0 1px 0 var(--focus);
}

.field__label {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  color: var(--fg-muted);
}

.field__label em {
  font-style: normal;
  color: var(--fg-faint);
}

.field input,
.field textarea {
  display: block;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0;
  font-family: var(--font-display);
  font-size: max(1.05rem, var(--t-lead));
  line-height: 1.42;
  letter-spacing: -0.015em;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-radius: 0;
  resize: vertical;
}

.field input:focus-visible,
.field textarea:focus-visible {
  outline: none;
}

/* Keep autofill from painting the fields pale blue. */
.field input:-webkit-autofill,
.field textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--fg);
  box-shadow: 0 0 0 1000px var(--bg) inset;
  caret-color: var(--fg);
}

/* ---- Pricing (src/render/pages/pricing.ts) ----
   One dark screen: the title, then a hand of the homepage's playing cards
   (.svc, services.css), the website in the middle in the accent and a little
   larger, the options on either side. The hand is fanned like cards held in
   one hand: every card stands on the same baseline and turns (--rot,
   pricing.ts) about one point far below the middle of the hand (--arc below
   the cards), so their bottom edges sit on one arc. A card under the pointer
   rises out of the hand along its own line, grows a little and comes to the
   front, and the cards on either side turn a little further away. It keeps
   the front a moment as it settles back (the z-index waits), so it never
   drops behind its neighbours mid-move. src/modules/hand.ts fans the hand
   out from a stack as it comes into view; until then (with JavaScript) it is
   hidden. Under the cards, the two buttons. Phones: a row to swipe through,
   opened on the website. */

.hand {
  --card: clamp(9.5rem, 12.5vw, 14rem); /* an option's width; the website is 1.125 times it */
  --arc: calc(var(--card) * 4.9); /* how far below the cards they turn about: far enough that neighbours never touch */
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--space-5);
  min-height: 100svh;
  padding-block: calc(var(--nav-h) + var(--space-5)) var(--space-6);
  overflow: clip;
}

/* While a menu turns the page away (menu.css), the dark band would vanish into
   the dark behind it: a faint light edge keeps it a page. */
html.menu-3d .hand {
  box-shadow: inset 0 0 0 1px rgb(244 241 236 / 0.14);
}

.hand__lead {
  max-width: none;
  padding-inline: var(--page-x);
  text-align: center;
}

/* As tall as the website card (5 : 7) with room above it to rise into, and
   below it for the outer cards, which come down along the arc. */
.hand__cards {
  position: relative;
  width: 100%;
  height: calc(var(--card) * 1.575 + 2.5rem);
  margin-bottom: calc(var(--card) * 0.5);
}

html.js .hand:not(.is-dealt) .hand__card {
  opacity: 0;
}

.hand__card {
  --w: var(--card);
  position: absolute;
  bottom: 0;
  left: 50%;
  z-index: 1;
  width: var(--w);
  margin-left: calc(var(--w) / -2);
  transform-origin: 50% calc(100% + var(--arc));
  /* --deal-y: the rise from below as the hand is dealt (hand.ts). */
  transform: rotate(calc(var(--rot, 0deg) + var(--spread, 0deg))) translateY(calc(var(--rise, 0rem) + var(--deal-y, 0px)));
  transition:
    transform 0.7s var(--ease-out),
    z-index 0s 0.35s;
}

/* While the hand is dealt (hand.ts) the cards follow it frame by frame. */
.hand.is-dealing .hand__card {
  transition: none;
}

/* The website: in the middle, a little larger and above the options. */
.hand__card--lead {
  --w: calc(var(--card) * 1.125);
  z-index: 2;
}

.hand__card .svc {
  transition: scale 0.7s var(--ease-out);
}

.hand__card .svc__front {
  box-shadow: 0 0.5rem 1.5rem -1rem rgb(0 0 0 / 0.6);
  transition: box-shadow 0.7s var(--ease-out);
}

.hand__card--lead .svc__front {
  background: var(--accent);
}

/* The price in the middle of the card, the note above it, the line under. */
.svc__price {
  align-self: center;
  display: grid;
  gap: 4cqi;
}

.svc__note {
  font: 500 clamp(0.625rem, 5.5cqi, 0.75rem) / 1.3 var(--font-meta);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  opacity: 0.7;
}

.svc__amount {
  font-size: 22cqi;
  line-height: 0.95;
  letter-spacing: -0.045em;
}

/* A price in words ("On request", "Monthly"): smaller, so it stays on one line. */
.svc__amount--words {
  font-size: 13cqi;
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.svc__price .svc__line {
  font-size: clamp(0.75rem, 7cqi, 1rem);
}

@media (hover: hover) {
  .hand__card:hover {
    --rise: -1.75rem;
    z-index: 3;
    transition-delay: 0s;
  }

  .hand__card:hover .svc {
    scale: 1.06;
  }

  .hand__card:hover .svc__front {
    box-shadow: 0 2.5rem 3rem -1.75rem rgb(0 0 0 / 0.85);
  }

  .hand__card:hover ~ .hand__card {
    --spread: 3deg;
  }

  .hand__card:has(~ .hand__card:hover) {
    --spread: -3deg;
  }
}

.hand__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  padding-inline: var(--page-x);
}

/* The light section under the cards (pricing.ts): the heading, then what
   every website includes beside the options and their prices, then how a
   project goes, then how to start. */
.pinfo {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-6);
  padding-block: var(--section-y);
}

.pinfo__head {
  grid-column: 1 / span 8;
  display: grid;
  gap: var(--space-3);
}

.pinfo__col {
  display: grid;
  align-content: start;
  gap: var(--space-2);
}

.pinfo__col--included {
  grid-column: 1 / span 6;
}

.pinfo__col--options {
  grid-column: 8 / -1;
}

.pinfo__list {
  border-top: 1px solid var(--hairline-strong);
}

.pinfo__list li {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr);
  column-gap: 1rem;
  row-gap: 0.3rem;
  align-items: baseline;
  padding-block: 1.1rem;
  border-bottom: 1px solid var(--hairline);
}

.pinfo__list li > p.muted {
  grid-column: 2;
}

/* An option: its name and price on one line, what it is under them. */
.pinfo__list .pinfo__option {
  grid-template-columns: minmax(0, 1fr) auto;
}

.pinfo__list .pinfo__option > p.muted {
  grid-column: 1 / -1;
}

.pinfo__name,
.pinfo__price {
  font-size: var(--t-h3);
  line-height: 1.15;
  letter-spacing: var(--tr-h3);
}

.pinfo__price {
  white-space: nowrap;
}

.pinfo__steps {
  grid-column: 1 / -1;
  display: grid;
  gap: var(--space-2);
}

.pinfo__step-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4) var(--gutter);
}

.pinfo__step-list li {
  display: grid;
  align-content: start;
  gap: 0.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--hairline-strong);
}

.pinfo__step {
  font-size: var(--t-h2);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h2);
}

.pinfo__cta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--hairline-strong);
}

.pinfo__cta .lead {
  max-width: 30em;
}

@media (max-width: 899px) {
  .pinfo__head,
  .pinfo__col--included,
  .pinfo__col--options {
    grid-column: 1 / -1;
  }

  .pinfo__step-list {
    grid-template-columns: 1fr;
  }
}

/* "What's included": a second .menu dialog, so it turns in like the site
   menu (menu.css, src/modules/menu.ts); here only its content. The label,
   the title and each item rise in one after another as the panel lands, as
   the menu's links do. */
.includes__body {
  grid-row: 2;
  align-self: center;
  display: grid;
  gap: var(--space-3);
  padding-block: var(--space-4);
}

.includes__title {
  font-size: clamp(2.25rem, 9cqi, 4.5rem);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h1);
}

.includes__list {
  border-top: 1px solid var(--hairline-strong);
}

.includes__list li {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr);
  column-gap: 1rem;
  row-gap: 0.3rem;
  align-items: baseline;
  padding-block: 1rem;
  border-bottom: 1px solid var(--hairline);
}

.includes__list li > p {
  grid-column: 2;
}

.includes__item {
  font-size: clamp(1.125rem, 4.5cqi, 1.625rem);
  line-height: 1.15;
  letter-spacing: var(--tr-h3);
}

.includes .menu__foot {
  justify-items: start;
}

@media (prefers-reduced-motion: no-preference) {
  .includes :is(.includes__label, .includes__title, .includes__list li) {
    opacity: 0;
    translate: 0 1.5rem;
    transition: none;
  }

  .includes.is-open :is(.includes__label, .includes__title, .includes__list li) {
    opacity: 1;
    translate: 0 0;
    transition:
      opacity 0.9s var(--ease-out) calc(0.35s + var(--k, 0) * 0.07s),
      translate 1.1s var(--ease-out-expo) calc(0.35s + var(--k, 0) * 0.07s);
  }

  .includes .includes__title {
    --k: 1;
  }

  .includes .includes__list li {
    --k: calc(2 + var(--i, 0));
  }
}

@media (max-width: 759px) {
  /* Phones: the fan does not fit, so the cards stand straight in a row to
     swipe through, the website in the middle (hand.ts opens the row on it). */
  .hand__cards {
    display: flex;
    gap: 1rem;
    height: auto;
    margin-bottom: 0;
    padding: 1rem 15vw;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }

  .hand__card,
  .hand__card--lead {
    position: relative;
    bottom: auto;
    left: auto;
    flex: 0 0 70vw;
    width: auto;
    margin-left: 0;
    transform: none;
    scroll-snap-align: center;
  }
}

/* ---- Onboarding (src/render/pages/onboarding.ts) ----
   One narrow column, as easy on a phone as on a desk, and quiet: the head,
   then the form one step at a time (src/modules/onboarding.ts, .is-stepped),
   each the step's title, its questions as the contact page asks them
   (.field, .chip--choice), files on a zone to tap or drop them on, then Next
   and Back. The head is the first step's title and goes after it, so a
   screen never has two headings. No counts, bars or lines between the
   questions; only the needed fields carry a mark (*). */

.onb-head,
.onb {
  max-width: calc(46rem + 2 * var(--page-x));
}

.onb-head {
  display: grid;
  gap: var(--space-3);
  padding-top: calc(var(--nav-h) + var(--space-6));
  padding-bottom: var(--space-4);
}

.onb-head[hidden] {
  display: none;
}

.onb {
  display: grid;
  gap: var(--space-4);
  padding-bottom: var(--section-y);
}

/* Without the head, the form starts where the head did. */
.onb-head[hidden] + .onb {
  padding-top: calc(var(--nav-h) + var(--space-6));
}

.onb [hidden] {
  display: none !important;
}

.onb__step {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.onb:not(.is-stepped) .onb__step + .onb__step {
  padding-top: var(--space-5);
}

/* One at a time: each comes in from a little below. */
@media (prefers-reduced-motion: no-preference) {
  .onb.is-stepped .onb__step {
    animation: cq-in 0.6s var(--ease-out-expo) both;
  }
}

.onb__legend {
  margin-bottom: var(--space-3);
  padding: 0;
}

.onb__title {
  font-size: var(--t-h2);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h2);
}

.onb__field {
  min-width: 0;
}

.onb__hint {
  margin-top: 0.35rem;
  text-wrap: pretty;
}

/* The mark on a needed field: the accent, quiet. */
.onb .field__label em {
  color: var(--accent-text);
}

.onb .field ::placeholder {
  color: var(--fg-faint);
  opacity: 1;
}

/* Choices get a little room from the line above them. */
.onb__choice {
  margin: 0;
  padding: var(--space-1) 0 0;
  border: 0;
}

.onb__choice .chips {
  margin-top: 0.85rem;
}

/* A dropdown set like the text fields: the answer in the display face on the
   line, a small chevron at the end instead of the browser's own. */
.onb__select select {
  display: block;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0 2rem 0 0;
  font-family: var(--font-display);
  font-size: max(1.05rem, var(--t-lead));
  line-height: 1.42;
  letter-spacing: -0.015em;
  color: var(--fg);
  background: transparent
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%23777' stroke-width='1.5'/%3E%3C/svg%3E")
    no-repeat right 0.25rem center;
  border: 0;
  border-radius: 0;
  appearance: none;
  cursor: pointer;
}

.onb__select select:focus-visible {
  outline: none;
}

.onb__select option {
  color: var(--fg);
  background: var(--bg);
}

/* A file zone: the input covers it, so a tap opens the picker and a drop lands in it. */
.onb__files {
  display: grid;
  gap: 0.6rem;
}

.onb__drop {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 6.5rem;
  padding: 1rem;
  border: 1px dashed var(--hairline-strong);
  border-radius: calc(var(--radius) * 3);
  background: var(--bg-2);
  text-align: center;
  cursor: pointer;
  transition:
    border-color var(--d-hover) var(--ease-out),
    background-color var(--d-hover) var(--ease-out);
}

.onb__drop input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.onb__drop:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

.onb__drop.is-over {
  border-color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 12%, var(--bg));
}

@media (hover: hover) {
  .onb__drop:hover {
    border-color: var(--fg);
  }
}

.onb__drop-text {
  font-weight: var(--w-strong);
}

.onb__list li {
  padding-block: 0.4rem;
  border-bottom: 1px solid var(--hairline);
  overflow-wrap: anywhere;
}

.onb__list .is-too-big {
  color: var(--accent-text);
}

.onb__check {
  margin-top: var(--space-2);
}

.onb__note {
  color: var(--accent-text);
}

.onb__note--done {
  color: var(--fg);
}

/* Next or Send first; Back after it, quiet. */
.onb__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.75rem;
}

.onb__back {
  order: 1;
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
}

@media (max-width: 559px) {
  /* Big enough to hit with a thumb. */
  .onb .chip--choice {
    min-height: 3rem;
  }

  .onb__nav .btn--solid {
    flex: 1;
  }

  .onb__nav .btn--solid .block,
  .onb__nav .btn--solid .block__face {
    width: 100%;
    justify-content: center;
  }
}

/* ---- Blog: the list of posts, then each post (src/render/pages/blog.ts) ---- */

/* One row per post between hairlines: the date on the left, the title and
   summary in the middle columns, an arrow at the far end. */
.posts {
  padding-bottom: var(--space-7);
}

.posts > li:first-child .posts__link {
  border-top: 1px solid var(--hairline-strong);
}

.posts__link {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 0.6rem;
  align-items: baseline;
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--hairline);
}

.posts__date {
  grid-column: 1 / span 3;
}

.posts__title {
  grid-column: 4 / span 7;
  font-size: var(--t-h3);
  line-height: 1.15;
  letter-spacing: -0.02em;
  transition: translate var(--d-hover) var(--ease-out);
}

.posts__summary {
  grid-column: 4 / span 7;
  max-width: 38em;
  color: var(--fg-muted);
}

.posts__arrow {
  grid-column: 12;
  grid-row: 1;
  justify-self: end;
  transition: translate var(--d-hover) var(--ease-out);
}

@media (hover: hover) {
  .posts__link:hover .posts__title {
    translate: 0.15em 0;
  }

  .posts__link:hover .posts__arrow {
    translate: 0.35em 0;
  }
}

/* A draft (the dev server only; a build leaves drafts out). */
.draft {
  margin-left: 0.5em;
  padding: 0.15em 0.45em;
  border: 1px solid currentColor;
  border-radius: var(--radius);
  color: var(--accent-text);
}

.post__head {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  padding-top: calc(var(--nav-h) + var(--space-5));
}

.post__bar {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gutter);
}

.post__title {
  grid-column: 3 / span 8;
  font-size: var(--t-h1);
  line-height: 1.02;
  letter-spacing: var(--tr-h1);
}

.post__summary {
  grid-column: 3 / span 8;
  max-width: 34em;
  color: var(--fg-muted);
}

.post__body {
  display: grid;
  gap: var(--space-5);
  padding-block: var(--space-6);
}

.post__foot {
  padding-bottom: var(--space-7);
}

/* ---- Small screens ---- */

@media (max-width: 899px) {
  .posts__date,
  .posts__title,
  .posts__summary,
  .post__title,
  .post__summary {
    grid-column: 1 / -1;
  }

  .posts__arrow {
    display: none;
  }

  .page-head__title,
  .page-head__intro,
  .case__hero,
  .case__facts,
  .case__next-text,
  .case__next-media,
  .cpage__title,
  .cpage__lede {
    grid-column: 1 / -1;
  }

  /* A way: the address with the arrow across from it. */
  .way {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .way__main {
    grid-column: 1;
  }

  .way__arrow {
    grid-column: 2;
  }

  .cfacts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }



  .case__facts,
  .cb-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .case__facts {
    column-gap: var(--gutter);
  }

  .cb-prose p,
  .cb-prose--lead p:first-child,
  .cb-list ul {
    grid-column: 1 / -1;
  }

  .cb-two {
    grid-template-columns: 1fr !important;
  }

  .cb-chart__row {
    grid-template-columns: 1fr;
    gap: 0.35rem;
  }

  .cb-decisions__head {
    display: none;
  }

  .cb-decisions__row {
    grid-template-columns: 2.5rem 1fr;
  }

  .cb-decisions__row > p {
    grid-column: 2;
  }
}

/*
 * Pricing: the bento (src/render/pricing-bento.ts), on /pricing/ and as
 * variant C on /pricing-lab/.
 * Twelve columns. Desktop rows:
 *   head      title (1–7), intro (9–12)
 *   1–2       website (1–6, two rows) | pages (7–9) connections (10–12)
 *                                     | cms (7–12)
 *   label     "Every website includes"
 *   3         four outline tiles, three columns each
 *   4         process, in ink (1–8) | closing (9–12)
 * Tiles: 12 px radius, up to 32 px padding, gaps of the gutter.
 */

.bento {
  --tile-pad: clamp(1.25rem, 2.2vw, 2rem);
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--gutter);
  padding-bottom: var(--section-y);
}

.bento :is(h1, h2, h3, h4, h5) {
  text-wrap: balance;
}

.bento :is(p, li) {
  text-wrap: pretty;
}

/* On /pricing/ it is the first thing on the page: room for the nav above. */
.pricing-page .bento {
  padding-top: calc(var(--nav-h) + var(--space-7));
}

.bento__head {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: end;
  row-gap: var(--space-3);
  padding-bottom: var(--space-5);
}

.bento__title {
  grid-column: 1 / span 7;
  max-width: 13em;
  font-size: var(--t-h1);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-h1);
  color: var(--display-ink);
}

.bento__head > p {
  grid-column: 9 / -1;
  max-width: 26em;
}

/* ---- Tiles ---- */

.bento__tile {
  position: relative;
  display: grid;
  align-content: start;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--tile-pad);
  border-radius: 12px;
}

/* Each tile opens with its name alone: no rule, nothing beside it. */
.bento__name {
  font-size: 1.125rem;
  line-height: 1.5rem;
  font-weight: var(--w-strong);
  letter-spacing: -0.01em;
}

.bento__price {
  display: grid;
  gap: 0.75rem;
  align-self: end;
}

.bento__amount {
  font-size: clamp(2.5rem, 4.4vw, 4.5rem);
  line-height: 0.9;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
}

/* "excl. VAT" beside an amount: small mono caps on the amount's baseline,
   faint (on the accent tile, ink at 70%). */
.bento__vat {
  margin-left: 0.5rem;
  font-size: var(--t-label);
  letter-spacing: var(--tr-label);
  white-space: nowrap;
  color: var(--fg-faint);
}

.bento__tile--lead .bento__vat {
  margin-left: 0.75rem;
  color: rgb(24 25 26 / 0.85); /* 5.6:1 on the accent (0.7 was 4.1) */
}

/* On the accent, the deep accent ring would be too faint: ink instead. */
.bento__tile--lead {
  --focus: var(--ink);
}

.bento__amount--words {
  font-size: clamp(2rem, 3.2vw, 3.25rem);
  letter-spacing: -0.04em;
}

.bento__line {
  max-width: 28em;
}

/* The website: two rows tall, in the accent. (Ink was tried: too dark.) */
.bento__tile--lead {
  grid-column: 1 / span 6;
  grid-row: span 2;
  grid-template-rows: auto 1fr auto;
  min-height: 36rem;
  background: var(--accent);
  color: var(--ink);
}

/* On the accent the solid button is ink, its turned face paper. */
.bento__tile--lead .block__face {
  background: var(--ink);
  color: var(--paper);
}

.bento__tile--lead .block__face--next {
  background: var(--paper);
  color: var(--ink);
}

.bento__tile--lead .bento__amount {
  font-size: clamp(5rem, 12vw, 12rem);
  line-height: 0.84;
  letter-spacing: -0.06em;
  margin-left: -0.04em;
}

.bento__tile--lead .bento__line {
  max-width: 20em;
  font-size: var(--t-lead);
  line-height: 1.4;
}

.bento__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

.bento__link {
  border-bottom: 1px solid currentColor;
  padding-bottom: 0.125rem;
}

/* The options: filled, the picture between the name and the price. */
/* Rows: the name, then whatever follows takes the rest (the picture, or the
   price at the bottom when there is no picture); no empty row is left over. */
.bento__tile--opt {
  grid-template-rows: auto 1fr;
  background: var(--paper-2);
  transition: background-color var(--d-hover) var(--ease-out);
}

@media (hover: hover) {
  .bento__tile--opt:hover {
    background: var(--rule-on-paper);
  }
}

.bento__tile--pages {
  grid-column: 7 / span 3;
}

.bento__tile--connections {
  grid-column: 10 / span 3;
}

/* The CMS runs the width of both. Its picture is a "?" in the top-right
   corner, level with the name, so the art row takes no room. */
.bento__tile--cms {
  grid-column: 7 / span 6;
}

.bento__tile--cms .bento__art {
  min-height: 0;
}

.bento__help {
  position: absolute;
  top: var(--tile-pad);
  right: var(--tile-pad);
  z-index: 2;
}

.bento__help-btn {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  font: 500 0.75rem / 1 var(--font-meta);
  color: var(--fg);
  cursor: help;
  transition:
    background-color var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out),
    color var(--d-hover) var(--ease-out);
}

/* What a CMS is: a small ink panel under the "?", right-aligned with it.
   Shown while the "?" is hovered, tapped or focused. */
.bento__help-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  display: grid;
  gap: 0.25rem;
  width: min(18rem, calc(100vw - 4rem));
  padding: 0.75rem 1rem;
  border-radius: 8px;
  background: var(--ink);
  color: var(--paper);
  font-size: var(--t-micro);
  line-height: 1.25rem;
  text-wrap: pretty;
  box-shadow: 0 1rem 2rem -1rem rgb(0 0 0 / 0.5);
  opacity: 0;
  visibility: hidden;
  translate: 0 -0.25rem;
  transition:
    opacity var(--d-fast) var(--ease-out),
    translate var(--d-fast) var(--ease-out),
    visibility 0s var(--d-fast);
}

.bento__help-title {
  font-weight: var(--w-strong);
}

.bento__help:is(:hover, :focus-within) .bento__help-panel {
  opacity: 1;
  visibility: visible;
  translate: 0 0;
  transition-delay: 0s;
}

.bento__help:is(:hover, :focus-within) .bento__help-btn {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.bento__help-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.bento__art {
  display: grid;
  align-content: center;
  gap: 0.75rem;
  min-height: 4rem;
}

/* Pages to count: three solid, then dashed ones that cost a page each. */
.bento__pages {
  display: flex;
  gap: 0.5rem;
}

.bento__pages span {
  width: 1.75rem;
  aspect-ratio: 3 / 4;
  border-radius: 2px;
  background: var(--ink);
  transition: translate var(--d-hover) var(--ease-out);
}

.bento__pages .is-extra {
  background: transparent;
  border: 1px dashed var(--faint-on-paper);
}

@media (hover: hover) {
  .bento__tile--pages:hover .bento__pages .is-extra {
    border-style: solid;
    border-color: var(--accent);
    background: var(--accent);
  }

  .bento__tile--pages:hover .bento__pages span:nth-child(4) {
    translate: 0 -0.25rem;
    transition-delay: 0.05s;
  }

  .bento__tile--pages:hover .bento__pages span:nth-child(5) {
    translate: 0 -0.25rem;
    transition-delay: 0.1s;
  }
}

.bento__pages .is-extra {
  transition:
    translate var(--d-hover) var(--ease-out),
    background-color var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out);
}

/* ---- What every website includes: outlines ---- */

.bento__label {
  grid-column: 1 / -1;
  padding-top: var(--space-5);
  scroll-margin-top: calc(var(--nav-h) + var(--space-3));
}

/* Three across (the website tile already says up to three pages). */
.bento__tile--inc {
  grid-column: span 4;
  grid-template-rows: auto 1fr;
  min-height: 15rem;
  border: 1px solid var(--hairline-strong);
}

.bento__tile--inc > div {
  align-self: end;
  display: grid;
  gap: 0.5rem;
}

.bento__figure {
  font-size: clamp(3rem, 5vw, 5rem);
  line-height: 0.9;
  letter-spacing: -0.05em;
  color: var(--accent-text);
}

/* A figure in words ("WCAG 2.2"): a size smaller, on one line, with room
   to its right for the "?". */
.bento__figure--words {
  font-size: clamp(2.5rem, 3.6vw, 3.75rem);
  white-space: nowrap;
  padding-right: 2rem;
}

.bento__swatches {
  display: flex;
  height: clamp(2.75rem, 4.5vw, 4.5rem);
}

.bento__swatches span {
  aspect-ratio: 1;
  height: 100%;
  border-radius: 50%;
  border: 1px solid var(--hairline-strong);
}

.bento__swatches span:nth-child(1) {
  background: var(--accent);
}

.bento__swatches span:nth-child(2) {
  margin-left: -0.75rem;
  background: var(--ink);
}

.bento__swatches span:nth-child(3) {
  margin-left: -0.75rem;
  background: var(--paper);
}

/* The site's reset covers h1–h4 only. */
.bento :is(h1, h2, h3, h4, h5) {
  margin: 0;
}

.bento__inc-name {
  font-size: var(--t-body-l);
  line-height: 1.5;
  font-weight: var(--w-strong);
  letter-spacing: -0.01em;
}

/* ---- How it goes (ink) and the closing tile ---- */

.bento__tile--steps {
  grid-column: 1 / span 8;
  gap: var(--space-4);
  margin-top: var(--space-5);
}

.bento__steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
}

.bento__steps li {
  display: grid;
  align-content: start;
  gap: 0.75rem;
}

.bento__step {
  font-size: var(--t-h3);
  line-height: 1.1;
  letter-spacing: var(--tr-h3);
}

.bento__tile--close {
  grid-column: 9 / span 4;
  align-content: space-between;
  justify-items: start;
  margin-top: var(--space-5);
  background: var(--paper-2);
}

.bento__close-line {
  font-size: var(--t-lead);
  line-height: 1.4;
  letter-spacing: -0.018em;
}

/* ---- Tablet: the options under the website, the rest in pairs ---- */

@media (max-width: 1099px) {
  .bento__title,
  .bento__head > p {
    grid-column: 1 / -1;
  }

  .bento__tile--lead {
    grid-column: 1 / -1;
    grid-row: auto;
    min-height: 28rem;
  }

  .bento__tile--pages {
    grid-column: 1 / span 6;
  }

  .bento__tile--connections {
    grid-column: 7 / span 6;
  }

  .bento__tile--cms {
    grid-column: 1 / -1;
  }

  .bento__tile--steps,
  .bento__tile--close {
    grid-column: 1 / -1;
  }

  .bento__tile--close {
    margin-top: 0;
  }
}

/* ---- Phones: one column, the includes two by two ---- */

@media (max-width: 699px) {
  .bento__head {
    padding-bottom: var(--space-3);
  }

  .bento__tile--pages,
  .bento__tile--connections {
    grid-column: 1 / -1;
  }

  .bento__tile--lead {
    min-height: 0;
  }

  /* Brand and hosting side by side, WCAG (too wide for half a phone)
     across under them; dense lets it drop below the pair. */
  .bento {
    grid-auto-flow: row dense;
  }

  .bento__tile--inc {
    grid-column: span 6;
    min-height: 11rem;
    padding: 1.25rem;
  }

  .bento__tile--inc:has(.bento__figure--words) {
    grid-column: 1 / -1;
    min-height: 0;
  }

  /* On a phone the outline tiles carry the name alone, unless the text holds
     a price (hosting). */
  .bento__inc-name + .bento__line:not(.bento__line--keep) {
    display: none;
  }

  .bento__steps {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * /pricing-lab/ (src/render/pages/pricing-lab.ts): five takes on the pricing
 * cards, each a whole section that could replace the hand on /pricing/.
 * Spacing in steps of 4 px; headings balance, text wraps pretty, so no line
 * ends on one word.
 */

.pl-lab :is(h1, h2, h3, h4) {
  text-wrap: balance;
}

.pl-lab p,
.pl-lab li {
  text-wrap: pretty;
}

.pl-lab__head {
  display: grid;
  gap: var(--space-3);
  padding-top: calc(var(--nav-h) + var(--space-7));
  padding-bottom: var(--space-6);
}

.pl-lab__head > p:last-child {
  max-width: 34em;
}

/* The caption above each variant: lab furniture, not part of the design. */
.pl-cap {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: var(--space-3);
  row-gap: 0.25rem;
  padding-top: var(--space-7);
  padding-bottom: var(--space-5);
  border-top: 1px solid var(--hairline);
}

.pl-cap .label {
  color: var(--accent-text);
}

.pl-cap__name {
  font-size: var(--t-h3);
  line-height: 1.1;
  letter-spacing: var(--tr-h3);
}

.pl-cap__note {
  grid-column: 2;
  max-width: 44em;
}

/* ================================================================
   A. Price list
   ================================================================ */

.pl-list {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-6);
  padding-bottom: var(--section-y);
}

.pl-list__lead {
  grid-column: 1 / span 5;
  display: grid;
  align-content: start;
  justify-items: start;
  gap: var(--space-3);
}

.pl-list__amount {
  font-size: clamp(5rem, 13vw, 12rem);
  line-height: 0.84;
  letter-spacing: -0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--display-ink);
  margin-left: -0.04em;
}

.pl-list__lead .lead {
  max-width: 22em;
}

.pl-list__inc {
  display: grid;
  width: 100%;
  margin: var(--space-2) 0;
  border-top: 1px solid var(--hairline);
}

.pl-list__inc li {
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--hairline);
}

.pl-list__inc li::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
  translate: 0 -0.0625rem;
}

.pl-list__menu {
  grid-column: 7 / -1;
  display: grid;
  align-content: start;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

.pl-list__rows {
  display: grid;
}

.pl-list__row {
  display: grid;
  gap: 0.5rem;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--hairline);
}

.pl-list__row:first-child {
  border-top: 1px solid var(--hairline-strong);
}

.pl-list__head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  font-size: var(--t-h3);
  line-height: 1.1;
  letter-spacing: var(--tr-h3);
}

.pl-list__dots {
  flex: 1;
  min-width: 2rem;
  border-bottom: 2px dotted var(--hairline-strong);
  translate: 0 -0.25rem;
}

.pl-list__price {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pl-list__row > p.muted {
  max-width: 30em;
}

@media (max-width: 899px) {
  .pl-list__lead,
  .pl-list__menu {
    grid-column: 1 / -1;
  }
}

/* ================================================================
   B. Build your price
   ================================================================ */

.pl-build {
  padding-block: var(--section-y);
}

.pl-build__inner {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-5);
  align-items: start;
}

.pl-build__title {
  grid-column: 1 / -1;
  max-width: 14em;
  font-size: var(--t-h1);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-h1);
  color: var(--display-ink);
}

/* The website as one flat playing card, in the accent. */
.pl-build__card {
  grid-column: 1 / span 4;
  container-type: inline-size;
  max-width: 24rem;
}

.pl-build__face {
  display: grid;
  align-content: start;
  gap: 4cqi;
  aspect-ratio: 5 / 7;
  padding: 7cqi;
  border-radius: 5cqi;
  background: var(--accent);
  color: var(--ink);
}

.pl-build__corner {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: auto;
}

.pl-build__name {
  font: var(--w-strong) clamp(0.875rem, 7cqi, 1.125rem) / 1.2 var(--font-display);
}

.pl-build__bar {
  width: 14cqi;
  height: auto;
}

.pl-build__note {
  margin-top: 16cqi;
  font: 500 clamp(0.625rem, 5cqi, 0.75rem) / 1.3 var(--font-meta);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  opacity: 0.7;
}

.pl-build__amount {
  font-size: 30cqi;
  line-height: 0.9;
  letter-spacing: -0.055em;
}

.pl-build__inc {
  display: grid;
  margin-top: 6cqi;
  font-size: clamp(0.8125rem, 6cqi, 1rem);
  line-height: 1.5;
}

.pl-build__inc li {
  padding: 2.5cqi 0;
  border-top: 1px solid rgb(24 25 26 / 0.2);
}

.pl-build__form {
  grid-column: 6 / -1;
  display: grid;
  border-top: 1px solid var(--hairline-strong);
}

.pl-build__opt {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--hairline);
  cursor: default;
}

label.pl-build__opt {
  cursor: pointer;
}

.pl-build__opt > :first-child {
  display: grid;
  gap: 0.25rem;
}

.pl-build__opt-name {
  display: block;
  font-size: var(--t-h3);
  line-height: 1.2;
  letter-spacing: var(--tr-h3);
}

.pl-build__opt-side {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The stepper: two round buttons round the count. */
.pl-step {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.pl-step__btn {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  font-size: 1.25rem;
  line-height: 1;
  transition:
    border-color var(--d-hover) var(--ease-out),
    background-color var(--d-hover) var(--ease-out),
    color var(--d-hover) var(--ease-out);
}

.pl-step__btn:disabled {
  opacity: 0.35;
}

@media (hover: hover) {
  .pl-step__btn:not(:disabled):hover {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--ink);
  }
}

.pl-step__value {
  min-width: 2.5rem;
  font-size: var(--t-h3);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The switch: a checkbox drawn as a pill with a knob. */
.pl-switch {
  appearance: none;
  position: relative;
  flex: none;
  width: 3rem;
  height: 1.75rem;
  margin: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: 1rem;
  cursor: pointer;
  transition:
    background-color var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out);
}

.pl-switch::after {
  content: "";
  position: absolute;
  top: 0.1875rem;
  left: 0.1875rem;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--fg);
  transition:
    translate var(--d-hover) var(--ease-out),
    background-color var(--d-hover) var(--ease-out);
}

.pl-switch:checked {
  border-color: var(--accent);
  background: var(--accent);
}

.pl-switch:checked::after {
  translate: 1.25rem 0;
  background: var(--ink);
}

.pl-switch:focus-visible,
.pl-step__btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.pl-build__sum {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-3);
  padding-top: var(--space-4);
}

.pl-build__sum > div {
  display: grid;
  gap: 0.5rem;
}

.pl-build__total {
  font-size: var(--t-h1);
  line-height: 0.9;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.pl-build__total.is-bumped {
  animation: pl-bump 0.5s var(--ease-out);
}

@keyframes pl-bump {
  from {
    translate: 0 0.25em;
    opacity: 0.2;
  }
}

@media (max-width: 899px) {
  .pl-build__card {
    grid-column: 1 / -1;
    width: min(20rem, 100%);
  }

  .pl-build__form {
    grid-column: 1 / -1;
  }
}

/* ================================================================
   D. Big rows
   ================================================================ */

.pl-rows {
  padding-block: var(--section-y);
}

.pl-rows__list {
  border-top: 1px solid var(--hairline-strong);
}

.pl-rows__row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 4fr) minmax(0, 3fr);
  align-items: center;
  column-gap: var(--gutter);
  padding: var(--space-4) var(--space-2);
  border-bottom: 1px solid var(--hairline);
  isolation: isolate;
  transition: color var(--d-hover) var(--ease-out);
}

/* The accent rises from the bottom edge of the row. */
.pl-rows__fill {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--accent);
  clip-path: inset(100% 0 0 0);
  transition: clip-path 0.6s var(--ease-out-expo);
}

.pl-rows__name {
  font-size: var(--t-h1);
  line-height: 0.92;
  letter-spacing: var(--tr-h1);
}

.pl-rows__row--lead .pl-rows__name::after {
  content: "";
  display: inline-block;
  width: 0.18em;
  height: 0.18em;
  margin-left: 0.12em;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: 0.5em;
  transition: background-color var(--d-hover) var(--ease-out);
}

.pl-rows__line {
  max-width: 24em;
  color: var(--fg-muted);
  transition: color var(--d-hover) var(--ease-out);
}

.pl-rows__price {
  display: grid;
  justify-items: end;
  gap: 0.5rem;
  font-size: var(--t-h2);
  line-height: 1;
  letter-spacing: var(--tr-h2);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.pl-rows__note {
  font: 500 var(--t-label) / 1.3 var(--font-meta);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--fg-faint);
  transition: color var(--d-hover) var(--ease-out);
}

@media (hover: hover) {
  .pl-rows__row:hover {
    color: var(--ink);
  }

  .pl-rows__row:hover .pl-rows__fill {
    clip-path: inset(0 0 0 0);
  }

  .pl-rows__row:hover :is(.pl-rows__line, .pl-rows__note) {
    color: rgb(24 25 26 / 0.7);
  }

  .pl-rows__row--lead:hover .pl-rows__name::after {
    background: var(--ink);
  }

  /* The other rows step back while one is lit. */
  .pl-rows__list:has(.pl-rows__row:hover) .pl-rows__row:not(:hover) {
    color: var(--fg-faint);
  }
}

.pl-rows__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-4);
}

@media (max-width: 899px) {
  .pl-rows__row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 0.75rem;
    padding-inline: 0;
  }

  .pl-rows__name {
    font-size: var(--t-h2);
  }

  .pl-rows__line {
    grid-row: 2;
    grid-column: 1 / -1;
  }

  .pl-rows__price {
    grid-row: 1;
    grid-column: 2;
    font-size: var(--t-h3);
  }
}

/* ================================================================
   E. The sum
   ================================================================ */

.pl-sum {
  display: grid;
  justify-items: center;
  gap: var(--space-5);
  padding-bottom: var(--section-y);
  text-align: center;
}

.pl-sum__title {
  max-width: 12em;
  font-size: var(--t-h2);
  line-height: 1.02;
  letter-spacing: var(--tr-h2);
  color: var(--display-ink);
}

.pl-sum__row {
  --card: clamp(10rem, 14vw, 15rem);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 3vw, 3rem);
  width: 100%;
  text-align: left;
}

.pl-sum__card {
  container-type: inline-size;
  width: var(--card);
}

.pl-sum__lead .pl-sum__card {
  width: calc(var(--card) * 1.2);
}

.pl-sum__face {
  display: grid;
  grid-template-rows: auto 1fr auto;
  aspect-ratio: 5 / 7;
  padding: 8cqi;
  border-radius: 6cqi;
  background: var(--ink);
  color: var(--paper);
  box-shadow: 0 1rem 2rem -1.5rem rgb(0 0 0 / 0.6);
}

.pl-sum__lead .pl-sum__face {
  background: var(--accent);
  color: var(--ink);
}

.pl-sum__corner {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 6cqi;
}

.pl-sum__name {
  font: var(--w-strong) clamp(0.75rem, 8cqi, 1.0625rem) / 1.2 var(--font-display);
}

.pl-sum__bar {
  flex: none;
  width: 14cqi;
  height: auto;
}

.pl-sum__face > div:nth-child(2) {
  align-self: center;
  display: grid;
  gap: 3cqi;
}

.pl-sum__note {
  font: 500 clamp(0.5625rem, 5.5cqi, 0.75rem) / 1.3 var(--font-meta);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  opacity: 0.65;
}

.pl-sum__amount {
  font-size: 24cqi;
  line-height: 0.92;
  letter-spacing: -0.05em;
}

.pl-sum__amount--words {
  font-size: 14cqi;
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.pl-sum__line {
  font-size: clamp(0.6875rem, 6.5cqi, 0.9375rem);
  line-height: 1.35;
  opacity: 0.8;
}

.pl-sum__plus {
  flex: none;
  font-family: var(--font-editorial);
  font-style: italic;
  font-size: clamp(3rem, 6vw, 6rem);
  line-height: 1;
  color: var(--accent-text);
}

/* The options lie as one stack, overlapping; hovering the stack fans it open. */
.pl-sum__stack {
  display: flex;
}

.pl-sum__stack .pl-sum__card {
  position: relative;
  transition:
    margin 0.7s var(--ease-out),
    rotate 0.7s var(--ease-out),
    translate 0.7s var(--ease-out);
}

/* Laid down loosely, each at its own small angle; the one under the pointer
   straightens and lifts. */
.pl-sum__stack .pl-sum__card + .pl-sum__card {
  margin-left: calc(var(--gutter) * -0.5);
}

.pl-sum__stack .pl-sum__card:nth-child(1) {
  rotate: -3deg;
  translate: 0 0.5rem;
}

.pl-sum__stack .pl-sum__card:nth-child(3) {
  rotate: 3deg;
  translate: 0 0.5rem;
}

@media (hover: hover) {
  .pl-sum__stack .pl-sum__card:hover {
    rotate: 0deg;
    translate: 0 -0.75rem;
    z-index: 1;
  }
}

.pl-sum__foot {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
}

@media (max-width: 899px) {
  .pl-sum__row {
    flex-direction: column;
  }

  .pl-sum__stack {
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--gutter);
  }

  .pl-sum__stack .pl-sum__card + .pl-sum__card {
    margin-left: 0;
  }

  .pl-sum__stack .pl-sum__card {
    rotate: 0deg;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pl-lab * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ---- The client's project page (src/render/pages/project.ts) ----
   One narrow column, the same on a phone as on a desk. Only the logo on top.
   Two cards on the page's second paper colour (status, website), the answers
   folded away under one line, and the contact on one line. Spacing in steps
   of 4 px. */

.proj-bar {
  max-width: calc(36rem + 2 * var(--page-x));
  padding-block: var(--space-3);
}

.proj-bar__brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.proj {
  display: grid;
  gap: var(--space-2);
  max-width: calc(36rem + 2 * var(--page-x));
  padding-top: var(--space-4);
  padding-bottom: var(--space-6);
}

.clients {
  max-width: calc(46rem + 2 * var(--page-x));
  padding-top: calc(var(--nav-h) + var(--space-6));
  padding-bottom: var(--section-y);
}

.proj p,
.proj h1,
.proj h2,
.proj dl,
.proj dd,
.proj ol {
  margin: 0;
}

.proj__wait {
  min-height: 50svh;
}

.proj__head {
  display: grid;
  gap: 0.5rem;
  margin-bottom: var(--space-2);
}

.proj__title {
  font-size: var(--t-h2);
  font-weight: var(--w-text);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h2);
  overflow-wrap: anywhere;
  text-wrap: balance;
}

.proj__name {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
}

/* The client's logo on a white tile (logos are made for white), beside the name. */
.proj__head.has-logo {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-2);
}

/* One square tile at every width, like an app icon: the logo fits inside
   whatever its shape (a wide one runs edge to edge, little padding so it
   stays legible), and the name beside it never gets squeezed. */
.proj__logo {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  flex: none;
  width: 4rem;
  height: 4rem;
  padding: 0.375rem;
  border: 1px solid var(--hairline);
  border-radius: 0.5rem;
  background: #fff;
}

.proj__logo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

@media (min-width: 960px) {
  .proj__logo {
    width: 5rem;
    height: 5rem;
    padding: 0.5rem;
  }
}

/* A picture of the website, like a case cover: top of the page in view. */
.proj__picture {
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: 0.25rem;
  background: var(--media-bg);
}

.proj__picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  transition: transform var(--d-slow) var(--ease-out);
}

@media (hover: hover) {
  a.proj__picture:hover img {
    transform: scale(1.03);
  }
}

/* A card: the status, the website. */
.proj__card {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: 0.5rem;
  background: var(--bg-2);
}

.proj__top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}

/* The stage, in one word: the first thing to read. */
.proj__stage {
  font-size: var(--t-h3);
  line-height: 1.2;
  letter-spacing: var(--tr-h3);
}

.proj__bar {
  display: grid;
  grid-template-columns: repeat(var(--stages, 4), minmax(0, 1fr));
  gap: 0.25rem;
  padding: 0;
  list-style: none;
}

.proj__bar li {
  height: 0.25rem;
  border-radius: 0.25rem;
  background: var(--hairline-strong);
}

.proj__bar .is-done {
  background: var(--fg);
}

.proj__bar .is-now {
  background: var(--accent);
}

.proj__text {
  color: var(--fg-muted);
  text-wrap: pretty;
}

/* The studio's latest note, under a hairline in the same card. */
.proj__note {
  display: grid;
  gap: 0.25rem;
  margin-top: 0.5rem;
  padding-top: var(--space-2);
  border-top: 1px solid var(--hairline-strong);
}

.proj__note p:last-child {
  text-wrap: pretty;
}

.proj__go {
  justify-self: start;
}

/* Your answers: one line that opens. */
.proj__answers {
  border: 1px solid var(--hairline-strong);
  border-radius: 0.5rem;
}

.proj__answers summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: var(--space-2) var(--space-3);
  list-style: none;
  cursor: pointer;
}

.proj__answers summary::-webkit-details-marker {
  display: none;
}

/* A plus that turns into a minus. */
.proj__plus {
  position: relative;
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
}

.proj__plus::before,
.proj__plus::after {
  content: "";
  position: absolute;
  inset: calc(50% - 0.75px) 0 auto;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--d-fast) var(--ease-out);
}

.proj__plus::after {
  transform: rotate(90deg);
}

.proj__answers[open] .proj__plus::after {
  transform: rotate(0deg);
}

.proj__answers dl {
  display: grid;
  gap: var(--space-2);
  padding: 0 var(--space-3) var(--space-3);
}

.proj__row {
  display: grid;
  gap: 0.25rem;
}

.proj__row dd {
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

/* The columns. On a phone one column: the main column dissolves (display:
   contents) so the plan slots in after the website card. From 960 px, when
   there is a plan, it stands beside the rest as its own column, level with
   the status card; the logo bar widens with the page. */
.proj__main {
  display: contents;
}

.proj__status { order: 1; }
.proj__web { order: 2; }
.proj__side { order: 3; }
.proj__messages { order: 4; }
.proj__answers { order: 5; }
.proj__help { order: 6; }

@media (min-width: 960px) {
  .proj:has(.proj__side),
  .proj-bar:has(~ main .proj__side) {
    max-width: calc(58rem + 2 * var(--page-x));
  }

  .proj:has(.proj__side) {
    grid-template-columns: minmax(0, 1fr) 20rem;
    column-gap: var(--space-3);
    align-items: start;
  }

  .proj:has(.proj__side) .proj__head {
    grid-column: 1 / -1;
  }

  .proj:has(.proj__side) .proj__main {
    display: grid;
    gap: var(--space-2);
    grid-column: 1;
  }

  .proj:has(.proj__side) .proj__side {
    grid-column: 2;
  }
}

/* Your plan: the price big, then what's included and the dates, each under
   a hairline in the same card. */
.proj__price {
  display: grid;
  gap: 0.25rem;
}

.proj__amount {
  font-size: var(--t-h3);
  line-height: 1.2;
  letter-spacing: var(--tr-h3);
}

.proj__block {
  display: grid;
  gap: 0.75rem;
  padding-top: var(--space-2);
  border-top: 1px solid var(--hairline-strong);
}

.proj__block h3 {
  margin: 0;
}

.proj__included {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A small tick in the accent before each line. */
.proj__included li {
  display: grid;
  grid-template-columns: 1rem minmax(0, 1fr);
  gap: 0.5rem;
  text-wrap: pretty;
}

.proj__included li::before {
  content: "";
  width: 0.625rem;
  height: 0.375rem;
  margin-top: 0.4em;
  border-left: 1.5px solid var(--accent-text);
  border-bottom: 1.5px solid var(--accent-text);
  transform: rotate(-45deg);
}

/* Each date the same shape at every width: what it is, small, over the day
   and how far off it is. */
.proj__dates {
  display: grid;
  gap: var(--space-2);
}

.proj__dates > div {
  display: grid;
  gap: 0.25rem;
}

.proj__dates dt {
  font-size: var(--t-micro);
  font-weight: var(--w-strong);
  line-height: var(--lh-micro);
  color: var(--fg-muted);
}

.proj__dates dd {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-variant-numeric: tabular-nums;
}

.proj__when {
  color: var(--fg-faint);
}

.proj__dates .is-soon .proj__when {
  color: var(--accent-text);
}

.proj__dates .is-past dd > :first-child {
  color: var(--fg-faint);
}

.proj__help {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  padding: var(--space-2) var(--space-3) 0;
}

@media (prefers-reduced-motion: no-preference) {
  .proj.is-ready > * {
    animation: cq-in 0.6s var(--ease-out-expo) both;
  }

  .proj.is-ready > :nth-child(2) { animation-delay: 0.05s; }
  .proj.is-ready > :nth-child(3) { animation-delay: 0.1s; }
  .proj.is-ready > :nth-child(n + 4) { animation-delay: 0.15s; }
}

@media (max-width: 559px) {
  .proj__card {
    padding: 1.25rem;
  }

  .proj__answers summary,
  .proj__help {
    padding-inline: 1.25rem;
  }

  .proj__answers dl {
    padding: 0 1.25rem 1.25rem;
  }

  /* The button as wide as the card, easy to hit with a thumb. */
  .proj__go,
  .proj__go .block,
  .proj__go .block__face {
    width: 100%;
    justify-content: center;
  }

  .proj__go {
    justify-self: stretch;
  }
}

/* ---- The studio's list (src/render/pages/studio.ts) ----
   Each project one row between hairlines (company, stage, date) that opens
   into its fields and Save. */

.clients__head {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.clients__head > *,
.clients__links,
.clients__actions {
  margin: 0;
}

.clients__list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--hairline-strong);
}

.clients__item {
  border-bottom: 1px solid var(--hairline-strong);
}

.clients__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  padding-block: var(--space-2);
  list-style: none;
  cursor: pointer;
}

.clients__summary::-webkit-details-marker {
  display: none;
}

.clients__company {
  flex: 1 1 12rem;
  font-size: var(--t-lead);
  letter-spacing: -0.018em;
}

.clients__stage {
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
}

.clients__form {
  display: grid;
  gap: var(--space-2);
  padding-bottom: var(--space-4);
}

.clients__links,
.clients__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.5rem;
}


/* The studio form's plan and dates: price and monthly side by side from
   560 px; each date a row of what it is and the day. */
.clients__pair {
  display: grid;
  gap: var(--space-2);
}

@media (min-width: 560px) {
  .clients__pair {
    grid-template-columns: 1fr 1fr;
  }
}

.clients__dates {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.clients__dates [data-dates] {
  display: grid;
  gap: 0.5rem;
}

.clients__date {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.75rem;
}

.clients__date input {
  min-width: 0;
  padding: 0.5rem 0;
  font: inherit;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hairline-strong);
  border-radius: 0;
}

.clients__date input:focus-visible {
  outline: none;
  border-bottom-color: var(--focus);
}

/* The studio's image slots: logo and picture side by side, each a preview
   with Upload/Replace and Remove under it. */
.clients__images {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-2);
}

.clients__image {
  display: grid;
  align-content: start;
  gap: 0.5rem;
  min-width: 0;
}

.clients__thumb {
  display: grid;
  place-items: center;
  height: 6rem;
  overflow: hidden;
  border: 1px solid var(--hairline-strong);
  border-radius: 0.25rem;
  background: var(--bg-2);
}

.clients__thumb--logo {
  padding: 0.75rem;
  background: #fff;
}

.clients__thumb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.clients__thumb--picture img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}

.clients__image-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
}

.clients__image-actions label {
  cursor: pointer;
}

.clients__image-actions label:focus-within {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.clients__remove {
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg-muted);
  font: inherit;
  cursor: pointer;
}

/* The bar's gap between the two phases: Onboarding (preview, go-ahead), then
   the project (design, build, live). */
.proj__bar .is-phase {
  margin-left: 0.5rem;
}

/* Under the ready preview: the yes, and a quiet way to ask first. */
.proj__decide {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
}

.proj__decide [role="status"]:empty {
  display: none;
}

.proj__ask {
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg);
  font: inherit;
  font-size: var(--t-micro);
  font-weight: var(--w-strong);
  cursor: pointer;
}

/* Messages: the studio's on the left on paper, the client's on the right in
   ink, then the box to write in. */
.proj__thread {
  display: grid;
  gap: 0.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.proj__msg {
  display: grid;
  gap: 0.25rem;
  max-width: 85%;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  background: var(--bg);
}

.proj__msg.is-client {
  justify-self: end;
  background: var(--fg);
  color: var(--bg);
}

.proj__msg.is-client .muted {
  color: color-mix(in srgb, var(--bg) 65%, transparent);
}

.proj__msg-text {
  white-space: pre-line;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

.proj__send {
  display: grid;
  gap: 0.75rem;
}

.proj__send textarea {
  box-sizing: border-box;
  width: 100%;
  min-height: 6rem;
  padding: 0.75rem 1rem;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  border-radius: 0.5rem;
  resize: vertical;
}

.proj__send textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
}

.proj__send-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
}

/* The studio: a waiting message flagged in the accent on the row, the thread
   (the client's lines marked with an accent rule), the answer box. */
.clients__new {
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--ink);
}

.clients__new[hidden] {
  display: none;
}

.clients__messages {
  display: grid;
  gap: 0.75rem;
}

.clients__thread {
  display: grid;
  gap: 0.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.clients__thread li {
  display: grid;
  gap: 0.25rem;
  padding-left: 0.75rem;
  border-left: 2px solid var(--hairline-strong);
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.clients__thread li.is-client {
  border-left-color: var(--accent);
}

.clients__answer {
  box-sizing: border-box;
  width: 100%;
  padding: 0.75rem;
  font: inherit;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--hairline-strong);
  border-radius: 0.375rem;
  resize: vertical;
}

.clients__answer:focus-visible {
  outline: none;
  border-color: var(--accent);
}

/* The onboarding's priced choices (src/render/pages/onboarding.ts): the
   included pages ticked and locked (no hover, no pointer), each extra
   page with its price beside its name, a little quieter. */
.chip--locked {
  gap: 0.375rem;
  cursor: default;
}

/* Not "picked" (ink) but "already there": quiet, with a tick. */
.chip--choice.chip--locked:has(input:checked) {
  background: var(--bg-2);
  border-color: var(--hairline-strong);
  color: var(--fg-muted);
}

.chip--locked::before {
  content: "";
  width: 0.5rem;
  height: 0.3rem;
  margin-top: -0.15rem;
  border-left: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
}

.chip--locked input {
  cursor: default;
}

@media (hover: hover) {
  .chip--locked:hover {
    border-color: var(--fg);
  }
}

.chip__price {
  margin-left: 0.375rem;
  font-weight: var(--w-text);
  opacity: 0.7;
}

/* The onboarding's success (src/render/pages/onboarding.ts), in place of the
   form once it is sent: a tick in the accent, the line, what happens next,
   the way to the project page, and that the link is in their email too. */
.onb__success {
  display: grid;
  justify-items: start;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-3);
  border-radius: 0.5rem;
  background: var(--bg-2);
}

.onb__success p {
  margin: 0;
  text-wrap: pretty;
}

.onb__tick {
  position: relative;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: var(--accent);
}

.onb__tick::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 45%;
  width: 1.1rem;
  height: 0.6rem;
  border-left: 2px solid var(--ink);
  border-bottom: 2px solid var(--ink);
  transform: translate(-50%, -50%) rotate(-45deg);
}

.onb__success-title {
  margin: 0;
  font-size: var(--t-h3);
  font-weight: var(--w-text);
  line-height: 1.2;
  letter-spacing: var(--tr-h3);
  text-wrap: balance;
}

.onb__success-title:focus {
  outline: none;
}

@media (prefers-reduced-motion: no-preference) {
  .onb__success:not([hidden]) {
    animation: cq-in 0.6s var(--ease-out-expo) both;
  }
}

@media (max-width: 559px) {
  .onb__success {
    padding: var(--space-3) 1.25rem;
  }

  .onb__success .btn,
  .onb__success .btn .block,
  .onb__success .btn .block__face {
    width: 100%;
    justify-content: center;
  }

  .onb__success .btn {
    justify-self: stretch;
  }
}

/* The FAQ, the free check and the landing pages (src/render/extras.ts,
   pages/faq.ts, pages/check.ts, pages/landing.ts). On the light page, built
   from the same 12 columns, hairlines and type as Work and the case pages. */

.xpage {
  padding-bottom: var(--section-y);
}

.xpage > * + * {
  margin-top: var(--section-y);
}

.xpage .page-head__title {
  text-wrap: balance;
}

.xpage p,
.xpage li {
  text-wrap: pretty;
}

/* ---- A landing page's head, like a case page (pages/landing.ts) ----
   The title large, the intro and the buttons beside it at its foot; under
   them the first project large in a 4:3 frame with the facts beside it (the
   case page's .case__facts). The frame is not the case page's (no
   view-transition-name), so nothing morphs into it. */

.xpage--landing > .case__body {
  margin-top: 0;
}

.xhero__lede {
  grid-column: 9 / -1;
  display: grid;
  gap: var(--space-4);
  padding-bottom: 0.6em;
}

.xhero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

.xpage--landing > .xhero__intro {
  margin-top: 0;
}

.xhero__intro {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  align-items: start;
}

.xhero__media {
  grid-column: 1 / span 8;
  display: grid;
  gap: var(--space-2);
}

.xhero__media img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--media-bg);
}

.xhero__facts {
  grid-column: 10 / -1;
}

.xmore {
  display: grid;
  gap: var(--space-4);
}

@media (max-width: 768px) {
  .xhero__lede,
  .xhero__media,
  .xhero__facts {
    grid-column: 1 / -1;
  }
}

/* ---- Questions: native <details>, one hairline each ---- */

.faq {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
}

.faq__title {
  grid-column: 1 / span 3;
}

.faq__list {
  grid-column: 4 / span 8;
  border-top: 1px solid var(--hairline-strong);
}

.faq__title + .faq__list {
  grid-column: 4 / span 8;
}

.faq > .faq__list:only-child {
  grid-column: 4 / span 8;
}

.faq__item {
  border-bottom: 1px solid var(--hairline);
}

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-block: var(--space-3);
  font-size: var(--t-h3);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h3);
  text-wrap: balance;
  cursor: pointer;
  list-style: none;
}

.faq__q::-webkit-details-marker {
  display: none;
}

.faq__q:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}

/* A plus that turns into a minus. */
.faq__icon {
  position: relative;
  flex: none;
  width: 0.75em;
  height: 0.75em;
}

.faq__icon::before,
.faq__icon::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: currentColor;
  transition: rotate var(--d-base) var(--ease-out);
}

.faq__icon::after {
  rotate: 90deg;
}

/* Open: a minus. modules/faq.ts sets .is-open the moment a question is
   clicked, so the plus turns back at once while the answer is still folding
   shut ([open] stays on until it has); without the module, [open] alone. */
.faq__item[open] .faq__icon::after,
.faq__item.is-open .faq__icon::after {
  rotate: 0deg;
}

html.js .faq__item[open]:not(.is-open) .faq__icon::after {
  rotate: 90deg;
}

/* The space under an answer is inside it (.faq__inner), so the height
   modules/faq.ts animates runs all the way to 0. */
.faq__a {
  max-width: 40em;
  color: var(--fg-muted);
}

.faq__inner {
  padding-bottom: var(--space-4);
}

@media (hover: hover) {
  .faq__q:hover {
    color: var(--accent-text);
  }
}

/* ---- The steps with their weeks ---- */

.xsteps {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
}

.xsteps > h2 {
  grid-column: 1 / span 3;
}

.xsteps__list {
  grid-column: 4 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gutter);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

.xsteps__item {
  display: grid;
  align-content: start;
  gap: var(--space-2);
}

.xsteps__when {
  color: var(--accent-text);
}

.xsteps__title {
  font-size: var(--t-h3);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-h3);
}

.xsteps__note {
  grid-column: 4 / span 6;
  color: var(--fg-muted);
}

/* The same weeks on the homepage's Process stage and in the pricing bento.
   On the homepage it is the first line of the step's text (process-variants.ts),
   so it waits and rolls in with it. */
.pv-words__when,
.bento__when {
  color: var(--accent-text);
}

.pv-words__when {
  display: block;
  margin-bottom: 0.5em;
}

.bento__when {
  margin-block: 0.25em 0.35em;
}

/* ---- A row of work ---- */

.xwork {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gutter);
}

.xwork__link {
  display: grid;
  gap: var(--space-2);
  color: inherit;
  text-decoration: none;
}

.xwork__media {
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--media-bg);
}

.xwork__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: scale var(--d-slow) var(--ease-out-expo);
}

.xwork__title {
  font-size: var(--t-body-l);
}

@media (hover: hover) {
  .xwork__link:hover .xwork__media img {
    scale: 1.04;
  }
}

/* ---- What clients say ---- */

.xreviews {
  display: grid;
  gap: var(--space-4);
}

.xreviews__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--gutter);
}

.xreviews__item {
  display: grid;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

/* ---- The closing call ---- */

.xstart {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-4);
  align-items: end;
  padding-top: var(--space-6);
  border-top: 1px solid var(--hairline-strong);
}

.xstart__title {
  grid-column: 1 / span 7;
  text-wrap: balance;
}

.xstart__actions {
  grid-column: 8 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

/* ---- The free check ---- */

.xcheck {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-6);
}

.xcheck__what {
  grid-column: 1 / span 6;
}

.xcheck__how {
  grid-column: 8 / -1;
}

.xcheck__what > h2,
.xcheck__how > h2 {
  margin-bottom: var(--space-3);
}

.xcheck__list,
.xcheck__steps {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline-strong);
}

.xcheck__list li {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
}

.xcheck__list li::before {
  content: "→";
  color: var(--fg-faint);
}

.xcheck__steps li {
  display: grid;
  gap: var(--space-1);
}

.xpage .xcheck__form {
  scroll-margin-top: var(--nav-h);
}

/* ---- Narrow screens: one column ---- */

@media (max-width: 768px) {
  .faq__title,
  .faq__list,
  .faq__title + .faq__list,
  .faq > .faq__list:only-child,
  .xsteps > h2,
  .xsteps__list,
  .xsteps__note,
  .xstart__title,
  .xstart__actions,
  .xcheck__what,
  .xcheck__how {
    grid-column: 1 / -1;
  }

  .xsteps__list {
    grid-template-columns: 1fr;
  }

  .xwork {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/*
 * Scroll-linked motion, all of it CSS scroll-driven animations. Rule of the
 * framework (docs/framework.md): if a value maps 1:1 to scroll position it is
 * declared here, not in JavaScript. Browsers without scroll timelines, and
 * people who ask for reduced motion, get the finished state of every
 * animation below.
 */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Statements ink in word by word as they cross the screen (home Studio,
       the footer). src/modules/enhance.ts splits the words and sets each --a / --b. */
    [data-words].is-split {
      view-timeline: --words block;
    }

    [data-words].is-split .word {
      animation: word-ink linear both;
      animation-timeline: --words;
      animation-range: cover var(--a) cover var(--b);
    }

    .studio__portrait-frame img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }

    /* The footer comes up in steps: the label row, then the two lines of the
       heading (their words inking in, [data-words] above), the intro and the
       button with the address, the row of links; last, the studio's name
       rises up out of the bottom edge as the page reaches its end (the footer
       clips it). */
    .footer__meta,
    .footer__statement > span,
    .footer__intro,
    .footer__actions,
    .footer__bar {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
    }

    .footer__statement > span + span {
      animation-range: entry 10% entry 75%;
    }

    .footer__intro {
      animation-range: entry 10% entry 70%;
    }

    .footer__actions {
      animation-range: entry 25% entry 85%;
    }

    .footer__name {
      animation: name-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 100%;
    }

    /* ---- Parallax: layers that move at their own speed through the view ---- */

    /* Hero: the slider and the pan of each mockup inside its frame both run in
       src/modules/ribbon.ts (they follow the slide, not the scroll position).
       Studio does the overtaking. */

    /* Studio: rises over the bottom of the hero strip, a little faster than the
       page, from the moment its top edge comes into view. The negative margin
       takes the same distance back out of the layout, so Work follows on. */
    .hero + .studio {
      --over: 20svh;
      position: relative;
      z-index: 2;
      margin-bottom: calc(-1 * var(--over));
      /* On a phone 20svh is a fraction of a pixel, and the moved section and
         the one after it can round apart, leaving a hairline of the light page
         between them. A strip of its own colour closes it. */
      box-shadow: 0 2px var(--bg);
      animation: slide-over linear both;
      animation-timeline: view();
      animation-range: entry-crossing 0% entry-crossing 70%;
    }

    /* The end of the page turns like the menu, on the other axis (menu.css):
       the page and the footer are two faces of one prism, joined at the
       footer's top edge. As the footer comes up, the page above tips back
       about that edge into the dark (the body behind it, ink from the first
       moment) and the footer swings up flat, the angle between them always
       the menu's fold; at the very end the footer lies flat and the last of
       the page leans away above it. Scrolling back turns it back. While any
       of the footer is in view (html.is-folding, src/modules/fold.ts) the
       page is cut to its last screen, so the rest of it does not come into
       view as it leans away, and the nav steps aside (nav.css). Only where
       the two can share the footer's timeline (timeline-scope), and not on
       phones (up to 768px wide), where the footer simply comes up with the
       page; while the menu turns (it measures the page flat first, menu.ts)
       the page lies flat. */
    @media (min-width: 769px) {
      @supports (timeline-scope: --footer) {
        html {
          timeline-scope: --footer;
        }

        .footer {
          view-timeline: --footer block;
          transform-origin: 50% 0;
          animation: fold-up linear both;
          animation-timeline: --footer;
          animation-range: entry 0% entry 100%;
        }

        /* At least a screen tall, so even a short page (the blog with one post)
           opens flat and only turns once it is scrolled. */
        main {
          min-height: 100svh;
          transform-origin: 50% 100%;
          animation: fold-back linear forwards;
          animation-timeline: --footer;
          animation-range: entry 0% entry 100%;
        }

        body {
          animation: fold-dark linear forwards;
          animation-timeline: --footer;
          animation-range: entry 0% entry 2%;
        }

        /* A class, not part of the animation: Chromium hit-tests an animated
           clip-path as if it applied all the time, and nothing above the last
           screen could be clicked. */
        html.is-folding:not(.menu-flat, .menu-3d) main {
          clip-path: inset(calc(100% - 100svh) 0 0 0);
        }

        html:is(.menu-flat, .menu-3d) :is(body, main, .footer) {
          animation: none;
        }
      }
    }

    /* What we do, scrolling in: the stage comes in ahead of the page and slows
       to a stop just as it pins, so it lands without a jolt. The offset is
       half the distance it is taken back over, eased in (y = x²), so the stage
       leaves at the page's speed and arrives at none. It overlaps Studio on
       the way in, so it sits above it. */
    .services.is-dealing {
      position: relative;
      z-index: 3;
    }

    .services.is-dealing .services__stage {
      animation: land linear both;
      animation-timeline: --deal;
      animation-range: entry 50% entry 100%;
    }

    /* The line rises in first, before the stage pins... */
    .services.is-dealing .services__lead {
      animation: rise-in linear both;
      animation-timeline: --deal;
      animation-range: entry 50% entry 95%;
    }

    /* ...then, pinned, the cards are dealt out of the middle one by one,
       each spinning onto its slot while the ring turns (deal.ts). */
    .services.is-dealing .svc {
      animation: deal-out linear both;
      animation-timeline: --deal;
    }

    .services.is-dealing .deal:nth-child(1) .svc {
      animation-range: contain 2% contain 15%;
    }

    .services.is-dealing .deal:nth-child(2) .svc {
      animation-range: contain 13% contain 26%;
    }

    .services.is-dealing .deal:nth-child(3) .svc {
      animation-range: contain 24% contain 37%;
    }

    /* Without the ring (phones, deal.ts): the line rises in, then each card
       as it comes into view, straightening up from a slight tilt (the other
       way on every second card), like being laid down. */
    .services:not(.is-dealing) .services__lead {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
    }

    .services:not(.is-dealing) .svc {
      animation: lay-down linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 75%;
    }

    .services:not(.is-dealing) .deal:nth-child(even) .svc {
      --tilt: 5deg;
    }

    /* What we do lifts off Process: Studio over the hero, inverted. Process
       comes out from under the cards instead of coming in over them: the
       section is laid out that far over the top of Process (the negative
       margin) and, as it scrolls away, rises that much faster than the page,
       so Process shows from under it, all of it by the time it has gone. */
    .services {
      --over: 20svh;
      position: relative;
      z-index: 3;
      margin-bottom: calc(-1 * var(--over));
      animation: lift-off linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }

    /* Big serif titles drift sideways as the section passes. */
    .section-title {
      animation: title-drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }

    /* Case page: the cover scrolls with the page, no drift and no lag
       (removed 2026-11-04 at Jelte's request: it felt bouncy). */

    /* Work on a phone: the cases under each other, each rising in as it
       comes into view (the parade is for wider screens, parade.ts). */
    @media (max-width: 768px) {
      .parade__item {
        animation: rise-in linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 60%;
      }
    }

    /* Chart bars grow out from the axis as they come in. */
    .cb-chart__bar {
      animation: bar-grow linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 35%;
    }

    .cb-media,
    .cb-quote,
    .cb-list,
    .cb-decisions__row {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 50%;
    }

    /* The portrait rises past the text beside it. */
    .studio__portrait {
      animation: lift-md linear both;
      animation-timeline: view();
      animation-range: cover;
    }

  }
}

@keyframes word-ink {
  from {
    color: var(--fg-faint);
  }
}

@keyframes drift {
  from {
    translate: 0 -7%;
  }

  to {
    translate: 0 7%;
  }
}

@keyframes rise-in {
  from {
    opacity: 0;
    translate: 0 1.75rem;
  }
}

/* The footer's name, up from below its line (the footer clips what is below). */
@keyframes name-rise {
  from {
    translate: 0 70%;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }

  to {
    translate: 0 0;
  }
}

@keyframes slide-over {
  to {
    translate: 0 calc(-1 * var(--over));
  }
}

/* The end of the page (above): the footer swings up flat about its top edge... */
@keyframes fold-up {
  from {
    transform: perspective(var(--menu-depth)) rotateX(calc(-1 * var(--menu-fold)));
    filter: brightness(0.6);
  }

  to {
    transform: perspective(var(--menu-depth)) rotateX(0deg);
    filter: brightness(1);
  }
}

/* ...while the page leans back about its bottom edge. Not dimmed, unlike
   the menu's turn: the logo over it blends with it (nav.css) and would fade. */
@keyframes fold-back {
  from {
    transform: perspective(var(--menu-depth)) rotateX(0deg);
  }

  to {
    transform: perspective(var(--menu-depth)) rotateX(var(--menu-fold));
  }
}

@keyframes fold-dark {
  to {
    background-color: var(--ink);
  }
}

@keyframes lift-off {
  to {
    translate: 0 calc(-1 * var(--over));
  }
}

@keyframes land {
  from {
    translate: 0 -25svh;
    animation-timing-function: cubic-bezier(0.333, 0, 0.667, 0.333);
  }

  to {
    translate: 0 0;
  }
}

/* A card from the middle of the ring out to its slot (--to-centre, services.css). */
@keyframes deal-out {
  from {
    opacity: 0;
    translate: 0 var(--to-centre);
    scale: 0.5;
    rotate: -90deg;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }

  25% {
    opacity: 1;
  }

  to {
    opacity: 1;
    translate: 0 0;
    scale: 1;
    rotate: 0deg;
  }
}

/* A card straightening up as it comes in (--tilt per card, above). */
@keyframes lay-down {
  from {
    opacity: 0;
    translate: 0 3rem;
    rotate: var(--tilt, -5deg);
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  }

  to {
    opacity: 1;
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes title-drift {
  from {
    translate: 6vw 0;
  }

  to {
    translate: -3vw 0;
  }
}

@keyframes bar-grow {
  from {
    scale: 0 1;
  }
}

@keyframes lift-md {
  from {
    transform: translateY(6%);
  }

  to {
    transform: translateY(-6%);
  }
}

@keyframes slide-right {
  from {
    translate: -4vw 0;
  }

  to {
    translate: 0 0;
  }
}

@keyframes slide-left {
  from {
    translate: 6vw 0;
  }

  to {
    translate: 0 0;
  }
}

@keyframes panel-open {
  from {
    clip-path: inset(0 var(--edge));
  }

  to {
    clip-path: inset(0 0);
  }
}

/*
 * View transitions. Cross-document transitions are on for every same-origin
 * navigation: home to a case page, About, Contact and back.
 */

@view-transition {
  navigation: auto;
}

::view-transition-group(root) {
  animation-duration: 0.45s;
  animation-timing-function: var(--ease-in-out);
}

/* Case: going from a case on /work/ (or the next case) to its case page, the
   picture's 4:3 frame becomes the page's cover frame
   (src/modules/page-transitions.ts names it on the way out; the case page
   names its cover the same, pages.css). */
::view-transition-group(case-media) {
  animation-duration: 0.9s;
  animation-timing-function: var(--ease-out-expo);
}

::view-transition-old(case-media),
::view-transition-new(case-media) {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * Every other page change by link or button turns like the menu does, only
 * without the menu: the next page comes in from the right while the one
 * before turns away to the left, two faces of one cylinder joined at an edge
 * that crosses the screen (the same fold and depth as the menu's turn,
 * menu.css). Back through history it turns the other way. The inline script
 * in index.html picks the type on pagereveal, and leaves it out for a page
 * change through the menu (its own turn, menu.ts) and for a card's picture
 * morphing into its case page (above). Behind the turning pages: the dark
 * inside of the cylinder, as when the menu opens.
 */
@media (prefers-reduced-motion: no-preference) {
  html:active-view-transition-type(turn-on, turn-back)::view-transition {
    background:
      linear-gradient(rgb(24 25 26 / 0.99), rgb(24 25 26 / 0.99)),
      var(--menu-backdrop) center / cover no-repeat var(--ink);
  }

  /* A case page's cover turns in with its page (it only morphs on its own
     from a picture of its case, above). */
  html:active-view-transition-type(turn-on, turn-back) .case__hero {
    view-transition-name: none;
  }

  html:active-view-transition-type(turn-on, turn-back)::view-transition-old(root),
  html:active-view-transition-type(turn-on, turn-back)::view-transition-new(root) {
    mix-blend-mode: normal;
    animation-duration: 1.1s;
    animation-timing-function: var(--ease-in-out);
    animation-fill-mode: both;
  }

  /* On: the page before about its right edge, the next one about its left. */
  html:active-view-transition-type(turn-on)::view-transition-old(root) {
    transform-origin: 100% 50%;
    animation-name: turn-away-left;
  }

  html:active-view-transition-type(turn-on)::view-transition-new(root) {
    transform-origin: 0 50%;
    animation-name: turn-in-right;
  }

  /* Back: mirrored. */
  html:active-view-transition-type(turn-back)::view-transition-old(root) {
    transform-origin: 0 50%;
    animation-name: turn-away-right;
  }

  html:active-view-transition-type(turn-back)::view-transition-new(root) {
    transform-origin: 100% 50%;
    animation-name: turn-in-left;
  }
}

@keyframes turn-away-left {
  to {
    transform: translateX(-100%) perspective(var(--menu-depth)) rotateY(calc(-1 * var(--menu-fold)));
    filter: brightness(0.6);
  }
}

@keyframes turn-in-right {
  from {
    transform: translateX(100%) perspective(var(--menu-depth)) rotateY(var(--menu-fold));
    filter: brightness(0.6);
  }
}

@keyframes turn-away-right {
  to {
    transform: translateX(100%) perspective(var(--menu-depth)) rotateY(var(--menu-fold));
    filter: brightness(0.6);
  }
}

@keyframes turn-in-left {
  from {
    transform: translateX(-100%) perspective(var(--menu-depth)) rotateY(calc(-1 * var(--menu-fold)));
    filter: brightness(0.6);
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
