/*
 * 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-on-paper: #85817a;
  --muted-on-ink: #a5a09a;
  --faint-on-ink: #7d7973;
  --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));
  --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(--accent);
  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 */
html.lenis,
html.lenis body {
  height: auto;
}

.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis-stopped {
  overflow: clip;
}

[data-lenis-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;
  }
}

/* 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(--accent);
  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;
}

/* 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. */
main,
.footer {
  transition: filter var(--menu-time-close) var(--ease-in-out);
}

html.menu-open :is(main, .footer) {
  filter: brightness(0.6);
  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;
  }

  .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);
  }

  .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 (they count up, enhance.ts). */
.studio__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: var(--space-4);
  border-top: 1px solid var(--hairline-strong);
}

/* 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 {
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  gap: 0.5rem;
  padding: 1.25rem var(--gutter) 0 0;
}

.stat + .stat {
  padding-left: var(--gutter);
  border-left: 1px solid var(--hairline);
}

.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: a bigger ring with bigger cards, the label above it instead of inside. */
/* Phones: no ring (deal.ts). The line, then the cards straight, one under
   the other; they rise in as they come into view (scroll.css). */
@media (max-width: 699px) {
  .services__lead {
    font-size: 2rem;
  }

  .services__cards {
    flex-direction: column;
    align-items: center;
    gap: var(--space-4);
  }

  .deal {
    width: min(17rem, 78vw);
  }
}
/* Process: the mark assembles itself while you read. Each step sits beside
   its own bar; the bars fill left to right in scroll order (scroll.css), one
   beat each, and each step lights up as its bar completes. */

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

/* The mark in 3D (src/modules/process-mark.ts): one canvas over the whole
   stage, behind the text. The flat bars keep their places (each solid bar
   stands on its flat one) but hide once it draws. Behind the bars, the
   accent glow: a WebGPU mesh gradient (shaders) where it can, this CSS
   gradient until then and anywhere else. */
.process__stage {
  position: relative; /* sticky from 640px tall (scroll.css), which holds the canvas as well */
}

.process__grid {
  position: relative;
  z-index: 1;
}

.process__gl {
  position: absolute;
  inset: 0;
  overflow: clip;
  pointer-events: none;
}

.process__gl > canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.process__gl > .process__glow {
  display: none;
  inset: auto;
  left: var(--mark-x);
  top: var(--mark-y);
  width: calc(var(--mark-w) * 1.9);
  height: calc(var(--mark-h) * 1.45);
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(closest-side, color-mix(in oklab, var(--accent) 70%, transparent), transparent);
  mask-image: radial-gradient(closest-side, #000 35%, transparent);
}

.has-glow .process__glow {
  display: block;
}

.process__glow.is-drawn {
  background: none;
  opacity: 0.6;
}

.has-mark-3d .process__bar {
  visibility: hidden;
}

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

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

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

.process__intro {
  max-width: 26em;
}

.process__steps {
  --h: min(50svh, 31rem);
  grid-column: 6 / -1;
  display: grid;
  grid-template-rows: 12fr 2fr 12fr 2fr 12fr;
  height: var(--h);
}

.step {
  display: grid;
  grid-template-columns: calc(var(--h) * 0.8) minmax(0, 1fr);
  column-gap: calc(var(--gutter) * 2);
  align-items: center;
  min-height: 0;
}

.step--1 {
  grid-row: 1;
}

.step--2 {
  grid-row: 3;
}

.step--3 {
  grid-row: 5;
}

.step__bar {
  height: 100%;
  min-height: 0;
  color: var(--display-ink);
}

.process__bar {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.step__text {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  column-gap: 0.75rem;
  row-gap: 0.35rem;
  align-items: baseline;
}

.step__text > :not(.index) {
  grid-column: 2;
}

/* The step's number on a block of the accent, like the count in the menu. */
.step__text .index {
  justify-self: start;
  padding: 0.4em 0.45em 0.35em;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--ink);
}

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

/* Each step is a button (its title): it brings the stage to that step
   (src/modules/process-mark.ts), and so does a click anywhere on the step,
   bar and all. Its title rolls on hover like every link. */
.step {
  cursor: pointer;
}

@media (hover: hover) {
  .step:hover .roll__prism {
    --turn: 60deg;
  }
}

.step__hit {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
}

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

.step:has(.step__hit:focus-visible) {
  outline: 2px solid currentColor;
  outline-offset: 0.5rem;
  border-radius: var(--radius);
}

.step__summary {
  color: var(--fg-muted);
  max-width: 28em;
}

@media (max-width: 1099px) {
  .process__head,
  .process__steps {
    grid-column: 1 / -1;
  }

  .process__head {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: end;
    column-gap: var(--gutter);
  }

  .process__head .eyebrow {
    grid-column: 1 / -1;
  }
}

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

  .process__steps {
    height: auto;
    grid-template-rows: none;
    gap: var(--space-4);
  }

  .step {
    grid-row: auto;
    grid-template-columns: 36vw minmax(0, 1fr);
    column-gap: var(--gutter);
  }

  .step__bar {
    height: auto;
    aspect-ratio: 32 / 12;
  }

  .step__text {
    grid-template-columns: 1fr;
  }

  .step__text > :not(.index) {
    grid-column: 1;
  }
}

/* Phones (up to 768px wide): no pinned stage (scroll.css). The mark stands
   whole at the top, its three bars stacked as in the logo (12 : 2 : 12 : 2 :
   12, each bar cropped from it at the same width, so they step right as in
   the mark), and the steps follow under it, all lit. Each step's bar and
   text are placed apart through display: contents; the step still takes the
   click (process-mark.ts), and its button shows its own focus ring here. */
@media (max-width: 768px) {
  .process__steps {
    --mark: min(62vw, 16rem);
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .step {
    display: contents;
  }

  .step__bar {
    grid-column: 1;
    width: var(--mark);
  }

  .step--2 .step__bar,
  .step--3 .step__bar {
    margin-top: calc(var(--mark) * 2 / 32);
  }

  .step--1 .step__bar {
    grid-row: 1;
  }

  .step--2 .step__bar {
    grid-row: 2;
  }

  .step--3 .step__bar {
    grid-row: 3;
  }

  .step__text {
    grid-column: 1;
    margin-top: var(--space-4);
  }

  .step--1 .step__text {
    grid-row: 4;
    margin-top: var(--space-6);
  }

  .step--2 .step__text {
    grid-row: 5;
  }

  .step--3 .step__text {
    grid-row: 6;
  }

  .step__hit:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 0.35rem;
    border-radius: var(--radius);
  }
}
.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 (Motion springs). */
.card-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 30;
  padding: 0.55rem 0.7rem;
  background: var(--accent);
  color: var(--ink);
  border-radius: var(--radius);
  pointer-events: none;
  opacity: 0;
  translate: -50% -50%;
  white-space: nowrap;
}

/* ---- 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-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);
}

.cb-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--hairline-strong);
}

.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);
}

/* 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);
}

/* In the middle of the row's height, level with the arrow. */
.way__label {
  grid-column: 1 / span 3;
  align-self: center;
}

.way__main {
  grid-column: 4 / span 7;
  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(--accent);
  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: four columns under the rows. */
.cfacts {
  display: grid;
  grid-template-columns: repeat(4, 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;
}

/* The form, beside what helps. */
.cwrite__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-5);
}

.cwrite__aside {
  grid-column: 1 / span 5;
  display: grid;
  align-content: start;
  gap: var(--space-5);
}

.ctips {
  display: grid;
  gap: var(--space-2);
}

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

.ctips__list li {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-block: 0.85rem;
  border-bottom: 1px solid var(--hairline);
}

/* The form: one question at a time (src/modules/contact-form.ts, .is-stepped),
   asked large, with one rule under the answer (driezie.studio) that turns to
   the accent while you type, and a bar in the accent filling question by
   question. Without the module the questions stand under each other. */
.cform {
  grid-column: 7 / -1;
  display: grid;
  align-content: start;
  gap: 0.25rem;
}

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

.cform__top {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.75rem;
}

.cform__progress {
  height: 2px;
  margin-bottom: var(--space-3);
  overflow: hidden;
  background: var(--hairline);
}

.cform__progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  transform-origin: 0 50%;
  transform: scaleX(var(--p, 0));
  transition: transform 0.7s var(--ease-out-expo);
}

/* A question: what is asked, a hint, the answer. */
.cq {
  min-width: 0;
  margin: 0;
  padding: 0.5rem 0 1.1rem;
  border: 0;
  border-bottom: 1px solid var(--hairline-strong);
}

.cq__ask {
  display: block;
  padding: 0;
  font-size: var(--t-h3);
  line-height: 1.15;
  letter-spacing: var(--tr-h3);
  color: var(--fg);
}

.cq__hint {
  margin-top: 0.4rem;
  color: var(--fg-muted);
}

.cq--choice .chips {
  margin-top: 1.1rem;
}

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

@keyframes cq-in {
  from {
    opacity: 0;
    translate: 0 1rem;
  }
}

.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);
}

.field:focus-within {
  border-bottom-color: var(--accent);
}

.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);
}

.cform__note {
  margin-top: 0.75rem;
  color: var(--accent-text);
}

/* Cloudflare Turnstile's widget, with the last question. */
.cform__check {
  margin-top: var(--space-3);
}

.cform__note a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* The answer button first; Back after it, quiet. */
.cform__submit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.75rem;
  margin-top: var(--space-3);
}

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

/* ---- 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. No counts, bars or lines between the questions. */

.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-5);
}

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

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

.onb__step {
  display: grid;
  gap: var(--space-3);
  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-2);
  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;
}

.onb__choice {
  margin: 0;
  padding: 0;
  border: 0;
}

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

/* 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(--accent);
  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,
  .cwrite__aside,
  .cform {
    grid-column: 1 / -1;
  }

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

  .way__label {
    grid-column: 1;
    grid-row: 1;
  }

  .way__arrow {
    grid-column: 2;
    grid-row: 1;
  }

  .way__main {
    grid-column: 1 / -1;
  }

  .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;
  }
}

/*
 * 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: as the cover scrolls away the picture drifts down inside
       its frame, slower than the page. It starts centred, as it lands from
       /work/ (pages.css), so nothing moves until the page does. */
    .case__hero img {
      animation: cover-drift linear both;
      animation-timeline: view();
      animation-range: exit-crossing 0% exit-crossing 100%;
    }

    /* 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;
    }

    /* Process: a sticky stage. The first step and its bar are there from the
       start; the other two bars fill one beat each. Not on phones (up to
       768px wide): there the mark stands whole above the steps (process.css). */
    @media (min-height: 640px) and (min-width: 769px) {
      .process {
        height: 250svh;
        padding-block: 0;
        view-timeline: --process block;
      }

      .process__stage {
        position: sticky;
        top: 0;
        min-height: 100svh;
        display: grid;
        align-items: center;
        padding-block: calc(var(--nav-h) + var(--space-3)) var(--space-3);
      }

      .step:not(.step--1) .process__bar {
        animation: bar-fill linear both;
        animation-timeline: --process;
      }

      /* The glow behind the 3D mark (process.css) brightens and opens up as
         the mark comes together. */
      .process__glow {
        animation: glow-up linear both;
        animation-timeline: --process;
        animation-range: contain 0% contain 100%;
      }

      /* Each step lights up while it is the current one and steps back once
         the next begins (keyframes below, over the whole stage). */
      .step__text {
        animation: linear both;
        animation-timeline: --process;
        animation-range: contain 0% contain 100%;
      }

      .step--2 .process__bar {
        animation-range: contain 22% contain 50%;
      }

      .step--3 .process__bar {
        animation-range: contain 58% contain 86%;
      }

      .step--1 .step__text {
        animation-name: step-1;
      }

      .step--2 .step__text {
        animation-name: step-2;
      }

      .step--3 .step__text {
        animation-name: step-3;
      }
    }
  }
}

@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;
  }
}

/* Within the 4% the cover's 1.08 zoom leaves over each edge. */
@keyframes cover-drift {
  to {
    translate: 0 3.5%;
  }
}

@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);
  }
}

@keyframes glow-up {
  from {
    opacity: 0.3;
    scale: 0.75;
  }
}

@keyframes bar-fill {
  from {
    clip-path: inset(0 100% 0 0);
  }

  to {
    clip-path: inset(0 0 0 0);
  }
}

/* The steps, as fractions of the process stage, in step with the bars
   beside them: the first lit from the start, the next two each lighting up
   as their bar fills (src/modules/process-mark.ts STOPS scrolls to each). */
@keyframes step-1 {
  0%,
  25% {
    opacity: 1;
    translate: 0 0;
  }

  35%,
  100% {
    opacity: 0.4;
    translate: 0 0;
  }
}

@keyframes step-2 {
  0%,
  25% {
    opacity: 0.22;
    translate: 0.75rem 0;
  }

  35%,
  60% {
    opacity: 1;
    translate: 0 0;
  }

  70%,
  100% {
    opacity: 0.4;
    translate: 0 0;
  }
}

@keyframes step-3 {
  0%,
  60% {
    opacity: 0.22;
    translate: 0.75rem 0;
  }

  70%,
  100% {
    opacity: 1;
    translate: 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;
  }
}
