/* ════════════════════════════════════════════════════════════════════════
   css/work-camera.css — /work/camera, THE MEDIA PORTFOLIO.
   service.tnaado.ca · Tier 2 · pairs with work/camera.html + css/chrome.css

   THE ORGANISING IDEA IS A CONTACT SHEET, and it is a literal one: a paper
   ground, a rebate edge down the left, a mono masthead across the top, and
   frames printed on it in a fixed grid with every frame numbered in the
   corner. Nothing on the sheet is precious. Twelve rectangles the same size
   is not a failure of variety here — it is what the object is, and it is what
   makes the numbering mean anything. The variety comes from the photographs.

   WHY THIS FILE EXISTS AT ALL, given css/site.css has .cs-grid/.cs-card. That
   component is 86KB into a stylesheet built on the legacy design system
   (--on-black, --text-sm, --font-mono, --ink flipping to bone inside light
   scopes), and the old /media-portfolio had to re-skin it from white-on-ink to
   ink-on-bone in a SECOND file, which is exactly how the empty-plate bug below
   happened. This page carries its own paint and reads only the chrome's
   contract: the brand tokens, resolved with the locked values as fallbacks.

   THE BUG THIS FILE FIXES, stated plainly because two pages rendering the same
   table must fail identically. site.css defines .cs-card__media--empty ONCE, as
   a #0d0d0d plate with a red radial and an italic single letter at
   rgba(bone,.22) — about 1.9:1 on that ground. css/media-portfolio.css then
   re-skinned the cards white-on-bone and never overrode it. So on the live
   media portfolio a row with no image renders as a black hole punched into a
   white card with a nearly invisible letter inside it, and the reader cannot
   tell that from an image that failed to load. Here there are FIVE named states,
   no two of them share a paint, and every ratio below was measured off rendered
   pixels rather than computed from the declared colours:

     RICH    frame, client, title, description.
     EMPTY   no frame was supplied for this row. A FACT about the record.
             Hatched paper plate, the full TITLE in the display face, and a mono
             line that says so. Measured 15.34:1 title, 5.17:1 label.
     ERROR   a frame was supplied and did not load. A FAULT.
             The same plate geometry tinted red with a red rule along the top, a
             different mono line, the title still legible. Measured 14.97:1
             title, 5.86:1 label. A reader can tell it from EMPTY at thumbnail
             size, and so can a screenshot in a bug report.
     THIN    no description. The paragraph is omitted and the card KEEPS ITS
             HEIGHT (--frame-meta-min) wherever it has row-mates, so a thin row
             does not shorten its neighbours or leave a ragged sheet.
     ZERO    the sheet came back with no rows at all. A composition that fills
             the sheet and names two places to go, not a hidden sentence.
             Measured 15.34:1 heading, 8.87:1 body, 5.77:1 label.

   COLOUR IS LOCKED AND EVERY TEXT VALUE HERE IS MEASURED.
     ink  #0a0a0a   red #c8102e   bone #f5f1e8
     --red at 3.37:1 on ink is BANNED as small text and appears in this file
     only as rules, ticks and fills. Small red text is #ef5c6e on ink (6.06:1)
     and #a4132c on bone (6.6:1). #c8102e is allowed as display type at 24px
     and over, where 3:1 is the floor.

   MOTION is transform and colour only, gated to hover:hover and pointer:fine,
   and removed entirely under prefers-reduced-motion. No filter, no blur, no
   box-shadow animation.
   ════════════════════════════════════════════════════════════════════════ */

/* ── TOKENS ────────────────────────────────────────────────────────────────
   Declared on :root so css/chrome.css's locals (--c-ink: var(--ink, …) etc.)
   resolve against this page rather than falling back. The names match
   css/site.css so a future merge is a deletion, not a rename. */
:root {
  --ink: #0a0a0a;
  --bone: #f5f1e8;
  --red: #c8102e;

  /* Small red text. Two values because the ground decides which is legal. */
  --red-ink: #ef5c6e;          /* on ink  — 6.06:1 */
  --red-bone: #a4132c;         /* on bone — 6.6:1  */

  /* Type on ink. Both lifted off the chrome, where they are already measured. */
  --dim-ink: #b9b3a6;          /* 9.4:1 on ink */
  --hair-ink: rgba(245, 241, 232, .14);

  /* Type on paper. Composited and measured on --paper below, not on bone,
     because no text on this page sits on pure bone. */
  --ink-body: rgba(10, 10, 10, .78);
  --ink-mute: rgba(10, 10, 10, .68);
  --hair-bone: rgba(10, 10, 10, .16);

  /* THE SHEET'S OWN GROUNDS. --paper is the sheet, --plate is the rebate and
     the empty frame, one step down so a missing frame reads as a hole in the
     paper rather than as a card. */
  --paper: #f2ede1;
  --plate: #e8e2d5;
  --plate-err: #eddcdd;

  --display: 'Fraunces', 'Iowan Old Style', 'Times New Roman', serif;
  --sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --rail: 74rem;
  --pad: clamp(1.15rem, 4vw, 2.5rem);
  --ease: cubic-bezier(.22, 1, .36, 1);

  /* The height a card's text block holds even when it has no description, so
     the THIN state costs the sheet nothing. Three clamped lines of .875rem at
     1.55 plus the label and title above them. */
  --frame-meta-min: 10.25rem;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--bone);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.6;
  /* The sheet is the widest thing on the page and it is a grid, so nothing
     here can push the document wider than the viewport. Stated rather than
     assumed: measured at 320 / 390 / 768 / 1440. */
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; }

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

.rail { max-width: var(--rail); margin: 0 auto; padding: 0 var(--pad); }

/* ── CLEARING THE FIXED CHROME IS NOT THIS FILE'S JOB ──────────────────────
   It was, for about an hour. .chrome-live .chrome is position:fixed and
   css/chrome.css deliberately never stated the slab's height, so this page
   measured it at runtime and padded <main> by it — the "Media portfolio" kicker
   was underneath the bar at 1440 and at 390 without that.

   While this page was being built, the /media agent found the same fault from
   the storm-off branch and fixed it in css/chrome.css at the system level:

     html.chrome-live { --chrome-bar-h: 5.5rem }   … 7.5rem below 860px
     html.chrome-live:not(.sc-live) body { padding-top: var(--chrome-bar-h) }
     html.chrome-live:not(.sc-live) { scroll-padding-top: … }

   NO NESTED COMMENT DELIMITERS IN THIS FILE. The line above originally carried
   an inline `slash-star 7.5rem below 860px star-slash`, and CSS comments do not
   nest: that inner terminator closed this block early, the prose after it was
   parsed as declarations, and the parser ate the next rule while recovering —
   which silently removed .mast's 56px of top padding and put the kicker 6px
   under the bar. Caught by measuring the masthead's computed padding, not by
   reading the file.

   That is the right home for it, and both fixes applied at once produced 182px
   of clearance against a 94px bar. The local one is therefore GONE rather than
   kept as a smaller number: one owner for one measurement. scroll-padding-top on
   the root also covers every anchor landing, which is strictly better than the
   per-section scroll-margin-top this file had.

   Recorded because the numbers disagree and the shared file should win only on
   purpose: the slab measures 94px at 1440 and 86px at 390 on this page, against
   5.5rem (88px) and 7.5rem (120px). Desktop under-reserves by 6px, which the
   masthead's own 56px of padding absorbs; the phone over-reserves by 34px, which
   is dead space above the kicker on every Tier 2 page. Reported, not patched. */

/* ── THE MASTHEAD ──────────────────────────────────────────────────────────
   Ink, compact, and it does the whole job of Tier 2 self-explanation: what
   this page is, whose work is on it, and where the OTHER portfolio lives. A
   stranger off a paid ad has to be able to read those three things without
   scrolling into the sheet. */
.mast { background: var(--ink); padding: clamp(2.25rem, 5vw, 3.5rem) 0 clamp(2rem, 4vw, 3rem); }

.kicker {
  display: flex; align-items: center; gap: .7rem;
  margin: 0 0 1.1rem;
  font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--red-ink);
}
.kicker::before {
  content: ''; flex: 0 0 auto; width: 26px; height: 2px; background: var(--red);
}

.mast__h1 {
  margin: 0 0 1.1rem;
  font-family: var(--display); font-weight: 400;
  /* 2.05rem floor = 32.8px, so the italic red run clears the 3:1 large-text
     floor with --red rather than needing the lifted value. */
  font-size: clamp(2.05rem, 5.2vw, 3.35rem);
  line-height: 1.06; letter-spacing: -.015em;
  color: var(--bone);
  max-width: 26ch;
}
.mast__h1 em { font-style: italic; color: var(--red); }

.mast__lede {
  margin: 0 0 .8rem;
  font-size: clamp(1rem, 1.4vw, 1.0625rem);
  color: var(--dim-ink);
  max-width: 56ch;
}
.mast__lede a {
  color: var(--red-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}
@media (hover: hover) and (pointer: fine) {
  .mast__lede a:hover { color: var(--bone); text-decoration-color: var(--red); }
}

/* THE COUNTS. Every figure in here is countable off this page or off the
   repo, which is the only reason any of them are printed. No figure from
   PRICES.md appears on this page at all — /work/camera is not in that file's
   list of pages that carry prices, and a portfolio that quotes a day rate is
   repeating its parent. */
.counts {
  display: grid; gap: 1px;
  grid-template-columns: 1fr;
  margin: clamp(1.6rem, 3.5vw, 2.4rem) 0 0;
  padding: 0; list-style: none;
  background: var(--hair-ink);
  border: 1px solid var(--hair-ink);
}
@media (min-width: 40rem) { .counts { grid-template-columns: repeat(3, 1fr); } }
.counts > li { background: var(--ink); padding: .9rem 1.05rem 1rem; min-width: 0; }
.counts dt {
  margin: 0 0 .35rem;
  font-family: var(--mono); font-size: .625rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--dim-ink);
}
.counts dd {
  margin: 0;
  font-family: var(--display); font-weight: 400; font-size: 1.0625rem;
  line-height: 1.3; color: var(--bone);
  overflow-wrap: anywhere;
}
.counts dd b { font-weight: 400; font-variant-numeric: tabular-nums; }

/* ── THE SHEET ─────────────────────────────────────────────────────────────
   Paper on ink. The section keeps a black margin around itself on purpose:
   a contact sheet is an object you are handed, so it has an edge. */
.sheet {
  background: var(--ink);
  padding: 0 0 clamp(2.5rem, 5vw, 4rem);
}
.sheet__paper {
  max-width: var(--rail);
  margin: 0 auto;
  background: var(--paper);
  color: var(--ink);
  /* The rebate: the unexposed strip down the left edge of a real sheet, where
     the roll is identified. Hatched, so it reads as film stock rather than as
     a decorative bar. */
  border-left: clamp(10px, 2.2vw, 22px) solid var(--plate);
}

/* THE SHEET MASTHEAD. Mono, uppercase, and the only place the sheet names
   itself. Wraps at every width — measured at 320. */
.sheet__mast {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .35rem clamp(.8rem, 2.5vw, 1.6rem);
  margin: 0;
  padding: .85rem clamp(1rem, 3vw, 1.6rem);
  border-bottom: 2px solid var(--ink);
  font-family: var(--mono); font-size: .625rem; font-weight: 500;
  letter-spacing: .17em; text-transform: uppercase;
  color: var(--ink-mute);
}
.sheet__mast b { font-weight: 500; color: var(--ink); }
.sheet__mast span { white-space: nowrap; }

.sheet__grid {
  display: grid;
  gap: clamp(.85rem, 2vw, 1.35rem);
  grid-template-columns: 1fr;
  margin: 0; padding: clamp(1rem, 3vw, 1.6rem);
  list-style: none;
  counter-reset: frame;
}
@media (min-width: 37.5rem) { .sheet__grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 62.5rem) { .sheet__grid { grid-template-columns: repeat(3, 1fr); } }

/* ── ONE FRAME ─────────────────────────────────────────────────────────────
   The whole card is the link and the link goes to the FRAME ITSELF at full
   size. That is what you do with a contact sheet — you point at 07 and ask to
   see it big — and it is the one target that needs no viewer, no script and no
   detail page that does not exist yet. With JS off it still works, because it
   is a link to an image. */
.frame { display: flex; min-width: 0; }

.frame__link {
  display: flex; flex-direction: column; width: 100%;
  background: #fff;
  border: 1px solid var(--hair-bone);
  text-decoration: none; color: inherit;
  transition: border-color .2s var(--ease), transform .2s var(--ease);
}

/* NO clip-path ON THE CARD, deliberately. The old page chamfered every third
   card, and a chamfer clips a focus outline — which is half of why the old
   focus state was a border-alpha change and nothing else. The frames are
   rectangles because that is what a frame is, and the ring below can therefore
   be a real ring. */
.frame__box {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--plate);
  border-bottom: 1px solid var(--hair-bone);
}
.frame__box img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: var(--fp, 50% 50%);
  display: block;
}

/* THE FRAME NUMBER. Mono, bottom-left, INSIDE the image box, on a tick — the
   single detail that turns twelve cards into one sheet. The chip exists
   because the ground is a photograph: a numeral with nothing behind it is
   legible on eleven frames and invisible on the twelfth. Measured on the chip,
   not on the picture, so the number is 17.7:1 on every frame in the sheet. */
.frame__n {
  counter-increment: frame;
  position: absolute; z-index: 2; left: 0; bottom: .6rem;
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .22rem .5rem .22rem .42rem;
  background: rgba(10, 10, 10, .9);
  font-family: var(--mono); font-size: .625rem; font-weight: 500;
  letter-spacing: .14em; line-height: 1.4;
  font-variant-numeric: tabular-nums;
  color: var(--bone);
}
.frame__n::before {
  content: ''; width: 2px; height: .8em; background: var(--red);
}
.frame__n::after { content: counter(frame, decimal-leading-zero); }

/* ── THE THREE PLATE STATES ────────────────────────────────────────────────
   Same geometry, three paints, and the counter still increments on all of
   them: a row that has no picture is still a row on the sheet and still has a
   number you can quote back. */
.frame__plate {
  position: absolute; inset: 0; z-index: 1;
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: .5rem;
  padding: 1rem 1.1rem 2.4rem;
  text-align: left;
}
/* THE CAPTION SCRIM, and it exists because of a measurement rather than a
   preference. The hatch is a 1px rule every 7px, so a glyph sits partly on the
   base plate and partly on a rule — and the rule is the darker of the two. On
   the ERROR plate, #a4132c against the base #eddcdd is 5.86:1, but against a
   hatch rule (rgba(200,16,46,.16) over that base, rgb(231,187,193)) it drops to
   4.19:1, under the 4.5 floor for a 9px label. Lowering the hatch alpha would
   have cleared it by about a tenth of a point, which is not a fix.

   So the bottom of the plate is returned to a flat, known ground and the hatch
   is left to do its work above the text. Solid to 56% and faded out by 80%,
   which puts the whole caption block on the base colour at every card width the
   sheet uses. z-index -1 keeps it above the box's hatched background and below
   the plate's own text: .frame__plate is positioned with a z-index, so it is a
   stacking context and the negative index cannot escape it. Re-measured off
   rendered pixels afterwards, not recomputed by hand. */
.frame__plate::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to top,
    var(--pl, var(--plate)) 0%,
    var(--pl, var(--plate)) 56%,
    rgba(255, 255, 255, 0) 80%);
}
.frame__plate__label {
  margin: 0;
  font-family: var(--mono); font-size: .5625rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
}
.frame__plate__title {
  margin: 0;
  font-family: var(--display); font-weight: 400; font-style: italic;
  font-size: clamp(1.15rem, 2.6vw, 1.5rem); line-height: 1.15;
  color: var(--ink);
}

/* ══ THE MARKUP FOR THE EMPTY STATE ═══════════════════════════════════════
   It lives here rather than in work/camera.html because an example and the rules
   it depends on drift the moment they are in different files — and because every
   line of it contains a BEM modifier or a custom property, i.e. two consecutive
   hyphens, which the HTML spec forbids inside a comment. Legal here, invalid
   there. A prerender writes this in place of the <img> when a row has no image.

     <div class="frame__box frame__box--empty" data-title="THE FULL TITLE">
       <span class="frame__n" aria-hidden="true"></span>
       <div class="frame__plate">
         <p class="frame__plate__label">No frame supplied</p>
         <p class="frame__plate__title">THE FULL TITLE</p>
       </div>
     </div>

   ERROR is the same markup with frame__box--error and the label "Frame missing".
   Nothing needs to author it: the fall-through at the foot of work/camera.html
   builds exactly this when a supplied frame fails to load.

   AND THE ZERO-ROW COMPOSITION, which replaces the cards entirely rather than
   sitting hidden beside them:

     <li class="sheet__none">
       <p class="sheet__none__label">Sheet 01 &middot; 0 frames</p>
       <h3 class="sheet__none__h">WHAT HAPPENED, IN ONE LINE</h3>
       <p class="sheet__none__body">WHAT IT MEANS FOR THE READER</p>
       <div class="sheet__none__go">
         <a class="btn btn--solid" href="/contact">…</a>
         <a class="btn" href="/product-photography-toronto">…</a>
       </div>
     </li>
   ═══════════════════════════════════════════════════════════════════════════ */

/* EMPTY — a fact. Paper, one step down from the sheet, hatched. */
.frame__box--empty {
  --pl: var(--plate);
  background:
    repeating-linear-gradient(45deg,
      rgba(10, 10, 10, 0) 0 6px,
      rgba(10, 10, 10, .085) 6px 7px),
    var(--plate);
}
.frame__box--empty .frame__plate__label { color: rgba(10, 10, 10, .62); }

/* ERROR — a fault. Same hatch, tinted, and a red rule along the top so the
   two are distinguishable in a screenshot at thumbnail size. */
.frame__box--error {
  --pl: var(--plate-err);
  background:
    repeating-linear-gradient(45deg,
      rgba(200, 16, 46, 0) 0 6px,
      rgba(200, 16, 46, .16) 6px 7px),
    var(--plate-err);
  box-shadow: inset 0 3px 0 0 var(--red);
}
.frame__box--error .frame__plate__label { color: var(--red-bone); }

.frame__meta {
  display: flex; flex-direction: column;
  flex: 1 1 auto;
  gap: .45rem;
  padding: .95rem 1.05rem 1.05rem;
}
/* THE THIN STATE'S WHOLE FIX, and it is deliberately conditional. A card with
   no description holds the height of one that has three lines, so it cannot
   shorten its row-mates or leave the sheet ragged — but that reservation only
   means anything when the card HAS row-mates. Measured at 390 on the first
   build: in the single-column layout the reserved block read as a hole under
   frame 10's title, because there was nothing beside it to line up with. So the
   floor starts at the two-column breakpoint and the phone lets a thin card be
   short, which is correct there. */
@media (min-width: 37.5rem) {
  .frame__meta { min-height: var(--frame-meta-min); }
}
.frame__who {
  margin: 0;
  font-family: var(--mono); font-size: .5625rem; font-weight: 500;
  letter-spacing: .17em; text-transform: uppercase;
  color: var(--red-bone);
  overflow-wrap: anywhere;
}
.frame__title {
  margin: 0;
  font-family: var(--display); font-weight: 400;
  font-size: 1.0625rem; line-height: 1.25;
  color: var(--ink);
}
.frame__desc {
  margin: 0;
  font-size: .875rem; line-height: 1.55;
  color: var(--ink-body);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.frame__open {
  margin: auto 0 0;
  display: flex; align-items: center; gap: .45rem;
  font-family: var(--mono); font-size: .5625rem; font-weight: 500;
  letter-spacing: .17em; text-transform: uppercase;
  color: var(--red-bone);
}
.frame__open i {
  font-style: normal;
  transition: transform .2s var(--ease);
}

/* ── FRAME INTERACTION, all three states of it ─────────────────────────────
   Hover is gated so a touch device never sticks in it. :active is a real
   state, not a side effect of :hover. :focus-visible is a RING drawn with
   box-shadow rather than outline — an outline is what the old page lost to
   `outline: none` in site.css and to its own chamfer, and a ring made of two
   shadows cannot be clipped by either. */
@media (hover: hover) and (pointer: fine) {
  /* The border is a RULE, so --red is legal there. The label is 9px text, so it
     is not: the first build hovered it to --red, which measures 5.89:1 on the
     card's white and would have passed a contrast check while still breaking the
     standing rule that #c8102e is for rules, fills and display type at 24px and
     over. It goes to ink instead — a bigger change than the red would have been
     (7.75:1 to 19.8:1) and one that cannot drift onto a darker card later. */
  .frame__link:hover { border-color: rgba(200, 16, 46, .5); }
  .frame__link:hover .frame__open { color: var(--ink); }
  .frame__link:hover .frame__open i { transform: translateX(3px); }
}
.frame__link:active { transform: translateY(1px); border-color: var(--red); }
.frame__link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 6px var(--red-bone);
  border-color: var(--red-bone);
}
/* Windows high-contrast throws every colour away, including a box-shadow, so
   the ring has to be restated as a real outline there. */
@media (forced-colors: active) {
  .frame__link:focus-visible { outline: 3px solid CanvasText; outline-offset: 2px; }
}

/* ── ZERO ROWS ─────────────────────────────────────────────────────────────
   A real composition, not a hidden sentence. It occupies the sheet the way
   twelve frames would, says what happened in the mono voice the rest of the
   sheet uses, and gives the reader the two places worth going instead. The
   prerender writes this block INTO the grid region in place of the cards when
   the query returns nothing, which is why it is styled to sit inside
   .sheet__grid and span it. */
.sheet__none {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; gap: 1rem;
  padding: clamp(2rem, 6vw, 3.5rem) clamp(1.15rem, 4vw, 2.5rem);
  background:
    repeating-linear-gradient(45deg,
      rgba(10, 10, 10, 0) 0 6px,
      rgba(10, 10, 10, .085) 6px 7px),
    var(--plate);
  border: 1px solid var(--hair-bone);
}
/* EVERY SELECTOR IN THIS BLOCK IS A BARE CLASS, and that is a fix rather than a
   style preference. The first build styled the body copy as `.sheet__none p`,
   which is specificity (0,1,1) and therefore outranked `.sheet__none__label`
   and `.sheet__none__h` at (0,1,0) — so the mono label and the display heading
   both rendered at 15px in the body colour, and the composition read as three
   identical paragraphs. Measured, not spotted by eye: the label came back as
   15px JetBrains Mono in rgba(10,10,10,.78) instead of 9px in .62. A class plus
   a type selector inside a component is a trap; there are none here now. */
.sheet__none__label {
  margin: 0;
  font-family: var(--mono); font-size: .5625rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: rgba(10, 10, 10, .62);
}
.sheet__none__h {
  margin: 0;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.4rem, 4vw, 2rem); line-height: 1.1;
  color: var(--ink);
  max-width: 30ch;
}
.sheet__none__body {
  margin: 0; max-width: 54ch;
  font-family: var(--sans); font-size: .9375rem; font-weight: 400;
  color: var(--ink-body);
}
.sheet__none__go { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .3rem; }

/* ── THE SELECTION NOTE ────────────────────────────────────────────────────
   Short, on ink, and every number in it is a file count somebody can check. */
.note { background: var(--ink); padding: clamp(2rem, 4vw, 3.25rem) 0; }
.note__inner {
  display: grid; gap: clamp(1.25rem, 3vw, 2.5rem);
  grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
  .note__inner { grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); }
}
.note__h2 {
  margin: 0;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.1;
  color: var(--bone);
}
.note__h2 em { font-style: italic; color: var(--red); }
.note__body p { margin: 0 0 .85rem; color: var(--dim-ink); max-width: 62ch; }
.note__body p:last-child { margin-bottom: 0; }
.note__body b { color: var(--bone); font-weight: 600; font-variant-numeric: tabular-nums; }
/* Measured, not assumed: the first build of this page left this link at the
   user-agent default, which paints #0000EE on ink at 2.3:1 and reads as a
   defect on a page whose entire argument is that the details were checked. */
.note__body a {
  color: var(--red-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}
@media (hover: hover) and (pointer: fine) {
  .note__body a:hover { color: var(--bone); text-decoration-color: var(--red); }
}
.note__body a:focus-visible,
.mast__lede a:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--ink), 0 0 0 5px var(--red-ink);
}
/* :active on an inline link is the state that is always forgotten, and on a
   phone it is the ONLY state a reader ever sees — there is no hover there and
   focus-visible does not fire for a tap. Two of the three states on these two
   links were present after the first build; this is the third. */
.note__body a:active,
.mast__lede a:active { color: var(--bone); text-decoration-color: var(--red); }

/* ── BOOK ──────────────────────────────────────────────────────────────────
   The floor. Every Tier 2 page ends here. */
/* THE FLOOR, and the height is load-bearing for a reason worth writing down.
   js/chrome.js's followDocument() marks the beat whose top has passed a reading
   line a third down the viewport. A page's LAST section can therefore only ever
   be marked if it is tall enough that scrolling to the very bottom brings its
   top above that line — otherwise the fifth beat is permanently unreachable and
   the row marks "the archive" while the reader is looking at the booking panel.
   Measured at 1440x900 on the first build: #book's top bottomed out at 528px
   against a reading line at 306px, and "03 The archive" stayed lit.

   70svh clears it at both sizes measured (270px against 306px at 1440x900, 253px
   against 287px at 390x844) and is a size the closing panel deserves anyway —
   STRUCTURE.md calls this beat the floor. svh rather than vh so a phone's
   collapsing address bar cannot resize it mid-scroll.

   The underlying wart belongs to the chrome, not to this page: any Tier 2 page
   whose final section is shorter than about two thirds of the viewport will lose
   its last beat the same way. Reported rather than patched here — js/chrome.js
   is not this page's file to edit. */
.book {
  background: var(--ink);
  border-top: 3px solid var(--red);
  padding: clamp(2.25rem, 4.4vw, 3.5rem) 0 clamp(2.25rem, 4.4vw, 3.5rem);
  min-height: 70svh;
  display: flex; flex-direction: column; justify-content: center;
}
.book__h2 {
  margin: 0 0 .9rem;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.9rem, 4.6vw, 2.9rem); line-height: 1.08;
  color: var(--bone);
}
.book__h2 em { font-style: italic; color: var(--red); }
.book__lede { margin: 0 0 1.8rem; color: var(--dim-ink); max-width: 52ch; }
.book__row { display: flex; flex-wrap: wrap; gap: .7rem; }

/* ── BUTTONS ───────────────────────────────────────────────────────────────
   Two weights, one shape. :hover gated, :active real, :focus-visible a ring
   that clears both grounds. */
.btn {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .8rem 1.15rem;
  border: 1px solid var(--hair-ink);
  font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .15em; text-transform: uppercase; text-decoration: none;
  color: var(--bone); background: transparent;
  transition: background-color .2s var(--ease), border-color .2s var(--ease),
              color .2s var(--ease), transform .2s var(--ease);
}
.btn i { font-style: normal; transition: transform .2s var(--ease); }
.btn--solid { background: var(--red); border-color: var(--red); color: #fff; }
@media (hover: hover) and (pointer: fine) {
  .btn:hover { border-color: var(--bone); }
  .btn:hover i { transform: translateX(3px); }
  .btn--solid:hover { background: #a4132c; border-color: #a4132c; }
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--ink), 0 0 0 5px var(--red-ink);
}
/* On the paper ground the ring has to be inverted, or it is a red glow on a
   red glow. Used by the zero-row composition, which is the only place a button
   sits on paper. */
.sheet__none .btn { color: var(--ink); border-color: var(--hair-bone); }
.sheet__none .btn--solid { color: #fff; border-color: var(--red); }
@media (hover: hover) and (pointer: fine) {
  .sheet__none .btn:hover { border-color: var(--ink); }
}
.sheet__none .btn:focus-visible {
  box-shadow: 0 0 0 2px var(--plate), 0 0 0 5px var(--red-bone);
}
@media (forced-colors: active) {
  .btn:focus-visible { outline: 3px solid CanvasText; outline-offset: 2px; }
}

/* ── REDUCED MOTION ────────────────────────────────────────────────────────
   Only the movement goes. Every colour change and every focus ring stays:
   a reader who asked for less motion did not ask for fewer state signals. */
@media (prefers-reduced-motion: reduce) {
  .frame__link, .frame__open i, .btn, .btn i { transition: none; }
  .frame__link:hover .frame__open i,
  .btn:hover i { transform: none; }
  .frame__link:active, .btn:active { transform: none; }
}
