/* GRISAILLE side quests. Studio Peter Varga.
   Loaded ALONGSIDE grisaille.css, never instead of it: that file stays the one
   source of truth for :root tokens, @font-face, and the base reset (*, body,
   img, ::selection, :focus-visible, .chrome-top/.wordmark/.button/.p-sold/
   .numeral/.chapter-lead, all reused directly below). This file adds ONLY the
   layout a quest page needs that the main page has no use for.

   Chapter I stays in its own tonal register end to end (black through steel,
   the same band chamber-dark/chamber-bridge occupy on the main rail) rather
   than crossing into the site's light half: it is that chapter's deep-dive,
   so it keeps that chapter's own darkness. Every step below is a SOLID
   background-color, not a gradient, so the contrast sweep in
   work/qa/close-approach.mjs can read a real computed color at each section
   instead of sampling pixels out of a blend.

   ---------- SESSION H: the deck (horizontal panels) ----------
   Zach's iPhone walkthrough directive: "enter the work sections are fantastic,
   horizontal scroll through sections would be preferred so vertical scroll
   stays dedicated to the path." So the two axes now mean two different things
   across the whole demo: the main rail owns VERTICAL (the path through the
   body of work), and a quest owns HORIZONTAL (stepping sideways off the path
   to browse one piece, then rejoining). A quest page therefore does not scroll
   vertically at document level at all.

   Mechanically that is ONE native CSS scroll-snap scroller: .quest-track with
   scroll-snap-type: x mandatory, one .quest-panel per section, each a
   viewport-wide snap child. No script drives the scrolling, so momentum,
   rubber-banding, snap physics and accessibility all stay the platform's own
   (js/quest.js only READS the position to light the strip, and adds keyboard
   and button shortcuts on top). With JS off, every panel is still reachable by
   swipe/trackpad and by the strip's real in-page fragment links, which the
   browser resolves by scrolling this container -- so the no-JS document is not
   merely readable, it is fully navigable.

   Long-form reading survives the flip because each panel is its own vertical
   scroller (overflow-y: auto): a panel whose text exceeds the height scrolls
   WITHIN itself rather than growing the page.

   This file is loaded ONLY by quests/<slug>/index.html (grep the <link> tags:
   nothing else references it), which is why it may size html/body unqualified
   below without reaching any other page in the site. */

/* ---------- SESSION I: the luxury type pass (R30) ----------

   Zach: "the horizontal swipe on enter the work is fantastic, but text font and
   reveal feels very plain and not luxurious and premium."

   The font was the first half of it, and it was measurable rather than a matter
   of taste. assets/fonts/fraunces-300.woff2 IS a variable font, but its fvar
   carries opsz ALONE (9 to 144): wght, SOFT and WONK were pinned out when it was
   subset. Verified with fontTools 4.63:
     [('opsz', 9.0, 9.0, 144.0)]
   and no rule anywhere in the site set font-variation-settings or
   font-optical-sizing, so the browser's default `font-optical-sizing: auto`
   mapped opsz to the used pixel size and landed every role in the face's TEXT
   cut. The display cut -- the Didone-adjacent one with the fine hairlines, which
   is the register the brief is asking for -- has never been on screen.

   Fraunces has two cuts and the seam sits between opsz 72 and opsz 88: measured
   on this machine, the hairline of `o` collapses from 40 units to 16 units of
   2000/em across that step while the stem barely moves. Above the seam the face
   is 8 to 10:1 stroke contrast. Below it, 2.4:1. Crossing the seam is the whole
   change, and it costs zero bytes of new drawing.

   Crossing it needs SOFT, which the shipped file does not have: at opsz 144 a
   SOFT-0 hairline is 10.8 units = 0.41 CSS px at 76px, which composites at a
   fraction of its intended luminance on a near-black ground and reads as thin
   and washed out. SOFT is the hairline control and is nearly free in stem
   weight (opsz 144: SOFT 0 to 32 moves the hairline 10.8 -> 30.9 units, +186%,
   while the stem moves 115.8 -> 117.3, +1.3%). So R30's condition fired and this
   file ships a three-axis subset, built from the OFL variable master already in
   this brand at ../petervarga-home/assets/fonts/fraunces-var.woff2 (no download,
   no new licence obligation), wght pinned to 300 per the one-weight-per-face
   rule, opsz + SOFT + WONK live, the SAME 222 codepoints the static face
   carries so coverage cannot regress:

     assets/fonts/fraunces-var-300.woff2   58,708 bytes   (47.8% of R30's 120 KB)
     fvar: opsz 9-144, SOFT 0-100, WONK 0-1     GSUB: liga, rvrn     GPOS: kern

   rvrn's three FeatureVariation records survive the subset, which matters: rvrn
   is what performs the optical glyph substitution across the seam, and a naive
   subset is the classic way to break it.

   THE HAIRLINE LAW, and why there is no @media (min-resolution) block.
   A role uses the highest optical size whose hairline still lands at or above
   1.05 device pixels on the ground it is set on; SOFT is raised only as far as
   that floor requires. Measured (hairline of `o`, units of 2000/em, sampled down
   the counter's centre line, then converted at the role's own size):

     role        opsz SOFT WONK   hair(u)   CSS px    @1x dev   @2x dev
     title 76px   144   32    1      30.9     1.17       1.17      2.34   clears
     title 35px   144   32    1      30.9     0.54       ---       1.08   clears
     stmt  43px    72   32    1      53.1     1.14       1.14      2.28   clears
     stmt  24px    72   32    1      53.1     0.64       ---       1.27   clears
     body  24px    24   20    0      64.9     0.78       0.78      1.56   see below
     body  17px    24   20    0      64.9     0.55       ---       1.10   clears

   Every display-side role clears 1.05 device px at 1x, so it also clears at 2x,
   and the one role that misses at 1x (body at 24px, 0.78) cannot be rescued at
   any dppx: reaching 1.05 device px at 24px needs 88 units and the face tops out
   at 88 units only at SOFT 100, which flattens stroke contrast to 1.7:1 and
   turns Fraunces into a slab. So the honest answer there is to stop climbing.
   The consequence is the useful one: NO dppx media query is needed anywhere,
   because no role's answer changes with device pixel ratio. Body SOFT 20 is not
   a hairline chase, it is references/taste.md's light-on-dark compensation ("one
   step more weight") spent on the only axis a single-weight face has left.

   WONK splits by register, which is the second measurable defect. The shipped
   face has WONK baked ON at every size (Fraunces' own rvrn rule only tames the
   alternates below about opsz 9.6), so running prose renders the wonky h/m/n/s
   forms that read as a signature at 76px and as a novelty face at 24px. WONK 0
   on the text register is the fix and costs nothing.

   Registers, per R30. Three, and a fourth would be a costume:
     DISPLAY    opsz 144, SOFT 32, WONK 1   h1 only
     STATEMENT  opsz  72, SOFT 32, WONK 1   .quest-quote only
     TEXT       opsz  24, SOFT 20, WONK 0   everything else (the page default)
   R30 allows the statement anywhere in opsz 72 to 96. It sits at 72, derived:
   the three registers have to be separable by eye, and separability is a
   measurable property of the advance. Mean advance over the real Mammatus
   statement string, em per character:
     opsz 144 -> 0.3501    opsz 96 -> 0.3553    opsz 72 -> 0.3997   opsz 24 -> 0.4375
   At opsz 96 the statement sits 1.5% from the title, which is not a register,
   it is a rounding error. At 72 it sits 14% clear of the title and 9% clear of
   the body, so all three read as different cuts without any size change doing
   the work. */

@font-face {
  font-family: 'Fraunces Deck';
  src: url('../assets/fonts/fraunces-var-300.woff2') format('woff2-variations');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
/* Metric-overridden fallbacks so the swap cannot reflow (R20's bar: "fonts
   staged so nothing reflows"). ascent/descent/line-gap are Fraunces' own typo
   metrics as fractions of its 2000 em, so the fallback's line box is identical
   to the real face's from first paint; size-adjust matches MEAN LOWERCASE
   ADVANCE, not x-height, because advance is what decides where lines break and
   therefore whether the swap moves anything. Measured from the binaries on this
   machine at the text register (opsz 24 / SOFT 20 / WONK 0):
     Fraunces 0.5114 em   Georgia 0.4991 em (102.5%)   Times New Roman 0.4593 em (111.4%)
   Two families rather than one because Georgia is 8.9 points closer and one
   size-adjust cannot serve both; a local() that does not resolve simply falls
   through to the next family, so the selection costs nothing. */
@font-face {
  font-family: 'Fraunces Deck G';
  src: local('Georgia');
  ascent-override: 97.80%; descent-override: 25.50%; line-gap-override: 0%;
  size-adjust: 102.5%;
}
@font-face {
  font-family: 'Fraunces Deck T';
  src: local('Times New Roman'), local('Times'), local('Tinos'), local('Liberation Serif');
  ascent-override: 97.80%; descent-override: 25.50%; line-gap-override: 0%;
  size-adjust: 111.4%;
}

:root {
  /* Redefined for quest pages only (this file loads nowhere else, and it loads
     AFTER grisaille.css). The static 'Fraunces' is deliberately NOT in the
     chain: the subset carries the identical 222 codepoints, so there is no glyph
     it could rescue, and leaving it out means a quest page never fetches it.
     Cold quest load: 58,708 instead of 34,512, so +24,196 bytes. Arriving from
     the path with fraunces-300 already cached: +58,708. Both stated. */
  --display: 'Fraunces Deck', 'Fraunces Deck G', 'Fraunces Deck T', 'Times New Roman', serif;

  /* The two-register modular scale (R30). Desktop 1.333 from an 18px base:
       18 · 24 · 32 · 42.7 · 56.9 · 75.8
     Mobile 1.2 from a 17px base:
       17 · 20.4 · 24.5 · 29.4 · 35.3
     Two ratios rather than one because a 390px frame cannot absorb a fourth:
     at 1.333 the fifth step is 56.9px and a 30-character title needs 10.79 em =
     614px against a 302px content box. The scale is picked to fit the frame.
     Roles take steps, not arbitrary numbers: title = top step (76 / 35),
     statement = two steps below it (42.7 -> 43 / 24.5 -> 24), and the body sits
     four steps below the title at both viewports (24 / 17). */
  --qp-title: 76px;
  --qp-statement: 43px;
  --qp-close: 32px;
  --qp-body: 24px;
  /* R36a FOLIO: one size for every folio surface on the deck, 13 desktop and 12
     phone, so cite, source, caption and the closing colophon are one register
     rather than four sizes of small print. */
  --qp-folio: 13px;

  /* Measures, in multiples of the role's OWN size so the character count is
     exact at every viewport. This is the trap the shipped file fell into:
     `.quest-panel-inner { max-width: var(--measure) }` is 34em resolved against
     .quest-panel-inner's inherited 16px = 544px, which at the 24px body is
     50.7ch, and `.quest-quote { max-width: 30em }` at 40px is 1200px capped by
     that same 544px = 31.8 characters -- a centred eight-line column, which is
     the single loudest "plain" signal on the deck. Writing the cap as a multiple
     of the role's own size token removes the em-resolution question entirely.
       body      26 x 24px = 624px / 0.4474 em-per-char at opsz 24 = 58.1ch
       statement 23 x 43px = 989px / 0.3997 em-per-char at opsz 72 = 57.5ch
     Advances measured from the real copy strings in this deck, not from a
     generic frequency table.

     R39a / EC1, and the two units are BOTH right about different things. The
     harness reads the ruled band in CSS `ch`, which is the advance of the digit
     zero, and this is a didone-adjacent face whose lining figures are far wider
     than its mean lowercase advance: measured, 1ch at opsz 24 is 14.953px, i.e.
     0.623em against the 0.4474em mean above, a 39% gap. So the same 624px column
     is 58.1 characters of this deck's real copy AND 41.7 CSS ch, and R39a's
     45-to-72 band is in the second unit. Neither number was wrong; the file was
     answering in a unit the ruling does not use, which is the more useful thing
     to record than the arithmetic.

     The body multiplier moves 26 to 30 to satisfy both readings at once, chosen
     by rendering the real paragraphs at 26/28/29/30/31/32 and counting line
     boxes rather than by modelling:
       26 -> 624px, 41.7ch  OUT of the band, 2 of 6 paragraphs inside R30's 52-62
       29 -> 696px, 46.5ch  in band by 1.5ch, 5 of 6
       30 -> 720px, 48.2ch  in band by 3.2ch, 5 of 6      <- shipped
       32 -> 768px, 51.4ch  in band, but only 3 of 6 (two lines run to 66 and 69)
     30 takes the band headroom without spending the character target, and the
     one paragraph outside 52-62 is venetian's three-liner, whose short last line
     drags the per-line average rather than its measure being wrong.
     Below 720px this is frame-bound and the token never binds: at 390 the
     content box is 301px, measured 28.4ch, inside R39a's 28-to-50 band for
     narrow media. Widening the desktop token cannot move it. */
  --qp-measure-body: calc(30 * var(--qp-body));
  --qp-measure-statement: calc(23 * var(--qp-statement));
  --qp-measure-close: calc(20 * var(--qp-close));

  /* The peek veil's ceiling per panel type. A media sliver stays far lighter
     than a text sliver (an artwork edge is genuinely inviting and a column of
     clipped words is not) but it is no longer zero: at zero the boundary between
     two graphite panels was a hard vertical seam with no depth at all. */
  --qp-veil-text: 1;
  --qp-veil-media: 0.18;

  /* Strip height. A floor, not a fixed height -- the strip is a grid row, so
     the deck above it takes whatever is left automatically and this number
     never has to be exactly right for the layout to hold. It is used as an
     approximation in one place only (the media max-height calc), where being
     a few px off costs a few px of video, never a broken frame. */
  --qs-h: 64px;
  /* The strip's three groups, as tokens rather than as numbers repeated in a
     layout rule and again in a hit-target rule. --qs-*-gap is read twice each:
     once as the flex gap and once as the width of the invisible target that
     tiles it, which is what lets adjacent 44px-tall targets touch without ever
     overlapping. Change the gap and the targets follow. */
  --qs-ch-d: 28px;                 /* chapter numeral, the circled mark */
  --qs-ch-gap: clamp(8px, 1.6vw, 18px);
  --qs-sq: 10px;                   /* R33d sub-stop plate */
  --qs-sub-gap: clamp(14px, 2vw, 26px);
  /* The return panel's receded plate. Height-driven so it keeps the deck's own
     plate proportions at every viewport, and capped in vh so it can never crowd
     the closing line on a short window. */
  --qr-cover-h: clamp(280px, 60vh, 520px);
  /* The sliver of the NEXT panel left visible at the right edge: the standing
     invitation to keep going sideways. Also where .qp-next sits. */
  --qp-peek: clamp(20px, 4.2vw, 72px);
  /* Panel gutter. Floored ABOVE the edge-arrow width so panel text can never
     sit underneath an arrow, at any viewport. */
  --qp-pad: clamp(34px, 5vw, 72px);
  /* A tighter gutter for the two panels that hold MEDIA rather than text. On a
     phone the plates are width-bound (a near-square artwork can never be
     height-bound at 390px), so every px of gutter came straight off the
     artwork: at --qp-pad the Mammatus plate stood 302px wide in a 754px-tall
     frame. Text keeps the wider gutter, because text is what an edge arrow
     would actually obscure; an arrow over the corner of a plate reads as an
     overlay control, which is what it is. On desktop both media panels are
     height-bound, so this changes nothing there. */
  --qp-media-pad: clamp(20px, 3vw, 56px);
  /* Clearance for .chrome-top, which is absolutely positioned over the frame
     and (because nothing scrolls now) behaves as fixed chrome. */
  --qp-top: clamp(62px, 9vh, 104px);
}

/* The chrome sits over WHICHEVER panel is showing, so its floor is the lightest
   ground on the deck, not the darkest. At the shipped 0.78 opacity it measured
   11.24:1 over black but only 3.72:1 over the steel details panel. Full-strength
   lime-white is 5.16:1 there and 18.71:1 over black, so one value clears every
   panel and the hover state moves the underline rather than the alpha. */
.quest-chrome-link {
  position: relative;
  font: 11px/1 var(--utility); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--lime-white); text-decoration: none; pointer-events: auto;
  white-space: nowrap;
  border-bottom: 1px solid transparent; padding-bottom: 2px;
}
.quest-chrome-link:hover, .quest-chrome-link:focus-visible { border-bottom-color: currentColor; }
/* R83d row 6c: same hit-area-overlay pattern as the chapter marks and the
   sub-stop plates above (1013-1022, 1077-1083), never a grown glyph or
   label. This control's own text box is already 44px wide before this
   rule (~147px at every width this site ships), so only height needs the
   overlay; width is left at 100% of the anchor rather than forced to a
   fixed number, since the label's own length already clears the floor. */
.quest-chrome-link::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 100%; height: 44px;
  transform: translate(-50%, -50%);
}

/* ---------- Page frame ---------- */

/* The document itself is exactly one viewport and never scrolls: the vertical
   axis belongs to the path, so a vertical gesture here either scrolls the text
   inside the current panel or (at the very end of the deck) rejoins the path,
   which is js/quest.js's WS3 return. svh, not vh: with no document scroll the
   mobile browser chrome can never retract, so the small viewport IS the
   visible one and vh would silently overshoot it. */
html, body { height: 100%; }
body { overflow: hidden; overscroll-behavior: none; }

.quest-main {
  height: 100%;
  display: grid;
  grid-template-columns: 100%;
  grid-template-rows: minmax(0, 1fr) auto;
  background: var(--black); color: var(--lime-white);
  overflow: hidden;
  /* The TEXT register is the page default and the two display roles override it,
     rather than the other way round. font-variation-settings inherits, so this
     one declaration reaches the numeral, the cite, the source, the caption, the
     dd and the strip numerals without any of them being listed. Schibsted has
     none of these axes and simply ignores the declaration.
     font-optical-sizing: none alongside it: the explicit 'opsz' already wins
     over the initial `auto`, but stating it makes the intent legible and stops a
     future edit that drops the variation string from silently reverting the
     whole deck to the browser's automatic mapping, which is the exact defect
     this pass exists to fix. */
  font-variation-settings: 'opsz' 24, 'SOFT' 20, 'WONK' 0;
  font-optical-sizing: none;
}

/* ---------- The deck ---------- */

.quest-track {
  grid-row: 1; grid-column: 1;
  display: flex; align-items: stretch;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;                 /* programmatic + fragment jumps only; a real swipe is never eased by this */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;                   /* the strip, the arrows and the peek sliver are the affordance */
  /* S13. The deck is a pager and a panel is a column, so the two axes belong to
     two different scrollers and the browser should never have to guess which one
     a touch meant.

     The axis-LOCKED form of this (pan-x here, pan-y on the panel) is the obvious
     spelling and it is wrong, measured rather than reasoned. touch-action
     intersects down the ancestor chain, so pan-x on the track against pan-y on
     the panel intersects to NONE for any gesture the track would handle. Driven
     with real CDP touch events at 390x568:
       axis-locked   vertical drag inside a panel: scrollTop 0 -> 133 (works)
                     horizontal drag across the deck: scrollLeft 0 -> 0 (DEAD)
       both axes     vertical: scrollTop 0 -> 133      horizontal: 0 -> 370
     So the deck could not be swiped at all under the literal form. Naming both
     axes on both elements keeps each scroller free on its own axis and still
     takes pinch-zoom and double-tap-zoom off the deck, which is the actual
     gesture collision on a phone and the whole point of the ruling. */
  touch-action: pan-x pan-y;
}
.quest-track::-webkit-scrollbar { width: 0; height: 0; }

.quest-panel {
  position: relative;
  flex: 0 0 calc(100% - var(--qp-peek));
  min-width: 0;
  scroll-snap-align: start;
  scroll-snap-stop: always;                /* one panel per flick: this is a pager, not a filmstrip */
  display: flex; flex-direction: column;
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior-y: contain;
  padding: var(--qp-top) var(--qp-pad) clamp(28px, 5vh, 64px);
  touch-action: pan-x pan-y;               /* S13: see the measurement on .quest-track */

  /* ---- the reveal's state, and its RESOLVED default (R30) ----
     Every one of these resolves to the finished frame with no script having
     run: --qp-lit 1 is full ink, --qp-draw 1 is the rule fully drawn. That is
     the Factory rule "Motion never produces the content" expressed as initial
     values rather than as a promise, and it is why the pre-JS state and the
     reduced-motion state need no separate code path. */
  --qp-lit: 1;      /* 0 = grisaille ghost, 1 = full ink. Written per BLOCK by js/quest.js */
  --qp-draw: 1;     /* the counter-rule's own progress, written per PANEL */

  /* The panel's own ground as three plain numbers. The ghost is not an opacity
     on the text: it is a real colour, mixed here, so getComputedStyle().color
     returns an honest rgb() at EVERY value of --qp-lit. That matters twice.
     An alpha ramp on text is invisible to a sweep that reads computed colour,
     so a mid-transition contrast failure would pass silently; and the deck's
     existing contrast gate (work/qa/close-approach.mjs PASS 6) parses
     getComputedStyle(el).color with /[\d.]+/g, which a color-mix() serialising
     as `color(srgb 0.38 0.37 0.37)` would feed 0-to-1 numbers into a 0-to-255
     formula. rgb() with calc() channels avoids both. */
  --qp-gr: 5; --qp-gg: 5; --qp-gb: 6;      /* --black #050506 */
  --qp-floor: 0.38;                        /* R30's ghost floor */
}
.quest-statement, .quest-return { --qp-gr: 10; --qp-gg: 10; --qp-gb: 11; }   /* --gesso    #0A0A0B */
.quest-narrative, .quest-motion { --qp-gr: 35; --qp-gg: 35; --qp-gb: 38; }   /* --graphite #232326 */
/* The details panel is the one place the ruled 0.38 floor does not clear R30's
   own 3.0:1 mid-transition bar, and the arithmetic says why rather than taste:
   on a mid-gray ground lime-white tops out at 5.16:1, so a 0.38 ghost measures
   2.08:1. The floor is raised to the smallest value that clears 3.0 with margin:
   0.64 measures 3.16:1. The floor is a derived number per ground, not a constant
   that happens to be 0.38 everywhere. */
.quest-meta { --qp-gr: 99; --qp-gg: 104; --qp-gb: 111; --qp-floor: 0.64; }   /* --steel    #63686F */

/* ---------- C7: the FOLIO ink, and its own derived floor ----------

   The kicker was the only element on an arriving panel at full strength while
   the prose it heads was still a ghost, so the panel's least important line led
   its arrival. Measured in flight by critic-c: kicker rgb(169,173,180) against
   both prose blocks at rgb(116,115,115).

   The reason it was excluded is in js/quest.js's own comment and it was correct
   arithmetic: the kicker's ink is --pewter, and pewter run down the prose floor
   of 0.38 measures 2.12 to 2.18:1 on the four grounds, under R30's 3.0:1
   mid-transition bar. What that shows is not "the kicker cannot rise", it is
   that a floor derived for ONE ink cannot be reused for a quieter one.

   So the folio ink gets its own floor, derived rather than picked. The ghost is
   a GREY, not an alpha, and the invariant is that the folio ghost sits at the
   same composited grey the prose ghost sits at. Solve for it on the luminance
   channel:

     ground + (247 - ground) * floor  ==  ground + (folio - ground) * ffloor
     ffloor = floor * (245 - ground_g) / (folio_g - ground_g)

   which yields, per panel, and MEASURED after the fact rather than asserted:

     ground      folio ink   ffloor   ghost ratio   full ratio
     black       pewter      0.561    3.40:1        9.04:1
     gesso       pewter      0.566    3.44:1        8.79:1
     graphite    pewter      0.578    3.30:1        6.96:1
     steel       lime-white  0.640    3.16:1        5.16:1

   Every value clears 3.0:1, and because the two ghosts land on the same grey the
   kicker can no longer be brighter than the prose it heads at ANY point in the
   transition, which is C7's actual complaint. The steel row falls out of the same
   formula for free: there the folio ink IS the prose ink, so the factor is 1 and
   ffloor collapses to that panel's own 0.64. No literal, no fourth number.

   If a browser cannot divide by a var() in calc the whole custom property is
   invalid, --qp-ffloor falls back to the prose floor, and the plain `color`
   declaration ahead of the ramp keeps the shipped full-strength pewter. The
   degradation is the old behaviour, never an unreadable one. */
.quest-panel {
  --qp-fr: 169; --qp-fg: 173; --qp-fb: 180;                 /* --pewter #A9ADB4 */
  --qp-ffloor: calc(var(--qp-floor) * (245 - var(--qp-gg)) / (var(--qp-fg) - var(--qp-gg)));
}
.quest-meta { --qp-fr: 247; --qp-fg: 245; --qp-fb: 241; }   /* --lime-white, the panel's own ink */

/* (THE RISE's own colour rule lives at the foot of this file, after every role
   rule, because it has to WIN against them: `.quest-narrative p` and
   `.quest-quote` both set an explicit colour, and a bare `.qp-rise` would lose
   the cascade to the first and tie-and-lose to the second.) */
/* The LAST panel is deliberately a full viewport wide. With every panel one
   peek narrower than the track, the scroller's maximum scrollLeft would stop
   short of the last panel's own snap position, so the deck could never land
   flush on it. Widening only the last one makes max scroll come out at exactly
   (n - 1) x panelWidth, which keeps EVERY snap position -- the last included --
   an exact multiple of one panel width. work/qa/close-approach.mjs PASS 8
   asserts that multiple, so this is a checked property, not a hope. */
.quest-panel:last-child { flex-basis: 100%; }
/* The leading edge of every panel after the first. The hairline alone was not
   enough: where two neighbours share a tone (graphite narrative followed by a
   graphite motion panel on quests II and III) the peek sliver showed the same
   colour as the panel you were already on, so the invitation to keep going
   sideways read as nothing at all. A soft depth gradient on the leading edge
   makes the sliver read as a panel EDGE regardless of what the two tones are.
   It is a background-IMAGE, not a colour: the element's own background does not
   move when its content scrolls (only background-attachment:local would), so it
   stays pinned to the edge, and getComputedStyle().backgroundColor still returns
   the real solid ground the contrast sweep in work/qa/close-approach.mjs reads.
   It only ever DARKENS, and every panel runs light text on its ground, so it
   can only move contrast upward -- never a hidden AA regression. It also fades
   out well inside --qp-pad, so no text ever sits on it. */
.quest-panel + .quest-panel {
  border-left: 1px solid rgba(247, 245, 241, 0.14);
  background-image: linear-gradient(90deg, rgba(5, 5, 6, 0.45), rgba(5, 5, 6, 0) clamp(20px, 3.5vw, 52px));
}

/* margin:auto rather than justify-content:center. Both centre a lone child in
   a column flex box, but justify-content clips the TOP of an overflowing child
   and puts it out of scroll reach; auto margins collapse to zero instead, so a
   panel whose text is taller than the frame simply becomes scrollable from its
   real first line. */
/* The measure is per PANEL, because the measure is per ROLE and each panel
   carries one. --measure (34em against an inherited 16px) is gone: see the
   :root note for why one shared em cap was wrong in two directions at once. */
.quest-panel-inner { margin: auto; width: 100%; max-width: var(--qp-measure-body); }
.quest-statement .quest-panel-inner { max-width: var(--qp-measure-statement); }
.quest-return .quest-panel-inner { max-width: var(--qp-measure-close); }

/* ---------- Panel: the work (hero) ---------- */

.quest-hero {
  align-items: center; justify-content: center;
  background: var(--black);
  overflow: hidden;                        /* the plate is height-fitted below, so this panel never needs to scroll */
  padding: var(--qp-top) var(--qp-media-pad) clamp(44px, 7vh, 92px);
}
/* Height-first fit against the panel's own definite height (the panel is a
   stretched flex item of the track, so a percentage max-height resolves here).
   The plate is therefore whole at first paint at every viewport -- no crop, no
   scroll to see the rest of it. */
.quest-hero img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto; object-fit: contain; margin: 0;
}
.quest-trail {
  position: absolute; left: var(--qp-pad); bottom: clamp(16px, 3vh, 30px);
  font: 11px/1 var(--utility); letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--pewter); margin: 0;
}

/* ---------- Panel: words ---------- */

.quest-statement { background: var(--gesso); }
.quest-chapter-head { text-align: center; margin-bottom: clamp(26px, 6vh, 72px); }
/* The numeral and the SOLD badge both shipped as dimmed text: opacity 0.7 and
   0.55 on lime-white (css/grisaille.css:290, :314), which measure 3.36:1 and
   2.71:1 on gesso, under the 4.5 floor every other element on this deck is held
   to. They are folio marks rather than voice, so they take the quiet ink at full
   strength instead of the loud ink dimmed: pewter measures 8.79:1 on gesso and
   6.96:1 on graphite, reads quieter than the title it sits above, and carries no
   opacity for a sweep to be blind to. Scoped to .quest-chapter-head so
   css/grisaille.css, which belongs to another writer, is untouched. */
.quest-chapter-head .numeral { margin: 0 auto; opacity: 1; color: var(--pewter); }
.quest-chapter-head .p-sold { margin-left: 16px; vertical-align: middle; opacity: 1; color: var(--pewter); }
/* DISPLAY register. opsz 144 is R30's ruling and is also the better hairline at
   both viewports: at 35px, opsz 144 / SOFT 32 gives 1.08 device px on a 2dppx
   phone where opsz 96 / SOFT 24 gives 1.01, under the floor. So one rule, both
   viewports, no dppx branch. Tracking -0.006em: taste.md's optical ramp is about
   -0.4px per doubling above 32px and 32 to 76px is 1.25 doublings, so -0.5px =
   -0.0066em at 76px; at 35px the ramp gives 0, which the mobile block sets.
   Leading 1.02: at opsz 144 the lowercase/cap ratio falls 0.664 -> 0.628, so the
   shipped 1.06 reads looser at the same number; 1.06 x (0.628/0.664) = 1.003,
   plus taste.md's light-on-dark leading compensation. */
.quest-chapter-head h1 {
  font: 300 var(--qp-title)/1.02 var(--display);
  font-variation-settings: 'opsz' 144, 'SOFT' 32, 'WONK' 1;
  letter-spacing: -0.006em; margin-top: 20px;
  text-wrap: balance;
}

/* Meta label register: tracked caps in the utility face. The 0.8 opacity is
   dropped and the token moves to pewter, which is a HIGHER ratio than the dimmed
   silver it replaces (8.79 against 7.59 on gesso, 6.96 against 6.38 on graphite)
   and removes an alpha the contrast sweep cannot see.

   Scoped through .quest-panel, and that is a fix rather than a habit. The
   material panel's kicker is a <p>, so `.quest-narrative p` (0,1,1) outranked a
   bare `.quest-kicker` (0,1,0) and dressed that one label in the BODY role:
   measured at 24px Fraunces with 0.096px tracking where every other panel's
   label is 11px Schibsted at 2.2px. It has been wrong since the shorthand was
   written and it was invisible because a serif label at body size still looks
   deliberate; this pass made it loud by flattening the body to 24px and drawing
   a rule above it. (0,2,0) puts the label register back in charge everywhere.
   Same reason for the cite and source rules below. */
.quest-panel .quest-kicker {
  font: 11px/1 var(--utility); letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--pewter); text-align: center; margin: 0 0 26px;
}
/* The one ornament this pass adds, and it is functional: a catalogue page has a
   rule at the head of its column, and this one's WIDTH is the panel's own
   arrival progress, so it draws itself across the measure as the panel lands.
   scaleX on a 1px block, so it is compositor-only and costs nothing per frame;
   default 1, so with no script it is simply a drawn rule. One per text panel. */
.quest-kicker::before {
  content: ''; display: block; height: 1px; margin: 0 0 16px;
  background: currentColor; opacity: 0.34;
  transform: scaleX(var(--qp-draw)); transform-origin: left center;
}
.quest-hero .quest-kicker::before { display: none; }

/* STATEMENT register. opsz 72 for register separability (see the :root note);
   SOFT 32 because at 43px a SOFT-0 hairline is 39.9u = 0.86 CSS px, under the
   1.05 device-px floor at 1x, and SOFT 32 lifts it to 53.1u = 1.14. Leading
   1.32: 1.34 x (0.655/0.674) for the opsz change, plus light-on-dark. */
.quest-quote {
  max-width: none; margin: 0 auto; padding: 0;
  font: 300 var(--qp-statement)/1.32 var(--display);
  font-variation-settings: 'opsz' 72, 'SOFT' 32, 'WONK' 1;
  letter-spacing: 0; text-align: var(--qp-axis); color: var(--lime-white);
  text-wrap: pretty;
}
.quest-quote p { margin: 0; }
/* Pitch P10: the quote's own paragraph belongs to the quote. MEASURED, not read:
   a .quest-quote placed in the Material panel (Mammatus, the series line) computed
   43px on the blockquote and 24px TEXT on the <p> inside it, because
   `.quest-narrative p` (0,1,1) sets font, colour and the TEXT axes on EVERY p in
   that panel, the quote's included, so the pull quote rendered as body copy in
   quotation marks. Same class of defect as the kicker note above: a panel-wide
   `p` rule outranking a role it was never written for. (0,2,1) hands every
   property the panel rule sets back to the quote, in any panel, and the Rise's
   colour ramp on the blockquote now reaches its text too. On the Words panel,
   where no panel rule touches the p, every value was already inherited, so this
   changes nothing there. */
.quest-panel .quest-quote p {
  font: inherit; letter-spacing: inherit; color: inherit;
  font-variation-settings: inherit; text-wrap: inherit;
}
/* ---------- B12: the attribution shares the quote's axis, by derivation ----------

   Shipped defect, VERIFIED at 390x844: the quote read `text-align: left` and both
   attribution lines under it read `center`, so one block carried two axes. It was
   not a missing declaration. The mobile block DID say
   `.quest-cite, .quest-source { text-align: left }` (0,1,0), and it lost to
   `.quest-panel .quest-cite { text-align: center }` (0,2,0) further up the file:
   a specificity mismatch, silent, and invisible to anyone reading the media query.

   Restating the override at a higher specificity would fix this instance and
   leave the class open, because the next axis change has to remember three
   selectors again. So the axis is now ONE value the statement panel owns and the
   three roles consume. There is no arrangement of this file in which the quote
   and its attribution can disagree, at any viewport, because there is only one
   declaration to change. */
.quest-panel { --qp-axis: center; }
/* ---------- R36a: FOLIO and CHROME, one rule each ----------

   The role map collapses twenty tracked-caps roles into two sans registers, and
   the deck was carrying four near-misses of them: cite at 13px/0.04em, source at
   11px/0.04em, caption at 13px/0.04em and the return footer at 12px/0.06em, all
   sentence case, all pewter, all almost but not quite one thing. FOLIO is
   13px desktop / 12px phone, leading 1.5, tracking 0.01em, sentence case, a
   NAMED ink and never an alpha. Listing the four surfaces in one selector is the
   point: a fifth folio line on a fourth deck cannot drift, because there is one
   declaration to add it to.

   The source line moves 11 -> 13/12 as part of this. It is the same role as the
   cite it sits under, and setting the two a step apart was the reason the block
   read as three sizes of small print rather than as one folio. */
.quest-panel .quest-cite,
.quest-panel .quest-source,
.quest-motion-caption,
.quest-return .site-foot p,
.quest-return .site-foot a {
  font-family: var(--utility); font-weight: 400;
  font-size: var(--qp-folio); line-height: 1.5; letter-spacing: 0.01em;
  text-transform: none; color: var(--pewter); opacity: 1;
  font-variation-settings: normal;
}

/* CHROME, the same treatment for the same reason. Both sans rules end with
   `font-variation-settings: normal`, and that is not decoration: the
   shorthand-repair rule at the foot of this file selects `.quest-narrative p`,
   which also matches `p.quest-source` and `p.quest-kicker`, so before this line
   two Schibsted roles were computing to a Fraunces TEXT triple by inheritance
   from a rule written for the prose beside them. It changed no pixel (Schibsted
   carries no opsz, SOFT or WONK) and it made every sans element on the deck
   answer a register question with a Fraunces answer, which is exactly the
   ambiguity R36a's row exists to remove. */
.quest-panel .quest-kicker,
.quest-meta dt,
.quest-trail,
.quest-chrome-link,
.qs-sub-name {
  font-variation-settings: normal;
}
.quest-panel .quest-cite {
  margin: 24px 0 0; text-align: var(--qp-axis);
}
/* Was --steel: 3.52:1 on gesso and 2.79:1 on graphite at 11px, the worst two
   values anywhere on the deck and both under AA. Pewter at full strength: 8.79
   and 6.96. It is still visibly the quietest thing on the panel. */
.quest-panel .quest-source {
  margin: 8px 0 0; text-align: var(--qp-axis);
}
.quest-source a { color: inherit; }

/* ---------- Panel: material ---------- */

.quest-narrative { background: var(--graphite); }
.quest-narrative .quest-kicker { text-align: left; }
/* TEXT register. Leading 1.60 is taste.md's "body at 1.6" with the light-on-dark
   step already in it; tracking +0.004em is the same rule's "a touch more
   tracking", which at 24px is +0.096px, the bottom of the useful range, which is
   where it belongs on a serif. WONK 0 is inherited from .quest-main. */
.quest-narrative p {
  font: 300 var(--qp-body)/1.60 var(--display);
  letter-spacing: 0.004em;
  color: var(--lime-white); margin: 0;
  text-wrap: pretty;
}
.quest-narrative p + p { margin-top: 20px; }
.quest-narrative { --qp-axis: left; }
.quest-narrative .quest-source { margin-top: 22px; }
/* Pitch P10: under a cite the source is the second line of one folio, so it keeps
   the Words panel's 8px pair gap; the 22px above is for a source that follows prose. */
.quest-narrative .quest-cite + .quest-source { margin-top: 8px; }
.quest-narrative .quest-chapter-head { text-align: left; margin-bottom: clamp(22px, 4vh, 48px); }
.quest-narrative .quest-chapter-head .numeral { margin: 0; }

/* THE PANEL HEAD (critic fix 5). Two of the three narrative panels were
   headless: swiping from Words to Material dropped from a composed spread to a
   panel whose largest element was an 11px label, while venetian's narrative
   panel already carried a proper head. A monograph text spread is quiet, but it
   is never headless: it keeps a running head or a section title so the reader
   knows which room they are in.

   The fix is NOT to promote the opening sentence. That is a magazine standfirst,
   it would be a fourth register against this deck's own three-register
   derivation, and it would break the catalogue schema unevenly because the three
   narrative panels do not carry the same content shape. Instead the panel's
   EXISTING kicker becomes the head, in the display register, in the position
   venetian's title occupies. No word is added, removed or reordered.

   It takes the display register rather than the statement register because the
   measurable is a head at or above 48px desktop and 30px mobile: the statement
   register is 43/24 and misses at both, the display register is 76/35 and
   clears both. It is an <h2>, not a styled <p>, because the statement panel
   already spends the <h1>, and it sits inside .quest-chapter-head so it joins
   the rise set for free and the panel arrives as one composed event rather than
   as a bright label with its content catching up. Colour is inherited rather
   than declared, which is what lets the rise reach it. */
.quest-narrative .quest-panel-head {
  font: 300 var(--qp-title)/1.02 var(--display);
  font-variation-settings: 'opsz' 144, 'SOFT' 32, 'WONK' 1;
  letter-spacing: -0.006em;
  color: inherit; margin: 0;
  text-wrap: balance;
}
/* The rule keeps doing its drawn-arrival job, beneath the head instead of above
   the label, which is where venetian's already sits relative to its title. */
.quest-narrative .quest-panel-head::after {
  content: ''; display: block; height: 1px; margin: clamp(18px, 3vh, 30px) 0 0;
  background: currentColor; opacity: 0.34;
  transform: scaleX(var(--qp-draw)); transform-origin: left center;
}

/* ---------- Panel: details ---------- */

/* session E fix, still standing: this section previously set color: var(--ink)
   (near-black) directly on var(--steel) (a mid-gray), measuring ~3.38:1 by the
   site's own contrast formula -- under the 4.5:1 AA floor every other quest
   section is held to. Fixed by keeping the same steel band (a deliberate
   mid-tone beat between the dark words/material panels and the return) but
   setting light text on it, matching every other panel's polarity. */
/* Session I: the session-E fix raised dd to lime-white and left the LABELS at
   opacity 0.72, which measures 3.55:1 on steel, under the same 4.5 floor the
   fix was made to clear. They were unswept because PASS 6 samples five
   hand-picked selectors and neither the label nor the kicker is one of them.
   Both go to lime-white at full strength: 5.16:1. The hierarchy that the
   dimming was carrying is already carried four other ways -- 11px against 24px,
   caps against sentence case, the utility face against the display face, and
   0.2em tracking against 0.004em -- so nothing is lost by removing the one
   carrier that was costing legibility. */
.quest-meta { background: var(--steel); color: var(--lime-white); }
/* text-align (critic fix 4): the label was centred over a flush-left definition
   list with a full-width rule above it, three alignments in one 120px band and
   the most obviously unresolved composition on the deck. .quest-narrative and
   .quest-motion both override the centred default; .quest-meta was simply
   missing the same declaration. */
.quest-meta .quest-kicker { color: var(--lime-white); text-align: left; }
.quest-meta dl { display: grid; gap: 22px; margin: 0; }
.quest-meta dt {
  font: 11px/1 var(--utility); letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--lime-white); margin: 0 0 6px;
}
/* dd joins the body role rather than sitting 6px below it for no stated reason.
   At the shipped 18px inside the shipped 34em cap it ran to 76 characters. */
.quest-meta dd {
  font: 300 var(--qp-body)/1.55 var(--display);
  letter-spacing: 0.004em; margin: 0;
}

/* ---------- Panel: the living still ---------- */

/* A solid ground, same principle as every other panel: the contrast sweep
   needs a real computed background-color, never a gradient. graphite matches
   quest-narrative's own tone so the motion beat reads as a continuation of the
   material story, not a new register. */
.quest-motion { background: var(--graphite); text-align: center; padding-inline: var(--qp-media-pad); }
.quest-motion .quest-panel-inner { max-width: none; }

/* ---------- C8: the rule heads the plate, not the panel ----------

   Measured before, venetian at 1440: the kicker's hairline ran 1234.6px over a
   434.3px clip, so the rule headed a column 2.84x wider than anything in it. On
   every other panel the rule and its content share one measure, which is what
   makes it read as a catalogue rule rather than a divider.

   CSS could not shrink-wrap to the answer. `width: fit-content` on the inner
   would size to the video's INTRINSIC 1248px, not to the height-bound 434px it
   actually renders at, because a replaced element's max-content contribution
   ignores max-height. So the plate's width is DERIVED instead, from the same
   three terms the clip was already budgeted against, and the video is then sized
   BY that value rather than beside it: one number, two consumers, no way for the
   rule and the plate to disagree at any viewport.

   --qp-plate-ar is the living stills' shared 1248x1664 (0.75), which is the
   generation recipe, not a coincidence of these three files. It is a literal, so
   it is guarded rather than trusted: the harness row compares the rendered rule
   width to the rendered media width within 5%, and a fourth living still cut at
   another ratio fails that row loudly instead of drifting. */
.quest-motion {
  --qp-plate-ar: 0.75;
  --qp-plate-h: max(200px, calc(100svh - var(--qs-h) - var(--qp-top) - 176px));
  --qp-plate-w: min(100%, calc(var(--qp-plate-h) * var(--qp-plate-ar)));
}
.quest-motion .quest-kicker, .quest-motion-caption {
  max-width: var(--qp-plate-w); margin-inline: auto;
}
/* The living stills are portrait (1248x1664). Inside the deck a panel is a
   fixed frame rather than an open column, so the clip is budgeted against the
   frame explicitly: full viewport, less the strip, less the top chrome
   clearance, less the kicker + caption + their margins. max() keeps the value
   sane (never negative, which would drop the declaration entirely) on a very
   short window; if it ever does not fit, the panel's own overflow-y is the
   honest fallback rather than a crop. */
.quest-motion-video {
  display: block; width: var(--qp-plate-w); max-width: 100%; height: auto;
  margin: 0 auto; background: var(--black);
}
.quest-motion-caption { margin: 20px auto 0; }

/* ---------- Panel: return ---------- */

.quest-return { background: var(--gesso); text-align: center; }
/* TEXT register at 32px, not a fourth register. B's threshold, measured: the
   swash forms read as a signature above 40px and as a mannerism below it, and
   the closing line sits at 32/20. So it takes WONK 0 and opsz 24 by inheritance,
   where its hairline is 1.01 device px at 1x, rather than the statement's cut,
   where at 32px it would have been 0.85 and under the floor. Leading 1.42 is the
   shipped 1.40 plus the same light-on-dark step applied everywhere else. */
.quest-return-line {
  font: 300 var(--qp-close)/1.42 var(--display);
  letter-spacing: 0; margin: 0 auto 30px; max-width: none;
  text-wrap: pretty;
}
/* ---------- A7/C6: one button language across index and deck ----------

   critic-a: "the boxed outline button is also the only outlined button on the
   site; the index CTA is a filled bar", and "the two button languages read as
   two different sites". VERIFIED in source: css/grisaille.css:586 sets .button
   as an INK FILL (`background: var(--ink)`) and the rule this replaces set
   `background: transparent` on the deck, so one site shipped two registers.

   The register is now identical: the size, weight, tracking, 20px/44px padding
   and square corner all come from .button itself and are not restated here. The
   only thing that inverts is which end of the spectrum the fill sits at, and
   that is not a style choice, it is the same rule applied to a different ground.
   The index CTA is ink on the light chamber-studio; a deck is near-black end to
   end, so an ink fill there is #101012 on #0A0A0B, invisible as a SHAPE at
   1.05:1, and its hover (ink text on transparent over gesso) disappears
   outright. Filled means filled in the panel's own polarity: lime-white bar,
   ink lettering, 18.17:1 as text and as a shape.

   Hover keeps the index's own gesture, an inversion, run the other way: the
   fill drops out and the outline stays. Same motion, same 220ms, opposite pole.

   `.quest-return .button` is now the deck's ONLY button class. The strip's two
   remaining control (.quest-chrome-link) is a chrome link, a different job and a
   different register, so A7's "every button resolves to one of two named classes"
   reads as one on this page and one on the index. */
.quest-return .button {
  margin-top: 0; position: relative; z-index: 1;
  background: var(--lime-white); color: var(--ink);
  border-color: var(--lime-white);
}
.quest-return .button:hover, .quest-return .button:focus-visible {
  background: transparent; color: var(--lime-white); border-color: var(--lime-white);
}

/* ---------- The work stands behind its own closing line ----------

   The panel was a sentence and a button on empty gesso (PROPOSAL-SECOND-HALF
   opening: `d-quest-return`), which is the one frame in the deck that carries no
   work at all, at the moment the visitor decides whether to go back to the path.
   So the piece they just spent four panels with is here, receded: the Rise run
   backwards across a navigation, ghost on the way out exactly as it was ghost on
   the way in.

   It is a PLATE, not wallpaper. Sized off the panel's own height so it keeps the
   deck's plate proportions, masked so it dissolves into the ground instead of
   ending on a rectangle, and it is a direct child of the panel, which is the
   whole mechanism by which it joins the deck's existing depth tiers: js/quest.js
   selects plate layers as `:scope > img`, so this takes TIER_PLATE 0.20 with no
   new script and no new selector, and the same slack clamp keeps it from ever
   sliding far enough to expose its own edge.

   Contrast is bounded by arithmetic, not by hoping the image is dark. Worst case
   is a pure-white source pixel: composited at opacity 0.22 over gesso it lands at
   RGB 63.9, and lime-white against RGB 63.9 is 9.2:1, twice AA, so no image this
   panel could ever carry can push the closing line under the bar. That is why
   the number is 0.22 and not taste. (The deck's contrast sweep reads
   .quest-return's COMPUTED background, which is still gesso; the real composited
   value is the one just derived, and it is pixel-sampled in the lane's evidence
   run rather than inferred.)

   Frame 0: it is markup with a src, so it is complete without script, and it is
   decorative (alt=""), so nothing the visitor needs is inside it. loading=lazy
   costs the deck nothing on arrival because it is the last panel. */
.quest-return-cover {
  position: absolute; z-index: 0; pointer-events: none;
  left: 0; right: 0; margin-inline: auto;
  top: 50%; margin-top: calc(-0.5 * var(--qr-cover-h));
  height: var(--qr-cover-h); width: auto; max-width: 78%;
  opacity: 0.22;
  /* The mask ellipse is INSCRIBED in the plate's own box (50% x 50% at the
     centre), not merely larger-and-softer. Measured on the venetian and
     powder-room decks at the first setting (74% x 68% off-centre): the plate's
     top edge still carried 49% mask alpha, so a 0.22 image over gesso drew a
     visible rectangle where the photograph's own wall is lighter than the
     ground. An inscribed ellipse reaches alpha 0 at every edge and every corner
     by construction, so no source image can print its own frame here. */
  -webkit-mask-image: radial-gradient(50% 50% at 50% 50%, #000 0%, #000 26%, transparent 100%);
  mask-image: radial-gradient(50% 50% at 50% 50%, #000 0%, #000 26%, transparent 100%);
}
.quest-return .quest-panel-inner { position: relative; z-index: 1; }
/* The footer used to sit below the strip at the very bottom of the document,
   where (a) nothing could scroll to it once the deck took over and (b) it was
   already rendering .site-foot's ink-on-light palette on a black ground, i.e.
   invisible. It now lives at the foot of the RETURN panel, in this page's own
   polarity. */
.quest-return .site-foot {
  margin: clamp(34px, 7vh, 76px) auto 0; max-width: var(--qp-measure-close);
  border-top-color: rgba(247, 245, 241, 0.16);
  color: var(--pewter);                    /* was rgba(...,0.62) = 7.19:1; pewter is 8.79:1 and carries no alpha */
  text-align: left; font-size: var(--qp-folio);
}
.quest-return .site-foot a:hover, .quest-return .site-foot a:focus-visible { color: var(--lime-white); }

/* ---------- Edge arrows (built by js/quest.js) ---------- */

/* Overlaid on the deck via the same single grid cell the track occupies, NOT
   placed inside the track: a child of a scroller scrolls away with it. The
   next arrow lands on the peek sliver, so the two affordances read as one
   thing -- "the next panel is right there".

   Built in script rather than written into all three pages so the three can
   never drift, and because they are a genuine enhancement: without JS the
   strip's fragment links and a swipe already reach every panel. */
.qp-arrow {
  grid-row: 1; grid-column: 1; align-self: center; z-index: 30;
  appearance: none; -webkit-appearance: none; background: none; border: 0; padding: 0;
  width: clamp(30px, 3.4vw, 44px); height: clamp(64px, 10vh, 96px);
  display: grid; place-items: center; cursor: pointer;
  color: var(--lime-white); opacity: 0.6;
  filter: drop-shadow(0 0 7px rgba(5, 5, 6, 0.6));   /* holds the 3:1 non-text floor over the steel details panel too */
  transition: opacity 220ms ease, transform 320ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* ---------- The peek veil (built by js/quest.js) ---------- */

/* Sits over the peek zone only -- exactly the sliver of the NEXT panel, never
   any part of the current one, so it cannot dim the current panel's own right
   margin. Densest at the outer edge and dissolving inward, so a text panel's
   sliver reads as the panel receding into the deck's ground rather than as a
   strip of cut-off words. js/quest.js drives the opacity: on for a text panel,
   OFF for a media panel (an artwork or living-still sliver reads fine at full
   strength), and OFF at max scroll where there is no next panel at all -- the
   case a mask-image on .quest-track could not have expressed, since it would
   have veiled the last panel's own right margin too.

   Colour is the deck's ground, not the panel's, so this is atmosphere rather
   than a colour-matched cover: the sliver dims toward the same black the whole
   quest sits on. It never touches text (it is outside every panel's gutter)
   and never affects the contrast sweep, which samples computed backgrounds. */
/* Session I: the veil now runs on the reveal's own law rather than on a clock.
   It was a binary switch with a 260ms ease, so the veil ran on time while the
   deck ran on position and the two were unrelated, which is a large part of why
   the edge read as a control rather than as depth. Now:

     veil = (1 - p_next) * veilMax(next panel)

   where p_next is the INCOMING panel's own presence, the same scalar that drives
   its type's rise. Travel toward a panel and its ink comes up while its veil
   retracts, off one number. The transition is deleted because a continuous
   function driven by position does not need an easing to hide a jump, and the
   easing was the one way the veil could be caught mid-fade at a rest position.
   veilMax is a coefficient rather than a boolean so a media sliver is a special
   case in DEGREE, not in kind (see --qp-veil-media in :root).
   Light in this deck is subtraction: this darkens, the leading-edge gradient
   darkens, and every panel runs light text on its ground, so no overlay here can
   move a contrast ratio downward. */
.qp-edge-fade {
  grid-row: 1; grid-column: 1;
  justify-self: end; align-self: stretch;
  width: var(--qp-peek);
  z-index: 20; pointer-events: none;
  opacity: var(--qp-veil, 0);
  background: linear-gradient(to left, rgba(5, 5, 6, 0.94) 0%, rgba(5, 5, 6, 0.72) 42%, rgba(5, 5, 6, 0) 100%);
}

/* ---------- The return affordance (S14, built by js/quest.js) ----------

   The WS3 "past the end of the deck, rejoin the path" gesture used to fire from
   inside touchmove the instant 40px of vertical travel had happened, so the page
   could navigate away underneath a gesture the visitor had not finished making
   and might not have meant. It now accumulates and commits on touchend at about
   90px, and this is the part that makes that honest: the accumulation is VISIBLE
   while it happens, so the gesture has a state the visitor can read and abandon.
   A hairline that draws across the foot of the frame, scaleX and opacity from
   the same 0-to-1 progress, so it is compositor-only and it is the same mark the
   kicker rule uses. No copy: the client-facing text on this deck is frozen. */
.qp-return-cue {
  grid-row: 1; grid-column: 1;
  align-self: end; justify-self: stretch;
  height: 2px; margin-bottom: 0;
  z-index: 35; pointer-events: none;
  background: var(--lime-white);
  opacity: calc(var(--qp-return, 0) * 0.85);
  transform: scaleX(var(--qp-return, 0)); transform-origin: center bottom;
}

.qp-prev { justify-self: start; }
.qp-next { justify-self: end; }
.qp-arrow:hover, .qp-arrow:focus-visible { opacity: 1; }
.qp-prev:hover { transform: translateX(-3px); }
.qp-next:hover { transform: translateX(3px); }
.qp-arrow[hidden] { display: none; }
.qp-arrow svg { width: 14px; height: 25px; display: block; }

/* ---------- The strip (the pager) ---------- */

/* A grid row of .quest-main rather than position:fixed. Fixed made sense while
   the page scrolled underneath it; now the deck and the strip are two rows of
   one frame, which also means the deck's height is exactly "whatever the strip
   did not take" with no padding compensation to keep in sync.

   Always a solid color (same reasoning as every panel above) so the contrast
   sweep reads a real value, not a blend. Text runs light on dark because every
   quest page stays in its own dark register end to end; there is no tone
   crossing here to invert against, unlike the main rail's nav. */
.quest-strip {
  grid-row: 2; grid-column: 1;
  position: relative; z-index: 40;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: clamp(14px, 2.6vw, 40px);
  min-height: var(--qs-h); padding: 10px var(--qp-pad);
  background: var(--gesso); color: var(--lime-white);
  border-top: 1px solid rgba(247, 245, 241, 0.10);
}

.qs-chapters { display: flex; align-items: center; gap: var(--qs-ch-gap); list-style: none; margin: 0; padding: 0; }
.qs-ch { display: flex; align-items: center; }
.qs-ch > a, .qs-ch-num {
  position: relative;
  font: 300 13px/1 var(--display); letter-spacing: 0.04em;
  display: grid; place-items: center; width: var(--qs-ch-d); height: var(--qs-ch-d);
  border: 1px solid currentColor; border-radius: 50%;
  color: inherit; text-decoration: none; opacity: 0.55;
}
.qs-ch.is-current .qs-ch-num { opacity: 1; border-width: 1.5px; }
.qs-ch > a:hover, .qs-ch > a:focus-visible { opacity: 1; }
/* The circle is the MARK, not the target. A 28px (26px on the phone) glyph is
   under every pointer-target floor, and growing the glyph to 44px would eat the
   height B11 exists to give back, so the target is an overlay that tiles the
   pitch and overflows the strip's own padding box without touching layout.
   Height is capped at the strip so two rows of targets can never overlap. */
.qs-ch > a::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: calc(var(--qs-ch-d) + var(--qs-ch-gap)); height: min(44px, var(--qs-h));
  transform: translate(-50%, -50%);
}

/* The sub-stops are now a SIBLING of the chapter list rather than nested
   inside the current chapter's <li>. Nesting expressed the containment nicely
   but made the mobile strip impossible to lay out (the row could not break
   between the numeral and its own children without stranding II and III on a
   third line); the containment is carried by aria-label instead. Nothing reads
   the nesting -- js/quest.js and PASS 8 both select .qs-substops a flat. */
.qs-substops {
  display: flex; align-items: center;
  gap: var(--qs-sub-gap); list-style: none; margin: 0; padding: 0;
  min-width: 0; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.qs-substops::-webkit-scrollbar { width: 0; height: 0; }
/* auto margins on the ends, not justify-content: centred when there is room,
   and never clipping the first item out of scroll reach when there is not. */
.qs-substops li { flex: 0 0 auto; }
.qs-substops li:first-child { margin-left: auto; }
.qs-substops li:last-child { margin-right: auto; }

/* ---------- R33d: the sub-stops are PLATES, never a second numeral run ----------

   The rail on the main page owns one numeral system (chapter numerals where
   chambers begin) and one plate system (a stroked box per work). The deck is the
   same grammar one axis over, so its chapter routes keep the numerals and its
   sub-stops take the plate: a 1px stroked square, filled when it is the one you
   are on. Five uppercase words became five marks, which is also what buys B11
   its single row (measured: the label run was 236px of a 354px content box at
   390; the plate run is 100px).

   The WORD is not deleted, it is clipped: `.qs-sub-name` keeps the frozen copy
   in the DOM, so the accessible name, the no-JS link text and any text-only
   client are exactly what shipped before, and nothing here needs an aria-label
   duplicating a string that already exists.

   Alpha is gone with the labels. R33a's ink rule ("hierarchy comes from size,
   length and the ring, never alpha") applies for the same reason it does on the
   rail: state is now filled-vs-hollow, which is a shape, so the 0.6 opacity that
   used to carry it has no job left and every mark sits at 18.17:1 instead of
   6.78:1. */
.qs-substops a {
  position: relative; display: block;
  color: inherit; text-decoration: none;
  width: var(--qs-sq); height: var(--qs-sq);
  border: 1px solid currentColor;
  background: transparent;
  transition: background-color 160ms ease;
}
/* the ONE thing on this strip that changes on its own, driven by js/quest.js's
   position tracking rather than a direct hover/click -- the only element here
   that actually needs a reduced-motion override, below. */
.qs-substops a.is-current { background: currentColor; }
.qs-substops a:hover::before, .qs-substops a:focus-visible::before {
  content: ''; position: absolute; inset: -3px; border: 1px solid currentColor;
}
/* Same target rule as the chapter marks: an overlay exactly one pitch wide, so
   five adjacent targets tile without a single overlapping pixel. */
.qs-substops a::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: calc(var(--qs-sq) + var(--qs-sub-gap)); height: min(44px, var(--qs-h));
  transform: translate(-50%, -50%);
}
/* The label the mark stands for. Clipped rather than display:none, which would
   take it out of the accessibility tree along with the pixels. */
.qs-sub-name, .qs-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.qs-end { display: flex; align-items: center; gap: clamp(12px, 2vw, 26px); }
/* `.qs-continue` was removed entirely by R41(a), markup and rule, at every
   width. Wave 1 hid it under 720px because it duplicated the href of the chapter
   mark beside it under a second label (critic-a A7's "at most one distinct label
   per href"), and kept it on desktop only because a close-approach row keyed on
   it; the ruling closes that and makes the deck one shape at every viewport.

   What carries each intent now, so nothing is silently lost:
     leave the deck for the path   `.quest-chrome-link` in the strip, and
                                   `.quest-return .button` on the last panel.
                                   Both are `a[href*="?return="]`; one destination.
     move to a sibling deck        `.qs-ch > a`, the chapter numerals, which
                                   already carried those exact hrefs.
   Venetian is the one real loss and it is named rather than absorbed: its
   `.qs-continue` pointed at `../../#studio`, a destination no chapter mark
   carries. The phone build has not had it since wave 1, chamber IV is reached by
   traversing the path, which is the spine of the site, and R41(a) rules one exit
   at every width. */

/* The `font` shorthand RESETS font-variation-settings to `normal` (CSS Fonts 4
   lists it among the properties the shorthand resets), so every role that sets
   its type with the shorthand silently drops the authored register and falls
   back to the browser's automatic opsz mapping, which is the exact defect this
   whole pass exists to fix. The two display roles escaped it only because they
   restate the axes inside their own rule, after the shorthand. This was caught
   by the harness reading computed values, not by reading the stylesheet, which
   is the argument for measuring rather than reasoning: the declaration was
   present, inherited, and doing nothing.
   One rule, listing every shorthand-using Fraunces role: the three in this file
   plus the numeral and the strip numerals, whose shorthands live in
   css/grisaille.css and are re-registered here rather than edited there. */
.quest-narrative p, .quest-meta dd, .quest-return-line,
.quest-chapter-head .numeral, .qs-ch > a, .qs-ch-num {
  font-variation-settings: 'opsz' 24, 'SOFT' 20, 'WONK' 0;
}

/* ---------- THE RISE ---------- */

/* On the main rail a piece enters as a flat bone-gray grisaille ghost and
   extrudes into full relief and true tone as the camera approaches
   (DESIGN-NOTES.md, "Signature mechanism"). On the deck a panel's words enter as
   a ghost of the same ink and take light as the panel arrives. Same law, same
   grammar, one axis over, nothing imported.

     a = floor + (1 - floor) * lit

   which is exactly an opacity ramp from the floor to 1 composited over the
   ground, written as a colour instead so a sweep can read it. Measured at each
   panel's floor: 3.25:1 on black, 3.31 on gesso, 3.30 on graphite, 3.16 on
   steel. No word on this deck is ever under 3:1, mid-transition included.

   ZERO DURATIONS. There is no transition on this property and there must never
   be one: the value is a pure function of the deck's scroll position, so a
   180ms flick compresses the reveal and a two-second drag stretches it, and both
   end at exactly 1. A transition here would be the one way to strand a line
   half-lit at a rest position, which is the failure the workspace memory
   `scroll-driven-entrances-strand-content` records.

   Placed here, at the foot of the file and scoped through .quest-panel, so it
   outranks the explicit colours on .quest-quote and .quest-narrative p. */
.quest-panel .qp-rise {
  color: var(--lime-white);                /* the resolved value, and the fallback if calc-in-rgb is unsupported */
  color: rgb(
    calc(var(--qp-gr) + (247 - var(--qp-gr)) * (var(--qp-floor) + (1 - var(--qp-floor)) * var(--qp-lit))),
    calc(var(--qp-gg) + (245 - var(--qp-gg)) * (var(--qp-floor) + (1 - var(--qp-floor)) * var(--qp-lit))),
    calc(var(--qp-gb) + (241 - var(--qp-gb)) * (var(--qp-floor) + (1 - var(--qp-floor)) * var(--qp-lit))));
}
/* The same law in the folio ink (C7). One rule, one shape, two floors, because
   the ink differs and the floor is derived from the ink. The kicker's own
   counter-rule is `background: currentColor`, so the hairline rises with the
   label it belongs to rather than drawing at full strength above a ghost. */
.quest-panel .qp-rise-folio {
  color: var(--pewter);                    /* the shipped value, and the fallback */
  color: rgb(
    calc(var(--qp-gr) + (var(--qp-fr) - var(--qp-gr)) * (var(--qp-ffloor) + (1 - var(--qp-ffloor)) * var(--qp-lit))),
    calc(var(--qp-gg) + (var(--qp-fg) - var(--qp-gg)) * (var(--qp-ffloor) + (1 - var(--qp-ffloor)) * var(--qp-lit))),
    calc(var(--qp-gb) + (var(--qp-fb) - var(--qp-gb)) * (var(--qp-ffloor) + (1 - var(--qp-ffloor)) * var(--qp-lit))));
}
/* Focus beats choreography. A Tab into a panel that is not the current one
   scrolls it into view, and focus can land while its type is still a ghost.
   js/quest.js stops writing to a panel that contains focus, which is what makes
   this rule reachable at all: an inline custom property would otherwise win. */
.quest-panel:focus-within { --qp-lit: 1; --qp-draw: 1; }

@media (prefers-reduced-motion: reduce) {
  /* instant panel jumps: the pager still pages, it just does not travel */
  .quest-track { scroll-behavior: auto; }
  .qs-substops a { transition: none; }
  .qp-arrow { transition: none; }
  .qp-prev:hover, .qp-next:hover { transform: none; }
  /* The type is resolved, always. `!important` because js/quest.js writes
     --qp-lit as an inline custom property on each block, and an inline custom
     property beats a class rule: this is the one place the stylesheet has to be
     able to overrule the script, so that a reduced-motion visitor cannot see a
     ghost even for the frame before the script notices the preference. Same for
     the counter-rule and the return cue. */
  .quest-panel, .qp-rise, .qp-rise-folio { --qp-lit: 1 !important; --qp-draw: 1 !important; }
  /* The veil itself stays, unchanged from the session-H reasoning: it is a
     static fade rather than motion, and it is a pure function of position, so
     there is nothing here that a reduced-motion visitor needs protecting from.
     Only its old time-based cross-fade is gone, and that is gone for everyone. */
}

@media (max-width: 720px) {
  :root {
    --qs-h: 90px;
    /* The mobile half of the scale: 17 x 1.2^n. The title takes step 4 rather
       than step 5 because a 390px frame cannot absorb a fourth ratio, and the
       body sits four steps below the title exactly as it does on desktop, which
       is what makes the hierarchy read the same at both sizes. */
    --qp-title: 35px;
    --qp-statement: 24px;
    --qp-close: 20px;
    --qp-body: 17px;
    --qp-folio: 12px;
  }
  .quest-chapter-head h1, .quest-narrative .quest-panel-head { letter-spacing: 0; line-height: 1.06; }
  .quest-quote { line-height: 1.36; }
  .quest-return-line { line-height: 1.5; }
  .quest-narrative p, .quest-meta dd { line-height: 1.62; }
  /* Below 720px the statement's measure is frame-bound, not choice-bound: the
     content box is 390 - 20 (peek at its clamp floor) - 68 (two --qp-pad) =
     302px, and --qp-pad floors at 34px because it is derived above the edge-arrow
     width, so it is not available as a lever. At 24px that is 31.5 characters.
     A centred 31-character column reads as an accident; an even ragged right at
     a short measure reads as intentional, so the statement left-aligns here. */
  .quest-panel { --qp-axis: left; }
  .quest-chapter-head { text-align: left; }
  .quest-chapter-head .numeral { margin: 0; }
  /* ---------- B11: one row, one exit ----------

     Measured before: 90px of an 844px viewport (12.4% of the frame, and a
     stacked THREE visual rows because .qs-end ran as a column), carrying two
     competing links to two different places, one underlined and one not. The
     first thing a phone visitor meets after entering a work was chrome.

     Three changes, in the order they buy height:
     (a) the sub-stops are plates (R33d above), so the row that could not fit
         beside anything is now 100px wide instead of 236px and rejoins line 1;
     (b) exactly ONE exit. `.qs-continue` duplicated the href of the chapter mark
         beside it under a second label (critic-a A7's "at most one distinct label
         per href"); R41(a) removed it outright at every width, so the numeral
         keeps the lateral route and the close CTA is the only control on this bar
         that leaves the deck. Nothing viewport-specific is left in this lever;
     (c) with one row and one exit the height is content-driven again:
         26px of mark inside 2 x 11px of padding = 48px, and --qs-h is set to
         the same 48 so the token and the rendered box cannot disagree.
         48 + the 1px hairline = 49px against B11's 56px ceiling: the 7px of
         slack is deliberate, so a longer exit label on a future deck can grow
         the bar by a line-height without immediately breaking the gate.

     The three-column grid is the DESKTOP grid, unchanged. Desktop keeps both
     exits and its 64px calm; nothing above this media query moved. */
  :root {
    --qs-h: 48px;
    --qs-ch-d: 26px;
    --qs-ch-gap: 7px;
    --qs-sq: 9px;
    --qs-sub-gap: 11px;
  }
  .quest-strip { gap: 11px; padding: 11px 16px; }
  /* R83d row 6a/6b. sq+gap (the formula above, 1018-1022 and 1079-1083) is
     also the real flex gap, so it is what keeps adjacent marks tiling with
     no overlap; widening IT to reach 44px would need --qs-sub-gap at 35px
     (nearly 3x today's 11px), which does not fit the B11 budget above (five
     marks would need 185px of a strip whose middle grid column measures
     about 97px at 390px, forcing the row into its own horizontal scroll at
     rest). A flat 44px width overlay, decoupled from the real gap token,
     buys the floor without moving a single visible pixel; it costs the old
     "zero overlap between adjacent marks" property this formula used to
     have for free, between marks in the SAME list (measured in
     notes/quest.md, "## W4"; left as is, since it is two adjacent marks
     that already agree where an overlapping tap should go). */
  .qs-ch > a::after, .qs-substops a::after { width: 44px; }
  /* The same 44px overlay, centred by default, bleeds into the NEXT
     column's own hit area at the two seams where one list ends and another
     begins: chapters' "III" into the sub-stops' "Work" (measured 11.4px),
     and the sub-stops' own "Return" into the chrome link (measured 2.4px).
     Fork D: never let two DIFFERENT controls' hit areas overlap silently.
     Shrinking either gap token enough to close an 11px gap would need the
     chapters numerals nearly touching, so the fix is not the gap, it is
     the centring: the marks that border a seam get a one-sided overlay,
     growing only toward their own list, never toward the next control.
     The Return/chrome-link seam needed only Return's own side rewritten,
     since the chrome-link's overlay is already flush with its own anchor
     (width: 100% of the link, so zero horizontal bleed either way, see
     the rule above). The III/Work seam sits between two marks that BOTH
     default to the centred box, so re-pointing only III's growth left
     (measured) cut the overlap from 11.4px to 3.4px rather than clearing
     it: Work's own leading edge still reached back on its own. Work gets
     the mirrored treatment, growing only right into its own list, same
     lever as III, opposite direction; re-measured at 0px cross-list
     overlap after both. Every other mark keeps the centred overlay
     above; each seam needed exactly as many one-sided marks as it had
     bleeding sides, one or two. */
  .qs-chapters > .qs-ch:last-child > a::after,
  .qs-substops > li:last-child > a::after {
    left: auto; right: 0;
    transform: translateY(-50%);
  }
  .qs-substops > li:first-child > a::after {
    left: 0; right: auto;
    transform: translateY(-50%);
  }
  /* R36a correction. The first cut of this bar bought its room by dropping the
     exit to 10px/0.12em, which put the site's one remaining phone control
     OUTSIDE the CHROME band (11 to 12px, 0.16 to 0.22em) and made the strip a
     sixth register. The room comes out of the gaps instead, which are not a
     register: chapter gap 8 -> 7, sub-stop gap 13 -> 11, group gap 14 -> 11 and
     page padding 18 -> 16 return 20px, more than the 15px the larger label
     costs. The bar stays one row and stays under 56px; measured after. */
}

/* Pitch P8, trimmed by R22 (manager, 2026-09-23): the cross-document view transition that
   lived here was removed with its index.html half (a return from a quest faded through the
   protected opening). The tabular figures that sat here were removed too: in this face they
   widen commas and periods. */
