/* Ahmad Dibo — page-specific layouts
   Landing, Index/Archive, Experiment case study, Ethos, Connect. */

/* ====================================================================== */
/*  LANDING                                                                 */
/* ====================================================================== */

/* The landing is a single grid row set roughly a third down the page:
   the wordmark repeats in the wordmark column and the studio identification
   sits on the 57.3% INDEX line, directly under the nav cluster. Nothing
   else — no statement, no portrait. The page is mostly air by intent. */
/* The landing is exactly one screen — there is nothing below the fold, so it
   must not scroll. The old calc(100vh - 64px) hardcoded a header height that
   is really ~82px, and then added --s-5 of bottom padding on top, overflowing
   the viewport by ~18px and producing a scroll that led nowhere. Measuring the
   header live and including the padding in the box keeps it exact at any
   width, where a hardcoded number only works at one. */
.landing {
  /* header = --s-3 top + one --t-small line + --s-4 bottom (see .site-head),
     rebuilt from the same tokens so it tracks any change to the scale. */
  min-height: calc(100dvh - (var(--s-3) + (var(--t-small) * 1.5) + var(--s-4)));
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  /* no top padding: .landing__id owns the offset so it can be matched to
     the first-content line of the inner pages exactly. */
  padding-block: 0 var(--s-5);
}

/* The identification block sits on the shared --first-line, so moving
   between the landing and any inner page never makes the type jump. */
.landing__id {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: baseline;
  margin-top: var(--first-line);
}

/* the repeated wordmark matches the header wordmark exactly — same size
   (--t-small, the site's body scale), same weight, same metrics */
.hero-wordmark {
  display: block;
  font-size: var(--t-small);
  font-weight: var(--w-regular);
  letter-spacing: 0;
  line-height: 1.5;
  text-transform: none;
  margin: 0;
}
/* © sits inline with the wordmark, never superscripted (matches the header) */
.hero-wordmark sup {
  font-size: 1em;
  font-weight: inherit;
  vertical-align: baseline;
}

.landing__studio {
  /* nudge right by the same optical offset the details column uses so the
     "C" of CREATIVE lands on the "I" of Index above it */
  padding-left: var(--nav-start);
}



.landing__studio-name {
  grid-column: 2;
  padding-left: var(--nav-start);   /* start on the "I" of Index */
  font-size: var(--t-small);
  font-weight: var(--w-bold);
  letter-spacing: 0;
  line-height: 1.5;
  text-transform: uppercase;
  margin: 0;
  justify-self: start;
  width: max-content;              /* the first line sets the measure */
  max-width: 100%;
}

.landing__line {
  display: block;
}

/* the second line is pushed to the full measure so both lines end evenly,
   the gap falling between "diagnosis" and "to" */
.landing__line--split {
  display: flex;
  justify-content: space-between;
}

/* first open only (see the head script in index.html): the line sets as
   ordinary text with a normal word space, then js/main.js pushes "to
   measured outcomes." out to the full measure. */
.intro .landing__line--split {
  display: block;
}

@media (max-width: 800px) {
  .landing__id {
    grid-template-columns: 1fr;
    row-gap: var(--s-3);
    margin-top: var(--s-6);
  }
  .landing__studio {
    padding-left: 0;
  }
  /* the two-line break is balanced for the nav-column measure; once the
     layout stacks there is no column to balance against, so let it flow. */
  .landing__studio-name {
    width: auto;
  }
  .landing__line,
  .landing__line--split {
    display: inline;
  }
}

.landing__foot {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--s-3);
  flex-wrap: wrap;
}

/* a small index of destinations at the foot of the landing,
   bracket-tick style, echoing the city '+' list (Image 1) */
.dest {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
}
.dest a {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-size: var(--t-h2);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
  line-height: 1.04;
  width: max-content;
  transition: color var(--dur-fast) var(--ease);
}
.dest a .plus {
  font-size: 0.5em;
  font-weight: var(--w-regular);
  color: var(--ink-mute);
  transition: transform var(--dur) var(--ease);
}
.dest a:hover {
  color: var(--ink-mute);
}
.dest a:hover .plus {
  transform: rotate(90deg);
}

/* ====================================================================== */
/*  PAGE HEAD (shared by Index / Ethos / Connect)                          */
/* ====================================================================== */

.page-head {
  padding-top: var(--s-5);
}
.page-head__title {
  font-size: var(--t-h1);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  margin-bottom: var(--s-3);
}
/* A page-head that carries a key beside its title. The two share one grid
   row on the SAME tracks as the nav, so the key starts on the "I" of Index
   (col-1 + the header gap) and baseline-aligns with the H1 — no hand-tuned
   percentage, and it tracks the nav at every width. */
.page-head--split {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  column-gap: var(--nav-start);
  align-items: baseline;
}
.page-head--split .page-head__title {
  grid-column: 1;
}
/* the key sits tight under its title — close the H1's own bottom margin
   only where the key follows, so every other page-head is untouched. */
.page-head__title:has(+ .page-head__key) {
  margin-bottom: var(--s-1);
}

.page-head__key {
  grid-column: 2;
  font-size: var(--t-small);
  font-weight: var(--w-regular);
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  margin: 0 0 var(--s-3);          /* match the title's bottom margin */
}

@media (max-width: 800px) {
  /* stacked: no nav to align to, and the key sits under the title */
  .page-head--split {
    display: block;
  }
  .page-head__key {
    margin-top: var(--s-2);
  }
}

.page-head .rule-strong {
  margin-bottom: var(--s-2);
}

.page-meta {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  flex-wrap: wrap;
}

/* ====================================================================== */
/*  INDEX — projects grouped by scope (A. Corpus / B. Reddere / C. Motus)  */
/* ====================================================================== */

/* each scope is two columns on the nav grid: the scope letter over its
   collection description fills the wordmark column (col 1), and the project
   rows sit in col 2, starting on the 57.3% Index line. No hero media —
   the index is pure type; structure comes from whitespace alone.
      col 1 (57.3%)          |  col 2 (1fr)
      LETTER + DESCRIPTION   |  CODE · NAME rows
*/
.scope {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: start;
  column-gap: 0;
  padding-top: var(--s-6);            /* air between scopes */
}
.scope:first-of-type {
  padding-top: var(--s-5);
}

/* left column — the bare scope letter over the collection description.
   the description stops well short of the Index line so the justified
   measure stays readable (~52% of the page, per the reference). */
.scope__head {
  grid-column: 1;
  padding-right: 12%;
}
/* sentence case, no letterspacing — the index reads in the same voice as
   the nav; the group is marked by its letter (A.), not by shouting. */
.scope__label {
  font-size: var(--t-h1);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  text-transform: none;
  color: var(--ink);
  margin: 0 0 var(--s-3);
}
/* When the letter follows its description (scope B), the gap belongs ABOVE
   it, not below — the margin is positional, so mirror it rather than
   leaving a stray space under the letter. */
.scope__desc + .scope__label {
  margin: var(--s-3) 0 0;
}

.scope__desc {
  font-size: var(--t-small);
  line-height: 1.45;                  /* match the body prose */
  color: var(--ink);
  margin: 0;
}
/* the dates follow the description on the next line, set tight against it so
   the pair reads as one block of metadata under the scope letter. */
.scope__desc + .scope__desc {
  margin-top: 0;
}

/* A second head/list pair inside the same .scope: it forms its own grid row,
   so the collection text and its rows sit on the same line. Column 1 keeps
   the head's reading inset; column 2 keeps the nav-aligned row track. */
.scope__head--second {
  grid-column: 1;
  padding-right: 12%;
}
.scope__list--second {
  grid-column: 2 / -1;
  min-width: 0;
}
/* the letter-offset only applies to the pair that carries the scope letter */
.scope:has(.scope__head > .scope__label:first-child) .scope__list--second {
  padding-top: 0;
}
/* ...but the second head opens with .scope__desc--group, which pushes its
   description down by --s-6 to read as its own block. The rows share the grid
   row and would otherwise stay pinned to its top, leaving the code floating
   above the description it labels. Match the offset with the same token so
   the pair stays locked together at every width. */
.scope__head--second + .scope__list--second,
.scope:has(.scope__head > .scope__label:first-child)
  .scope__head--second + .scope__list--second {
  padding-top: var(--s-6);
}

/* a second collection inside one head: it needs the air two separate
   .scope sections used to give it, so it reads as its own block rather
   than a fourth metadata line. Same token as the gap between scopes. */
.scope__desc--group {
  margin-top: var(--s-6);
}
.scope__desc + .scope__desc--group {
  margin-top: var(--s-6);   /* beat the "+ .scope__desc { margin-top: 0 }" rule */
}

/* right side — the rows are their own grid spanning tracks 2→end, using
   the SAME track widths as the nav above so the code lands under "Index"
   and the name under "About". Rows are set at the description's leading so
   each group reads as one block. */
/* The list starts on the col-1 line — the same line "Index" starts on.
   Each row then steps by --nav-item, the exact Index→About distance, so
   the code sits under "Index" and the name under "About" at every width.
   Rows stay real boxes: one hover/click target per <a>, and a tall
   description beside them never stretches the row rhythm. */
/* The rows drop to the description's first line, so each group reads as
   "LETTER / description | rows" rather than the rows racing the letter.
   The offset is the scope letter's own height — it is set at --t-h1 with
   line-height 1, so its box is exactly --t-h1 — plus the margin below it.
   Built from the same tokens as .scope__label, never a fixed pixel value:
   --t-h1 is a clamp() and drifts with viewport width. */
.scope__list {
  grid-column: 2 / -1;
  min-width: 0;
  /* The rows carry the 1.9 row rhythm while the description sets at 1.45.
     Both are --t-small, so the row's taller line box adds half the surplus
     leading ABOVE its first glyph and the code lands low against the line it
     labels. Lift by exactly that half-leading — derived from the two
     line-heights, so it tracks --t-small at every width instead of pinning a
     pixel value that only holds at one. */
  margin-top: calc(var(--t-small) * (1.45 - 1.9) / 2);
}
/* Drop the rows to the description's first line ONLY where the letter leads
   the head (scope A). The offset is the letter's own box — --t-h1 at
   line-height 1 — plus its bottom margin, built from the same tokens since
   --t-h1 is a clamp() that drifts with width. Where the description leads
   instead (scope B), the rows already start on that line and need nothing. */
.scope:has(.scope__head > .scope__label:first-child) .scope__list {
  padding-top: calc(var(--t-h1) + var(--s-3));
}

.scope__row {
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;
  align-items: baseline;
  column-gap: 0;
  line-height: 1.9;                   /* row rhythm */
  color: var(--ink);
  padding-left: var(--nav-start);     /* clears the header gap → "I" of Index */
}
.scope__code {
  position: relative;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.scope__name {
  position: relative;
  font-size: var(--t-small);
  font-weight: var(--w-regular);
  text-transform: none;
  letter-spacing: 0;
}
/* a coded name (e.g. "n = 11") stays roman lowercase, tabular */
.scope__name--code {
  font-variant-numeric: tabular-nums;
}
/* The click affordance is the word "Explore", not a dash. It appears on
   hover starting on the "N" of Notice — the header's right-gutter item.
   The row spans tracks 2→end, so its right edge IS the wrap's right edge,
   the same edge "Notice" is flush to. Notice is an auto-width column, so
   its left edge sits exactly one "Notice" width in from that edge: offset
   the label by --notice-w and left-align its text, and the "E" lands on
   the "N" at every width. Built from the type, not a hardcoded percentage
   (the nav is the ruler — see the alignment-grid note in CLAUDE.md).
   Absolutely positioned so nothing in the row shifts when it appears. */
.scope__row {
  position: relative;
}
.scope__explore {
  position: absolute;
  /* Anchor the label's LEFT edge on the Notice line. The row's right edge
     IS the wrap's right edge, and "Notice" (an auto column flush to that
     edge) starts exactly --notice-w in from it. So stretch the box from
     that line to the row's right edge and left-align the text inside it:
     the "E" lands on the "N" and the word runs toward the page edge. */
  left: auto;
  right: 0;
  width: var(--notice-w);
  top: 0;
  text-align: left;
  font-size: var(--t-small);
  font-weight: var(--w-regular);
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  color: var(--ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
/* The whole group is the target: hovering the row, or the description and
   hero plate that belong to it, raises the same label. .is-hover is set by
   js/main.js, which pairs each .scope__head with the .scope__list beside it. */
.scope__row:hover .scope__explore,
.scope__row:focus-visible .scope__explore,
.scope__row.is-hover .scope__explore { opacity: 1; }

/* rows with no destination yet (href="#") get no label and no pointer — the
   affordance should only promise a page that exists. */
.scope__row[href="#"] { cursor: default; }
.scope__row[href="#"] .scope__explore { content: none; display: none; }

/* Hero media returns for scope B only — scope A stays pure type. The figure
   sits in the left column under the description, so its left edge starts on
   the same line as the scope letter and the wordmark. It cancels the
   head's 12% right padding (that inset keeps the justified prose to a
   readable measure; the image wants the column's full width) and runs to the
   col-1 line. Unframed, and set in its source colour. */
.scope__media {
  /* space below as well as above: a --t-h1 letter can follow the figure and
     must not sit against the image edge. */
  margin: var(--s-4) 0 var(--s-5);
  margin-right: -12%;               /* cancel .scope__head's reading inset */
}
/* Opt-out: hold the image to where the prose above it actually ENDS. The
   Motus description is a single ragged line, so it stops short of the
   head's 12% box; matching the column would overhang the words. The width
   is set in ch against --t-small so it tracks the type, not the viewport.
   (Re-measure if that sentence is ever reworded — this is its measure.) */
.scope__media--measure {
  margin-right: 0;
  width: 100%;
  max-width: 84.5ch;                /* = where "…workout tracking" ends */
  font-size: var(--t-small);        /* ch resolves against the desc's type */
}
.scope__media--measure img,
.scope__media--measure video {
  width: 100%;
}
/* Index plates render in their source colour. The grayscale rule still holds
   everywhere else on the site (portrait, contact sheet, experiment plates);
   here the product's own interface colour is information, not decoration. */
.scope__media img,
.scope__media video {
  display: block;
  width: 100%;
  height: auto;
}
/* NOTE: the index is otherwise pure type, per the comment in work.html. The
   R&D and Motus groups each carry one figure, using .scope__media--measure.
   A colour opt-out (.scope__media--color) lived here for the Zozy plate,
   whose price scale is meaningless in grayscale; it was removed with the
   figure. Re-add it there rather than in the index if that plate comes back. */

/* corpus-crash.mp4 is back in the index, in the B / R&D head, held to the same
   --measure as the Motus plate below it so both groups share one figure
   column. It autoplays muted and looped, so it is a moving plate and carries
   no controls; the poster covers the frame before it decodes. It is already
   monochrome, so the index colour exemption above is moot for it.
   black-tshirt-index.mp4 / -poster.jpg remain in img/ unused. */

@media (max-width: 800px) {
  .scope {
    grid-template-columns: 1fr;        /* stack the head, then the rows */
    row-gap: var(--s-3);
    padding-top: var(--s-6);
  }
  .scope__head {
    grid-column: 1;
    padding-right: 0;
  }
  .scope__list {
    grid-column: 1;
  }
  .scope__row {
    grid-template-columns: 3.5em 1fr;  /* code column tightens on narrow */
    padding-left: 0;                   /* no nav to align to when stacked */
  }
  /* no Notice column to align to once stacked, and no hover on touch — the
     label would only crowd the name. */
  .scope__explore {
    display: none;
  }
  .scope__media {
    margin-right: 0;                   /* head inset is already 0 when stacked */
  }
}

/* ====================================================================== */
/*  EXPERIMENT CASE STUDY (Airport Futures ref)                            */
/* ====================================================================== */

/* running header, pinned left + right */
.runner {
  display: flex;
  justify-content: space-between;
  padding-block: var(--s-2);
  border-bottom: 1px solid var(--rule);
}
.runner span {
  font-size: var(--t-micro);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--ink-mute);
}

.case-intro {
  font-size: var(--t-h2);
  font-weight: var(--w-regular);
  letter-spacing: var(--tracking-tight);
  line-height: 1.18;
  max-width: 28ch;
  padding-block: var(--s-6) var(--s-7);
}

/* the four boxed columns with full vertical rules between */
.stages {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: 1.5px solid var(--rule-strong);
}
.stage {
  padding: var(--s-3) var(--s-3) var(--s-5);
  border-right: 1px solid var(--rule);
}
.stage:first-child {
  padding-left: 0;
}
.stage:last-child {
  border-right: 0;
  padding-right: 0;
}
.stage__label {
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  margin-bottom: var(--s-3);
}
.stage__body {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
}

/* timeline strip under the boxes */
.timeline {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--rule);
  margin-top: var(--s-1);
}
.timeline__step {
  padding-top: var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-mute);
  position: relative;
}
.timeline__step[data-active]::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: 28px;
  height: 2px;
  background: var(--ink);
}
.timeline__step[data-active] {
  color: var(--ink);
}

/* ====================================================================== */
/*  ETHOS                                                                   */
/* ====================================================================== */

/* numbered entries: number in a narrow left gutter, body in a readable
   column that starts at the same indent across all entries. */
/* padding, not margin: margins collapse against the page-head and pull the
   first entry above the shared --first-line. --s-5 matches .scope's own
   first-section space, so About/Notice start exactly where Index does. */
.ethos {
  padding-top: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}

/* number sits in the far-left gutter; body text begins at the INDEX
   column (~57% across), aligned with the rest of the site's grid. */
.entry {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;   /* number col, then body at INDEX line */
  align-items: start;
}
.entry__num {
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  padding-top: 0.15rem;          /* optically align with first text line */
}
/* an entry with no number still begins on the INDEX line (Notice) */
.entry > .entry__body:only-child {
  grid-column: 2;
}
/* About — number + section name share the left gutter, the same pairing the
   experiment pages use. The number stays tabular so the column of 01-10 lines
   up; the section name sits one space after it and wraps under itself. */
/* About §03 — Experience borrows the Index's scope grid: the number and the
   section name sit in column 1, the employer rows on the Index line. Unlike an
   Index row a CV row is several lines deep, so the row switches from a baseline
   grid to a top-aligned one and the code keeps the nav-item track. */
.entry--cv {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: start;
}
.entry--cv .scope__head {
  grid-column: 1;
  padding-right: 12%;
  /* line the role up with the section name above it: the number column
     (2.2ch) plus the index grid's own gap. */
  padding-left: calc(2.2ch + var(--s-2));
}
/* Education and Skills are short records, not prose blocks: their lines read
   left-aligned rather than justified, so the ragged edge stays deliberate. */
.entry--cv .scope__detail .cv__lines li,
.entry--cv .scope__detail .scope__name,
.entry--cv .scope__detail .cv__meta {
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}

/* the first record clears the section label above it — the achievements move
   with it so the title and its own detail keep a shared top edge. This applies
   only where the left column carries its own record title (§03 Experience);
   Education and Skills have an empty left column, so the offset would drop
   their first line below the section label instead of onto it. */
.entry--cv:has(.scope__head) .scope__head:not(.scope__head--second),
.entry--cv:has(.scope__head) .scope__list:not(.scope__list--second) {
  padding-top: var(--s-3);
}
/* §04/§05 — no left-column title: the first line starts on the label's own
   line, the way §01 and §02 do. */
.entry--cv:not(:has(.scope__head)) .scope__list {
  padding-top: 0;
}
/* each role is its own grid row: title block left, achievements right. The
   index's --s-6 group offsets do not apply — a CV row is one record, so the
   pair sits on a shared top edge and the gap between roles does the parting. */
.entry--cv .scope__head--second,
.entry--cv .scope__list--second {
  padding-top: var(--s-4);
}
.entry--cv .scope__list {
  grid-column: 2 / -1;
  margin-top: 0;                   /* no baseline lift: blocks, not rows */
  min-width: 0;
  padding-left: var(--nav-start);  /* start on the "I" of Index */
  /* stop the measure short of the right gutter so the justified edge lands
     just before the "N" of Notice in the nav above. "Notice" is 6 characters
     at --t-small plus a word space, so the inset is built from the type scale
     rather than pinned to one viewport width. */
  padding-right: 7ch;
}
.scope__row--cv {
  align-items: start;              /* was baseline, for one-line rows */
  line-height: 1.45;               /* the prose rhythm, not the row rhythm */
  grid-template-columns: 1fr;      /* no code column: the role leads the row */
  /* .scope__list already carries the nav-start offset; the row must not add
     a second one or these records indent past the rows above them. */
  padding-left: 0;
}
.scope__detail {
  min-width: 0;
}
.entry--cv .scope__name {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  font-weight: var(--w-regular);
  text-transform: none;
  letter-spacing: 0;
}

.ethos .entry__index {
  display: grid;
  grid-template-columns: 2.2ch 1fr;
  gap: var(--s-2);
  align-items: baseline;
  margin: 0;
  padding-top: 0.15rem;
}
.ethos .entry__section {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  max-width: 22ch;
}
.entry__body {
  max-width: 46ch;
  padding-left: var(--nav-start);   /* start on the "I" of Index, like the rest */
}
.entry__body p {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
}
.entry__action {
  margin-top: var(--s-3);
}
/* §06 is a single action standing alone in its entry — nothing above it to
   clear, so it starts on the number's line like every other section. */
.entry__body > .entry__action:only-child {
  margin-top: 0;
}

/* The portrait spans the nav from the "I" of Index to the "t" of About: the two
   labels plus the one gap between them. Measured off the live nav at --t-small
   — Index 31.81px + About 33.98px = 65.79px = 4.1119rem — with --nav-gap left
   as a token, since that gap is a clamp that drifts with viewport width. rem
   rather than ch: ch would resolve against this element's own 16px, not the
   nav's 13px. */
.entry__portrait {
  display: block;
  width: 8.0723rem;                /* 129.16px — the measured Index→About span */
  max-width: 100%;
  height: auto;
  filter: grayscale(100%);
}
/* §01's verdict line follows its own paragraph, so it sits closer than an
   action link standing alone in an entry. */
.ethos .entry__body p + .entry__action {
  margin-top: var(--s-2);
}

/* §01 is a two-line statement, not a prose block: it takes the landing h1's
   treatment — the line break is chosen and held by nbsp, so the measure stays
   ragged and the word spacing never stretches to fill it. */
.entry--ethos .entry__body p {
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}

/* ── NOTICE — the legal page. Numbered clauses in the same voice as About;
   sub-lists are numbered 01/02/03 to match the gutter rather than bulleted,
   since the site has no bullet anywhere else. ── */
/* the date sits on the INDEX line with the clause bodies, so it rides the same
   two-track grid as .entry rather than starting in the left gutter. It is set
   at the body size in full ink: it is the page's first line, and every other
   page opens at --t-small in --ink, so anything quieter here reads as the type
   jumping when you switch pages. */
.notice__updated {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  font-size: var(--t-small);
  color: var(--ink);
  margin: 0 0 var(--s-5);
}
.notice__updated > span {
  grid-column: 2;
  padding-left: var(--nav-start);   /* start on the "I" of Index, like the body */
}
/* clauses run to several paragraphs; without this they merge into one block */
.notice__body .entry__body p + p {
  margin-top: var(--s-2);
}
/* the notice's section name sits above its clause rather than in the gutter,
   so it needs the body's own offset and a little air under it */
.notice__body .entry__section {
  margin-bottom: var(--s-2);
}
/* a line that introduces a list — never justified, it is a single short line */
.notice__lead {
  margin-top: var(--s-3);
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}
.notice__list {
  list-style: none;
  margin: var(--s-2) 0 0;
  padding: 0;
  counter-reset: notice-item;
}
.notice__list li {
  counter-increment: notice-item;
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;   /* number, then text */
  align-items: baseline;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink);
  padding-bottom: var(--s-2);
}
.notice__list li::before {
  content: counter(notice-item, decimal-leading-zero);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

@media (max-width: 800px) {
  .notice__updated {
    padding-left: 0;                /* no nav to align to once stacked */
  }
}

/* the seven-angle contact sheet under entry 01 */
.entry__sheet {
  margin: var(--s-4) 0 0;
}
.entry__sheet img {
  width: 100%;
  height: auto;
  filter: grayscale(100%);       /* strictly black & white */
}

@media (max-width: 800px) {
  .entry {
    grid-template-columns: 2.5rem 1fr;
    column-gap: var(--s-2);
  }
  .entry__body {
    padding-left: 0;               /* no nav to align to when stacked */
  }
  /* the CV entries are their own grid: stack them too, so the record column
     falls under the section label instead of overlapping it. */
  .entry--cv {
    grid-template-columns: 2.5rem 1fr;
    column-gap: var(--s-2);
  }
  /* stacked, the label sits under its number in column 1, so that column has
     to hold the longest section name ("Education") rather than just "04". */
  .ethos .entry,
  .ethos .entry--cv {
    grid-template-columns: 5.5rem 1fr;
  }
  .entry--cv .scope__head {
    grid-column: 2;
    padding-left: 0;
    padding-right: 0;
  }
  .entry--cv .scope__list {
    grid-column: 2;
    padding-left: 0;               /* no nav to align to when stacked */
    padding-right: 0;
  }
  /* stacked, the record title sits directly above its own achievements, so
     the pair needs no extra clearance between them. */
  .entry--cv:has(.scope__head) .scope__list:not(.scope__list--second) {
    padding-top: var(--s-2);
  }
  .entry--cv .scope__list--second {
    padding-top: var(--s-2);
  }
  /* the number column is 2.5rem stacked — too narrow to hold the number and
     the section name side by side, so the name drops under its number rather
     than spilling into the record column beside it. */
  .ethos .entry__index {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .ethos .entry__section {
    max-width: none;
  }
}

/* ====================================================================== */
/*  CONNECT                                                                 */
/* ====================================================================== */

/* Contact is a spec sheet, not a display line: the CONTACT title carries the
   page and the details sit as label/value rows on the shared grid. */
/* same first-content line as every other page — not a viewport-relative
   clamp, which drifted with window height. */
.connect {
  padding-top: var(--s-5);
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  padding-bottom: 38vh;            /* extra air below so footer needs a scroll */
}

/* the rows begin on the 57.3% INDEX line, nudged by the same optical offset
   the project-details column uses, so labels align under "Index". */
.connect__col {
  grid-column: 2;
  padding-left: var(--nav-start);
}
/* contact rows sit tighter than the experiment spec sheet — the values are
   one line each, so the generous meta-row padding reads as drift here. */
.connect__col .meta-row {
  padding-block: 0;
  line-height: 1.55;
}

@media (max-width: 800px) {
  .connect {
    grid-template-columns: 1fr;
  }
  .connect__col {
    grid-column: 1;
    padding-left: 0;
  }
}

/* ====================================================================== */
/*  COLOPHON — spec sheet (label -> value), aligned to the nav grid        */
/* ====================================================================== */

/* padding, not margin: a margin collapses against the page-head and pulls the
   first row above the shared --first-line, so the type jumps when you switch
   pages. Same reason .ethos uses padding-top. */
.colophon {
  padding-top: var(--s-5);
  display: flex;
  flex-direction: column;
}
.spec {
  display: grid;
  /* label in left column; value starts at the INDEX column (57.3%) */
  grid-template-columns: var(--col-1) 1fr;
  align-items: baseline;
  padding-block: var(--s-1);
}
/* the first row's top padding would push it below the shared --first-line */
.spec:first-child {
  padding-top: 0;
}
/* sentence case, no letterspacing — the same voice as the nav and the index,
   so the colophon's first column reads like every other page's */
.spec__label {
  font-size: var(--t-small);          /* match the nav */
  font-weight: var(--w-regular);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}
.spec__value {
  font-size: var(--t-small);
  color: var(--ink);
  max-width: 40ch;
}

@media (max-width: 800px) {
  /* (10) label left, value right on the same line, with tactful spacing */
  .spec {
    grid-template-columns: auto auto;
    justify-content: space-between;
    gap: var(--s-3);
    padding-block: var(--s-2);
  }
  .spec__value {
    text-align: right;
  }
}

/* ====================================================================== */
/*  PROJECT DETAILS — big statement + black box / meta table              */
/* ====================================================================== */

/* Legacy big statement — still used by experiment-03 and -lyrics, which have
   not been moved onto the A1 case-head pattern yet. */
.case-statement {
  font-size: clamp(1.5rem, 3.8vw, 3.5rem);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1.05;
  text-transform: uppercase;
  padding-block: var(--s-6) var(--s-6);
  margin: 0;
}

/* Case head — the ledger code (A1) carries the page in the wordmark column,
   with the thesis set as running text on the INDEX line beside it. The code
   is the title; the sentence explains it. */
.case-head {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: start;
  padding-block: var(--s-5) var(--s-6);
}

.case-head__code {
  font-size: var(--t-h1);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  margin: 0;
}

.case-head__thesis {
  /* same optical nudge as the details column so the sentence starts on the
     "I" of Index rather than the raw 57.3% grid line */
  padding-left: var(--nav-start);
  font-size: var(--t-small);
  font-weight: var(--w-regular);
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  text-align: justify;
  -webkit-hyphens: auto;
  hyphens: auto;
  margin: 0;
}

/* code-only head (experiment-01): with no thesis beside it, the code moves off
   the wordmark column and onto the INDEX line, like every other aligned block.
   The empty first track holds the 57.3% offset; --nav-start clears the same
   optical gap the details column used, so the "A" starts on the "I" of Index. */
.case-head--code-only .case-head__code {
  grid-column: 2;
  padding-left: var(--nav-start);
}
/* Going Index → A1 must not make the prose jump. On work.html the first body
   line of a scope sits --s-5 + label + --s-2 below the page title; here the
   code IS the title and there is no label row, so the head→body gap must add
   up to that same distance. .case-body already contributes --s-7 of top
   padding (shared with experiment-03 / -lyrics, so it is not touched); this
   pulls the head back by the difference so §01 lands on the Corpus line. */
.case-head--code-only {
  padding-bottom: 0;
}
/* experiment-lyrics keeps its launch panel after the strip — it is the only
   route into the interactive graph. Same two-track grid as the head, so the
   panel starts on the INDEX line under the code rather than in the wordmark
   column where the old .proj-details plate sat. */
.case-launch {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: start;
}
.case-launch .launch {
  grid-column: 2;
  width: auto;                /* the track sets the width now, not calc(100% - 2rem) */
  margin-left: var(--nav-start);
}
/* the head's zero bottom padding is right when prose follows, but the plate
   needs air off the code — restore it only in that case. */
.case-head--code-only:has(+ .case-launch) {
  padding-bottom: var(--s-4);
}
/* the body below carries its own --s-7 top padding, so the panel only needs
   enough to clear its closing rule — --s-6 here stacked into a dead band. */
.case-head--code-only + .case-launch {
  padding-bottom: var(--s-3);
}

@media (max-width: 800px) {
  .case-launch {
    grid-template-columns: 1fr;
  }
  .case-launch .launch {
    grid-column: 1;
    margin-left: 0;
  }
}

/* A3 puts the launch panel between head and body. The body's default --s-7
   was tuned to open a page, not to follow a block that already closed itself
   with a rule — stacked on the panel's own padding it read as a dead band, so
   the panel case takes the smaller step. */
.case-launch + .case-body {
  padding-top: var(--s-5);
}

/* A3 has the launch panel between head and body, so the body is no longer an
   adjacent sibling of the head — no Corpus-line correction applies there. */
.case-head--code-only + .case-body {
  /* work.html spends --s-5, then the scope label (one --t-small line at the
     .scope__desc leading, plus its --s-2 bottom margin), then --s-2 again
     before the body. Rebuild that sum from the same tokens rather than a
     measured pixel value, so it tracks any change to the scale. */
  padding-top: calc(
    var(--s-5) + (var(--t-small) * 1.6) + var(--s-2) + var(--s-2) + 0.3rem
  );
}

@media (max-width: 800px) {
  .case-head {
    grid-template-columns: 1fr;
    row-gap: var(--s-3);
    padding-block: var(--s-4) var(--s-5);
  }
  .case-head__thesis {
    padding-left: 0;
  }
  /* no nav to align to once stacked */
  .case-head--code-only .case-head__code {
    grid-column: 1;
    padding-left: 0;
  }
}

/* black box (left) + meta details (right).
   the details column starts at 57.3% — the same INDEX line the rest of
   the site aligns to (footer, .entry, .spec all use this). */
.proj-details {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  padding-block: 0 var(--s-7);
  align-items: start;   /* the plate is its own block, not a full-height fill */
}
/* the nav text sits inside a bracket cell with 0.4rem left padding + 1px
   border, so the 57.3% grid line lands left of the visible "I". nudge the
   details column right by that offset so labels align to the I of INDEX
   (and values to the I of INFO). */
.proj-details__col {
  padding-left: var(--nav-start);
}
/* experiment-03: black composite box removed — keep the details column pinned
   to the second track (its left edge on the 57.3% INDEX line) instead of
   letting it slide into the empty first column. */
.proj-details--no-box .proj-details__col {
  grid-column: 2;
}

/* black placeholder box — right edge lands on the right return of the
   wordmark bracket (the wordmark cell ends ~3.5rem short of the 57.3% INDEX
   line, accounting for the gap before INDEX), and stretches to the full
   height of the project details via align-items: stretch. */
.proj-details__box {
  width: calc(100% - 2rem);
  aspect-ratio: 4 / 3;               /* a plate, sized in its own right */
  background: var(--ink);
  margin: 0;
  overflow: hidden;
  position: relative;                /* anchor for the fill canvas */
}
.proj-details__box img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(100%);           /* strictly black & white */
  display: block;
}
/* experiment-03: the woven composite sizes itself, not to the 4:3 plate */
.proj-details__box:has(#ms-hero) {
  aspect-ratio: auto;
  min-height: 320px;
}
.proj-details__box #ms-hero {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.proj-details__eyebrow {
  font-size: var(--t-micro);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: var(--s-1);
}

/* LAUNCH — the door into the interactive graph. Deliberately NOT a
   .proj-details__box: that class paints a black 4:3 plate, and wearing it here
   meant the panel was one load-order slip away from turning black again. This
   is its own thing — bare prose on paper, like the rest of the site. */
.launch {
  margin: 0;
  /* mirror .entry__body exactly: 46ch resolved at the inherited 1rem, NOT at
     the paragraph's --t-small — sizing the box down first shrinks the ch and
     the measure lands ~50px narrow of the body column. */
  font-size: var(--t-body);
  max-width: 46ch;
}
/* the lede is body prose: same size, leading, and colour as .entry__body p, so
   the door into the graph reads as part of the report rather than as a
   callout. It stays ragged — it is a single short line, and the site never
   stretches those (cf. .entry__action). */
.launch__lede {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  margin: 0 0 var(--s-3);
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}
/* the link carries no box; the accent is interaction-only, per the tokens */
.launch__btn {
  display: inline-block;
  /* the standing line is one unit of meaning: when the column is too narrow
     to hold it, balance the break instead of orphaning the last word of the
     client name onto a line of its own. A nbsp in the markup keeps the
     company name itself atomic. */
  text-wrap: balance;
  font-size: var(--t-small);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  border: 0;
  padding: 0;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
/* Only an ACTUAL link reddens. The slot is a <p> on the case pages, so the
   bare :hover below would tint the whole line — and a nested <a> inherits
   that colour, making the static text look interactive. Scoped to a[...] so
   a plain line never reacts, and the rule beneath it handles a link sitting
   inside the line. */
a.launch__btn:hover,
a.launch__btn:focus-visible {
  background: none;
  color: var(--accent);
  outline: none;
}
.launch__btn a:hover,
.launch__btn a:focus-visible {
  color: var(--accent);
  outline: none;
}
/* right column — label/value table, no rules, all black. The column starts
   on the Index line, so stepping one --nav-item lands the value under
   "About", exactly like the index rows. */
.meta-row {
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;
  gap: 0;
  padding-block: var(--s-1);
}
.meta-row__label {
  font-size: var(--t-small);
  color: var(--ink);
}
.meta-row__value {
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink);
}
.meta-row__value p {
  margin: 0;
}
.meta-row__value p + p {
  margin-top: 0.35em;
}

@media (max-width: 800px) {
  .proj-details {
    grid-template-columns: 1fr;
    row-gap: var(--s-5);
  }
}

/* ---------------------------------------------------------------------- */
/*  CASE REPORT BODY — numbered sections (mirrors the Info .entry pattern)  */
/* ---------------------------------------------------------------------- */

/* reuses the global .entry grid (57.3% 1fr): the index sits in the far-left
   gutter aligned with the wordmark, the prose begins at the INDEX line.
   the body is nudged by the bracket-cell glyph offset so it aligns to the
   visible "I" of INDEX, matching the project details above. */
.case-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  padding-block: var(--s-7);
}
/* .entry__body already carries the --nav-start offset globally; the case
   body only widens the prose measure. The measure matches the About page's
   experience lines (.entry--cv .scope__list): fill the column and stop 7ch
   short of the right gutter, so the justified edge lands just before the "N"
   of Notice. A fixed ch cap is not used — it would hold the prose narrower
   than the CV lines it sits alongside. */
.case-body .entry__body > p,
.case-body .entry__body > .fig-table {
  max-width: none;
}
.case-body .entry__body {
  max-width: none;                 /* beat the global .entry__body 46ch cap */
  padding-right: 7ch;
}

/* ── TLDR ─────────────────────────────────────────────────────────────────
   A scannable precis of the seven sections, set above the case body. Same
   label/value idiom as .meta-row: the section title sits on the Index line
   and its one-line summary steps one --nav-item to land under "About". No
   rules, no boxes, no grey — the alignment carries the structure, exactly as
   the nav does. Labels are shortened to clear --nav-item ("Learning", not
   "Key learning and what I would do differently"); the full heading still
   names each section below. */
.tldr {
  display: grid;
  grid-template-columns: var(--col-1) 1fr;
  align-items: start;
  padding-bottom: 0;
}
/* The summary sits close under the code — it reads as the code's own caption
   rather than as a separate block, so it takes a tighter offset than the
   first-content-row rule .case-body would otherwise carry here. */
.case-head--code-only + .tldr {
  padding-top: var(--s-4);
}
/* .case-body carries its own --s-7 of top padding, which is the parting
   between the summary and the report proper. The summary has already opened
   the page, so that full measure reads as a hole — step it down one. */
.tldr + .case-body {
  padding-top: var(--s-6);
}
.tldr__body {
  grid-column: 2;                   /* the prose column, starting on Index */
  padding-left: var(--nav-start);   /* start on the "I" of Index */
  padding-right: 7ch;               /* same measure as the prose below */
}
/* label on the Index line, its line stepping one --nav-item to land under
   "About" — the same idiom as .meta-row and the index ledger rows. */
.tldr__row {
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;
  align-items: baseline;
  padding-block: var(--s-1);
}
/* A1 carries four lines, not the eight or nine the other case pages use:
   they are one metadata block rather than a set of sections, so they close
   up to a single run of leading — the same reason consecutive .scope__desc
   paragraphs are set tight on the index. */
.tldr--tight .tldr__row {
  padding-block: 0;
}
.tldr__label,
.tldr__line {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  margin: 0;
}
/* the summary lines are single statements, never stretched — the same reason
   .entry__action exists. ragged right, like every other label column. */
.tldr__line {
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}

/* Between the stacked breakpoint and ~1000px the prose column is too narrow
   to hold a summary line beside its label without wrapping — and a wrapped
   scan line defeats the point. Drop the label above its line there, the same
   arrangement the stacked layout uses. */
@media (max-width: 1000px) {
  .tldr__row {
    grid-template-columns: 1fr;
    padding-block: var(--s-2);
  }
  /* stacked, the label sits above its line, so the rows still need parting
     to stay legible as pairs — but less than the full-section measure. */
  .tldr--tight .tldr__row {
    padding-block: var(--s-1);
  }
}

/* left column: index at top, then the enlarged figure in the gutter below it */
.entry__aside {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  /* keep figures clear of the prose that begins at the INDEX line */
  padding-right: var(--s-5);
}

/* number + section header on one line in the left gutter; header lowercase */
.entry__index {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: var(--s-2);
  padding-top: 0.15rem;        /* optically align with the first text line */
}

/* figure index: title + (+) per row, stacked one over the other.
   the graph itself only appears enlarged in the gutter when (+) is clicked. */
.fig-list {
  list-style: none;
  margin: var(--s-4) 0 0;
  padding: 0;
}
.fig-list__item {
  /* label, then (+) aligned to the CONNECT line (the "C" of CONNECT in the
     nav). measured: the (+) starts at ~96.3% of the body span. */
  display: grid;
  grid-template-columns: 96.3% 1fr;
  align-items: baseline;
  padding-block: var(--s-1);
}
.fig-list__label {
  font-size: var(--t-small);     /* same as the prose */
  line-height: 1.45;             /* match the body prose */
  color: var(--ink);
}

.entry__num {
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.entry__section {
  font-size: var(--t-small);
  color: var(--ink);
}

.case-body .entry__body p {
  font-size: var(--t-small);     /* match the gutter index/figure label size */
  line-height: 1.45;             /* match the info page body */
  margin-bottom: var(--s-2);
}
/* Register headword opening a paragraph ("Anchor (Parent One)"). Italic alone
   marks it — no weight change and no colour, the same way the nav marks the
   current page by a single property. */
.case-body .entry__body .ms-reg {
  font-style: italic;
  font-weight: inherit;
  color: var(--ink);
}
.case-body .entry__body p:last-child {
  margin-bottom: 0;
}
/* A metrics ledger set inside case prose. .fig-table carries no outer margin
   of its own (it is normally held inside a .fig plate), so the surrounding
   paragraph rhythm has to be restated here — prose margin-bottom alone leaves
   the following paragraph butted against the closing rule. */
.case-body .entry__body .fig-table {
  margin: var(--s-3) 0;
}
.case-body .entry__body .fig-table:last-child {
  margin-bottom: 0;
}
.case-body .entry__body p strong {
  font-weight: var(--w-bold);
}

/* ── per-section technical reveal ─────────────────────────────────────────
   a quiet [ Technical ] door beneath each section's prose; opens a
   hairline-ruled block with model specs, exact stats, and plate figures. */
.technical {
  margin-top: var(--s-4);
  border-top: 1px solid var(--rule);
}
.technical__toggle {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding-top: var(--s-2);
  font-size: var(--t-micro);
  font-weight: var(--w-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
  user-select: none;
}
.technical__toggle::-webkit-details-marker { display: none; }
.technical__toggle::before {
  content: "+";
  display: inline-block;
  width: 1ch;
  font-weight: var(--w-regular);
  color: var(--ink-mute);
}
.technical[open] > .technical__toggle::before { content: "–"; }
.technical__toggle:hover { color: var(--B81411); }
.technical__body {
  padding-top: var(--s-3);
}
.technical__lead {
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--ink-mute);
  margin: 0 0 var(--s-3);
}

/* compact label/value spec list inside the technical block */
.spec-list {
  margin: 0;
  border-top: 1px solid var(--rule);
}
.spec-list__row {
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;
  gap: 0;
  padding-block: var(--s-1);
  border-bottom: 1px solid var(--rule);
}
.spec-list dt {
  font-size: var(--t-micro);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-mute);
}
.spec-list dd {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink);
}

/* eyebrow row for the enlarged figure built in the gutter (label + close) */
.plate__eyebrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  font-size: var(--t-small);     /* same size as the prose */
  font-weight: var(--w-regular); /* unbolded, like the prose */
  color: var(--ink);
  margin: 0 0 var(--s-2);
}
/* the + / – control that surfaces a figure enlarged in the gutter */
.plate__zoom {
  flex: none;
  appearance: none;
  background: none;
  border: 0;
  padding: 0 0.25rem;
  font: inherit;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}
.plate__zoom:hover,
.plate__zoom[aria-expanded="true"] {
  color: var(--B81411);
}

/* enlarged figure surfaced in the section gutter (the left white space).
   It is taken OUT OF FLOW: the gutter is empty space the figure fills, so
   opening one must not grow the aside and shove every later section down the
   page. The aside is the positioning context; the figure spans its full width
   (minus the padding-right that keeps it clear of the prose) and is free to
   run past the aside's short index line into the white space below. */
.entry__aside {
  position: relative;
}
.entry__zoom {
  position: absolute;
  /* below .entry__index (one --t-small line, ~23px) plus the aside's own gap.
     Out of flow, the flex `gap` no longer separates them, so it is restated. */
  top: calc(1.5rem + var(--s-4));
  left: 0;
  right: var(--s-5);       /* mirror .entry__aside's padding-right */
}
/* §05: the gutter figure lines up with the "Live model" row that opens it.
   The row sits one prose block + the list's own margin below the body top,
   while the gutter starts under .entry__index — so the figure is nudged up by
   the difference. Keyed to the section, not a global change. */
.entry--model .entry__zoom {
  top: calc(1.5rem + var(--s-4) - 1.28rem);
}
.entry__zoom:empty {
  display: none;
}
/* §02 Methods holds the tallest plate on the page — Fig. 02, the decision tree.
   Opened, it runs ~40px past where §03 Findings begins. Rather than shrink the
   plate, the Methods row reserves the extra height up front: §03 simply starts
   lower and NOTHING moves when a figure is opened or closed.

   The reservation is a vertical length, so it CANNOT be derived from --col-1
   (a percentage — a percentage min-height resolves against the parent's
   HEIGHT, not the gutter's width, and silently collapses). It is stated
   directly, in the same aspect terms the plate is measured in: gutter width
   0.573vw-ish x 1143/1956. clamp() keeps it sane at the extremes. */
.entry--methods .entry__aside {
  min-height: clamp(0px, calc((100vw * 0.573 - var(--s-5)) * 0.584 + var(--s-4)), 40rem);
}
.entry__zoom-fig {
  margin: 0;
}
.entry__zoom-fig img {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--rule);
}

/* ── CSS figures (the matplotlib plates, rebuilt as live HTML) ───────────────
   Clinical voice: white ground, near-black ink, hairline rules, tabular
   numerals, Helvetica. No color. Each lives hidden in the body and is cloned
   into the gutter when its (+) is pressed. */
.fig {
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--ink);
}
.fig[hidden] { display: none; }

/* shared row scaffold: label · track · value, baseline-aligned */
.fig__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.35rem;
  padding-block: 0.4rem;
}
.fig__row + .fig__row {
  border-top: 1px solid var(--rule);
}

/* ── bar figures (Fig. 01 importance, Fig. 03 accuracy) ── */
.fig-bar__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.3rem;
  padding-block: 0.45rem;
}
.fig-bar__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}
.fig-bar__label {
  font-size: var(--t-micro);
  color: var(--ink);
}
.fig-bar__value {
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.fig-bar__track {
  position: relative;
  height: 0.5rem;
  background: none;
}
.fig-bar__fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--ink);
}
/* a marker line on the track (e.g. the 66% baseline on the accuracy chart) */
.fig-bar__mark {
  position: absolute;
  top: -0.25rem;
  bottom: -0.25rem;
  width: 1px;
  background: var(--ink);
}
.fig-bar__mark::after {
  content: attr(data-label);
  position: absolute;
  left: 0.3rem;
  top: -0.9rem;
  font-size: 0.5625rem;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}
/* a confidence interval whisker drawn as a hairline just above the bar */
.fig-bar__ci {
  position: absolute;
  top: -0.3rem;
  height: 1px;
  background: var(--ink);
}
.fig-bar__ci::before,
.fig-bar__ci::after {
  content: "";
  position: absolute;
  top: -0.16rem;
  height: 0.34rem;
  width: 1px;
  background: var(--ink);
}
.fig-bar__ci::before { left: 0; }
.fig-bar__ci::after { right: 0; }

/* ── table figures (Fig. 04 recovery by tier, Fig. 05 hypotheses) ── */
.fig-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
}
.fig-table th,
.fig-table td {
  text-align: right;
  padding: 0.4rem 0 0.4rem 0.75rem;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}
.fig-table th:first-child,
.fig-table td:first-child {
  text-align: left;
  padding-left: 0;
  white-space: normal;
}
.fig-table thead th {
  font-size: 0.5625rem;
  font-weight: var(--w-regular);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
}
.fig-table tbody tr:last-child td {
  border-bottom: 1px solid var(--ink);
}
.fig-table td.is-mute { color: var(--ink); }
.fig-table .fig-pass { letter-spacing: 0.04em; }

/* small caption under a figure (units / read-this-as) */
.fig__note {
  margin-top: var(--s-2);
  font-size: 0.5625rem;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ── §05 live model: draggable condition cards → predicted tier ──────────────
   Clinical voice. No boxes: conditions are ruled rows like the figure tables,
   the tray is an open well under a single rule, and the verdict reads as a
   meta-row in ink. The accent is reserved for interaction (hover / focus /
   active drop), never as decoration. */
/* the live model is collapsed behind a figure (+) toggle and surfaced in the
   gutter, like the other figures; the source block stays hidden until opened */
.predictor[hidden] { display: none; }

.predictor {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-4) var(--s-5);
  /* no inset: the left column starts on the figure's own left edge, so every
     label lines up with the "Live model" title above it. */
  padding: 0;
}
.predictor__eyebrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  margin: 0 0 var(--s-2);
}
.predictor__count {
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-micro);
}

/* the two card columns */
.predictor__cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* a single condition card */
.pcard {
  border-bottom: 0;
  padding: 0.4rem 0;
  font-size: var(--t-micro);
  line-height: 1.4;
  cursor: grab;
  user-select: none;
  display: block;
  background: none;
  transition: color var(--dur-fast) var(--ease);
}
.pcard:hover { color: var(--accent); }
.pcard:focus-visible {
  outline: none;
  color: var(--accent);
}
.pcard:active { cursor: grabbing; }
.pcard.is-dragging { opacity: 0.4; }
.pcard.is-used { opacity: 0.3; pointer-events: none; }
.pcard__title {
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--ink);
}
.pcard__note {
  font-size: var(--t-micro);
  line-height: 1.3;
  color: var(--ink-mute);
}

/* the tray well that receives dropped cards */
.predictor__tray {
  display: flex;
  flex-direction: column;
}
.predictor__cards--tray {
  min-height: 7rem;
  border-top: 0;
  padding: 0.5rem 0 0;
  gap: 0;
  transition: border-color var(--dur-fast) var(--ease);
}
.predictor__cards--tray.is-over {
  /* drop feedback without a rule: the accent is interaction-only */
  color: var(--accent);
}
.predictor__cards--tray:empty::before {
  content: "";
}
/* a card sitting in the tray gets a removable affordance */
.predictor__cards--tray .pcard {
  cursor: pointer;
}
.predictor__cards--tray .pcard::after {
  content: "×";
  position: absolute;
  top: 0.4rem;
  right: 0;
  font-size: var(--t-micro);
  line-height: 1;
  color: var(--ink-mute);
}
.predictor__cards--tray .pcard {
  position: relative;
  padding-right: 1.6rem;
}
.predictor__cards--tray .pcard:hover::after { color: var(--accent); }
.predictor__hint {
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--ink-mute);
  margin: var(--s-1) 0 0;
}
.predictor__hint[hidden] { display: none; }

/* verdict — spans the full width under the two columns */
/* the verdict sits in the tray's own column, so "Predicted tier" lines up
   vertically with "Today" rather than spanning the whole figure. */
.predictor__verdict {
  grid-column: 2;
  border-top: 0;
  padding-top: var(--s-3);
  margin-top: var(--s-1);
}
/* label + value share one baseline, as in .fig-table's rows */
.predictor__verdict .predictor__eyebrow {
  margin-bottom: 0;
  padding-bottom: 0.4rem;
  border-bottom: 0;
}
/* the verdict line reads "Predicted tier: best" — label and value together on
   the left, so it does not span the figure like the other eyebrows. */
.predictor__eyebrow--verdict {
  justify-content: flex-start;
  gap: 0.35em;
}
.predictor__tier {
  font-size: var(--t-micro);
  font-weight: var(--w-regular);
  letter-spacing: 0;
  /* tier values are lowercase data (best/middle/worst); the empty-state prompt
     is a sentence, so the casing comes from the text itself. */
  text-transform: none;
  color: var(--ink);
  margin: 0;
}
.predictor__because {
  font-size: var(--t-micro);
  line-height: 1.45;
  color: var(--ink);
  max-width: 52ch;
  margin: var(--s-1) 0 var(--s-3);
}
/* the default morning carries no reason line — collapse it so the reset
   control doesn't float on reserved empty space. */
.predictor__because:empty { display: none; }

/* ====================================================================== */
/*  RESPONSIVE                                                              */
/* ====================================================================== */

@media (max-width: 800px) {
  /* case-report sections: stack aside (index + figures) above the prose,
     each full width, instead of the cramped 2.5rem gutter the Info page uses */
  .case-body .entry {
    display: block;
  }
  .case-body .entry__aside {
    padding-right: 0;
    margin-bottom: var(--s-3);
  }
  /* stacked: there is no gutter to float into, so the figure returns to normal
     flow above the prose. Absolute here would overlap the text. */
  .case-body .entry__zoom,
  .case-body .entry--model .entry__zoom {
    position: static;
    top: auto;
    right: auto;
    margin-top: var(--s-3);
  }
  .case-body .entry__body {
    padding-left: 0;
    padding-right: 0;              /* no nav to align to once stacked */
    max-width: none;
  }
  /* one column once stacked — the 57.3% track would hold the summary to the
     left half of a narrow screen. */
  .tldr {
    grid-template-columns: 1fr;
  }
  .tldr__body {
    grid-column: 1;
    padding-left: 0;               /* no nav to align to once stacked */
    padding-right: 0;
  }
  /* label above its line rather than beside it — there is no About column */
  .tldr__row {
    grid-template-columns: 1fr;
    padding-block: var(--s-2);
  }
  .tldr--tight .tldr__row {
    padding-block: var(--s-1);
  }
  .stages {
    grid-template-columns: 1fr;
  }
  .stage {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    padding-inline: 0;
  }
  .stage:last-child {
    border-bottom: 0;
  }
  .timeline {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--s-1);
  }
  .predictor {
    grid-template-columns: 1fr;
    gap: var(--s-4);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   experiment 03 · Many Selves — three retained figures
   Clinical: white paper, black marks, single accent on hover/focus only.
   ───────────────────────────────────────────────────────────────────────── */
.ms-fig { margin-block: var(--s-4) var(--s-2); }
/* figures surfaced into the .entry__aside gutter, below the section index.
   the aside is the wide 57.3% column, so they get full room; the matrix is
   table-layout:fixed at width:100%, so it fits that column without scrolling. */
.ms-fig--gutter { margin-block: var(--s-3) 0; }
.ms-fig--gutter canvas { max-width: 100%; }
.ms-fig__cap {
  font-size: var(--t-micro); letter-spacing: var(--tracking-wider);
  text-transform: uppercase; color: var(--ink-mute); margin: 0 0 var(--s-2);
}
/* Fig. title styled to match experiment-01's fig-list labels (Title Case,
   prose size) rather than the uppercase micro caption. */
.ms-fig__title { margin: 0 0 var(--s-2); }
/* explanatory note under a figure — quiet prose, matches .ms-mtx-notes voice */
.ms-fig__note {
  font-size: var(--t-micro); line-height: 1.5; color: var(--ink-mute);
  letter-spacing: 0; text-transform: none; margin: var(--s-2) 0 var(--s-2);
}
.ms-canvas-wrap { border-bottom: 1px solid var(--rule-strong); padding-block: var(--s-2); }
#ms-pixels, #ms-recipe { display: block; width: 100%; }

/* ── Fig. 01 share bars — same component as A1's Fig. 01, closed by one rule.
   Non-leading rows recede; the quarter's leading self is the only full ink. ── */
.ms-share {
  border-bottom: 1px solid var(--rule-strong);
  padding-block: var(--s-2);
}
.ms-share .fig-bar__row + .fig-bar__row { border-top: 1px solid var(--rule); }
.ms-share .fig-bar__label,
.ms-share .fig-bar__value { color: var(--ink-mute); }
.ms-share .fig-bar__fill { background: #b4b4b4; }
/* the track is a true 0–100% field: show its full extent so a 10% bar reads as
   a tenth of the whole rather than just a short mark floating on white. */
.ms-share .fig-bar__track { background: var(--rule); }
.ms-share .fig-bar__row.is-dom .fig-bar__label,
.ms-share .fig-bar__row.is-dom .fig-bar__value { color: var(--ink); }
.ms-share .fig-bar__row.is-dom .fig-bar__fill { background: var(--ink); }

/* ── Fig. 02 time scrubber — flat, typographic, no chrome ── */
.ms-scrub { display: flex; align-items: center; gap: 1.1rem; margin-bottom: var(--s-2); }
/* flat glyph button, keyed to the same language as .predictor__reset: no
   circle, no fill — the ▶ / ❚❚ glyph in ink, accent on hover and while playing */
.ms-scrub__play {
  flex: none; appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0; font: inherit;
  font-size: 12px; line-height: 1; color: var(--ink); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.2em;                                   /* stop ▶↔❚❚ width jitter */
  transition: color var(--dur-fast) var(--ease);
}
.ms-scrub__play:hover, .ms-scrub__play:focus-visible { color: var(--accent); outline: none; }
.ms-scrub__play.playing { color: var(--accent); }
/* the track wraps the range + the glyph overlay; it grows to fill the row */
.ms-scrub__track { flex: 1; position: relative; display: flex; align-items: center; }
.ms-scrub__slider {
  flex: 1; appearance: none; -webkit-appearance: none; height: 1px;
  background: var(--rule-strong); outline: none; margin: 0; cursor: pointer;
}
/* native thumb kept as an invisible hit area — the visible mark is the glyph
   canvas drawn on top (ms-scrub__thumb-icon), which changes to the
   correspondent surfaced highest at the current quarter */
.ms-scrub__slider::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none; width: 14px; height: 14px;
  background: transparent; border: 0; border-radius: 0; cursor: pointer;
}
.ms-scrub__slider::-moz-range-thumb {
  width: 14px; height: 14px; background: transparent;
  border: 0; border-radius: 0; cursor: pointer;
}
/* glyph overlay: centered on the thumb, x set from JS via --thumb-x */
.ms-scrub__thumb-icon {
  position: absolute; top: 50%; left: var(--thumb-x, 0px);
  width: 14px; height: 14px; transform: translate(-50%, -50%);
  pointer-events: none;
}
/* seat the first line (the quarter) on the play/slider centerline instead of
   centering the whole two-line block, which rode slightly high */
.ms-scrub__when { flex: none; min-width: 122px; text-align: right; align-self: flex-start; }
.ms-scrub__when b {
  font-size: var(--t-micro); font-weight: var(--w-regular); color: var(--ink);
  letter-spacing: var(--tracking-wider); text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.ms-scrub__when span { display: block; font-size: var(--t-micro); color: var(--ink); letter-spacing: var(--tracking-wide); font-variant-numeric: tabular-nums; margin-top: 2px; }
.ms-key--glyph.is-dom { color: var(--ink); font-weight: var(--w-medium); }

/* shared legend of keyed marks — two rows of three, columns evenly spanning
   the full width of the visualization above. Small all-caps labels. */
.ms-legend {
  display: grid; grid-template-columns: repeat(3, max-content);
  gap: 0.3rem 2.5rem; margin-top: var(--s-2);
}
.ms-key {
  appearance: none; border: 0; background: none; font: inherit;
  font-size: var(--t-micro); color: var(--ink); cursor: default;
  text-transform: uppercase; font-variant-numeric: tabular-nums;
  display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.15rem 0;
  white-space: nowrap; justify-self: start;
  transition: color var(--dur-fast) var(--ease);
}
.ms-key:hover, .ms-key:focus-visible { color: var(--accent); outline: none; }
.ms-sw { width: 11px; height: 11px; display: inline-block; }
.ms-key--glyph canvas { display: block; }

/* ── spectrum matrix ── */
.ms-mtx-wrap { padding-bottom: 0.4rem; }
table.ms-mtx { border-collapse: collapse; font-size: var(--t-micro); border: 0; width: 100%; table-layout: fixed; }
/* Cells are ruled, not boxed: 156 full boxes read as a grid of containers and
   drown the marks. Horizontal rules only — the pies supply the column rhythm,
   in the same language as .fig-table. */
table.ms-mtx th, table.ms-mtx td { padding: 0; text-align: center; border: 0; border-bottom: 1px solid var(--rule); }
table.ms-mtx .ms-rowlab {
  text-align: left; padding: 0 0.9rem 0 0.6rem; color: var(--ink); white-space: nowrap;
  font-size: var(--t-micro);
}
table.ms-mtx .ms-rowlab sup { color: var(--ink-mute); font-weight: var(--w-regular); font-size: 0.72em; margin-left: 2px; vertical-align: top; }
/* column 1 width is set on the AXIS corner so table-layout:fixed respects it */
table.ms-mtx thead .ms-corner { width: 11.5rem; border: 0; border-bottom: 1px solid var(--ink); vertical-align: bottom; }
table.ms-mtx thead .ms-corner span { display: block; text-align: left; padding: 0.5rem 0.6rem; font-size: var(--t-micro); letter-spacing: 0; text-transform: none; color: var(--ink); }
/* horizontal column headers — name only, no glyph, no rotation */
table.ms-mtx thead .ms-colhead { vertical-align: bottom; border: 0; border-bottom: 1px solid var(--ink); }
table.ms-mtx thead .ms-colhead .ms-colhead__stack {
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  gap: 0; width: 100%; padding: 0.35rem 0.15rem 0.4rem; min-width: 0;
}
table.ms-mtx thead .ms-colhead .ms-colhead__stack canvas { display: block; margin: 0 auto; }
/* text-indent offsets the trailing letter-spacing so the label optically
   centers under the glyph */
table.ms-mtx thead .ms-colhead .ms-colhead__label { position: relative; display: block; text-align: center; font-size: var(--t-micro); color: var(--ink); font-weight: var(--w-medium); letter-spacing: 0; text-indent: 0; text-transform: none; white-space: nowrap; }
/* Row hover reads in the same language as column focus: spotlight the row,
   recede the rest. The accent is interaction-only but is spent on the scrubber
   and the legend; inside the matrix the emphasis is carried by contrast alone,
   so a row and a column highlight the same way. Guarded by :has so the dimming
   only engages while a data row is actually hovered. */
table.ms-mtx tbody:has(tr:not(.ms-grouprow):hover) tr:not(:hover):not(.ms-grouprow) {
  opacity: 0.26;
  transition: opacity var(--dur-fast) var(--ease);
}
table.ms-mtx tbody tr:not(.ms-grouprow):hover td { background: #fafafa; }
table.ms-mtx tbody tr:hover .ms-rowlab { color: var(--ink); }
/* group rows are set by their name alone, no box and no caps — the same way
   the index marks a scope by its letter rather than by uppercasing it */
table.ms-mtx .ms-grouprow td { padding: 0.9rem 0 0.3rem 0.7rem; text-align: left; font-size: var(--t-micro); letter-spacing: 0; text-transform: none; color: var(--ink); background: #fff; border: 0; border-bottom: 1px solid var(--ink); }
/* cell hugs the pie so the pie sits dead-center in the td; the rank badge
   overflows to the corner without stealing centering space. vertical margin
   gives the pie air so it doesn't touch the row rules. */
.ms-cell { width: 20px; height: 20px; position: relative; margin: 5px auto; }
.ms-cell canvas { display: block; margin: 0 auto; }
.ms-cell .ms-tip { position: absolute; left: 50%; bottom: 100%; transform: translate(-50%, -4px); background: var(--ink); color: #fff; font-size: var(--t-micro); font-weight: var(--w-medium); padding: 3px 7px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--dur-fast); z-index: 5; }
.ms-cell:hover .ms-tip { opacity: 1; }
.ms-mtx-notes { margin-top: var(--s-2); }
.ms-mtx-notes p { font-size: var(--t-micro); color: var(--ink); line-height: 1.6; letter-spacing: 0; text-transform: none; margin: 0.3rem 0; }
.ms-mtx-notes sup { color: var(--ink); font-weight: var(--w-bold); margin-right: 5px; }
.ms-scale { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; align-items: center; margin: var(--s-2) 0; font-size: var(--t-small); color: var(--ink-mute); }
.ms-scale .ms-si { display: flex; align-items: center; gap: 0.5rem; }

/* ── Fig. 03 · rank badge, sortable headers, column focus, self panel ── */
/* Rank duplicates what the pie already encodes, on all 156 cells. Only the
   leader is worth standing marks: rank 1 stays, the rest recede to hover. */
.ms-cell .ms-rk { position: absolute; bottom: -3px; right: -7px; font-size: 8px; line-height: 1; font-weight: var(--w-medium); color: var(--ink-mute); font-variant-numeric: tabular-nums; opacity: 0; transition: opacity var(--dur-fast); }
table.ms-mtx tbody tr:hover .ms-cell .ms-rk { opacity: 1; }
.ms-cell .ms-rk.r1 { color: var(--ink); font-weight: var(--w-bold); opacity: 1; }

/* column focus: spotlight one self, dim the rest */
table.ms-mtx.focusing tbody td.dim,
table.ms-mtx.focusing thead th.dim { opacity: 0.26; transition: opacity var(--dur-fast) var(--ease); }
table.ms-mtx tbody td.col-hot { background: #fafafa; }

/* per-self summary strip (updates on column focus) */
.ms-mtx-self:not(:empty) { margin-top: var(--s-2); }
.ms-mtx-self .ms-scard { border-top: 1px solid var(--rule-strong); padding-top: var(--s-2); }
.ms-mtx-self .ms-who { display: flex; align-items: center; gap: 0.6rem; margin-bottom: var(--s-2); }
.ms-mtx-self .ms-who canvas { display: block; }
.ms-mtx-self .ms-who .ms-nm { font-size: var(--t-small); font-weight: var(--w-medium); letter-spacing: 0; }
.ms-mtx-self .ms-stat { display: flex; gap: 2rem; margin-bottom: var(--s-2); }
.ms-mtx-self .ms-stat > div { display: flex; flex-direction: column; }
.ms-mtx-self .ms-stat b { font-size: var(--t-small); font-weight: var(--w-medium); line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.ms-mtx-self .ms-stat span { font-size: var(--t-micro); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink-mute); margin-top: 4px; }
.ms-mtx-self .ms-axes { font-size: var(--t-small); color: var(--ink-mute); line-height: 1.6; margin-top: 0.4rem; }
.ms-mtx-self .ms-axes .ms-k { display: inline-block; min-width: 12ch; font-size: var(--t-micro); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ink); margin-right: 0.5rem; }
.ms-mtx-self .ms-axes .ms-pk { color: var(--ink); }

/* index · §02 woven-marks composite thumbnail — white marks on black,
   matching the other media boxes. */
.archive__media--weave { background: var(--ink); }
.archive__media--weave #ms-index-weave { width: 100%; height: 100%; display: block; }

/* Fig. 04 — no rules, headers in proper case, all ink.
   Keyed to .fig-table--plain, not #fig-recovery: the zoom handler strips the
   id when it clones the figure into the gutter, so an id selector would never
   match the copy the reader actually sees. Fig. 05 keeps the ruled language. */
.fig-table--plain th,
.fig-table--plain td,
.fig-table--plain thead th,
.fig-table--plain tbody tr:last-child td {
  border-bottom: 0;
}
.fig-table--plain thead th {
  font-size: var(--t-micro);
  letter-spacing: 0;
  text-transform: none;
}
.fig-table--plain td.is-mute { color: var(--ink); }
.fig-table--plain .fig-pass { letter-spacing: 0; }

/* ══ A4 · n = 3 — onset, matrix, trajectory ═══════════════════════════════
   Same rules as A2's figures: ruled rows, no boxes, no color carrying value.
   The accent is interaction-only. What is new here is trust: the face layer
   failed its own agreement test, so unsupported channels must read as
   unsupported at a glance — hatched fill, muted label — without a legend. */

/* ── Fig. 01 · onset. One ruled row per channel; three marks on a shared
   signed axis whose centre is the subject's own baseline. ─────────────── */
.af-onset { margin-top: var(--s-2); }
.af-onset__scale {
  display: flex; justify-content: space-between;
  font-size: var(--t-micro); color: var(--ink-mute);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  padding-left: 10.5rem; padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--ink);
}
.af-row { display: flex; align-items: center; gap: 0.6rem; border-bottom: 1px solid var(--rule); padding: 0.42rem 0; }
.af-row__label {
  flex: none; width: 10rem; font-size: var(--t-micro); color: var(--ink);
  letter-spacing: 0; text-transform: none;
}
.af-row__track { position: relative; flex: 1; height: 15px; }
/* the zero line is the subject at rest — the only standing rule in the track */
.af-row__zero { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--rule); }
.af-mark {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  font-size: 9px; line-height: 1; font-variant-numeric: tabular-nums;
}
.af-mark b {
  display: block; font-weight: var(--w-bold); color: var(--ink);
  background: var(--paper); padding: 0 2px;
}
/* a mark drawn from a window with gaps is hollow: the instrument did not see
   all of it, and the figure must not pretend otherwise */
.af-mark--gap b { color: var(--ink-mute); font-weight: var(--w-regular); text-decoration: underline dotted; }
.af-mark .ms-tip {
  position: absolute; left: 50%; bottom: 100%; transform: translate(-50%, -5px);
  background: var(--ink); color: #fff; font-size: var(--t-micro); font-weight: var(--w-medium);
  padding: 3px 7px; white-space: nowrap; opacity: 0; pointer-events: none;
  transition: opacity var(--dur-fast); z-index: 5;
}
.af-mark:hover b { color: var(--accent); }
.af-mark:hover .ms-tip { opacity: 1; }

/* ── Fig. 02 · the matrix. Ruled, never boxed. ────────────────────────── */
table.af-mtx { border-collapse: collapse; width: 100%; table-layout: fixed; font-size: var(--t-micro); border: 0; }
table.af-mtx th, table.af-mtx td { padding: 0; border: 0; border-bottom: 1px solid var(--rule); text-align: center; }
table.af-mtx thead .af-mtx__corner { width: 11.5rem; border: 0; border-bottom: 1px solid var(--ink); }
table.af-mtx thead .af-mtx__head { border: 0; border-bottom: 1px solid var(--ink); vertical-align: bottom; }
table.af-mtx thead .af-mtx__head button {
  appearance: none; background: none; border: 0; padding: 0.4rem 0.2rem;
  font: inherit; font-weight: var(--w-bold); color: var(--ink); cursor: pointer;
  letter-spacing: 0; width: 100%;
}
table.af-mtx thead .af-mtx__head button:hover,
table.af-mtx thead .af-mtx__head button:focus-visible { color: var(--accent); outline: none; }
table.af-mtx .af-mtx__ch {
  text-align: left; padding: 0 0.9rem 0 0.6rem; color: var(--ink);
  white-space: nowrap; font-weight: var(--w-regular); letter-spacing: 0;
}
table.af-mtx tr.is-unsup .af-mtx__ch { color: var(--ink-mute); }
.af-flag { color: var(--ink-mute); font-weight: var(--w-regular); font-size: 0.72em; margin-left: 2px; vertical-align: top; }

/* the cell: a midline, and a fill that grows up for an increase, down for a
   decrease. Length is magnitude; side is direction. No color encodes value. */
.af-cell { position: relative; display: block; width: 100%; height: 34px; margin: 1px auto; }
.af-cell::before { content: ""; position: absolute; left: 10%; right: 10%; top: 50%; height: 1px; background: var(--rule); }
/* the fill is a column, not a rule: width carries nothing, height carries |Δ|.
   A narrow column reads as a deviation off the midline; a wide one reads as a
   horizontal rule and loses its direction. min-height keeps a near-zero
   deviation visible as a mark rather than vanishing into the midline — an
   unmoved channel is a finding too. */
.af-cell i { position: absolute; left: calc(50% - 5px); width: 10px; min-height: 1.5px; background: var(--ink); }
.af-cell.is-up i { bottom: 50%; }
.af-cell.is-down i { top: 50%; }
/* unsupported: hatched and light. Published, but not evidence. */
.af-cell--unsup i {
  background: repeating-linear-gradient(45deg, #b4b4b4 0 2px, transparent 2px 4px);
}
.af-cell .ms-tip {
  position: absolute; left: 50%; bottom: 100%; transform: translate(-50%, -2px);
  background: var(--ink); color: #fff; font-size: var(--t-micro); font-weight: var(--w-medium);
  padding: 3px 7px; white-space: nowrap; opacity: 0; pointer-events: none;
  transition: opacity var(--dur-fast); z-index: 5;
}
.af-cell:hover .ms-tip { opacity: 1; }
.af-cell:hover i { background: var(--accent); }
.af-td.is-dim { opacity: 0.26; transition: opacity var(--dur-fast) var(--ease); }

/* ── Fig. 03 · trajectory. One stacked rule per session. ──────────────── */
.af-traj { margin-top: var(--s-2); border-top: 1px solid var(--ink); }
.af-traj__row { display: flex; align-items: center; gap: 0.8rem; padding: 0.5rem 0; border-bottom: 1px solid var(--rule); }
.af-traj__k { flex: none; width: 2rem; font-size: var(--t-small); font-weight: var(--w-bold); color: var(--ink); }
.af-traj__bar { flex: 1; display: flex; height: 12px; }
.af-traj__n { flex: none; font-size: var(--t-micro); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.af-band { position: relative; display: block; height: 100%; }
/* three grays, not three colors — the bands are ordinal, not categorical */
.af-band--fades { background: #d0d0d0; }
.af-band--stable { background: #8f8f8f; }
.af-band--escalates { background: var(--ink); }
.af-band .ms-tip {
  position: absolute; left: 50%; bottom: 100%; transform: translate(-50%, -4px);
  background: var(--ink); color: #fff; font-size: var(--t-micro); font-weight: var(--w-medium);
  padding: 3px 7px; white-space: nowrap; opacity: 0; pointer-events: none;
  transition: opacity var(--dur-fast); z-index: 5;
}
.af-band:hover .ms-tip { opacity: 1; }
.af-traj__key { display: flex; gap: 1.2rem; margin-top: var(--s-2); font-size: var(--t-micro); color: var(--ink-mute); }
.af-traj__key span { display: flex; align-items: center; gap: 0.4rem; }
.af-traj__key i { width: 14px; height: 8px; display: inline-block; }

/* §04 holds the tallest figure on the page — Fig. 02, the 32-channel matrix.
   At 1418px against a 611px section it runs ~810px past where §05 Trajectory
   begins, so opened it would collide with it.

   A1's .entry--methods answers this by reserving the height up front, which
   keeps the page still on toggle. That trade is wrong here: A1's plate is
   ~600px, this one is 1418px, and reserving it would leave the section
   permanently gaping in the COLLAPSED state — the state the reader is in
   almost always. Better to pay the reflow on the click the reader chose to
   make than to hold that much white space open for a figure nobody opened.

   So §04's gutter figure joins normal flow ONLY while open: .is-open drops the
   absolute positioning, and the aside — not a reservation — grows to the
   figure's real height and pushes §05 down. Closed, the aside is its natural
   one-line height and there is no white space at all.

   Keyed to .is-open (set by the zoom handler in main.js) and scoped to
   .entry--matrix, so §03 and §05 keep the standard floating-gutter behavior:
   measured open, Fig. 01 (578px) and Fig. 03 (276px) are both shorter than
   their own sections and can never reach the section below. */
.entry--matrix .entry__zoom.is-open {
  position: static;
  right: auto;
}

@media (max-width: 800px) {
  .af-onset__scale { padding-left: 0; }
  .af-row { flex-direction: column; align-items: stretch; gap: 0.3rem; }
  .af-row__label { width: auto; }
  table.af-mtx thead .af-mtx__corner { width: 8rem; }
  table.af-mtx .af-mtx__ch { white-space: normal; }
}

/* ── Index filter — the scope letter as control ─────────────────────────
   The letter carries the same type as .scope__label and must look exactly
   like it: a filter is offered, never announced. Per the accent rule the
   red appears on hover/focus only; the ACTIVE state is a line-through,
   the same "you are here" mark the nav uses. No box, no fill, no weight
   change. */
.scope__label--filter {
  display: inline-block;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0 0 var(--s-2);
  font: inherit;
  font-size: var(--t-small);
  font-weight: var(--w-medium);
  line-height: inherit;
  color: var(--ink);
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  cursor: pointer;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.scope__label--filter:hover,
.scope__label--filter:focus-visible {
  color: var(--accent);
}
/* active filter — struck through, matching the current nav tab */
.scope__label--filter.is-active {
  text-decoration: line-through;
}

/* .scope is display:grid, which overrides the hidden attribute (the same
   trap .predictor[hidden] documents). The filter hides groups via .hidden,
   so it must be re-asserted here or a filtered-out scope stays on screen. */
.scope[hidden] { display: none; }

/* .scope:first-of-type sets the ledger's top air. Once a group is hidden
   that selector can land on a hidden element, so the filter script marks
   the first VISIBLE group instead. */
.scope.is-first {
  padding-top: var(--s-5);
}

@media (prefers-reduced-motion: reduce) {
  .scope__label--filter { transition: none; }
}

/* ── A1 RESOURCES — the thesis reference list. The site has no bullet
   anywhere (cf. the Notice clauses), so entries are ruled rows in the same
   language as .fig-table: hanging indent, ragged right, no marker. ── */
.ref {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--ink);
}
.ref__item {
  font-size: var(--t-micro);
  line-height: 1.45;
  color: var(--ink);
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
  padding-left: 1.5em;
  text-indent: -1.5em;   /* hanging indent, as a bibliography sets */
}
.ref__item:last-child {
  border-bottom: 1px solid var(--ink);
}

/* ====================================================================== */
/*  RESUME — inside the numbered About entries                             */
/* ====================================================================== */

/* The resume does not introduce a layout. It is written into the existing
   numbered-entry system: each section is an .entry whose number sits in the
   gutter and whose records run down the 46ch body column, at --t-small like
   every other line on the page. The only new mark is the hairline between
   records. */

/* A section label sits on the entry's own line, quiet, above its records. */
/* Labels and record metadata stay ragged right — only prose is justified. */
.entry__body .cv__label {
  font-size: var(--t-small);
  color: var(--ink-mute);
  margin-bottom: var(--s-3);
  text-align: left;
  hyphens: none;
}

/* One record. Org and meta stack, then the lines beneath — all within the
   body measure, so the resume reads as continuous with the prose above it. */
.cv__item {
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--rule);
}
.cv__item + .cv__item {
  padding-top: var(--s-3);
}
.cv__item:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* The record's header: name on one line, role and date beneath it in mute,
   set as the index sets its metadata. */
.entry__body .cv__org,
.entry__body .cv__meta,
.entry--cv .cv__meta {
  font-size: var(--t-small);
  line-height: 1.45;
  text-align: left;
  -webkit-hyphens: none;
  hyphens: none;
}
.entry__body .cv__org { color: var(--ink); }
.entry__body .cv__meta,
.entry--cv .cv__meta {
  color: var(--ink);
  margin-bottom: var(--s-2);
}

/* The lines themselves — ragged right, as labels and captions are. */
.cv__lines {
  margin: 0;
  padding: 0;
  list-style: none;
}
.entry__body .cv__lines li,
.entry--cv .cv__lines li {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  /* justified so the achievement blocks close flush on both edges, the same
     treatment the site's prose gets; hyphenation keeps the word spacing even
     at this measure. The role/company line above stays ragged — it is a label. */
  text-align: justify;
  -webkit-hyphens: auto;
  hyphens: auto;
}
.cv__lines li + li {
  margin-top: var(--s-1);          /* bullets read as one block; the gap
                                      between roles (--s-4) does the parting */
}
/* a list of credentials is a stack of labels, not separate prose blocks: the
   lines run on consecutively, set only by their own leading. */
.cv__lines--tight li + li {
  margin-top: 0;
}

/* Skills read as label + value on one baseline, the pattern .proj-details
   already uses, held inside the same body measure. */
.cv__pair {
  display: grid;
  grid-template-columns: var(--nav-item) 1fr;
  align-items: baseline;
  font-size: var(--t-small);
  line-height: 1.45;
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--rule);
}
.cv__pair + .cv__pair {
  padding-top: var(--s-2);
}
.cv__pair:last-child {
  border-bottom: none;
  padding-bottom: 0;
}
.cv__pair dt { color: var(--ink); }
.cv__pair dd { margin: 0; }

@media (max-width: 800px) {
  .cv__pair {
    grid-template-columns: 1fr;
    row-gap: var(--s-1);
  }
}

/* ---------------------------------------------------------------------- */
/*  CASE PULL — A1 (case-a1.html)                                          */
/* ---------------------------------------------------------------------- */

/* The one-line verdict that closes a section. Marked by a rule in the left
   margin rather than by quotation marks or italics — the same restraint the
   nav uses for "you are here". It is a statement, never stretched, so it
   stays ragged like .tldr__line. */
.entry__pull {
  margin: var(--s-3) 0 0;
  padding-left: var(--s-2);
  border-left: 2px solid var(--ink);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--ink);
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}

/* ── Case-report sub-structure (A1) ──────────────────────────────────────
   Two labels inside an .entry__body, for sections that carry named parts:
   the approach's Discovery / Framing / Tradeoffs, and each named decision.
   Set in the body face at body size — the rank is carried by space above
   and by standing alone on its line, never by a weight or colour change,
   the same restraint .entry__pull and the nav's "you are here" use.
   Ragged right like every other label on the site. */
.entry__label {
  margin: var(--s-3) 0 0;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}
/* A label opening its section has the section number to clear, not a
   paragraph — so it starts on the number's line like .entry__action does. */
.entry__body > .entry__label:first-child {
  margin-top: 0;
}
/* A label binds to what follows it: no gap between them, so the pair reads as
   one unit and the space above the label is what separates one named part
   from the next. The gap lives on the label's own margin-bottom, since
   .case-body .entry__body p carries one — zero it at matching specificity. */
.case-body .entry__body .entry__label {
  margin-bottom: 0;
}
.entry__label + p,
.entry__label + .entry__tradeoffs {
  margin-top: 0;
}

/* The four structural labels — Discovery / Framing / Tradeoffs / Core loop —
   name a PART of a section rather than making a claim, so they are set in
   caps to read as apparatus. A named decision or learning keeps sentence
   case: that is a sentence, and capitalising it would shout the argument.
   Caps at this size need a little tracking to stay legible; this is the
   one place on the site that carries any, kept to the minimum that works. */
.entry__label--part {
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* The tradeoff list: arrow-led fragments, one per line. Not prose, so it
   stays ragged and unjustified; no bullets anywhere on this site, so the
   arrow is the only marker and it hangs outside the text block. */
.entry__tradeoffs {
  margin: 0;
  padding: 0;
  list-style: none;
}
.entry__tradeoffs li {
  position: relative;
  margin-top: var(--s-1);
  padding-left: 1.6em;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);
  text-align: left;
  -webkit-hyphens: manual;
  hyphens: manual;
}
.entry__tradeoffs li:first-child {
  margin-top: 0;
}
.entry__tradeoffs li::before {
  content: "\2192";
  position: absolute;
  left: 0;
  color: var(--ink);
}

/* ── B5 screens: four app captures set as one row across the case body,
   between §01 and §02. Starts on the wordmark line like the entries. ── */
.case-plates {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s-4);
}
.case-plates__item {
  margin: 0;
}
.case-plates__item img {
  display: block;
  width: 100%;
  height: auto;
}
.case-plates__item figcaption {
  margin-top: var(--s-2);
  font-size: var(--t-small);
}
@media (max-width: 800px) {
  .case-plates {
    grid-template-columns: repeat(2, 1fr);
  }
}
.case-plates__item img {
  filter: grayscale(100%);         /* site rule: imagery carries no colour */
}
