/* ==========================================================================
   davidhbalan.com
   Direction: precise product. Stone ground, one moss accent (since 25 October
   2026; near-white and cobalt from 9 to 25 October), tonal surfaces instead of
   lines, one radius system, one typeface (Geist).
   Grouping comes from spacing and surface tone, never from borders.
   ========================================================================== */

/* ---------- Tokens ---------- */
:root {
  --paper: #EEE9DF;
  --surface: #E7E1D4;
  --surface-2: #D8D0C0;
  --ink: #1E1C1A;
  --ink-2: #36322E;
  --mute: #645E58;
  --accent: #5C6B3A;
  --accent-deep: #434F29;
  --accent-soft: #E1E5D0;
  --line: #D8D0C0;
  --tile: #FFFFFF;        /* behind pictures that were made for white: white, whatever the ground */
  --on-accent: #F6F4EC;   /* text on an accent fill */
  --band: #1E1C1A;        /* once the home page's dark band; kept for the token set */
  --glass-edge: rgba(255, 255, 255, 0.55);

  --display: "Geist", "Helvetica Neue", Arial, sans-serif;
  --text: "Geist", "Helvetica Neue", Arial, sans-serif;

  --r: 14px;
  --r-sm: 10px;
  --r-lg: 20px;
  --shadow-sm: 0 1px 2px rgba(30, 28, 26, 0.06), 0 8px 24px -18px rgba(30, 28, 26, 0.25);
  --shadow-md: 0 1px 2px rgba(30, 28, 26, 0.06), 0 16px 40px -24px rgba(92, 107, 58, 0.3);

  --max: 1160px;
  --gutter: clamp(16px, 4vw, 40px);
  --measure: 62ch;
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 40px;
  --space-5: 64px;
  --space-6: clamp(48px, 5vw, 72px);
}

/* ---------- Theme ----------
   Light by default, dark when the system asks or the nav toggle says so (16
   October 2026). The colour tokens are written once more here with light-dark()
   and `color-scheme` picks the side: `light dark` on the root follows the
   system; html.theme-light or html.theme-dark (put there before the first paint
   by the head script, remembered in localStorage as dhb-theme) force it; and
   body.dark keeps the Dream page dark whatever the theme. Browsers without
   light-dark() keep the light tokens above and the explicit body.dark set
   further down, so they never go dark but nothing breaks. */
@supports (color: light-dark(#000, #fff)) {
  :root {
    color-scheme: light dark;
    --paper: light-dark(#EEE9DF, #141312);
    --surface: light-dark(#E7E1D4, #1E1D1B);
    --surface-2: light-dark(#D8D0C0, #2A2826);
    --ink: light-dark(#1E1C1A, #ECE7DD);
    --ink-2: light-dark(#36322E, #CFC8BD);
    --mute: light-dark(#645E58, #A39C93);
    --accent: light-dark(#5C6B3A, #A6B57A);
    --accent-deep: light-dark(#434F29, #C2CF9A);
    --accent-soft: light-dark(#E1E5D0, #2C301F);
    --line: light-dark(#D8D0C0, #2A2826);
    --tile: light-dark(#FFFFFF, #ECE7DD);
    --on-accent: light-dark(#F6F4EC, #141312);
    --band: light-dark(#1E1C1A, #1E1D1B);
    --glass-edge: light-dark(rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.06));
    --shadow-sm: 0 1px 2px light-dark(rgba(30, 28, 26, 0.06), rgba(0, 0, 0, 0.3)), 0 8px 24px -18px light-dark(rgba(30, 28, 26, 0.25), rgba(0, 0, 0, 0.6));
    --shadow-md: 0 1px 2px light-dark(rgba(30, 28, 26, 0.06), rgba(0, 0, 0, 0.35)), 0 16px 40px -24px light-dark(rgba(92, 107, 58, 0.3), rgba(0, 0, 0, 0.7));
  }
  html.theme-light { color-scheme: light; }
  html.theme-dark, body.dark { color-scheme: dark; }
}
/* Pictures made for white sit on a slightly dimmed tile in the dark theme, and
   are taken down a notch themselves so their whites match it. */
html.theme-dark :is(.problem-media.logo, .problem-media > .tile, .short-media.slides, .short-media.logos, .tokens, .gallery, .screens, .badge-row, .illos, .wires, .lightbox) :is(img, video) { filter: brightness(0.93); }
@media (prefers-color-scheme: dark) {
  html:not(.theme-light) :is(.problem-media.logo, .problem-media > .tile, .short-media.slides, .short-media.logos, .tokens, .gallery, .screens, .badge-row, .illos, .wires, .lightbox) :is(img, video) { filter: brightness(0.93); }
}

/* ---------- Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--text);
  font-size: 1.0625rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--accent); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
a:hover { color: var(--accent-deep); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
p { margin: 0 0 1em; }
ul, ol { padding-left: 1.2em; }
li { margin-bottom: 0.45em; }
.problems > li, .steps > li, .flowmap > li, .contact-list > li, .edu > li, .site-nav li { max-width: none; }
.main > h2, .main > h3 { margin-bottom: var(--space-2); }
strong { font-weight: 600; }

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  letter-spacing: -0.02em;
  /* pretty, not balance: balance split two-line headings into two equal short
     lines, which read as headlines not filling their column (David, 25
     October 2026: the Dream h1 and the milestones card title) */
  text-wrap: pretty;
}
h1 { font-size: clamp(2.25rem, 1.4rem + 3.6vw, 4rem); line-height: 1.04; letter-spacing: -0.035em; }
h2 { font-size: clamp(1.625rem, 1.3rem + 1.4vw, 2.375rem); line-height: 1.1; letter-spacing: -0.03em; }
h3 { font-size: clamp(1.1875rem, 1.05rem + 0.6vw, 1.4375rem); line-height: 1.2; }
h4 { font-size: 1rem; line-height: 1.3; }

.skip-link {
  position: absolute; left: -9999px; top: 8px;
  background: var(--ink); color: var(--paper); padding: 8px 14px; z-index: 50; border-radius: var(--r-sm);
}
.skip-link:focus { left: 8px; }

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

/* Small functional labels. Sentence case, never all caps. */
.label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--mute);
  margin: 0 0 6px;
}

/* ---------- Header ---------- */
.site-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
.site-header .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); flex-wrap: wrap;
  padding-top: 16px; padding-bottom: 12px;
}
.wordmark { font-weight: 600; font-size: 1.0625rem; letter-spacing: -0.02em; color: var(--ink); text-decoration: none; }
.wordmark:hover { color: var(--accent); }
/* The DB monogram (assets/mark-2.svg) as a CSS mask, so it takes the text colour
   around it: ink in the header, mute in the footer, and the dark page's tokens. */
.mark {
  display: inline-block; width: 0.9em; height: 1em; vertical-align: -0.14em;
  background: currentColor;
  -webkit-mask: url(../mark-2.svg) center / contain no-repeat; mask: url(../mark-2.svg) center / contain no-repeat;
}
.wordmark .mark { margin-right: 9px; opacity: 0.88; }
.site-footer .mark { margin-right: 8px; opacity: 0.6; }
.site-nav { display: flex; align-items: center; }
.site-nav ul { display: flex; gap: 10px; list-style: none; margin: 0; padding: 0; }
/* The phone menu (David, 19 October 2026): a <details> before the list that
   holds Work (with the case studies inline), About and the Dream pill.
   Hidden on wide screens, where the full row shows; the phone rules at the
   bottom of the file swap them. A <details> so it opens with scripts off. */
.site-nav .nav-more { display: none; }
.site-nav li { margin: 0; }
.site-nav a {
  font-weight: 500; font-size: 0.9375rem; color: var(--mute); text-decoration: none;
  padding: 8px 14px; border-radius: 999px; transition: background 0.2s, color 0.2s;
}
.site-nav a:hover, .site-nav a[aria-current="page"] { background: var(--surface); color: var(--ink); }
/* The gliding pill (David, 25 October 2026: "a pill shape around Work, and
   when you click About it genie effects into About"). site.js puts one
   `.glide` span in the nav list and in the step bar; it sits behind the
   current item and slides to the hovered link (nav) or the new current step
   (step bar) instead of switching on and off. With `view-transition-name`
   it also morphs from one page's nav to the next's during the cross-fade on
   mouse devices. The per-link backgrounds go quiet while a glider is there;
   scripts off keeps them. */
.site-nav > ul, .step-nav ol { position: relative; }
.site-nav > ul > li > a, .step-nav a { position: relative; z-index: 1; }
.glide {
  position: absolute; left: 0; top: 0; z-index: 0; border-radius: 999px; pointer-events: none;
  background: var(--surface); opacity: 0;
  transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1), width 0.45s cubic-bezier(0.2, 0.7, 0.2, 1), height 0.45s cubic-bezier(0.2, 0.7, 0.2, 1), opacity 0.2s;
}
.glide.on { opacity: 1; }
.glide.still { transition: none; }
.site-nav > ul > .glide { view-transition-name: nav-pill; }
/* The header is its own layer in the page-to-page cross-fade, so it holds
   still while the page slides in beneath it and the pill's morph lines up
   with it (25 October 2026, found while checking the transitions) */
.site-header { view-transition-name: site-header; }
.site-nav ul.has-glide > li > a:not(.nav-fill):hover, .site-nav ul.has-glide > li > a:not(.nav-fill)[aria-current="page"] { background: transparent; }
.step-nav ol > .glide { background: var(--accent-soft); }
.step-nav ol.has-glide a[aria-current] { background: transparent; }
/* Press feedback on the filled pill and the switches: a small squeeze that
   springs back (David, 25 October 2026: "a little microanimation when you
   click on dream, maybe a scale in; same for the dark mode icon") */
.site-nav a.nav-fill, .nav-toggle { transition: background 0.2s, color 0.2s, scale 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); }
.site-nav a.nav-fill:active, .nav-toggle:active { scale: 0.9; }
/* Dream is the one filled pill in the nav, so it pulls the eye (David,
   18 October 2026). Cobalt like .button; the hover and current states go a
   shade deeper instead of grey. */
.site-nav a.nav-fill { background: var(--accent); color: var(--on-accent); font-weight: 600; }
.site-nav a.nav-fill:hover, .site-nav a.nav-fill[aria-current="page"] { background: var(--accent-deep); color: var(--on-accent); }
/* "All work" takes the filled pill on phones (David, 20 October 2026: "I don't want
   people to forget why they came"); Dream moves into the phone menu, still filled */
.site-nav li.phone-only { display: none; }
/* Work opens a list of the case studies (David, 17 October 2026). The links are
   in the HTML, so the list works with scripts off: CSS shows it on hover and
   keyboard focus, site.js adds the tap-to-open on touch screens (first tap opens,
   second follows the link), aria-expanded, Escape, and closing on a tap elsewhere.
   A solid paper panel on the site's shadow, centred under the pill; the ::after
   strip bridges the gap so the pointer can travel down without closing it. */
.site-nav .has-menu { position: relative; }
.site-nav .has-menu::after { content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 10px; }
.site-nav .menu {
  display: block; position: absolute; top: calc(100% + 8px); left: 50%; z-index: 30;
  width: max-content; min-width: 236px; max-width: min(320px, calc(100vw - 32px)); margin: 0; padding: 6px; list-style: none;
  background: var(--paper); border-radius: var(--r);
  box-shadow: var(--shadow-md), inset 0 1px 0 var(--glass-edge);
  opacity: 0; visibility: hidden; transform: translate(-50%, -4px);
  transition: opacity 0.2s, transform 0.2s, visibility 0s 0.2s;
}
/* Keyboard focus opens it through site.js (so Escape can close it); the focus-within rule is for scripts off */
.site-nav .has-menu:hover .menu, html:not(.js) .site-nav .has-menu:focus-within .menu, .site-nav .has-menu.is-open .menu {
  opacity: 1; visibility: visible; transform: translate(-50%, 0);
  transition: opacity 0.2s, transform 0.2s;
}
.site-nav .menu li { margin: 0; }
.site-nav .menu a {
  display: block; padding: 8px 12px; border-radius: var(--r-sm);
  color: var(--ink); font-weight: 500; font-size: 0.9375rem; line-height: 1.3;
}
.site-nav .menu li:first-child a { color: var(--mute); margin-bottom: 2px; }
.site-nav .menu a:hover, .site-nav .menu a[aria-current="page"] { background: var(--surface); color: var(--ink); }

/* Icon toggles at the end of the nav (motion, then theme). A round hit
   area the height of the pills; pressed state in the soft accent. The two <g>
   groups inside the SVG are the off and on drawings. */
/* The two switches sit as a pair, set off from the four links (David, 19 October 2026) */
/* The theme switch sits at the same gap as the links (David, 25 October 2026;
   it stood 26px off until then) */
.site-nav li.tool { margin-left: 0; }
.nav-toggle {
  display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0;
  border: 0; border-radius: 999px; background: transparent; color: var(--mute); cursor: pointer;
  font: inherit; transition: background 0.2s, color 0.2s;
}
.nav-toggle:hover { background: var(--surface); color: var(--ink); }
.nav-toggle[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-deep); }
.nav-toggle svg { width: 20px; height: 20px; display: block; }
.nav-toggle .when-on, .nav-toggle[aria-pressed="true"] .when-off { display: none; }
.nav-toggle[aria-pressed="true"] .when-on { display: block; }
.nav-toggle[aria-pressed="true"] span.when-on { display: inline; }

/* ---------- Two columns: the point on the left, the note on the right ---------- */
/* The copy takes the width and the side panel keeps a fixed share (David,
   25 October 2026: headlines and body copy edge to edge within the padding;
   the columns were 7:4 before). A row whose side is empty runs full width. */
.seam-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
  gap: var(--space-3) var(--space-4);
  align-items: start;
}
.seam-row:not(:has(> .side:not(:empty))) { grid-template-columns: minmax(0, 1fr); }
.seam-row > .side {
  background: var(--surface);
  border-radius: var(--r);
  padding: var(--space-3);
}
.seam-row > .side p:last-child, .seam-row > .side ul:last-child { margin-bottom: 0; }
.seam-row > .side:empty { display: none; }

/* ---------- Home: hero ---------- */
/* Full bleed (25 October 2026, David's refresh after the dot field and the
   disc came out: "no one likes the moving molecules"). The hero is the first
   screen edge to edge and runs under the header, which sits clear over it
   (body.bleed, below) and tints again once the page scrolls. The copy runs
   the full width of the page column. Under it, the scene: the name set
   enormous in the ground's own darker tone, the cutout portrait rising
   through it with five cards resting on a curve around the head (the front
   pair sharp, the pair at the sides turned away and softened, one far
   behind), and an ink strip along the bottom edge with three facts. The hero
   is as tall as its content, so nothing floats. */
.hero { padding: var(--space-3) 0 0; }
.hero.home { padding: 0; }
.hero.home .hero-stage {
  --pad-x: var(--gutter);
  position: relative; display: grid;
  grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr);
  padding: 84px var(--pad-x) 0;
  /* The ground melts into the page instead of ending on a line (David, 25
     October 2026): the surface fades to paper over the lower part of the scene,
     and the portrait and ghost name fade with it (masks below). */
  background: linear-gradient(to bottom, var(--surface) 0, var(--surface) calc(100% - 230px), var(--paper) 100%);
  overflow: hidden; isolation: isolate;
}
@media (min-width: 1240px) { .hero.home .hero-stage { --pad-x: calc((100vw - var(--max)) / 2); } }
html:not(.js) .hero.home .hero-stage { padding-top: var(--space-3); }
.hero.home .hero-meta {
  grid-row: 1; display: flex; justify-content: space-between; gap: var(--space-2);
  font-size: 0.75rem; font-weight: 500; color: var(--mute);
}
.hero.home .hero-copy { grid-row: 2; width: 100%; margin: var(--space-3) 0 0; text-align: center; }
.hero.home .thesis { max-width: none; font-size: clamp(2rem, 1rem + 3.4vw, 4rem); line-height: 1.0; letter-spacing: -0.04em; margin: 0 auto var(--space-2); }
.hero.home .hero-intro { max-width: 80ch; margin: 0 auto; font-size: clamp(1.0625rem, 1rem + 0.45vw, 1.3125rem); line-height: 1.5; }
/* The second sentence starts its own line (David, 25 October 2026) */
.hero.home .hero-intro .turn { display: block; margin-top: 0.35em; }
.hero.home .thesis .turn { display: block; } /* "I design how people" / "understand complex things." (David, 25 October 2026) */
.hero.home .hero-actions { justify-content: center; }
.hero.home .hero-scene {
  grid-row: 3; position: relative; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
  margin: var(--space-2) calc(-1 * var(--pad-x)) 0; padding: 0 var(--pad-x) var(--space-4);
}
.hero-ghost {
  position: absolute; left: 0; right: 0; bottom: 46px; margin: 0; z-index: 0;
  text-align: center; white-space: nowrap; overflow: hidden;
  font-weight: 700; font-size: min(15vw, 15.5rem); line-height: 0.8; letter-spacing: -0.07em;
  color: var(--surface-2);
  -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 100%); mask-image: linear-gradient(to bottom, #000 30%, transparent 100%);
}
.hero-ghost span { display: inline-block; }
.hero-cut {
  grid-column: 1; grid-row: 1; align-self: end; justify-self: center;
  width: min(36%, 420px); margin: 0; z-index: 2;
  pointer-events: none;
}
/* The halo (David, 25 October 2026, chosen over the card belt: "H is kinda
   fire"). A thin ellipse drawn round the head at eye level and tipped into
   the page, with the eight topics as small chips sitting on it: "what my
   world revolves around". The ring is turned 22.5 degrees so no chip sits
   over the face; the front half sits in front of the portrait, the back
   half behind the head, a little smaller and softer. Each chip carries its
   unit position (--sx, --sy); the ellipse radii (--rx, --ry) scale per
   breakpoint. Flat, no perspective, still. */
.hero-cut { --rx: 310px; --ry: 90px; --cy: 41%; }
.hero-cut-inner { position: relative; }
/* The portrait melts into the ground: a soft edge all round, the foot faded
   (David: "is there a way to blend me better?"), on a faint tonal glow */
/* The portrait sits in a clean circle around the head and shoulders (36% of the
   picture's width, centred on the halo's centre), with a thin accent line drawn
   on its edge, so the chip ring has something to orbit (David, 25 October 2026:
   "circle with a line goes so hard, love how it connects"). Chosen over a
   feathered circle and a tighter mist. The name below shows whole now. */
.hero-cut img {
  position: relative; z-index: 1; width: 100%; height: auto;
  -webkit-mask-image: radial-gradient(ellipse 36% 36% at 50% var(--cy), #000 98.5%, transparent 100%);
  mask-image: radial-gradient(ellipse 36% 36% at 50% var(--cy), #000 98.5%, transparent 100%);
}
.hero-cut-inner::before {
  content: ""; position: absolute; left: 50%; top: var(--cy); width: 90%; height: 90%; z-index: 0;
  transform: translate(-50%, -50%);
  background: radial-gradient(ellipse at center, color-mix(in srgb, var(--accent-soft) 70%, transparent) 0, transparent 62%);
}
.hero-cut-inner::after {
  content: ""; position: absolute; left: 50%; top: var(--cy); width: 72%; aspect-ratio: 1; translate: -50% -50%;
  border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--accent) 55%, var(--surface-2));
  z-index: 2; pointer-events: none;
}
.hero-halo {
  position: absolute; left: 50%; top: var(--cy); z-index: 0;
  width: calc(var(--rx) * 2); height: calc(var(--ry) * 2); margin: calc(-1 * var(--ry)) 0 0 calc(-1 * var(--rx));
  border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--accent) 35%, var(--surface-2));
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), #000); mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), #000);
}
.hero-ring { position: absolute; left: 50%; top: var(--cy); width: 0; height: 0; margin: 0; padding: 0; list-style: none; }
.hero-ring li {
  position: absolute; left: calc(var(--sx) * var(--rx)); top: calc(var(--sy) * var(--ry)); transform: translate(-50%, -50%);
  padding: 7px 12px; border-radius: 999px; white-space: nowrap;
  background: var(--paper); box-shadow: var(--shadow-md); font-size: 0.8125rem; font-weight: 600; color: var(--ink);
}
.hero-ring li::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin: 0 7px 1px 0; }
.hero-ring li.front { z-index: 3; }
.hero-ring li.back { z-index: 0; transform: translate(-50%, -50%) scale(0.86); opacity: 0.7; filter: blur(0.6px); }
.thesis {
  font-weight: 600;
  font-size: clamp(1.5rem, 1rem + 1.7vw, 2.375rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  margin: 0 0 var(--space-2);
  max-width: 18ch;
}
.hero-intro { font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); color: var(--mute); }
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: var(--space-3); align-items: center; }

/* ---------- Home: the load-in ----------
   The first time a visit lands on the home page (html.intro from the head
   script, once per session; the cover itself is .intro-cover, a different
   name on purpose: `.intro` would match the html element too), the monogram draws itself in over a blank
   ground and the page fades up behind it: stroke trace, fill bloom, then the
   cover goes, all CSS, about two seconds. site.js holds the hero entrance
   until the cover has gone. Nothing under reduced motion, nothing with
   scripts off (the class is never set). David asked for it on 25 October
   2026, after the loader on sofiaelamrani.com: "just my logo animating in". */
.intro-cover { display: none; }
/* Keyed to html.intro alone, which the head script sets before the first
   paint. It was html.js.intro until 25 October 2026, and site.js adds .js at
   the end of the page, so the page showed for a beat before the cover did
   (David saw the flash). The class is only ever set by a script, so scripts
   off never shows the cover. */
html.intro .intro-cover {
  display: grid; place-items: center; position: fixed; inset: 0; z-index: 100;
  background: var(--paper);
  animation: intro-out 0.5s ease 1.75s both;
}
.intro-mark { width: clamp(56px, 7vw, 84px); height: auto; overflow: visible; }
.intro-mark path {
  fill: var(--accent); fill-opacity: 0;
  stroke: var(--accent); stroke-width: 7; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: intro-trace 0.9s cubic-bezier(0.65, 0, 0.35, 1) forwards, intro-fill 0.45s ease 0.85s forwards;
}
.intro-mark .b { animation-delay: 0.15s, 1s; }
@keyframes intro-trace { to { stroke-dashoffset: 0; } }
@keyframes intro-fill { to { fill-opacity: 1; } }
@keyframes intro-out { to { opacity: 0; visibility: hidden; } }
.motion-reduce .intro-cover { display: none; }

/* ---------- Home: header over the hero ----------
   body.bleed: the header is fixed and clear while the page sits at the top,
   then takes its usual tint and blur. Scripts off: the header stays sticky
   and the hero loses its top padding (above). */
html.js body.bleed .site-header { position: fixed; left: 0; right: 0; background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; transition: background 0.3s; }
html.js.scrolled body.bleed .site-header { background: color-mix(in srgb, var(--paper) 86%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }

.button {
  display: inline-flex; align-items: center; gap: 8px;
  font-weight: 600; font-size: 0.9375rem;
  background: var(--accent); color: var(--on-accent);
  padding: 12px 20px; text-decoration: none; border-radius: var(--r-sm);
  transition: background 0.2s, transform 0.2s;
}
.button:hover { background: var(--accent-deep); color: var(--on-accent); transform: translateY(-1px); }
.text-link {
  display: inline-flex; align-items: center;
  font-weight: 500; font-size: 0.9375rem; color: var(--ink); text-decoration: none;
  background: var(--surface); padding: 12px 18px; border-radius: var(--r-sm);
  transition: background 0.2s;
}
.text-link:hover { background: var(--surface-2); color: var(--ink); }
.main .text-link, .side .text-link, p > .text-link { background: none; padding: 0; color: var(--accent); text-decoration: underline; }
.main .text-link:hover, p > .text-link:hover { background: none; }

/* ---------- Sections ---------- */
/* Spacing sits above a section only, so two sections meeting share one step. */
.section { padding: var(--space-6) 0 0; }
.section.contact { padding-bottom: var(--space-6); }
.section-head { margin-bottom: var(--space-4); display: flex; justify-content: space-between; align-items: end; gap: 12px 24px; flex-wrap: wrap; }
.section-head p { color: var(--mute); margin: 0; font-size: 0.9375rem; }
/* Folded sections (David, 20 October 2026): How I work and More problems keep
   their heads on the page and hide the rest behind a disclosure, so the home
   page reads shorter. A <details>, so it opens with scripts off; the head is the
   summary and the disc at its end turns over when open. */
.fold > summary { list-style: none; cursor: pointer; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: end; gap: 12px 24px; }
.fold > summary::-webkit-details-marker { display: none; }
.fold:not([open]) > summary { margin-bottom: 0; }
.fold .chev { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 999px; background: var(--surface); color: var(--mute); transition: background 0.2s, color 0.2s; }
.fold .chev svg { width: 18px; height: 18px; transition: transform 0.25s; }
.fold > summary:hover .chev { background: var(--accent-soft); color: var(--accent-deep); }
.fold[open] .chev svg { transform: rotate(180deg); }
.fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; border-radius: var(--r-sm); }

/* ---------- Home: problems grid ---------- */
/* Each card leads with an image from the case study, then the label, the
   problem as a headline, the outcome, and a foot with the read line and a
   small arrow disc. The whole card is one link through the h3. */
.problems {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px;
}
.problem {
  position: relative;
  display: grid; grid-template-rows: auto minmax(0, 1fr);
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition: transform 0.25s, box-shadow 0.25s;
}
.problem:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.problem-media { aspect-ratio: 16 / 10; background: var(--surface-2); overflow: hidden; }
.problem-media img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1); }
/* A card can carry two captures: .wide for the stacked layout, .tall for the expanding row's portrait slot */
.problem-media img.tall { display: none; }
.problem:hover .problem-media img { transform: scale(1.03); }
.problem-media video { width: 100%; height: 100%; object-fit: cover; display: block; background: var(--tile); }
/* A .tile wraps a picture that needs its own frame (the type tile, the Edu loop); it fills the slot */
.problem-media .tile { position: absolute; inset: 0; overflow: hidden; }
.problem-media { position: relative; }
/* A logo on white, or a typographic tile in a project's own colors (set --bg, --fg and --rule inline) */
.problem-media.logo { background: var(--tile); display: grid; place-items: center; padding: 18% 14%; }
.problem-media.logo img { height: auto; max-height: 100%; object-fit: contain; }
.problem-media.type { position: relative; background: var(--bg, var(--ink)); }
.problem-media.type .tile {
  position: absolute; inset: 0;
  display: grid; align-content: space-between; padding: var(--space-3);
  background: var(--bg, var(--ink)); color: var(--fg, var(--paper));
  font-weight: 600; font-size: clamp(1.75rem, 1.1rem + 2.4vw, 2.75rem); line-height: 0.98; letter-spacing: -0.035em;
}
.problem-media.type .tile::before { content: ""; width: 44px; height: 5px; border-radius: 999px; background: var(--rule, var(--accent)); }
.problem-body { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); }
.problem .label { margin: 0; }
.problem h3 {
  font-size: clamp(1.375rem, 1.1rem + 1.2vw, 1.875rem);
  line-height: 1.1; letter-spacing: -0.03em;
}
.problem h3 a { color: var(--ink); text-decoration: none; }
/* The whole card is the link. z-index keeps the overlay above the hover transforms
   on the image and the arrow disc, which would otherwise paint over it and eat clicks. */
.problem h3 a::after { content: ""; position: absolute; inset: 0; z-index: 2; border-radius: var(--r); }
.problem:hover h3 a { color: var(--accent); }
.problem .outcome { font-size: 0.9375rem; color: var(--ink-2); margin: 0; }
.problem .fine { font-size: 0.875rem; color: var(--mute); margin: -8px 0 0; }
.problem-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: auto; padding-top: var(--space-1); }
.problem .read { font-weight: 500; color: var(--accent); font-size: 0.9375rem; }
.problem:hover .read { text-decoration: underline; }
.go {
  flex: none; width: 36px; height: 36px; border-radius: 999px;
  background: var(--surface-2); color: var(--ink);
  display: grid; place-items: center;
  transition: background 0.2s, color 0.2s, transform 0.25s;
}
.go svg { width: 16px; height: 16px; }
.problem:hover .go { background: var(--accent); color: var(--on-accent); transform: translate(2px, -2px); }
/* Wide screens with a mouse: an expanding row. At rest each card is a quarter of
   the row (--rest). The copy block fills it, with the text held at a fixed width
   (--copyw, the card less a 20% slack strip), and the picture slot is zero wide.
   Hover or focus a card and it grows by twice the slack, the slot opening to three
   slacks wide (the growth plus the strip the copy block gives up), on one soft
   curve after a short beat. Only the cards on the picture's side absorb the
   growth, so the hovered card's copy edge stays put: the left pair opens to the
   right and pushes the cards after it; the right pair opens to the left and
   pushes the cards before it. Nothing is open at rest. Phones, tablets and
   touch screens keep the stack. */
@media (min-width: 961px) and (hover: hover) {
  .problems {
    display: flex; gap: 14px; container-type: inline-size;
    --rest: calc((100cqw - 42px) / 4);
    --slack: calc(var(--rest) * 0.2);
    --copyw: calc(var(--rest) - var(--slack));
    --g: calc(var(--slack) * 2);
    --mediaw: calc(var(--slack) * 3);
    --ease: cubic-bezier(0.55, 0.06, 0.12, 1); --t: 0.7s; --delay: 0.15s;
  }
  /* .js .problems > .problem outranks the page's reveal rule (.js [data-reveal]), whose
     transition list would otherwise replace this one and make the widths snap.
     Opacity and transform stay listed so the one-time reveal still plays. */
  .problems > .problem, .js .problems > .problem {
    flex: none; width: var(--rest); min-width: 0;
    display: flex; flex-direction: row-reverse; align-items: stretch;
    transition:
      width var(--t) var(--ease) var(--delay),
      box-shadow 0.25s,
      opacity 0.65s cubic-bezier(0.16, 1, 0.3, 1) calc(var(--i, 0) * 80ms),
      translate 0.65s cubic-bezier(0.16, 1, 0.3, 1) calc(var(--i, 0) * 80ms);
  }
  .problem:nth-child(n+3) { flex-direction: row; }
  .problems > .problem:is(:hover, :focus-within, .is-open) { width: calc(var(--rest) + var(--g)); }
  /* Who makes room: the three after card 1, the two after card 2, the two before card 3, the three before card 4 */
  .problems:has(.problem:nth-child(1):is(:hover, :focus-within, .is-open)) .problem:nth-child(n+2),
  .problems:has(.problem:nth-child(4):is(:hover, :focus-within, .is-open)) .problem:nth-child(-n+3) { width: calc(var(--rest) - var(--g) / 3); }
  .problems:has(.problem:nth-child(2):is(:hover, :focus-within, .is-open)) .problem:nth-child(n+3),
  .problems:has(.problem:nth-child(3):is(:hover, :focus-within, .is-open)) .problem:nth-child(-n+2) { width: calc(var(--rest) - var(--g) / 2); }
  .problem-body { position: relative; z-index: 1; flex: 1 1 auto; min-width: var(--copyw); background: var(--surface); padding: 18px 16px 14px; gap: 12px; }
  .problem-body > * { width: calc(var(--copyw) - 32px); }
  .problem h3 { font-size: 1.0625rem; max-width: none; }
  .problem .outcome { font-size: 0.875rem; line-height: 1.5; }
  .problem .fine { font-size: 0.8125rem; margin-top: -6px; }
  .problem .read { font-size: 0.875rem; }
  .problem-foot { gap: 8px; padding-top: 0; }
  .problem-media { flex: none; width: 0; position: relative; aspect-ratio: auto; background: var(--surface); transition: width var(--t) var(--ease) var(--delay); }
  .problem:is(:hover, :focus-within, .is-open) .problem-media { width: var(--mediaw); }
  /* The picture, pinned to the card's outer edge, exactly the width the slot opens to */
  .problem-media > img, .problem-media > video, .problem-media > .tile {
    position: absolute; top: 0; right: 0; left: auto; inset: auto; width: var(--mediaw); height: 100%;
    object-fit: cover; transform: none;
  }
  .problem:nth-child(n+3) .problem-media > img, .problem:nth-child(n+3) .problem-media > video, .problem:nth-child(n+3) .problem-media > .tile { right: auto; left: 0; }
  .problem:hover .problem-media img { transform: none; }
  /* The one-pass demo (site.js adds .is-open) also lifts the card and tints the headline like a hover */
  .problem.is-open { transform: translateY(-2px); box-shadow: var(--shadow-md); }
  .problem.is-open h3 a { color: var(--accent); }
  .problem.is-open .go { background: var(--accent); color: var(--on-accent); transform: translate(2px, -2px); }
  /* The Cornerstone phone capture is a little wider than the slot, so it sits whole, with the
     site's own colors carried past its edges: the dark bar's green above, the sage page below.
     The split hides behind the picture, so it holds at any slot height. */
  .problem-media > img.tall { display: block; object-fit: contain; background: linear-gradient(to bottom, var(--tall-top, #CECEBA) 50%, var(--tall-bottom, #CECEBA) 50%); }
  .problem-media > img.wide { display: none; }
  .problem-media.logo { display: block; padding: 0; background: var(--surface); }
  .problem-media.logo > img { height: 100%; max-height: none; object-fit: contain; padding: 0 20%; background: var(--tile); }
  .problem-media.type { background: var(--surface); }
  .problem-media.type .tile { padding: 20px; font-size: 1.625rem; }
  /* The Edu loop: a white tile with the video centered at 2.4 slot widths, so the mark
     sits at about 60% of the slot with air around it instead of filling it edge to edge */
  .problem-media .tile > video {
    position: absolute; left: 50%; top: 50%;
    width: calc(var(--mediaw) * 2.4); height: auto; aspect-ratio: 16 / 9;
    transform: translate(-50%, -50%); object-fit: contain;
  }
  .problem-media > .tile:has(> video) { background: var(--tile); }
}

.problem.in-progress {
  grid-column: 1 / -1;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2);
  padding: var(--space-3);
  box-shadow: none;
  background: var(--paper);
  outline: 2px dashed var(--surface-2);
  outline-offset: -2px;
  margin-top: 0;
}
.problem.in-progress:hover { transform: none; box-shadow: none; }
/* Since 19 October 2026 the card carries a render and links to the milestones page.
   The expanding-row rules above size .problem-media to a zero-width slot, so the
   picture gets its own frame here, at every width. */
.problem.in-progress .problem-media { width: auto; flex: none; position: static; aspect-ratio: 16 / 10; border-radius: var(--r-sm); background: var(--tile); }
.problem.in-progress .problem-media > img { position: static; width: 100%; height: 100%; object-fit: cover; transform: none; }
.wip-body { display: flex; flex-direction: column; gap: var(--space-2); }
.wip-body h3 { font-size: 1.1875rem; line-height: 1.3; }
.wip-body p { font-size: 0.9375rem; color: var(--ink-2); margin: 0; }
.wip-body .status { align-self: flex-start; margin-bottom: 0; }
.wip-body .read { color: var(--accent); font-weight: 500; }
@media (min-width: 761px) {
  .problem.in-progress { grid-template-columns: minmax(0, 320px) minmax(0, 1fr); gap: var(--space-3); align-items: center; }
}
.compare img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; border-radius: var(--r-sm); margin-bottom: 12px; background: var(--tile); }
.compare ul { margin: 0 0 var(--space-2); padding-left: 1.1em; }
.compare li { margin-bottom: 8px; font-size: 0.9375rem; }
.status {
  display: inline-block;
  font-weight: 500; font-size: 0.8125rem;
  background: var(--accent-soft); color: var(--accent-deep);
  padding: 4px 10px; border-radius: 999px; margin-bottom: 12px;
}

/* ---------- Home: how I work ---------- */
.steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; counter-reset: step; }
.steps li { counter-increment: step; margin: 0; background: var(--surface); border-radius: var(--r); padding: var(--space-3); }
.steps li::before {
  content: counter(step);
  display: inline-grid; place-items: center;
  width: 32px; height: 32px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent-deep);
  font-weight: 600; font-size: 0.875rem; margin-bottom: 14px;
}
.steps.five { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.steps h3 { font-size: 1.0625rem; margin-bottom: 6px; }
.steps p { font-size: 0.9375rem; color: var(--mute); margin: 0; }

/* ---------- Short cards (more problems solved) ---------- */
/* One row per project: the tile on the left, the story on the right, with the
   three short answers side by side. David asked for a single column on
   12 October 2026 because the 3-up grid never filled cleanly with five items. */
.shorts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.short {
  margin: 0; background: var(--surface); border-radius: var(--r); padding: var(--space-2);
  display: grid; grid-template-columns: minmax(0, 220px) minmax(0, 1fr); gap: var(--space-3); align-items: start;
}
.short-media { position: relative; background: var(--paper); border-radius: var(--r-sm); aspect-ratio: 16 / 10; display: grid; place-items: center; overflow: hidden; }
.short-media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.short-media.logos { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); place-items: stretch; gap: 2px; background: var(--surface-2); }
.short-media.logos > img { position: static; min-height: 0; object-fit: contain; background: var(--tile); }
/* Slides: the images stack and site.js crossfades them one at a time while the tile is on
   screen, pausing under the pointer. The first one shows without JavaScript or under reduced
   motion. Every image is exported at the tile's 16:10 shape and fills it, so logos keep their own
   background color (maroon, sky blue, cream) rather than sitting in a white frame. */
.short-media.slides { background: var(--tile); }
.short-media.slides > img { opacity: 0; object-fit: cover; background: var(--tile); transition: opacity 0.7s ease; }
.short-media.slides > img:first-child, .short-media.slides > img.is-on { opacity: 1; }
.short-media.slides.has-js > img:first-child:not(.is-on) { opacity: 0; }
.short-media.type {
  background: var(--accent-soft); color: var(--accent-deep);
  font-weight: 600; font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem); letter-spacing: -0.03em;
}
.short-media.type { gap: 10px; grid-auto-flow: column; }
/* Micro-animations in the three text tiles, run once as the row reveals (html.js
   plus .is-in from the reveal observer). Without JavaScript the final state shows. */
.short-media .rise { width: 0.9em; height: 0.9em; }
.short-media .nodes { width: 1.6em; height: 1.15em; }
.js .short:not(.is-in) .rise .shaft { stroke-dasharray: 20; stroke-dashoffset: 20; }
.js .short:not(.is-in) .rise .head { opacity: 0; }
.js .short.is-in .rise .shaft { animation: draw 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) 0.3s both; }
.js .short.is-in .rise .head { animation: appear 0.3s ease 0.75s both; }
.js .short:not(.is-in) .nodes .node { transform: scale(0); }
.js .short:not(.is-in) .nodes .link { stroke-dasharray: 40; stroke-dashoffset: 40; }
.nodes .node { transform-box: fill-box; transform-origin: center; }
.js .short.is-in .nodes .n1 { animation: pop 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) 0.3s both; }
.js .short.is-in .nodes .n2 { animation: pop 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) 0.55s both; }
.js .short.is-in .nodes .n3 { animation: pop 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) 0.8s both; }
.js .short.is-in .nodes .l1 { animation: draw 0.4s ease 0.6s both; }
.js .short.is-in .nodes .l2 { animation: draw 0.4s ease 0.85s both; }
.js .short.is-in .nodes .l3 { animation: draw 0.4s ease 1.1s both; }
@keyframes draw { from { stroke-dashoffset: 40; } to { stroke-dashoffset: 0; } }
@keyframes appear { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop { from { transform: scale(0); } to { transform: scale(1); } }
.short-body { padding: var(--space-1) var(--space-1) var(--space-1) 0; }
.short h3 { font-size: 1.125rem; margin-bottom: 4px; }
.short-body > p:not(.label):not(.todo) { font-size: 0.9375rem; color: var(--ink-2); }
.short dl { margin: var(--space-2) 0 0; font-size: 0.9375rem; display: grid; grid-auto-flow: column; grid-template-rows: auto auto; gap: 0 var(--space-3); }
.short dt { font-weight: 500; font-size: 0.8125rem; color: var(--accent); }
.short dd { margin: 2px 0 0; color: var(--ink-2); }

/* ---------- AI teaser band ---------- */

/* ---------- Contact ---------- */
.contact h2 { font-size: clamp(2.25rem, 1.6rem + 3vw, 4rem); letter-spacing: -0.04em; margin-bottom: var(--space-3); }
.contact-list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 10px; }
.contact-list li { margin: 0; }
.contact-list a {
  display: inline-flex; font-weight: 500; font-size: 1rem; text-decoration: none; color: var(--ink);
  background: var(--surface); padding: 12px 18px; border-radius: var(--r-sm); transition: background 0.2s;
}
.contact-list a:hover { background: var(--surface-2); color: var(--ink); }
.contact-list a.button { background: var(--accent); color: var(--on-accent); font-weight: 600; transition: background 0.2s, transform 0.2s; }
.contact-list a.button:hover { background: var(--accent-deep); color: var(--on-accent); transform: translateY(-1px); }

.site-footer { padding: var(--space-3) 0 var(--space-4); font-size: 0.8125rem; color: var(--mute); }
.site-footer .wrap { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
.site-footer .foot-name { display: inline-flex; align-items: center; }
/* The motion switch lives here since 25 October 2026 (David: out of the nav,
   into the footer), one button holding the icon and the words. */
.site-footer .nav-toggle {
  width: auto; height: auto; display: inline-flex; align-items: center; gap: 8px;
  margin-right: -8px; padding: 6px 10px 6px 8px; border-radius: var(--r-sm);
  font-size: 0.8125rem; font-weight: 500; color: var(--mute);
}

/* Two case studies are client work and sit behind a password (25 October
   2026, when the site went public); their home cards say so. */
.problem .lock { display: inline-block; margin-left: 6px; padding: 2px 8px 3px; border-radius: 999px; background: var(--accent-soft); color: var(--accent-deep); font-size: 0.6875rem; font-weight: 600; vertical-align: 1px; }

/* ---------- Case study ---------- */
.case-head { padding: var(--space-5) 0 var(--space-3); }
.back {
  display: inline-flex; font-weight: 500; font-size: 0.875rem; text-decoration: none; color: var(--mute);
  background: var(--surface); padding: 8px 14px; border-radius: 999px; margin-bottom: var(--space-4);
}
.back:hover { background: var(--surface-2); color: var(--ink); }
.case-head h1 { max-width: none; margin-bottom: var(--space-3); }
.dek { font-size: clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem); color: var(--mute); max-width: none; }

.tldr { background: var(--surface); border-radius: var(--r-lg); padding: var(--space-4) var(--space-3); margin: var(--space-4) 0; }
.tldr h2 { font-size: 1.0625rem; margin-bottom: var(--space-3); color: var(--mute); font-weight: 500; letter-spacing: 0; }
.tldr-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.tldr-grid h3 { font-size: 0.875rem; color: var(--accent-deep); margin-bottom: 6px; font-weight: 600; }
.tldr-grid p { font-size: 1rem; margin: 0; }

.credits { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); margin: 0 0 var(--space-3); padding: 0; }
.credits div { margin: 0; }
.credits dt { font-weight: 500; font-size: 0.8125rem; color: var(--mute); }
.credits dd { margin: 4px 0 0; font-size: 0.9375rem; }

/* Jump targets land a little below the pinned header (56px, 61px on phones), not under
   it. #work sits on the heading block, not the section, so the jump skips the section's
   top padding and the heading arrives with about 28px of air (David, 17 October 2026) */
.section[id] { scroll-margin-top: 72px; }
.section-head[id] { scroll-margin-top: 88px; }
/* A jump target only fades in: the reveal's 18px slide would shift where the jump lands */
.js .section-head[id][data-reveal] { translate: none; }
.step { padding: var(--space-5) 0 0; scroll-margin-top: 112px; }
.step-title { display: flex; align-items: center; gap: 14px; margin-bottom: var(--space-3); }
.step-title h2 { scroll-margin-top: 112px; }
.dream-set h2[id] { scroll-margin-top: 132px; }
.step-num {
  display: inline-grid; place-items: center; flex: none;
  width: 40px; height: 40px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent-deep);
  font-weight: 600; font-size: 1rem;
}
.step .side p, .step .side li { font-size: 0.9375rem; color: var(--ink-2); }
.note-label { font-weight: 600; font-size: 0.8125rem; color: var(--accent-deep); margin-bottom: 6px; }
/* Sub-heading inside a step, for a before/after split. */
.step .sub { font-size: 0.9375rem; font-weight: 600; color: var(--accent-deep); margin: var(--space-5) 0 var(--space-2); }
.step .step-title + .sub { margin-top: 0; }
.pull {
  font-weight: 600;
  font-size: clamp(1.375rem, 1.1rem + 1vw, 1.875rem);
  line-height: 1.2; letter-spacing: -0.025em; color: var(--ink);
  max-width: none; margin: var(--space-4) 0;
}

/* Inline clips: muted, looping video that behaves like a GIF, started by site.js when in view */
.clip { margin: var(--space-3) 0 0; max-width: var(--measure); }
.clip.wide { margin: var(--space-4) 0; max-width: none; }
.clip video { display: block; width: 100%; height: auto; border-radius: var(--r); background: var(--surface); cursor: pointer; }
.clip figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 10px; }

.figure { margin: var(--space-4) 0; }
.figure > img, .figure .zoom > img { border-radius: var(--r); }
.figure figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 12px; }
.screens { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); background: var(--surface); border-radius: var(--r-lg); padding: var(--space-4) var(--space-3); }
.screens.two { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 760px; margin: var(--space-4) auto; }
.screens figure { margin: 0; }
.screens figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 10px; }
.tokens { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); align-items: start; }
.tokens img { width: 100%; border-radius: var(--r-sm); background: var(--tile); }
.tokens .span-all { grid-column: 1 / -1; }
.tokens .stack { display: grid; gap: var(--space-2); }

.flowmap { list-style: none; margin: var(--space-4) 0; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; counter-reset: flow; }
.flowmap li { counter-increment: flow; margin: 0; padding: var(--space-3) var(--space-2) var(--space-3) var(--space-3); background: var(--surface); border-radius: var(--r); }
.flowmap li::before {
  content: counter(flow);
  display: inline-grid; place-items: center;
  width: 28px; height: 28px; border-radius: 999px;
  background: var(--paper); color: var(--accent-deep);
  font-weight: 600; font-size: 0.8125rem; margin-bottom: 10px;
}
.flowmap h3 { font-size: 1rem; margin-bottom: 6px; }
.flowmap p { font-size: 0.9rem; margin: 0; color: var(--ink-2); }
.flowmap .loop { grid-column: 1 / -1; background: var(--accent-soft); display: flex; gap: 12px; align-items: baseline; }
.flowmap .loop::before { content: "\21BB"; width: auto; height: auto; background: none; font-size: 1.125rem; margin: 0; color: var(--accent-deep); }

/* Carousel: a snapping strip that site.js advances on its own and pauses on hover */
.carousel { margin: var(--space-4) 0; }
.carousel .track {
  list-style: none; margin: 0; padding: 4px 0 8px;
  display: flex; gap: 14px;
  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.carousel .track::-webkit-scrollbar { display: none; }
.carousel li { flex: 0 0 min(44%, 460px); scroll-snap-align: start; margin: 0; max-width: none; }
.carousel figure { margin: 0; }
.carousel .frame {
  aspect-ratio: 1 / 1; background: var(--surface); border-radius: var(--r); overflow: hidden;
  transition: transform 0.3s, box-shadow 0.3s;
}
.carousel .frame.pad { padding: 14%; }
.carousel li:hover .frame { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.carousel .frame img, .carousel .frame .zoom { width: 100%; height: 100%; }
.carousel .frame img { object-fit: contain; display: block; }
.carousel .frame:not(.pad) img { object-fit: cover; }
.carousel figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 10px; }
/* Filmstrip carousel: one height, each frame keeps its own aspect ratio (--ar on
   .frame). The frame's width is written out from the height and the ratio, and
   the caption contributes no width of its own, so each slide wraps its picture:
   left to intrinsic sizing, the slide took the image's full pixel width (1600px,
   capped at 92%) and the picture sat alone at the left of a wide empty slide
   (David, 18 October 2026, on the About page). */
.carousel.strip { --h: clamp(240px, 32vw, 420px); }
.carousel.strip li { flex: 0 0 auto; max-width: 92%; }
.carousel.strip .frame { height: var(--h); width: calc(var(--h) * var(--ar, 1)); max-width: 100%; aspect-ratio: var(--ar, 1); }
.carousel.strip .frame img { object-fit: cover; }
.carousel.strip figcaption { width: 0; min-width: 100%; }
.step .also { margin-top: var(--space-4); color: var(--mute); }
.step .provenance { margin-top: var(--space-3); font-size: 0.9375rem; color: var(--mute); }
.carousel-controls { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.carousel-controls .arrow {
  width: 36px; height: 36px; border-radius: 999px; border: 0; padding: 0;
  background: var(--surface); color: var(--ink); cursor: pointer; display: grid; place-items: center;
  transition: background 0.2s, color 0.2s;
}
.carousel-controls .arrow:hover { background: var(--accent-soft); color: var(--accent-deep); }
.carousel-controls .arrow svg { width: 16px; height: 16px; }
.carousel .dots { display: flex; gap: 6px; margin-left: 8px; }
.carousel .dots button { width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; background: var(--surface-2); cursor: pointer; transition: width 0.25s, background 0.25s; }
.carousel .dots button[aria-current] { width: 22px; background: var(--accent); }
.carousel .status { margin-left: auto; font-size: 0.8125rem; color: var(--mute); font-variant-numeric: tabular-nums; }

.gallery { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.gallery figure { margin: 0; }
.gallery img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: var(--r); background: var(--tile); }
.gallery figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 8px; }

/* Missing image placeholder (images to be added from the live site) */
.img-slot { position: relative; }
.img-slot.is-missing img { display: none; }
.img-slot.is-missing::before {
  content: attr(data-missing);
  display: grid; place-items: center; text-align: center;
  min-height: 220px; padding: 24px;
  border-radius: var(--r); background: var(--surface);
  font-size: 0.9375rem; color: var(--mute);
}

.next-case { padding: var(--space-5) 0 0; margin-top: var(--space-5); }
.next-case .wrap > .label { margin-bottom: 10px; }
.next-case a {
  display: block; max-width: none;
  background: var(--surface); border-radius: var(--r-lg); padding: var(--space-4) var(--space-3);
  font-weight: 600; font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.375rem); letter-spacing: -0.03em; line-height: 1.12;
  color: var(--ink); text-decoration: none; transition: background 0.2s, color 0.2s;
}
.next-case a:hover { background: var(--accent-soft); color: var(--accent-deep); }

/* ---------- AI page ---------- */
.compare { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: var(--space-4) 0; }
.compare > div { background: var(--surface); border-radius: var(--r); padding: var(--space-3); }
.compare > div:last-child { background: var(--accent-soft); }
.compare h3 { font-size: 1rem; margin-bottom: 10px; }

/* ---------- About, and the dark page set ---------- */
/* body.dark runs a page dark in both themes: the Dream page since 18 October
   2026 (About was dark from 12 to 18 October, then David asked for it to
   follow the theme like every other page). It swaps the tokens, so every
   component keeps its shape and only the palette changes. The accent lightens
   so small text and links stay readable on the dark ground. */
body.dark {
  --paper: #141312;
  --surface: #1E1D1B;
  --surface-2: #2A2826;
  --ink: #ECE7DD;
  --ink-2: #CFC8BD;
  --mute: #A39C93;
  --accent: #A6B57A;
  --accent-deep: #C2CF9A;
  --accent-soft: #2C301F;
  --line: #2A2826;
  --tile: #ECE7DD;
  --on-accent: #141312;
  --band: #1E1D1B;
  --glass-edge: rgba(255, 255, 255, 0.06);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px -18px rgba(0, 0, 0, 0.6);
  --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.35), 0 16px 40px -24px rgba(0, 0, 0, 0.7);
}
body.dark .skip-link { background: var(--accent); color: var(--paper); }

/* One surface panel, like the home hero: a small portrait on the left, the
   story and three pill links on the right. */
.about-stage {
  display: grid;
  grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
  gap: var(--space-4) var(--space-5);
  align-items: start;
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: var(--space-4);
}
.about-photo { border-radius: var(--r); overflow: hidden; background: #0B0B0B; box-shadow: var(--shadow-md); }
.about-photo img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; object-position: center top; }
.about-copy > .label { margin-bottom: var(--space-2); }
.about-copy h1 { margin-bottom: var(--space-3); }
.about-stage .hero-actions { margin-top: var(--space-3); }
.about-stage .text-link { background: var(--surface-2); }
.about-stage .text-link:hover { background: var(--accent-soft); color: var(--ink); }

/* The Dream page (18 October 2026): David's composites, posters, drone flights
   and his first 3D house, shown for their own sake. Dark like About, wider than
   the other pages, and the one place where pictures keep square corners and
   their whole frame (David's call: these deserve the full breadth). Motion rows
   hold GIFs behind a poster frame; the <source media> and site.js keep them
   still under reduced motion. */
body.dream main { --max: 1360px; }
.dream-head { padding: var(--space-4) 0 var(--space-2); }
.dream-head > .label { margin-bottom: var(--space-2); }
.dream-head h1 { margin-bottom: var(--space-3); }
.dream-grid { display: grid; gap: 14px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.dream-grid figure { margin: 0; }
.dream-grid img, .dream-grid .zoom { border-radius: 0; }
.dream-grid img { display: block; width: 100%; height: auto; background: var(--surface); }
/* Every row is one shape, so the rows square up top and bottom and the grid
   meets both edges (David, 18 October 2026). The composites are twelve 4:5
   pieces and two squares: a six-column grid holds three portraits per row and
   the two squares as a wider last row. Posters run four across; the six loops
   three across (their GIFs are 480px wide, so they stay sharp at a third of
   the measure). */
/* Twelve columns: 4:5 composites three to a row (span 4) or, with .quad, four
   to a row (span 3), and the squares (.sq) three to a row. Fourteen portraits
   split 3 + 3 + 4 + 4 since 18 October 2026, when Dune and Underwater city
   joined; BLM made the squares a row of three. */
.dream-grid.flow { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.dream-grid.flow figure { grid-column: span 4; }
.dream-grid.flow figure.quad { grid-column: span 3; }
.dream-grid.flow figure.sq { grid-column: span 4; }
.dream-grid.posters { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dream-grid.motion img { aspect-ratio: 16 / 9; object-fit: cover; }
body.dream .lightbox img { border-radius: 0; }
@media (max-width: 960px) {
  .dream-grid, .dream-grid.posters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dream-grid.flow figure, .dream-grid.flow figure.quad { grid-column: span 6; }
  .dream-grid.flow figure.sq { grid-column: span 4; }
}
@media (max-width: 560px) {
  /* One piece per line on phones, every chapter (David, 18 October 2026) */
  .dream-grid, .dream-grid.posters, .dream-grid.flow { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .dream-grid.flow figure, .dream-grid.flow figure.quad, .dream-grid.flow figure.sq { grid-column: auto; }
}

/* At a glance: a tall timeline card on the left, education and tools stacked on the right */
.facts { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: 14px; }
.fact { background: var(--surface); border-radius: var(--r); padding: var(--space-3); }
.fact.tall { grid-row: 1 / 3; }
.fact h3 { font-size: 0.8125rem; font-weight: 600; color: var(--accent-deep); margin-bottom: var(--space-2); letter-spacing: 0; }
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { display: grid; grid-template-columns: 7em minmax(0, 1fr); gap: 2px 16px; margin: 0; padding: 10px 0; max-width: none; }
.timeline li:first-child { padding-top: 0; }
.timeline .when { font-size: 0.8125rem; color: var(--mute); font-variant-numeric: tabular-nums; padding-top: 4px; }
.timeline strong { display: block; }
.timeline .where { display: block; font-size: 0.9375rem; color: var(--ink-2); }
.edu { list-style: none; padding: 0; margin: 0; }
.edu li { margin-bottom: 14px; }
.edu li:last-child { margin-bottom: 0; }
.edu strong { display: block; }
.kit { margin: 0; }
.kit dt { font-weight: 500; font-size: 0.8125rem; color: var(--mute); margin-top: 10px; }
.kit dt:first-child { margin-top: 0; }
.kit dd { margin: 2px 0 0; font-size: 0.9375rem; color: var(--ink-2); }
.craft-intro { margin-bottom: var(--space-2); }

/* ---------- Draft notes (remove before launch) ---------- */
.todo {
  border: 2px dashed #B9861B;
  background: #FFF8E6;
  color: #4A3A10;
  padding: 14px 16px;
  margin: 16px 0;
  border-radius: var(--r-sm);
  font-size: 0.9375rem;
  line-height: 1.45;
}
.todo::before { content: "To fill in: "; font-weight: 600; }
.todo p, .todo li { font-size: inherit; margin-bottom: 0.4em; }

/* ---------- Live design tokens for a project's own system (Micromind) ----------
   The hex values sit inline in the HTML because they're that project's
   colors, shown as content. Site chrome still uses the tokens above. */
.ds { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4) var(--space-3); background: var(--surface); border-radius: var(--r-lg); padding: var(--space-3); }
.ds > div { min-width: 0; }
.ds h4 { font-size: 0.8125rem; font-weight: 500; color: var(--mute); margin-bottom: 10px; }
.ds .marks { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); align-items: center; }
.ds .marks img { height: 26px; width: auto; }
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 8px; }
.swatch { display: grid; gap: 3px; font-size: 0.75rem; font-weight: 500; color: var(--ink-2); }
.swatch i { display: block; height: 44px; border-radius: var(--r-sm); }
.swatch code { font-family: ui-monospace, Menlo, monospace; font-size: 0.6875rem; font-weight: 400; color: var(--mute); }
.specimen p { margin: 0 0 8px; max-width: none; }
.specimen .h { font-family: "Sora", var(--display); font-weight: 700; font-size: 1.5rem; line-height: 1.25; letter-spacing: 0; }
.specimen .b { font-family: "Inter", var(--text); font-size: 0.9375rem; line-height: 1.55; color: var(--ink-2); }
.specimen .l { font-family: "Inter", var(--text); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mute); }
.ds .full { grid-column: 1 / -1; }
.ds .swatches.grouped { grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
.ds .swatch.lead { grid-column: span 1; }
/* Montserrat specimen for the first Micromind version */
.specimen.mont .h1 { font: 700 2rem/1.15 "Montserrat", var(--display); margin-bottom: 2px; }
.specimen.mont .h2 { font: 500 1.5rem/1.2 "Montserrat", var(--display); margin-bottom: 2px; }
.specimen.mont .h3 { font: 500 1.125rem/1.3 "Montserrat", var(--display); margin-bottom: 2px; }
.specimen.mont .h3l { font: 300 1.125rem/1.3 "Montserrat", var(--display); margin-bottom: 2px; }
.specimen.mont .p1 { font: 500 0.9375rem/1.5 "Montserrat", var(--text); color: var(--ink-2); margin-bottom: 2px; }
.specimen.mont .btn { display: inline-block; font: 600 0.8125rem/1 "Montserrat", var(--text); padding: 10px 18px; border-radius: 999px; background: #FD836B; color: #fff; margin-top: 6px; }
.specimen .meta { display: block; font-size: 0.6875rem; color: var(--mute); margin-bottom: 10px; }
/* ERLEADA design system block (oncology page) */
.ds .tints { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 4px; }
.ds .tints i { display: block; height: 32px; border-radius: 6px; }
.ds .meta { font-size: 0.8125rem; color: var(--mute); margin: 10px 0 0; max-width: none; }
.ds .pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: 6px; }
.ds .pair { position: relative; display: grid; place-items: center; aspect-ratio: 1; border-radius: var(--r-sm); line-height: 1; }
.ds .pair b { font-size: 1.5rem; font-weight: 700; }
.ds .pair small { position: absolute; left: 0; right: 0; bottom: 6px; text-align: center; font-size: 0.625rem; font-weight: 500; opacity: 0.85; }
.ds .pairs.avoid .pair::after { content: ""; position: absolute; top: 6px; right: 6px; width: 10px; height: 10px; border-radius: 50%; background: #E0413A; }
.ds .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.ds .specimen.type .face { font-size: 1.75rem; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 6px; }
.ds .specimen.type .desc { font-size: 0.9375rem; color: var(--ink-2); margin: 0 0 10px; }
.ds .weights { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.ds .weights li { margin: 0; font-size: 0.75rem; font-weight: 500; padding: 5px 10px; border-radius: 999px; background: var(--paper); color: var(--ink-2); }
.ds .weights li:nth-child(1) { font-weight: 300; }
.ds .weights li:nth-child(n+5) { font-weight: 700; }
.gallery.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gallery.natural img { aspect-ratio: auto; }
/* The milestone renders are all 3:2. `natural` alone drops the UA's width/height ratio,
   so a lazy image had no height until it loaded and the page jumped under the reader. */
.gallery.renders img { aspect-ratio: 3 / 2; }
.screens.phones img { width: 100%; aspect-ratio: 9 / 18; object-fit: cover; object-position: top; border-radius: var(--r); background: var(--tile); }
.badge-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; }
.badge-row img { width: 100%; border-radius: var(--r-sm); background: var(--tile); padding: 8%; }
.illos { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.illos img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--r-sm); background: var(--tile); }
.illos .floral { grid-column: span 2; }
.wires { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
.wires figure { margin: 0; }
.wires img { width: 100%; border-radius: var(--r-sm); background: var(--tile); }
.wires figcaption { font-size: 0.875rem; color: var(--mute); margin-top: 8px; }
.shapes { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 12px; }
.shapes span { display: inline-grid; place-items: center; height: 40px; padding: 0 16px; background: #5b8ce6; color: #fff; font: 700 12px/1 "Inter", var(--text); text-transform: uppercase; letter-spacing: 0.03em; }

/* ==========================================================================
   Interaction (behaviour in assets/js/site.js). Everything below is an
   enhancement: the page reads the same with it switched off.
   ========================================================================== */

/* ---------- Step nav (case studies) ---------- */
/* Pinned under the header. site.js measures the header into --header-h, and
   the bar starts 1px above its bottom edge, so no sliver of page shows between
   the two when the header's height lands on a fraction of a pixel (David saw
   one on his phone on the Dream page, 18 October 2026). */
.step-nav {
  position: sticky; top: calc(var(--header-h, 61px) - 1px); z-index: 5;
  --fade: 40px;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
/* The list runs out to the screen edge on phones (the gutters become its
   padding) and fades at whichever end has more pills hidden beyond it, so a
   visitor can tell the bar scrolls. site.js sets .more-left and .more-right
   from the scroll position; the fade is a mask on the list, so it takes on
   whatever the bar sits on (dark on the Dream page, paper elsewhere). */
@property --fade-l { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --fade-r { syntax: '<length>'; inherits: false; initial-value: 0px; }
.step-nav ol {
  list-style: none; margin: 0 calc(-1 * var(--gutter)); padding: 10px var(--gutter); counter-reset: nav;
  display: flex; gap: 4px;
  overflow-x: auto; scrollbar-width: none;
  --fade-l: 0px; --fade-r: 0px;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent);
  transition: --fade-l 0.25s, --fade-r 0.25s;
}
.step-nav.more-left ol { --fade-l: var(--fade); }
.step-nav.more-right ol { --fade-r: var(--fade); }
.step-nav ol::-webkit-scrollbar { display: none; }
.step-nav li { margin: 0; flex: none; counter-increment: nav; }
.step-nav a {
  display: inline-flex; gap: 8px; align-items: baseline;
  padding: 8px 14px; border-radius: 999px; white-space: nowrap;
  font-weight: 500; font-size: 0.875rem;
  color: var(--mute); text-decoration: none; transition: background 0.2s, color 0.2s;
}
.step-nav a::before { content: counter(nav); font-weight: 600; font-size: 0.75rem; color: var(--accent); }
.step-nav a:hover { background: var(--surface); color: var(--ink); }
.step-nav a[aria-current] { background: var(--accent-soft); color: var(--accent-deep); }
.step-nav a[aria-current]::before { color: var(--accent-deep); }
.step-nav.plain a::before { content: none; }

/* ---------- Lightbox ---------- */
.zoom {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: inherit; cursor: zoom-in; border-radius: var(--r);
}
.lightbox { border: 0; padding: 0; background: transparent; max-width: min(96vw, 1400px); max-height: 96vh; }
.lightbox::backdrop { background: rgba(20, 22, 29, 0.86); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.lightbox figure { margin: 0; display: grid; gap: 10px; justify-items: center; }
.lightbox img { max-width: 96vw; max-height: 84vh; width: auto; height: auto; object-fit: contain; background: var(--tile); border-radius: var(--r); }
.lightbox figcaption { font-size: 0.9375rem; color: #C9CEDC; text-align: center; max-width: 70ch; }
.lightbox-close {
  position: fixed; top: 16px; right: 16px;
  font-weight: 500; font-size: 0.9375rem; font-family: var(--text);
  background: var(--paper); color: var(--ink); border: 0; padding: 10px 16px; border-radius: 999px; cursor: pointer;
}
.lightbox-close:hover { background: var(--surface-2); }

/* ---------- Responsive ---------- */
@media (max-width: 960px) {
  .steps, .steps.five { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .flowmap { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .credits { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .screens { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .seam-row, .about-stage, .facts { grid-template-columns: minmax(0, 1fr); }
  .about-stage { padding: var(--space-3); gap: var(--space-3); }
  .about-stage .about-photo { width: min(60%, 220px); }
  .fact.tall { grid-row: auto; }
  .timeline li { grid-template-columns: minmax(0, 1fr); }
  .hero.home .hero-stage { padding-top: 80px; }
  .hero.home .hero-meta span:last-child { display: none; }
  .hero.home .hero-scene { margin-top: var(--space-2); }
  .hero-ghost { font-size: min(16.5vw, 8rem); bottom: 44px; }
  .hero-cut { --rx: 138px; --ry: 50px; --cy: 40%; width: min(72%, 300px); margin-top: var(--space-3); }
  .hero-ring li { font-size: 0.6875rem; padding: 4px 8px; }
  .problems { grid-template-columns: minmax(0, 1fr); }
  .fold > summary { grid-template-columns: minmax(0, 1fr) auto; }
  .fold > summary p { grid-column: 1 / -1; }
  .fold .chev { grid-column: 2; grid-row: 1; }
  .tldr-grid, .compare { grid-template-columns: minmax(0, 1fr); }
  .steps, .steps.five { grid-template-columns: minmax(0, 1fr); }
  .short { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
  .short-body { padding: 0 var(--space-1) var(--space-1); }
  .short-media.type { aspect-ratio: 16 / 6; }
  .short dl { grid-auto-flow: row; grid-template-rows: none; }
  .short dt + dd + dt { margin-top: 10px; }
  .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .carousel li { flex-basis: 80%; }
  .carousel.strip { --h: 56vw; }
  .tokens { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tokens .stack { grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ds { grid-template-columns: minmax(0, 1fr); }
  .badge-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .illos { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wires { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The header stays pinned on phones too (David, 17 October 2026); the step bar
     sits under it (at the measured header height), and step headings scroll in below both */
  .step, .step-title h2 { scroll-margin-top: 124px; }
  .dream-set h2[id] { scroll-margin-top: 136px; }
}
/* Just above the phone menu's range the full row is a few pixels too wide for
   the roomier gaps (it wrapped at 541 to about 600px), so the gaps and pills
   slim one step there */
@media (max-width: 640px) {
  .site-nav ul { gap: 6px; }
  .site-nav a { padding: 8px 12px; }
}
/* Phones and small tablets (David, 19 October 2026): Work, AI, About and the
   motion switch fold into a menu button, so the row is the name, the button,
   the Dream pill and the theme switch. The menu is a <details>, so it opens
   with scripts off; site.js closes it on Escape, on a tap elsewhere and after
   a link is chosen. The sheet is the Work list's panel, hung from the header
   and set in from the right gutter, so it fits a 360px screen. */
@media (max-width: 540px) {
  /* The headline's second line, "understand complex things.", is 12.3em wide in Geist at this tracking.
     On phones the size follows the width so the two-line break David chose holds (25 October 2026)
     instead of leaving "things." alone on a third line; 1.5rem is the floor (it wraps again under 320px). */
  .hero.home .thesis { font-size: max(1.5rem, min(1rem + 3.4vw, (100vw - 2 * var(--gutter)) / 12.4)); }
  .site-header .wrap { gap: 4px; }
  .wordmark { font-size: 1rem; }
  .site-nav { gap: 8px; }
  .site-nav > ul { gap: 8px; margin-right: -4px; }
  .site-nav > ul > li:not(:has(.nav-work)):not(:has([data-theme-toggle])) { display: none; }
  .site-nav li.phone-only { display: list-item; }
  .site-nav li.tool { margin-left: 0; }
  .site-header .wrap { position: relative; }
  .site-nav .nav-more { display: block; }
  .site-nav .nav-more > summary { list-style: none; cursor: pointer; }
  .site-nav .nav-more > summary::-webkit-details-marker { display: none; }
  .site-nav .nav-more[open] > summary { background: var(--surface); color: var(--ink); }
  .site-nav .nav-more[open] > summary .when-off { display: none; }
  .site-nav .nav-more[open] > summary .when-on { display: block; }
  .site-nav .sheet {
    position: absolute; top: calc(100% + 2px); right: var(--gutter); z-index: 30;
    display: block; width: min(280px, calc(100vw - 2 * var(--gutter))); margin: 0; padding: 6px; list-style: none;
    background: var(--paper); border-radius: var(--r);
    box-shadow: var(--shadow-md), inset 0 1px 0 var(--glass-edge);
  }
  .site-nav .sheet li { margin: 0; }
  .site-nav .sheet a { display: block; padding: 10px 12px; border-radius: var(--r-sm); color: var(--ink); font-size: 0.9375rem; }
  .site-nav .sheet a:hover, .site-nav .sheet a[aria-current="page"] { background: var(--surface); }
  .site-nav .sheet .sub { display: block; margin: 0 0 4px; padding: 0 0 0 12px; list-style: none; }
  .site-nav .sheet .sub a { padding: 7px 12px; font-size: 0.875rem; color: var(--mute); }
  .site-nav .sheet .sub a[aria-current="page"] { color: var(--ink); }
  .site-nav .sheet a.nav-fill { display: inline-block; margin: 6px 0 6px 12px; padding: 8px 14px; border-radius: 999px; color: var(--on-accent); font-weight: 600; }
  .site-nav .sheet a.nav-fill:hover, .site-nav .sheet a.nav-fill[aria-current="page"] { background: var(--accent-deep); color: var(--on-accent); }
  /* The motion switch is one button, icon and words together, so the words take the tap too */
  .site-nav .sheet .tool { margin-top: 2px; }
  .site-nav .sheet .tool .nav-toggle { width: auto; height: auto; display: flex; align-items: center; gap: 8px; padding: 8px 12px 8px 8px; border-radius: var(--r-sm); font-size: 0.875rem; font-weight: 500; color: var(--mute); }
  .site-nav .sheet .tool .nav-toggle svg { width: 20px; height: 20px; }
}
@media (max-width: 480px) {
  .flowmap { grid-template-columns: minmax(0, 1fr); }
  .credits { grid-template-columns: minmax(0, 1fr); }
  .wordmark { font-size: 0.9375rem; }
  .wordmark .mark { margin-right: 6px; }
  .site-nav a { font-size: 0.875rem; }
  .nav-toggle { width: 32px; height: 32px; }
}

/* ---------- Cursor (mouse only, added by site.js) ----------
   The outer div only carries the position (transform). The inner <i> is the
   visual: a dot, a ring over anything clickable, and a press that closes in
   around its own center. Keeping scale off the outer div matters: CSS applies
   the scale property before the transform, so scaling there moves the dot. */
.cursor { position: fixed; left: 0; top: 0; z-index: 60; width: 0; height: 0; pointer-events: none; opacity: 0; transition: opacity 0.2s; will-change: transform; }
.cursor.on { opacity: 1; }
.cursor i {
  position: absolute; left: 0; top: 0; display: block;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent);
  transform: translate(-50%, -50%) scale(var(--s, 1));
  transition: width 0.25s, height 0.25s, background 0.25s, box-shadow 0.25s, transform 0.12s;
}
.cursor.link i { width: 36px; height: 36px; background: transparent; box-shadow: inset 0 0 0 1.5px var(--accent); }
.cursor.down i { --s: 0.7; }
.cursor.link.down i { --s: 0.62; background: var(--accent-soft); }
.cursor.text { opacity: 0; }
@media (hover: none) { .cursor { display: none; } }

/* ---------- Motion: entrance and scroll reveals ----------
   Only when site.js has run (html.js). Elements marked data-reveal start a
   little low and transparent, and site.js adds .is-in when they enter the
   viewport. --i staggers siblings. Without JavaScript nothing is hidden. */
.js [data-reveal] {
  /* 0.001, not 0: Safari holds a page's first paint until it has visible content,
     and on the home page everything below the header waits for its entrance. At 0
     the page sat behind the old one until the entrance had played unseen (iPhone,
     16 October 2026); at 0.001 the text counts as content and the page shows at once */
  opacity: 0.001;
  translate: 0 18px;
  transition: opacity 0.65s cubic-bezier(0.16, 1, 0.3, 1), translate 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 80ms);
}
.js [data-reveal].is-in { opacity: 1; translate: none; }
.js .hero-cut[data-reveal] { translate: 0 28px; transition-duration: 0.9s; }
/* The offset is the `translate` property, not `transform`, so hover lifts and
   the card row's own transforms never fight it (25 October 2026). */
/* Belt and braces (25 October 2026). The head script now puts .js on the root
   before the first paint, so reveal targets are hidden from the first frame
   instead of flashing in and then hiding when site.js arrived (David saw the
   new page "load statically, then animate" after a page change). If site.js
   never runs, nothing may stay hidden: until it adds .ready, a zero-length
   animation shows everything after six seconds. The same for the headings
   that site.js splits into words: html.words-wait hides them until the
   split, and site.js takes the class off. */
.js:not(.ready) [data-reveal] { animation: reveal-fallback 0s linear 6s forwards; }
@keyframes reveal-fallback { to { opacity: 1; translate: none; } }
html.words-wait body[data-words] :is(h1, h2, .dek, .hero-intro):not(.words) { visibility: hidden; animation: words-fallback 0s linear 6s forwards; }
@keyframes words-fallback { to { visibility: visible; } }
/* On phones the hero entrance plays once per visit (html.arrived, set by the
   head script from sessionStorage, which site.js writes after the first play).
   iPhone Safari shows its stored snapshot of the last visit to a page while
   the new copy loads, so a return home showed the finished hero and then the
   live page's entrance dropped it and raised it again (16 October 2026). */
.js.arrived .hero-stage [data-reveal] { opacity: 1; translate: none; transition: none; }

/* ---------- Motion: words ----------
   Headings and lead paragraphs (h1, h2, .dek, .hero-intro) are split into
   words by site.js (`.words`: each word a `.w` holding an inner span) and
   slide up out of their own line boxes, 40ms apart, as the element enters
   the viewport (it shares the reveal observer; in the hero it waits for
   `shown` like everything else). The wrapper carries a little padding and the
   same negative margin so ascenders and descenders are not clipped. David
   asked for this on 25 October 2026 after sofiaelamrani.com. */
.words .w { display: inline-block; overflow: hidden; vertical-align: bottom; padding: 0.1em 0.05em 0.2em; margin: -0.1em -0.05em -0.2em; }
/* Easing: a long ease-out (David, 25 October 2026: "a little more easy ease,
   but not slower"), and a touch of motion blur that clears as each word lands */
.js .words .w > span { display: inline-block; transform: translateY(130%); filter: blur(6px); transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1), filter 0.55s cubic-bezier(0.16, 1, 0.3, 1); transition-delay: calc(var(--w, 0) * 40ms); }
.js .words.is-in .w > span { transform: none; filter: blur(0); }
.js.arrived .hero-stage .words .w > span { transform: none; filter: none; transition: none; }

/* ---------- Motion: page to page ----------
   A short cross-fade between pages in browsers that support view transitions.
   Others simply load the next page. Mouse and trackpad devices only: on iPhone
   the fade and the hero entrance fought each other (Safari shows a stored
   snapshot of the last visit while the new page loads, then the live page took
   over mid-entrance, seen frame by frame in the simulator, 16 October 2026).
   Phones keep the browser's own navigation and play the entrance once per visit. */
@media (hover: hover) and (pointer: fine) {
  @view-transition { navigation: auto; }
}
::view-transition-old(root) { animation: 0.2s ease-out both page-out; }
::view-transition-new(root) { animation: 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both page-in; }
@keyframes page-out { to { opacity: 0; } }
@keyframes page-in { from { opacity: 0; transform: translateY(10px); } }

/* ---------- Motion: the switch ----------
   Two sources decide whether the site moves: the system setting and the nav
   toggle (site.js, remembered in localStorage as dhb-motion). A short script in
   every <head> puts .motion-reduce or .motion-full on <html> before the first
   paint, and the toggle flips it live. The media query below still covers
   visitors with scripts off, when neither class is present; an explicit
   .motion-full choice overrides it. Keep the two rule sets identical. */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  html:not(.motion-full) { scroll-behavior: auto; }
  html:not(.motion-full) *, html:not(.motion-full) *::before, html:not(.motion-full) *::after { transition: none !important; animation: none !important; }
  .js:not(.motion-full) [data-reveal] { opacity: 1; translate: none; }
  html:not(.motion-full).words-wait body[data-words] :is(h1, h2, .dek, .hero-intro) { visibility: visible; }
  .js:not(.motion-full) .words .w > span { transform: none; filter: none; }
  html:not(.motion-full) .cursor { display: none; }
}
html.motion-reduce { scroll-behavior: auto; }
.motion-reduce *, .motion-reduce *::before, .motion-reduce *::after { transition: none !important; animation: none !important; }
html.motion-reduce::view-transition-old(root), html.motion-reduce::view-transition-new(root) { animation: none; }
.js.motion-reduce [data-reveal] { opacity: 1; translate: none; }
html.motion-reduce.words-wait body[data-words] :is(h1, h2, .dek, .hero-intro) { visibility: visible; }
.js.motion-reduce .words .w > span { transform: none; filter: none; }
.motion-reduce .cursor { display: none; }
