/* ghostavo.com
   Design system per BRIEF.md §8.
   Two deviations from the token table, both to hold WCAG AA (§11), both noted in NOTES.md:
   - body --muted darkened from #6B7B7A to #647473 (the brief value hits 3.94:1 on --surface)
   - --teal is 2.07:1 on --surface, so link text and chart strokes use --teal-deep and --slate.
     --teal keeps its job as the signature: handles, ticks, fills, underlines, active states. */

:root {
  --surface:     #F6F9F9;
  --ink:         #10201F;
  --teal:        #50C1B1;
  --teal-wash:   #E4F2EF;
  --teal-deep:   #1E5F58;
  --slate:       #3D5A80;
  --orange:      #B4531B;   /* the film's advertisement, and the incrementality wedge */
  --muted:       #647473;   /* text */
  --hairline:    #6B7B7A;   /* rules only, non-text */

  --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --serif: Newsreader, ui-serif, Georgia, "Times New Roman", serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --t-xs:  0.75rem;
  --t-sm:  0.8125rem;
  --t-base: 1.0625rem;
  --t-md:  1.25rem;
  --t-lg:  1.6rem;
  --t-xl:  2.1rem;

  --wrap: 1100px;
  --measure: 62ch;   /* prose stops where the eye starts losing the line */
  --wrap-wide: 1280px;
  --gap: clamp(1.25rem, 4vw, 2rem);
  --band: clamp(1.75rem, 3.4vw, 2.5rem);   /* halved: two of these met at every section join */
  --r: 0px;   /* the sheet has corners; radius is reserved for nothing */
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: 5rem; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font: 400 var(--t-base)/1.65 var(--sans);
  font-feature-settings: "kern", "liga";
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; font-weight: 400; line-height: 1.15; }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
em { font-style: italic; }

a { color: var(--teal-deep); text-decoration: none; border-bottom: 1px solid var(--teal); padding-bottom: 1px; }
a:hover { color: var(--ink); border-bottom-color: var(--ink); }

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

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gap); }
.section--wide .wrap { max-width: var(--wrap-wide); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 20;
  background: var(--ink); color: var(--surface); padding: .7rem 1rem; border: 0;
}
.skip:focus { left: 0; }

/* ── eyebrow / mono ─────────────────────────────── */
.eyebrow {
  font: 500 var(--t-xs)/1 var(--mono);
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
  margin: 0 0 1.4rem;
}


/* ── top bar ────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: 10;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(8px);
  border-bottom: 1px solid color-mix(in srgb, var(--hairline) 22%, transparent);
}
.topbar__inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; height: 3.5rem; }
.topbar__id { font: 500 var(--t-sm)/1 var(--mono); letter-spacing: .04em; border: 0; color: var(--ink); }
.topbar__nav { display: flex; gap: clamp(.85rem, 3vw, 1.75rem); }
.topbar__nav a {
  font: 400 var(--t-sm)/1 var(--sans); color: var(--muted);
  border: 0; padding-bottom: 3px; border-bottom: 1.5px solid transparent;
}
.topbar__nav a:hover { color: var(--ink); }
.topbar__nav a.is-current { color: var(--ink); border-bottom-color: var(--teal); }
/* The wordmark used to be dropped below 30rem to make room for the nav, which left the phone
   version of the site with no name on it at all. Measured, it was never the fight it looked
   like: at 375px the name and the four links come to about 268px inside a 339px row. The gap
   tightens instead, and only under 22rem does the name go. */
@media (max-width: 30rem) {
  .topbar__nav { gap: .8rem; }
  .topbar__id { letter-spacing: .02em; }
}
@media (max-width: 22rem) { .topbar__id { display: none; } .topbar__inner { justify-content: center; } }

/* ── hero ───────────────────────────────────────── */
.hero { padding-top: clamp(3rem, 9vw, 6rem); padding-bottom: clamp(2rem, 5vw, 3.5rem); }
.hero__name {
  font-family: var(--sans);
  font-size: clamp(3.1rem, 12vw, 7.5rem);
  letter-spacing: -.028em; line-height: .95; margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}
.hero__line {
  font-family: var(--sans);
  font-size: clamp(1.55rem, 4.1vw, 2.7rem);
  line-height: 1.2; letter-spacing: -.012em; max-width: 20ch; margin-bottom: 1.5rem;
}
.hero__body { max-width: 58ch; color: var(--ink); }
.hero__links { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 2rem; font-family: var(--sans); font-size: var(--t-sm); }

/* ── signature ──────────────────────────────────── */
.signature { padding-bottom: var(--band); }
.sig__h { font-family: var(--sans); font-size: var(--t-lg); max-width: 24ch; margin-bottom: .9rem; }
.sig__intro { max-width: 60ch; color: var(--muted); margin-bottom: 2rem; }

/* ── career strip ───────────────────────────────── */
.strip { border-block: 1px solid color-mix(in srgb, var(--hairline) 26%, transparent); padding-block: 1.1rem; }
.strip__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .4rem 1.5rem;
  font: 400 var(--t-sm)/1.6 var(--mono); color: var(--muted); letter-spacing: .01em;
}
.strip__list span { color: var(--ink); margin-right: .45em; }
.strip__list li:last-child { color: var(--ink); }

/* ── sections ───────────────────────────────────── */
/* A section is a bay on the sheet. Its heading is a drawing label, not a display line: set in
   the mono at label size and carrying its own position number, the way a bay elevation is
   annotated. The serif display line that used to sit here is the single strongest reason the
   page read as generated, because every site in this category opens a section the same way. */
.section { padding-block: var(--band); counter-increment: bay; }
main { counter-reset: bay; }
/* The divider and the label are one object, not two.
   A rule floating above a heading is a horizontal line doing nothing, and stacking it over a
   second rule under the heading is what made the joins feel loose. On a drawing the label sits
   ON the dimension line and the line runs out from it to the edge of the sheet, which is what
   this is: number, name, then the rule taking the rest of the measure. */
.section__h {
  display: flex; align-items: center; gap: .8rem;
  font: 500 var(--t-xs)/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); margin: 0 0 2.25rem;
}
.section__h::before {
  content: counter(bay, decimal-leading-zero);
  color: var(--muted); font-weight: 400; letter-spacing: .08em;
}
.section__h::after {
  content: ""; flex: 1 1 auto; height: 1px;
  background: color-mix(in srgb, var(--hairline) 38%, transparent);
}
.section__sub { max-width: var(--measure); color: var(--muted); margin-bottom: 3rem; }

/* ── introduction ───────────────────────────────── */
.intro { display: grid; grid-template-columns: 200px 1fr; gap: clamp(1.75rem, 5vw, 4rem); align-items: start; }
.intro__portrait { display: block; }
.intro__portrait img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 4 / 5; object-fit: cover;
  border-radius: var(--r); background: var(--teal-wash);
}
.intro__text { max-width: 62ch; }
.intro__meta { font: 400 var(--t-sm)/1.6 var(--mono); color: var(--muted); margin-top: 1.6rem; }
@media (max-width: 46rem) {
  .intro { grid-template-columns: 1fr; }
  .intro__portrait { max-width: 160px; }
}

/* ── point of view ──────────────────────────────── */
.piece { border-top: 1px solid color-mix(in srgb, var(--hairline) 26%, transparent); }
.piece:last-of-type { border-bottom: 1px solid color-mix(in srgb, var(--hairline) 26%, transparent); }
.piece h3 { font-size: inherit; }

.piece__toggle, .card__toggle {
  display: grid; align-items: baseline; width: 100%;
  background: none; border: 0; padding: 1.5rem 0; margin: 0;
  font: inherit; color: var(--ink); text-align: left; cursor: pointer;
}
.piece__toggle {
  grid-template-columns: 3.25rem minmax(0, 1fr) 1.5rem;
  grid-template-areas: "n t i" ".  d i";
  gap: .45rem .5rem;
}
.piece__n { grid-area: n; }
.piece__t { grid-area: t; }
.piece__toggle .piece__i { grid-area: i; }
.piece__d {
  grid-area: d; max-width: 62ch;
  font: 400 var(--t-sm)/1.55 var(--sans); color: var(--muted);
}
[aria-expanded="true"] .piece__d { color: var(--teal-deep); }
.piece__n { font: 400 var(--t-xs)/1.9 var(--mono); color: var(--muted); letter-spacing: .1em; }

.piece__t { font-family: var(--sans); font-size: clamp(1.25rem, 2.6vw, 1.7rem); line-height: 1.25; }
.piece__toggle:hover .piece__t, .card__toggle:hover .card__t { color: var(--teal-deep); }

.piece__i { position: relative; width: 14px; height: 14px; justify-self: end; align-self: center; }
.piece__i::before, .piece__i::after {
  content: ""; position: absolute; inset: 0; margin: auto; background: var(--teal-deep);
}
.piece__i::before { width: 14px; height: 1.5px; }
.piece__i::after  { width: 1.5px; height: 14px; transition: transform .25s ease-out; }
[aria-expanded="true"] .piece__i::after { transform: scaleY(0); }

.piece__panel, .card__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .25s ease-out; }
.piece__panel.is-open, .card__panel.is-open { grid-template-rows: 1fr; }
/* main.js says a no-JS visitor gets all the content open, and that stopped being true when the
   collapse moved into CSS: `0fr` is the default now, `.is-open` is added by script, so with
   scripting off the Work section was three headings and nothing under them. This restores the
   stated behaviour without an inline script, which the CSP would not allow anyway. */
@media (scripting: none) {
  .piece__panel, .card__panel { grid-template-rows: 1fr; }
}
.piece__inner, .card__inner { overflow: hidden; }
.piece__inner { padding-left: 3.75rem; }
@media (max-width: 40rem) { .piece__inner { padding-left: 0; } .piece__toggle { grid-template-columns: 2.5rem minmax(0, 1fr) 1rem; } }

.piece__inner > .prose { max-width: 66ch; padding-bottom: 2.5rem; }
.piece__inner > .chart { margin-bottom: 3rem; }
@media (min-width: 62rem) {
  .piece__inner { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3rem; align-items: start; }
  .piece__inner > .prose { padding-bottom: 3rem; }
  .piece__inner > .chart { position: sticky; top: 5rem; }
  /* The two column split exists to stand a chart beside the reading. A piece with no chart in it
     would otherwise take the first column and leave the second empty, setting the argument in half
     the measure for no reason. `.prose` keeps its own 66ch cap, so this widens nothing it should not. */
  .piece__inner:not(:has(> .chart)) { display: block; }
}

/* ── pulled figures ─────────────────────────────── */
.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 1px; margin: 1.9rem 0 1.6rem;
  background: color-mix(in srgb, var(--hairline) 26%, transparent);
  border-block: 1px solid color-mix(in srgb, var(--hairline) 26%, transparent);
}
.stat { background: var(--surface); padding: 1.05rem 1rem 1.15rem; }
.stat b {
  display: block; font: 400 clamp(1.65rem, 3.4vw, 2.15rem)/1 var(--mono);
  color: var(--teal-deep); letter-spacing: -.02em; margin-bottom: .5rem;
}
.stat span { display: block; font: 400 var(--t-xs)/1.45 var(--sans); color: var(--muted); }

/* ── charts ─────────────────────────────────────── */
.chart { margin: 0; }
.chart__h { font: 500 var(--t-sm)/1.4 var(--sans); color: var(--muted); margin-bottom: .9rem; }
.chart__h--second { margin-top: 2.25rem; }
.chart__svg { width: 100%; }
.chart__svg svg { display: block; width: 100%; height: auto; overflow: visible; }
.chart--sig .chart__svg { max-width: 780px; }
.chart figcaption { font: 400 var(--t-sm)/1.6 var(--sans); color: var(--muted); margin-top: 1.1rem; max-width: 56ch; }

.axis line, .axis path { stroke: var(--hairline); stroke-opacity: .45; shape-rendering: crispEdges; }
.axis text { fill: var(--muted); font: 400 11px var(--mono); }
.grid line { stroke: var(--hairline); stroke-opacity: .18; shape-rendering: crispEdges; }
.serie-label { font: 500 11px var(--mono); letter-spacing: .04em; }

/* ── controls ───────────────────────────────────── */
.control { margin-top: 1.5rem; display: grid; gap: .55rem; max-width: 32rem; }
.control label { font: 400 var(--t-sm)/1.4 var(--sans); color: var(--muted); }
.control output { font: 400 var(--t-sm)/1.5 var(--mono); color: var(--ink); }
.control output b { font-weight: 500; color: var(--teal-deep); }

input[type="range"] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 22px;
  background: none; cursor: pointer;
  touch-action: none;                    /* the page must not pan while the thumb is being dragged */
}
/* The travelled part of the track is filled, so the control reads as a value rather than a knob. */
input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; border-radius: 2px;
  background: linear-gradient(to right,
    var(--teal-deep) var(--pct, 0%),
    color-mix(in srgb, var(--hairline) 35%, transparent) var(--pct, 0%));
}
input[type="range"]::-moz-range-track {
  height: 3px; border-radius: 2px;
  background: linear-gradient(to right,
    var(--teal-deep) var(--pct, 0%),
    color-mix(in srgb, var(--hairline) 35%, transparent) var(--pct, 0%));
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 18px; height: 18px; margin-top: -7.5px;
  background: var(--teal); border: 2px solid var(--teal-deep); border-radius: 50%;
  transition: transform .12s cubic-bezier(.22,.61,.36,1), box-shadow .12s ease;
}
input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px; background: var(--teal); border: 2px solid var(--teal-deep);
  border-radius: 50%;
  transition: transform .12s cubic-bezier(.22,.61,.36,1), box-shadow .12s ease;
}
input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.12); }
input[type="range"]:hover::-moz-range-thumb     { transform: scale(1.12); }
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.2); box-shadow: 0 0 0 7px color-mix(in srgb, var(--teal) 16%, transparent);
}
input[type="range"]:active::-moz-range-thumb {
  transform: scale(1.2); box-shadow: 0 0 0 7px color-mix(in srgb, var(--teal) 16%, transparent);
}
input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px color-mix(in srgb, var(--teal) 30%, transparent); }
input[type="range"]:focus-visible::-moz-range-thumb     { box-shadow: 0 0 0 4px color-mix(in srgb, var(--teal) 30%, transparent); }

/* The signature control lines up with the plot area rather than the figure box. The chart
   publishes --track-l and --track-r; the input overhangs by half a thumb at each end so the
   thumb centre, not its edge, lands on 0% and on the slider's maximum. */
.chart--sig .control { max-width: none; padding-left: var(--track-l, 0); padding-right: var(--track-r, 0); }
.chart--sig .control input[type="range"] { width: calc(100% + 18px); margin-inline: -9px; }

@media (prefers-reduced-motion: reduce) {
  input[type="range"]::-webkit-slider-thumb,
  input[type="range"]::-moz-range-thumb { transition: none; }
  input[type="range"]:hover::-webkit-slider-thumb,
  input[type="range"]:hover::-moz-range-thumb,
  input[type="range"]:active::-webkit-slider-thumb,
  input[type="range"]:active::-moz-range-thumb { transform: none; }
}

.toggles { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.4rem; }
.readout { font: 400 var(--t-sm)/1.5 var(--sans); color: var(--muted); margin-top: 1rem; }
.readout b { font-weight: 500; color: var(--ink); }

.chip {
  font: 400 var(--t-xs)/1 var(--mono); letter-spacing: .05em;
  color: var(--muted); background: none; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--hairline) 38%, transparent);
  border-radius: 0; padding: .5rem .85rem;
  transition: background-color .15s, color .15s, border-color .15s;
}
.chip:hover { color: var(--ink); border-color: var(--teal-deep); }
.chip.is-on, .chip[aria-pressed="true"] { background: var(--teal-wash); color: var(--teal-deep); border-color: var(--teal); }
@media (prefers-reduced-motion: reduce) { .chip { transition: none; } }

/* ── selected work ──────────────────────────────── */
.filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 2.5rem; }
.grid { display: grid; gap: 0; }
/* No rules between entries. Each one is already a block of its own with space around it, and a
   line between every pair turned three items into a table. */
.card + .card { margin-top: clamp(1.75rem, 3.5vw, 2.75rem); }
.card[hidden] { display: none; }
.card h3 { font-size: inherit; }
.card__toggle { grid-template-columns: 1fr 1.5rem; grid-template-areas: "t i" "k i"; gap: .3rem .5rem; padding-bottom: .9rem; }
.card__t { grid-area: t; font-family: var(--sans); font-size: clamp(1.15rem, 2.2vw, 1.45rem); }
.card__k { grid-area: k; font: 400 var(--t-sm)/1.5 var(--sans); color: var(--muted); }
.card__toggle .piece__i { grid-area: i; }
.card__inner > * { max-width: 66ch; }
/* padding lives on the open state only: on a collapsed grid row, padding on the
   overflow-hidden element still renders as height and leaks the first lines through */
/* No transition on the padding: the panel's height is already animated by its grid row, and
   animating padding on top of that thrashes layout for a change nobody can see underneath. */
.card__inner { padding-bottom: 0; }
.card__panel.is-open .card__inner { padding-bottom: 2.25rem; }
.card__lede { color: var(--muted); }
/* A margin note, the way a caveat is written on a drawing: a hairline leader and the text set
   in the annotation face. It was a tinted panel behind a 2px coloured left border, which is the
   single most recognisable tell in generated interfaces. */
.privacy {
  border-left: 1px solid var(--teal-deep); background: none;
  padding: .1rem 0 .1rem 1.1rem; border-radius: 0;
  font: 400 var(--t-sm)/1.7 var(--mono); color: var(--muted);
}
.card__stack { display: flex; flex-wrap: wrap; gap: .45rem; margin: 0 0 1.5rem; padding-left: 0; }
.card__inner .card__stack { margin-top: 1.4rem; }
/* A legend line, not chips. Boxing three words each in their own rounded container was the
   habit he pointed at; a drawing lists its materials on one rule instead. */
.card__stack { column-gap: 0; }
.card__stack span {
  font: 400 var(--t-xs)/1.7 var(--mono); color: var(--muted);
  letter-spacing: .04em;
  border: 0; padding: 0;
}
.card__stack span + span::before { content: "/"; margin: 0 .6rem; opacity: .4; }

  font: 500 var(--t-sm)/1.5 var(--sans); color: var(--ink);
  margin-top: 1.4rem; display: flex; flex-wrap: wrap; gap: .75rem; align-items: baseline;
}

/* ── ask ────────────────────────────────────────── */
/* Was a question box posting to a Firebase function. It is now the install line for the MCP
   server, so the visitor's own agent does the asking and the answers are Gus's own words. */
.ask__line { max-width: var(--measure); color: var(--muted); margin-bottom: 1.75rem; }
/* The whole invitation is one line, so it carries the section on its own and reads in ink
   rather than in the grey used for supporting prose. */
.ask__line--lead {
  color: var(--ink);
  font: 400 var(--t-md)/1.35 var(--sans);
  letter-spacing: -.01em;
  margin-bottom: 1.25rem;
}

.mcp { display: flex; gap: .5rem; flex-wrap: wrap; max-width: 46rem; margin-bottom: 1.75rem; }

/* It stopped being a one-line command and became a message to paste into any chatbot, so it
   wraps and reads as a paragraph instead of scrolling sideways as a terminal line would. */
.mcp__cmd {
  flex: 1 1 22rem; min-width: 0;
  white-space: normal; overflow-wrap: anywhere;
  font: 400 var(--t-sm)/1.7 var(--mono); color: var(--ink);
  background: #FFF; border: 1px solid var(--hairline); border-radius: var(--r);
  padding: .75rem .9rem;
}
.mcp__copy {
  font: 500 var(--t-sm)/1.7 var(--mono); letter-spacing: .04em;
  background: var(--ink); color: var(--surface);
  border: 1px solid var(--ink); border-radius: var(--r);
  padding: .75rem 1.5rem; cursor: pointer;
  min-width: 6.5rem;   /* "Copy" and "Copied" must not resize the button between them */
}
.mcp__copy:hover { background: var(--teal-deep); border-color: var(--teal-deep); }
.mcp__copy.is-done { background: var(--teal-deep); border-color: var(--teal-deep); }

/* ── contact ────────────────────────────────────── */
.contact__links { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.5rem; font: 400 var(--t-sm) var(--mono); }

/* ── footer ─────────────────────────────────────── */
.foot { border-top: 1px solid color-mix(in srgb, var(--hairline) 26%, transparent); padding-block: 2rem; }
/* One line, one source. The film states the figure plainly and the credit lives here, so the
   copy reads like a sentence rather than a citation. */
.foot__inner { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem 2rem; font: 400 var(--t-xs) var(--mono); color: var(--muted); letter-spacing: .06em; }
/* Contact stopped being a section of its own and became two more items on this line. They keep
   the footer's register rather than arriving in link teal, which would make them the loudest
   thing on the quietest row of the page. */
.foot__inner a { color: var(--muted); border-bottom-color: color-mix(in srgb, var(--hairline) 45%, transparent); }
.foot__inner a:hover { color: var(--ink); border-bottom-color: var(--ink); }
/* The film now carries numbers in its headlines, and a published figure needs a named source
   somewhere on the page. One line, in the footer, at the smallest size that stays legible. */
.foot__src { margin-left: auto; letter-spacing: .02em; }
@media (max-width: 46rem) { .foot__src { margin-left: 0; } }
/* The one place the charts are qualified. It used to be said three times, above the argument. */
.foot__note { max-width: 52ch; margin-bottom: 1.5rem; font: 400 var(--t-xs) var(--mono); color: var(--muted); line-height: 1.7; }

/* ── entrance ───────────────────────────────────── */
.reveal > * { opacity: 0; transform: translateY(14px); }
.reveal.is-in > * {
  opacity: 1; transform: none;
  transition: opacity .5s cubic-bezier(.22,.61,.36,1), transform .5s cubic-bezier(.22,.61,.36,1);
  transition-delay: calc(var(--i, 0) * 26ms);
}
@media (prefers-reduced-motion: reduce) {
  .reveal > *, .reveal.is-in > * { opacity: 1; transform: none; transition: none; }
  .piece__panel, .card__panel, .card__inner { transition: none; }
  .piece__i::after { transition: none; }
}

/* ── scroll-world film ──────────────────────────────
   Theme tokens for the camera-flight engine (vendor/scrub-engine.js). Unlayered, so they
   beat the engine's own defaults, which sit inside `@layer sw` in scroll-world.css.
   --sw-bg is the exact background the dioramas were rendered on, so the poster, the video
   and the page are one continuous surface with no visible edge. */
:root, .sw-root {
  --sw-bg:       var(--surface);
  --sw-ink:      var(--ink);
  --sw-ink-soft: var(--muted);
  --sw-accent:   var(--teal-deep);
  --sw-font-display: var(--sans);
  --sw-font-body:    var(--sans);
}

/* The engine's stage is position:fixed at z-index 10, the same layer the topbar was on,
   so the film would paint over the nav. The topbar wins instead: it reads cleanly against
   the film because the scenes were rendered on --surface, the same cream it is tinted with. */
.topbar { z-index: 60; }

/* The <h1> exists for search engines, screen readers and the no-JS case. Once the engine
   mounts, the film's own copy carries the message, so the heading is taken out of the
   visual flow without being hidden from assistive tech. */
.film { position: relative; }
.film__h {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Multi-beat scene copy. A scene that carries two or three points stacks them in one grid
   cell so they cross-fade in place instead of pushing each other down the page; the row
   sizes to the tallest beat, so nothing reflows as they swap. Single-beat scenes are left
   in normal flow and behave exactly as the engine shipped them. */
/* Anchor the bottom, for EVERY scene.
   The engine centres the copy block on the viewport, so a block taller than half the screen
   hangs its last element below the fold, and the last element is the tag legend. Anchoring the
   bottom puts the legend a fixed distance above the viewport floor no matter how tall the block
   is or how many beats it has.
   This was previously scoped to multi-beat blocks only, which was a latent trap: cutting four
   scenes down to a single beat moved them back onto the engine's centring and the legend
   clipped again. The fix does not belong on a beat count. */
.sw-copy {
  top: auto;
  bottom: clamp(72px, 11vh, 132px);
  transform: none;
  max-height: calc(100vh - 200px);
}
.sw-copy:not([data-beats="1"]) {
  display: grid;
  grid-template-areas: "num" "beat" "cta";
  grid-template-rows: auto 1fr auto;
}
.sw-copy .sw-copy__tags { margin-top: 18px; }
.sw-copy:not([data-beats="1"]) > .sw-copy__num  { grid-area: num; }
.sw-copy:not([data-beats="1"]) > .sw-copy__beat { grid-area: beat; opacity: 0; will-change: opacity; }
.sw-copy:not([data-beats="1"]) > .sw-copy__cta  { grid-area: cta; }

/* The engine mounts seven fixed, full-viewport layers and never unmounts them: an opaque
   sky, a copy scrim, the stage, the route rail, the progress bar, the hint and its topbar.
   That is fine while the film owns the screen and wrong the moment anything follows it —
   a positioned z-index:0 element paints above non-positioned block content, so the whole
   page below the film sat under a cream sheet. `atmosphere:false` does not help: it only
   skips the sky's children, not the sky.
   Hiding by visibility rather than display keeps .sw-track's height, which is the scroll
   spacer the whole film is measured against. */
#world.is-past > * { visibility: hidden; }

/* Everything except the video has finished its job the moment the track ends; only the stage
   still has a frame worth dissolving. Anything else left painted over the page is just a veil. */
#world.is-exiting .sw-copylayer,
#world.is-exiting .sw-sky,
#world.is-exiting .sw-route,
#world.is-exiting .sw-scrollbar,
#world.is-exiting .sw-hint { visibility: hidden; }

/* ── the route rail ─────────────────────────────────
   The engine ships every label at opacity 0, revealed only on hover, so a visitor could not
   tell there were six stops or how far the film ran. The rail is the only thing on screen that
   can say "this is long on purpose" before someone decides whether to keep going, so all six
   names stay legible and the current one steps forward. */
.sw-route__label { opacity: .45; }
.sw-route__dot:hover .sw-route__label,
.sw-route__dot.is-active .sw-route__label { opacity: 1; }

/* One colour down the whole rail, and it is the orange the film reserves for the advertisement
   and then for the incrementality wedge. The engine tints each tick with its own section accent,
   which made the rail three colours and said nothing; --rail-mark ignores that and leaves the
   engine's inline --sw-accent untouched for the copy blocks that do use it.
   Every stop is outlined from the start, so the length of the film is visible on arrival. A stop
   fills when it is reached and STAYS filled, which is what makes the rail a record of where you
   have been rather than a single lit dot. */
.sw-route { --rail-mark: var(--orange); }
.sw-route__dot i {
  background: var(--sw-bg);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rail-mark) 55%, transparent);
}
.sw-route__dot:hover i { box-shadow: inset 0 0 0 1px var(--rail-mark); }
.sw-route__dot.is-done i,
.sw-route__dot.is-active i {
  background: var(--rail-mark);
  box-shadow: inset 0 0 0 1px var(--rail-mark);
}
.sw-route::after { background: var(--rail-mark); }
.sw-route::before { background: var(--rail-mark); opacity: .28; }

/* The base line says how long the film is; this says how far in you are. It has to sit on the
   SAME axis as the base line, and it did not: the base line moved to the tick column on the
   right and this fill was left at left:50%, so the rail rendered as two parallel lines in
   different places. Both are now anchored by --tick off the same right edge. */
/* Height in pixels, published by world-board.js as the centre of the active tick. A scaled
   percentage of the whole rail cannot land on a tick, because the scenes are not equally long
   and the ticks are equally spaced. */
.sw-route::after {
  content: "";
  position: absolute;
  right: calc(var(--tick) / 2 - .5px); top: 0;
  width: 1px;
  height: var(--sw-fill, 0px);
  background: var(--ink);
}
@media (prefers-reduced-motion: no-preference) {
  .sw-route::after { transition: height .28s cubic-bezier(.22,.61,.36,1); }
}

/* ── incrementality chart ───────────────────────────
   The channel chips filter the cloud; each carries its channel's colour so the legend and
   the marks are the same object. Pressed means isolated, not selected-among-many. */
.chart--inc { margin: 0; }
.inc__keys { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.inc__keys button[data-ch="offsite"] { --k: var(--slate); }
.inc__keys button[data-ch="instore"] { --k: var(--teal-deep); }
.inc__keys button[data-ch="onsite"]  { --k: var(--teal-deep); }
.inc__keys button::before {
  content: ""; display: inline-block; width: .55rem; height: .55rem; border-radius: 50%;
  background: var(--k); margin-right: .45rem; vertical-align: -1px;
}
.inc__keys button[data-ch="instore"]::before { background: var(--teal); }

/* ── film to page handover ──────────────────────────
   --sw-exit runs 0 to 1 across the engine's run-out viewport, published from world.js.
   The film dims across that band while the first section below rises into it. Both the
   film's background and the page are --surface, so what crosses over is only the diorama,
   not a colour change, and the seam reads as the camera pulling out of the world. */
#world .sw-stage,
#world .sw-copylayer,
#world .sw-route,
#world .sw-scrollbar,
#world .sw-hint { opacity: calc(1 - var(--sw-exit, 0)); }

/* The film used to hand over to the incrementality chart. The chart is gone — the film makes
   the argument now — so the run-out band delivers the reader straight to his face and bio. */
#intro {
  opacity: calc(0.9 + 0.1 * var(--sw-enter, 1));
  transform: translateY(calc((1 - var(--sw-enter, 1)) * 1vh));
}
@media (prefers-reduced-motion: reduce) {
  #world .sw-stage, #world .sw-copylayer, #world .sw-route,
  #world .sw-scrollbar, #world .sw-hint { opacity: 1; }
  #intro { opacity: 1; transform: none; }
}


/* ── prose measure ──────────────────────────────
   The wrap is 1100px because the cards, the rules and the film want that width. Prose does not:
   at 17px Inter that runs past 80 characters and the eye starts losing its place on the return.
   The column narrows, the container does not. */
.intro__text p, #ask .wrap > p, .card__lede { max-width: var(--measure); }


/* ── the film reads in the site's voice ─────────
   The families were already mapped, but the engine asks for weights this site does not load:
   700 on the serif titles and the eyebrow, 600 on the tags and the route labels. Newsreader and
   Inter are here at 400 only, so the browser was synthesising all of it, and a faked bold serif
   next to a real 400 serif is exactly the mismatch that made the film look like a different
   document. Everything below asks only for weights that exist: 400 for the text faces, 500 for
   IBM Plex Mono, which is the one face that ships two. */
#world .sw-copy__title {
  font-weight: 400;
  letter-spacing: -.018em;      /* the value .section__h uses, so a headline reads the same */
  line-height: 1.2;
  /* Down from the engine's clamp(2rem, 4.4vw, 3.5rem). At 3.5rem a headline carrying a figure
     and a clause ran four lines and filled half the screen, which fought the film instead of
     labelling it. The film is the thing being looked at; the words caption it. */
  font-size: clamp(1.45rem, 2.5vw, 2.05rem);
}
#world .sw-copy__eyebrow {
  font: 500 var(--t-xs)/1 var(--mono);
  letter-spacing: .04em;
  text-transform: none;         /* uppercase letterspaced labels are the tell we just removed */
  color: var(--teal-deep);
}
#world .sw-copy__num { font-family: var(--mono); }
/* One marked word per headline, and only on the scene the whole film is arguing toward. It takes
   the scene's own accent, which on Measurement is the same orange the wedge is drawn in, so the
   word and the picture are saying the same thing. */
#world .sw-mark {
  font-style: normal;
  color: var(--sw-accent, var(--teal-deep));
  border-bottom: 2px solid currentColor;
  padding-bottom: .06em;
}
#world .sw-copy__body {
  font-family: var(--sans);
  font-weight: 400;
  line-height: 1.65;            /* the body rhythm the page below uses */
}
/* line-height 1 on a legend clips descenders, and uppercase with tracking made the row wide
   enough to run past the copy column and off the screen. Lower case, real leading, and the row
   is allowed to wrap. */
#world .sw-copy__tags { flex-wrap: wrap; row-gap: 2px; max-width: 100%; }
#world .sw-copy__tags li {
  font: 400 var(--t-xs)/1.7 var(--mono);
  letter-spacing: .02em; text-transform: none;
}
#world .sw-route__label  { font: 500 var(--t-xs)/1.4 var(--mono); letter-spacing: .02em; text-transform: none; }
#world .sw-copy__cta a   { font: 400 var(--t-sm)/1 var(--sans); }


/* Agency years: the work, and whose it was. No year and no write-up, because the section is kept
   for depth rather than as current positioning, and a reader only needs to know what the job was
   and who it was for. */
.clients { list-style: none; margin: 1.4rem 0 0; padding: 0; }
.clients li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .9rem;
  padding: .55rem 0;
  border-top: 1px solid color-mix(in srgb, var(--hairline) 18%, transparent);
}
.clients li:first-child { border-top: 0; }
.clients li span { font: 400 var(--t-sm)/1.6 var(--mono); color: var(--muted); letter-spacing: .02em; }


/* ── 404 ──────────────────────────────────────────────
   The same drawing sheet, one bay wide. The code takes the position number's treatment so the
   page reads as part of the plan rather than as the host's default error screen. */
.miss { padding: clamp(5rem, 16vh, 9rem) var(--gap) clamp(4rem, 12vh, 7rem); max-width: var(--measure); }
.miss__code {
  font: 500 var(--t-xs)/1 var(--mono); letter-spacing: .14em;
  color: var(--orange); margin-bottom: 1.4rem;
}
.miss__h { font-family: var(--sans); font-size: clamp(1.6rem, 4vw, var(--t-xl)); margin-bottom: 1.1rem; }
.miss__body { color: var(--muted); }
.miss__back { font: 400 var(--t-sm)/1 var(--mono); margin-top: 2rem; }
.page-404 { display: flex; flex-direction: column; min-height: 100vh; }
.page-404 .miss { flex: 1; width: 100%; }


/* ── the film on a phone held upright ─────────────────
   The clips are 16:9 and a portrait phone is about 9:19. object-fit:cover on a box that shape
   shows roughly a third of the frame's width, and the third it throws away contains the
   television. That is not a crop, it is the loss of the film's only device: orange is the
   advertisement, established in scene one and cashed in scene four. Cropped to the sofa, the
   phone version argues nothing.

   So the frame stops filling the screen and becomes a plate near the top, on the cream it was
   rendered on, with the argument set below it on clear ground. That is the same drawing sheet
   the rest of the page is laid out as, which makes the phone a different view of the idea
   rather than a damaged copy of the desktop one.

   132% wide rather than 100%: at full width the plate is 211px tall on a 375px screen, a
   postage stamp. Sixteen percent off each edge buys a third more height and spends only plate
   margin, because the room itself occupies the middle 78% of the frame. */
@media (max-width: 860px) and (orientation: portrait) {
  /* The clips were rendered on this cream, not on the site's. Full bleed hid the difference;
     a plate would draw it as a rectangle.
     --sw-ink-soft comes down with it. The page's --muted is 4.63:1 on the site's cool cream and
     only 4.18:1 on this warmer, darker one, which is under AA for body text — the plate paid for
     itself in contrast without anyone asking. #5A6968 is 4.90:1 here and reads the same. */
  #world { --sw-bg: #EFEDE5; --sw-ink-soft: #5A6968; }

  #world .sw-scene__video,
  #world .sw-scene__render canvas,
  #world .sw-scene__still {
    top: 7vh; right: auto; bottom: auto; left: -16%;
    width: 132%; height: auto; aspect-ratio: 16 / 9;
    object-fit: cover; object-position: center;
  }
  /* The engine breathes the poster from scale(1.03) to scale(1.17) while its clip downloads.
     Full bleed that reads as a slow push in; on a plate it grows the plate out of its own band
     and over the copy. The engine writes it as an inline style, which is what makes !important
     the honest answer here rather than a shortcut. */
  #world .sw-scene__still { transform: none !important; }

  /* The copy is on cream now, not on the picture, so it needs neither a scrim nor a halo. */
  #world .sw-copylayer::before { display: none; }
  #world .sw-copy__title,
  #world .sw-copy__body { text-shadow: none; }

  /* 7vh of air, the plate (132vw at 16:9 is 74.25vw), then a gap. Driven by vw because the
     plate's height follows the width, not the screen height. */
  #world .sw-copy {
    top: calc(7vh + 74.25vw + 1.5rem);
    bottom: auto;
    max-height: none;
  }

  /* The index used to sit centred on the screen, which on the plate layout put it on top of the
     picture: six numbers over a diorama, none of them readable. It moves to the foot of the
     sheet instead, where there was dead space and where a bay index belongs anyway. The copy
     keeps a right margin wide enough that a long headline never runs under it. */
  #world .sw-route { top: auto; bottom: 4vh; transform: none; right: 14px; }
  #world .sw-copy { padding-right: 46px; }
}

/* A drawing of a mouse wheel, on a device with no mouse. It was wrong on a phone before the
   plate layout and it sits in the middle of the copy after it. */
@media (hover: none) and (pointer: coarse) {
  #world .sw-hint { display: none; }
}
