/* ==== base/tokens.css ==== */
/* ==========================================================================
   A · TOKENS
   --------------------------------------------------------------------------
   The whole palette, one place. Everything below this file in the bundle is
   allowed to assume these exist and is not allowed to invent new ones.

   THE TEXT TOKENS ALL CLEAR WCAG 2.2 AA (4.5:1) against all four page grounds
   — bone, bone-2, #ece0c9, #f3edde. An earlier pass had small text sitting at
   2.3–3.9:1: the topnav, the kickers, tags, map labels and
   signoff were every one of them failing. 0xac is the alpha floor (4.65:1 on
   the darkest ground). Do not reintroduce a lighter alpha for TEXT.

   The rule tokens are separate on purpose. A 1px non-text boundary needs 3:1,
   not 4.5:1, and hairlines set at text contrast look like underlines. Use
   --rule / --rule-2 for borders and separators, never for a glyph.
   ========================================================================== */
:root{
  /* Scroll progress rest state. The engine (main.js) writes --p per .sticky
     stage every frame, but only once it has booted — so on first paint, and on
     any element read before the first tick, a BARE var(--p) is invalid and its
     calc() collapses to the property's initial value. For #hero .germ that
     initial is opacity:1, which flashed the Germania woodcut to a full-strength
     multiply mass (read as "the building flashes black") before snapping to its
     .13 rest. A global 0 default makes every var(--p) resolve to the at-rest
     look until the stage that owns it overrides with a real value. */
  --p:0;

  --bone:#efe7d7;
  --ink:#1b2436;
  --night:#0b1020;

  /* text */
  --ink-70:#1b2436b3;       /* editorial body — 5.29:1 */
  --ink-60:#1b2436b8;       /* 5.33:1 */
  --ink-55:#1b2436b0;       /* 4.86:1 */
  --ink-45:#1b2436ac;       /* 4.65:1 — smallest type on the page */
  --ink-40:#1b2436ac;       /* 4.65:1 */

  /* non-text */
  --rule:#1b243638;
  --rule-2:#1b243673;
  --ink-22:#1b243638;       /* deprecated alias of --rule */

  --bone-2:#e7dcc7;
  --oxblood:#8a3324;
  --gold:#e9c46a;

  --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
  --serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  /* Titles only — the condensed titling serif loaded in §B. Kept separate from
     --serif because body copy wants the wider, lower-contrast face: Instrument
     Serif has one weight and tight hairlines, which is right for six words at
     6rem and wrong for a paragraph. The local stack is the fallback while the
     woff2 swaps in, so an unstyled first paint is still a serif. */
  --display:"Instrument Serif","Iowan Old Style",Palatino,Georgia,"Times New Roman",serif;

  --pad:8vw;
  --mx:0; --my:0;           /* pointer, -1..1, written by the engine */

  /* THE RAIL GUTTER — now permanently 0, and kept only as a name.

     It used to be the width of the column the fixed chapter rail owned: the
     rail was position:fixed, so it was outside flow and content knew nothing
     about it, and between about 820 and 1180px it landed inside live text —
     the contact address, the menu prices, the origin paragraph. Reserving a
     92/114px lane on the right was the fix.

     The rail is gone, so the lane is too, and every right-hand inset now
     matches its left-hand partner: the page is symmetrical again and the
     editorial frame centres in the whole viewport rather than in what was left
     of it. The three names below are load-bearing across a dozen section
     stylesheets, so the token stays at 0 rather than being deleted — one line
     to re-open a right-hand lane if anything ever needs one again. */
  --rail-gutter:0px;
  --pad-r:calc(var(--pad) + var(--rail-gutter));

  /* THE EDITORIAL FRAME — ordinary left- or right-aligned narrative shares one
     1180px frame, centred in the viewport. Below that width the familiar --pad
     inset still wins; centred cards, captions and stage furniture continue to
     use --pad / --pad-r instead. */
  --content-max:1180px;
  --frame-start:max(var(--pad),calc((100vw - var(--rail-gutter) - var(--content-max))/2));
  --frame-end:calc(var(--frame-start) + var(--rail-gutter));
}


/* ==== base/fonts.css ==== */
/* ==========================================================================
   B · FONTS
   --------------------------------------------------------------------------
   Instrument Serif (SIL OFL, github.com/Instrument/instrument-serif), used for
   section titles. Chosen by measuring it against 113 free serifs on outline
   overlap with the custom face Fornasetti commissioned from Alessio D'Ellena
   — it won by 18 points, because it is the only free serif that is CONDENSED
   (H is 0.71× cap height; Playfair is 0.95×, Prata 0.98×). That proportion is
   the whole reason a titling serif looks editorial rather than generic.

   SELF-HOSTED, not linked from fonts.googleapis.com — same reasoning as the
   vendored HTMX: a title should not depend on a third party being up, and the
   Google CSS costs an extra DNS + TLS handshake before the first glyph. Four
   files, 47 kB total.

   Because we ship the binaries rather than link them, we are redistributing
   them, and the OFL requires its own text to travel with the files: it is at
   public/fonts/OFL.txt. Do not delete it as stray weight — it is the licence
   the four woff2 files are here under.

   THERE IS ONLY ONE WEIGHT. Instrument Serif ships 400 regular + 400 italic
   and nothing else, so §D sets headings to `font-weight:400`. Do not put a
   500 or a 700 on a title: there is no bolder file to load, so the browser
   would synthesize one by smearing the outline, which on a high-contrast
   serif thickens the hairlines into mud. If a title ever genuinely needs more
   weight, it needs a different family, not a heavier number.
   ========================================================================== */

/* latin-ext first, latin second: unicode-range makes this a filter, not a
   cascade, so both are candidates and the browser downloads only the subsets
   the page's characters actually fall into. Ordinary English copy fetches the
   latin file alone. */
@font-face{
  font-family:"Instrument Serif";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("/test-site-v0.3/fonts/instrument-serif-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,
    U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,
    U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:"Instrument Serif";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("/test-site-v0.3/fonts/instrument-serif-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,
    U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* The italic is a real cut, not a slant — worth having because §hero's deck and
   the 1877 caption are both set in italic serif. */
@font-face{
  font-family:"Instrument Serif";
  font-style:italic;
  font-weight:400;
  font-display:swap;
  src:url("/test-site-v0.3/fonts/instrument-serif-italic-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,
    U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,
    U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:"Instrument Serif";
  font-style:italic;
  font-weight:400;
  font-display:swap;
  src:url("/test-site-v0.3/fonts/instrument-serif-italic-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,
    U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}


/* ==== base/reset.css ==== */
/* ==========================================================================
   A · RESET
   Deliberately three rules. A full normalize would be more code than the
   difference it makes on a page whose every element is styled here anyway.
   ========================================================================== */
*{box-sizing:border-box;margin:0;padding:0}
/* NO scroll-behavior:smooth HERE. It was global, and the page is 20 screens: a
   header click from the hero to the visit section animated ~18,000px as one
   uninterruptible ride, which is a long time to watch the site flick past and
   no way to tell where you ended up. Distance is the deciding factor and CSS
   cannot see distance, so the choice moved to the delegated click handler in
   main.js — smooth for a hop of two screens or less, instant beyond that.
   With JS off every link is an ordinary instant jump, which is the correct
   fallback rather than a degraded one. */
html{background:var(--bone)}
body{
  background:var(--bone);
  color:var(--ink);
  font-family:var(--serif);
  overflow-x:hidden;
}
svg{display:block;color:currentColor}


/* ==== base/chrome.css ==== */
/* ==========================================================================
   B · FIXED CHROME — wordmark, header links
   markup: partials/chrome.eta   ·   copy: site.json → nav

   The `.dark` class is written on <body> by the engine when an ink-ground
   section owns the viewport, and every flip in this file hangs off it. That is
   the entire mechanism: one class, set from the section registry's `dark`
   field, and the chrome inverts.
   ========================================================================== */

/* ---------------- skip link ----------------
   First thing in the tab order, invisible until focused. Cheaper than it was
   now that the chapter rail is gone — three header links rather than fifteen —
   but a keyboard reader still shouldn't have to tab the header to reach copy. */
.skip{position:fixed;top:-100px;left:var(--pad);z-index:100;
   font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;
   background:var(--ink);color:var(--bone);padding:.85rem 1.3rem;text-decoration:none;
   border-radius:2px;transition:top .18s ease}
.skip:focus-visible{top:26px;outline:2px solid var(--gold);outline-offset:2px}
/* the skip target. It takes focus so the NEXT tab continues from the content
   rather than from the top of the header, but it is a 20-screen-tall wrapper and
   a ring drawn around it would be a border around the whole page. */
#main:focus{outline:none}

/* ---------------- fixed mark ----------------
   The mark and the topnav are fixed and sit directly on whatever section is
   behind them — no plate, no scrim. body.dark flips their ink. */
.mark{position:fixed;top:26px;left:var(--pad);z-index:80;line-height:0}
.mark img{width:clamp(52px,4.4vw,68px);height:auto;mix-blend-mode:multiply;
   transition:filter .6s,opacity .6s}
/* on the ink-ground sections the ink wordmark would vanish — flip it to cream */
.dark .mark img{mix-blend-mode:screen;filter:invert(1) brightness(1.6)}
.mark:focus-visible{outline:2px solid var(--oxblood);outline-offset:6px}

/* ---------------- header links (balance the mark) ----------------
   inset to var(--pad) so they mirror the wordmark on the left; the 36px flex
   height matches the mark's box so both sit on one axis. */
.topnav{position:fixed;top:26px;right:var(--pad);z-index:80;
   height:36px;display:flex;align-items:center;gap:clamp(1.1rem,2.2vw,2rem)}
/* THE INK IS 10.5px; THE TARGET IS 44px. These are two different measurements
   and only the first one is a design decision. The links were 34×12 and 42×12
   — a third of the 44×44 minimum in both axes — because the box was the glyph.
   Padding grows the box without touching the type; the negative margin keeps
   the row's optical alignment where it was, so nothing moved on screen. */
/* THE CHROME'S INK IS ITS OWN TOKEN, because the chrome is the only place on
   the page where the ground is not a constant. --ink-45 is stated once in
   tokens.css and its comment gives its contrast as 4.65:1 — which is measured
   against bone, and true for every OTHER place it is used (the kicker numeral,
   the caption, the contact labels) because all of those sit on a section's own
   flat ground.

   The chrome does not. #night crossfades its ground, so the plate behind these
   links moves too, and the engine now clamps it at 30% of that fade (main.js,
   see the note beside ch.now). At the clamp the plate is rgb(171,166,160) and
   --ink-45 gives 3.05:1 on it — measured on the built page, not derived.

   Full-strength ink on the same plate is 6.43:1, and on ordinary bone it is
   12.63:1 rather than 4.89. So this is not "make it darker to pass a check":
   the 45% alpha exists to hold 10.5px of mono BACK from the copy it sits
   beside, and the chrome has no copy beside it — it sits in open margin over
   whatever the page happens to be doing. There is nothing here for it to
   defer to. The weight it was buying is bought instead by the size, the
   letterspacing and the caps.

   Kept as a variable rather than inlined so the two places the chrome states
   its ink cannot drift apart. */
.topnav{--chrome-ink:var(--ink)}
.topnav a{font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;
   color:var(--chrome-ink);text-decoration:none;transition:color .4s;
   display:flex;align-items:center;min-height:44px;padding:0 .35rem;margin:0 -.35rem}
.topnav a:hover{color:var(--oxblood)}
.topnav a:focus-visible{outline:2px solid var(--oxblood);outline-offset:4px}
.dark .topnav a{color:#efe7d7d8}
.dark .topnav a:hover{color:var(--gold)}
.dark .topnav a:focus-visible{outline-color:var(--gold)}

/* THE CHAPTER RAIL IS GONE. It was a fixed column of twelve links down the
   right-hand margin, and everything it needed is removed with it: the per-link
   feathered plates (each asking what was behind ITSELF, because a 418px column
   straddles chapter boundaries), the .on / .over-dark states main.js wrote per
   frame, and --rail-gutter, the 114px lane every content column had to leave
   empty from 761px up. The gutter token survives at 0 in tokens.css so the
   --pad-r / --frame-end names still resolve; see the note there. */


/* ==== base/chapter.css ==== */
/* ==========================================================================
   C · CHAPTER SHELL — the sticky-stage mechanic every chapter shares
   markup: page.eta (the <section> wrapper is written there, from the registry)

   A chapter with a `height` in the registry gets a runway that tall and one
   screen pinned inside it. Scroll through the runway, the pinned screen stays,
   and --p runs 0→1 across it. That is the only animation primitive on the site;
   every section's motion is a different reading of the same number.
   ========================================================================== */
.chapter{position:relative}
.sticky{position:sticky;top:0;height:100vh;overflow:hidden;display:flex;align-items:center}
.stage{position:absolute;inset:0;z-index:1;pointer-events:none}
/* The generic copy shell uses the viewport/stage insets. Ordinary narrative
   opts into .editorial so it shares the capped frame while cards, captions and
   centred compositions keep their authored geometry. */
.copy{position:relative;z-index:20;width:100%;padding:0 var(--pad-r) 0 var(--pad)}
.copy.editorial{padding-inline:var(--frame-start) var(--frame-end)}
.copy.center{text-align:center;padding-inline:var(--pad) var(--pad-r)}
.copy.center .body{margin-inline:auto}

/* placement modifiers — so the frame moves chapter to chapter, not just the
   mechanic. Without these every section is the same shot at a different speed. */
.copy.high{align-self:flex-start;padding-top:13vh}
.copy.low{align-self:flex-end;padding-bottom:14vh}
.copy.right{text-align:right}
.copy.right .body{margin-left:auto}
/* caption: type recedes to a label so the stage owns the frame */
.copy.caption{align-self:flex-end;padding-bottom:12vh;text-align:center}
.copy.caption .body{margin-inline:auto}
.copy.caption h2{font-size:clamp(1.8rem,3.6vw,2.7rem)}
.copy.caption .sub{font-size:clamp(.95rem,1.3vw,1.1rem)}

/* ---- the layer system ----
   .layer  = pointer parallax (depth --d, in px per unit of pointer travel)
   .motion = scroll/effect, written per section
   .anchor = centring, so an element that centres itself does NOT also carry
             .layer's transform. That collision is real: an ID rule's
             translate(-50%,-50%) silently wins and kills the depth. #collection
             .obj / .pmot is the same split, which is why the collage reads. */
.layer{position:absolute;will-change:transform;
  transform:translate3d(calc(var(--mx)*var(--d,10)*1px), calc(var(--my)*var(--d,10)*1px), 0)}
.anchor{position:absolute;left:var(--ax,50%);top:var(--ay,50%);width:var(--aw,auto);
  transform:translate(-50%,-50%)}
.anchor > .layer{position:relative;left:auto;top:auto;right:auto;bottom:auto;width:100%}
.drift{animation:drift var(--dur,18s) ease-in-out infinite}
@keyframes drift{
  0%,100%{transform:translateY(-3px) rotate(var(--rot,0deg))}
  50%{transform:translateY(3px) rotate(0deg)}
}


/* ==== base/type.css ==== */
/* ==========================================================================
   D · SHARED TYPE
   The classes every section is allowed to use without declaring anything. A
   section may re-scale these (§3 holds h2 down to 3.4rem because it carries a
   sentence, not a title) but should not re-invent them.
   ========================================================================== */
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.32em;text-transform:uppercase;
   color:var(--oxblood);margin-bottom:1.1rem}
.kicker .n{color:var(--ink-45)}

/* ---- .ink, NOT .dark ------------------------------------------------------
   THE INVERTED TYPE IN THIS FILE HANGS OFF `.ink`, WHICH THE ENGINE WRITES ON
   EACH CHAPTER, NOT OFF `.dark`, WHICH IT WRITES ON <body>.

   They are not the same question. body.dark means "the section under the fixed
   chrome is ink" — it exists for the wordmark and the topnav, which float over
   whatever is at the top of the screen. A section's own heading is not floating
   over anything; it is inside one section and should follow that section.

   Reading body.dark here was a bug that only a phone could show. On desktop
   every dark chapter is a 100vh sticky stage, so the viewport owner IS the only
   section on screen and the two agree. At 390px they stop agreeing: sections
   are as tall as their content and two share the screen, so when the ink
   #footer took the viewport centre the bone #contact plate was still half
   visible — and every rule below fired on it. "COME IN." went cream on bone and
   effectively disappeared; the kicker went gold on bone beside it.

   `.ink` is per chapter and set from the same threshold, so a dark section
   inverts its own type whether or not it owns the middle of the screen, and a
   light section never inverts because of its neighbour.
   -------------------------------------------------------------------------- */
.ink .kicker .n{color:#efe7d7c4}
/* THE KICKER'S OWN COLOUR INVERTS TOO, and it is this file's job rather than
   each dark section's. Every other class here has an .ink pair below; .kicker
   had one for its .n child and none for itself, so oxblood stayed oxblood on
   ink. #ceiling and #rooms each patched it to gold locally and looked fine,
   which is exactly what hid the hole: #night never patched it, and its kicker
   sat at 4.42:1 halfway through the bone→night transition and 3.09:1 at the
   end of it. Both fail AA 4.5:1, measured off rendered pixels rather than off
   the declared token — the token was never the problem.

   Gold, not cream: the two sections that already solved this chose gold, it is
   the accent the dark half of the page uses everywhere else (the 1877
   constellations, .hand b), and it holds 9.3–10.8:1 on ink. A local
   override is still allowed — this is the floor, not a ceiling. */
.ink .kicker{color:var(--gold)}

/* Section titles are the display serif (§B). font-weight is 400 because that
   is the only weight that exists — see the warning in fonts.css.

   THE THREE VALUES BELOW ARE ONE DECISION, NOT THREE. Titles are set in caps,
   and caps are not lowercase with the shift key held — the letterforms sit in a
   different box, so tracking and leading both have to move with the transform.
   Change one of these and re-check the other two.

   letter-spacing is POSITIVE (+.015em) where mixed case wanted negative. Caps
   have no ascenders or descenders to interlock and no round counters to close,
   so the sidebearings a lowercase fit relies on aren't there; at 6rem the
   flat-sided letters (H·I·L·M·N) butt together without a little air. This is
   the opposite of the -.015em that was here before Instrument Serif arrived.

   line-height TIGHTENS from .95 to .84. Nothing reaches below the baseline in
   caps and nothing reaches above the cap line, so the ink occupies about 72% of
   the box Palatino's mixed case filled — leaving .95 would open a visible gap
   between the two lines of every title that carries a <br>. */
h1,h2{font-family:var(--display);font-weight:400;text-transform:uppercase;
   line-height:.84;letter-spacing:.015em}
h1{font-size:clamp(3rem,9vw,7.5rem)}
h2{font-size:clamp(2.6rem,7.5vw,6rem)}

.sub{font-style:italic;color:var(--ink-60);font-size:clamp(1.02rem,1.7vw,1.35rem);margin-top:.7rem}
.body{max-width:32ch;margin-top:1.4rem;font-size:1.06rem;line-height:1.62;color:#1b2436c0}
.tag{margin-top:1.1rem;font-family:var(--mono);font-size:10px;letter-spacing:.06em;
   color:var(--ink-45);text-transform:uppercase}

/* An unanswered fact, marked as one. Not a design element — these are meant to
   be conspicuous until someone fills them in. */
.tk{font-family:var(--mono);font-size:.86em;letter-spacing:.08em;
    text-transform:uppercase;color:var(--oxblood);
    border-bottom:1px dashed currentColor;padding-bottom:1px}

/* A hyphenated PROPER NOUN, held on one line. A plain U+002D is a break
   opportunity, and no CSS property refuses it — hyphens, word-break and
   overflow-wrap were each measured against "Over-the-Rhine" at 390px and all
   six values broke it identically, because none of them governs an explicit
   hyphen. Only marking the span does.

   Scoped to a class rather than applied to every hyphen on the page, because
   most of them SHOULD break: "sixteen-foot ceilings", "a hundred-voice singing
   society" and "twenty-eight rooms" all break at 320px and are correctly set
   that way — they are compounds in running prose, and refusing them would cost
   a ragged line to fix nothing. A neighbourhood's name is not a compound. */
.nb{white-space:nowrap}

.ink h1,.ink h2{color:#f2ecdd}
.ink .sub{color:#efe7d7aa}
.ink .body{color:#efe7d7bb}
.ink .tag{color:#efe7d7c4}

.cue{position:absolute;bottom:28px;left:50%;transform:translateX(-50%);z-index:30;
     font-family:var(--mono);font-size:10.5px;letter-spacing:.28em;color:var(--ink-45);
     text-transform:uppercase}
.ink .cue{color:#efe7d7c4}


/* ==== sections/hero.css ==== */
/* ==========================================================================
   §1 · PROLOGUE — the building dissolves under the wordmark
   markup: sections/hero.eta   ·   copy: 01-hero.json

   ONE screen, not two. The Germania sits low and scroll DRIVES it: it swells
   and lifts as it fades out from under the wordmark.
   ========================================================================== */
/* the ground the fixed chrome borrows while this chapter owns the viewport —
   see the note in base/chrome.css. The hero paints no background of its own,
   so this states what body's --bone already puts behind it. */
#hero{--ground:var(--bone)}
/* THE BUILDING HOLDS AND LINGERS — it does not dissolve to nothing.
   Earlier this faded `.13 - var(--p)*.13`, ending AT ZERO, over a 120vh section
   whose sticky pin is only 20vh (height - 100vh) — so the germ reached 0 within
   ~180px of scroll and the emptied hero then scrolled away for another 900px.
   That read exactly as the complaint: "the building fades away to nothing almost
   immediately." Two changes fix it, on explicit direction:

   1. The section is 190vh (see the registry), giving a ~90vh pin that matches
      the other chapters, so the hero HOLDS as you arrive instead of leaving at
      once. The rhythm is now uniform, not an 8×-shorter outlier.
   2. The range bottoms out at .05, not 0 (`.13 - var(--p)*.08`): the drawing
      stays a faint presence through the whole hold and only recedes as the copy
      scrolls off, rather than evaporating under the wordmark.

   It still must never darken enough to compete with the wordmark — .13 at rest
   is the ceiling for that, and .05 is deliberately barely-there, not absent. The
   motion-germ scale/translate below is the hold's "second thing to do" so the
   longer pin is not dead time. */
/* POINTER PARALLAX, RESTORED. The germ is a `.layer` with --d:6 in the markup,
   so chapter.css's .layer rule should have given it pointer drift — but that
   rule sets `transform`, and so does this one (the centring translate), and
   `#hero .germ` outspecifies `.layer`, so the pointer transform was silently
   dropped. That is the exact ID-beats-.layer collision the note in chapter.css
   warns about, and it left the FIRST thing on the page dead to the mouse.
   Folding both into one transform fixes it: centre first, then drift by the
   pointer (var(--d)=6, the depth the markup already declares). .motion-germ
   (the scroll scale) is a child element, so it never collides with either. */
#hero .germ{left:58%;top:100%;will-change:transform;
   transform:translate(-50%,-58%) translate3d(calc(var(--mx,0)*var(--d,6)*1px),calc(var(--my,0)*var(--d,6)*1px),0);
   width:min(74vw,880px);opacity:calc(.13 - var(--p)*.08)}
#hero .germ img{width:100%;display:block;mix-blend-mode:multiply}
#hero .motion-germ{transform:scale(calc(1 + var(--p)*.12)) translateY(calc(var(--p)*-3vh))}
#hero .copy{max-width:none}

/* The h1 is a wrapper for the drawing and nothing else — §D's display clamp,
   caps transform and tracking would all apply to an empty box and only serve to
   add a line of leading above the wordmark. */
.wordmark-h1{font-size:0;line-height:0;letter-spacing:0;margin:0}
/* A whisper of COUNTER-drift on the title, opposite the building and a third
   its distance: the germ (--d 6) drifts one way, the wordmark 2px the other, so
   the two separate in depth under the pointer instead of sliding as one flat
   plane. Small on purpose — the title is the letterform and must stay crisp. */
.wordmark{width:min(52vw,500px);margin:0 auto;mix-blend-mode:multiply;display:block;will-change:transform;
   transform:translate3d(calc(var(--mx,0)*-2px),calc(var(--my,0)*-2px),0)}
.wordmark-fig{width:min(18vw,175px);margin:.2rem auto 0;mix-blend-mode:multiply;display:block}

.hero-line{margin-top:1.8rem;font-size:clamp(1.15rem,2.3vw,1.7rem);font-style:italic;color:var(--ink)}
.hero-where{margin-top:.5rem;font-family:var(--mono);font-size:11px;letter-spacing:.24em;
   text-transform:uppercase;color:var(--ink-55)}

/* the three phases, stated up front — bar now, delicatessen soon, rooms later */
.phasing{margin-top:2.6rem;display:inline-flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:0}
.phasing .ph{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
   color:var(--ink-55);padding:0 1.15rem;position:relative}
/* THE SEPARATOR HANGS OFF THE ITEM AFTER IT, NOT THE ONE BEFORE.
   It was ::after on :not(:last-child) — correct on one line, wrong the moment
   the row wraps: at 390px the third phase drops to its own line and the rule
   belonging to the second is left dangling in the white space at the end of the
   line above. A separator drawn on the LEFT of every item except the first
   cannot end a line, because the thing it is attached to starts one. Same three
   marks between the same three items when they fit; nothing trailing when they
   don't. The `left:0` sits on the padding edge, so the gap either side is the
   same 1.15rem it always was. */
.phasing .ph:not(:first-child)::before{content:"";position:absolute;left:0;top:50%;width:1px;height:11px;
   background:var(--rule);transform:translateY(-50%)}
.phasing .ph b{color:var(--ink);font-weight:400;font-family:var(--serif);font-style:normal;
   font-size:1.02rem;letter-spacing:0;text-transform:none;display:block;margin-bottom:.28rem}
.phasing .ph .now{color:var(--oxblood)}


/* ==== sections/ceiling.css ==== */
/* ==========================================================================
   §2.5 · THE CEILING — you look up into the painting
   markup: sections/ceiling.eta   ·   copy: 03-ceiling.json   ·   data: fresco-bands.json

   The one photograph on the site that behaves like a place rather than a
   picture. The section identifies the ceiling and its painter while scrolling
   dollies a camera into the fresco.

   WHAT MAKES IT A DOLLY AND NOT A ZOOM
   The depth in the image is the depth the artist PAINTED — the vortex. So the
   plate is sliced into nine concentric shells about the vortex's eye, each
   scaled about that same eye. Every shell grows as you scroll, but the OUTER
   ones grow more, so they sweep outward past the camera while the deep centre
   barely moves. That differential is the parallax; there are no lateral
   offsets at all.

   Nothing is cut out of the plate. The band alphas are a partition of unity —
   they sum to 1 everywhere — so at zero motion the nine shells recomposite to
   exactly the source photograph. No holes, no inpainting, no ghost sockets.

   THE VIGNETTE IS A SIBLING OF .cscene, NOT A CHILD — deliberately.
   Only .cscene takes the dolly and the pointer tilt. Nesting .cgrade inside it
   would make the vignette inherit those transforms and swim with the ceiling;
   it must stay nailed to the viewport so the frame holds still while the
   ceiling opens through it.
   ========================================================================== */
#ceiling{--ground:#14161a;position:relative;background:#14161a;color:var(--bone)}

/* Overscan AND aspect. Every shell is positioned as a percentage of this box,
   so the box must carry the plate's own 4:3 (1448×1086) or the whole fresco is
   stretched to whatever shape the viewport happens to be. An earlier version
   used inset:-9%, which inherits the viewport ratio — barely visible on a
   laptop, and on a 390×844 phone it smeared the vortex into a vertical column.
   So: cover-fit the plate, then overscan by 18%, because the dolly pulls outer
   edges inward and the corners must never go black.
     width  = max(118vw, 118vh × 4/3)
     height = max(118vh, 118vw × 3/4)
   Centred with the `translate` property, not transform — the tilt writes
   cscene.style.transform every frame and would clobber it. */
#ceiling .cscene{position:absolute;left:50%;top:50%;translate:-50% -50%;
   width:max(118vw,157.334vh);height:max(118vh,88.5vw);z-index:1;
   perspective:1400px;transform-style:preserve-3d}
#ceiling .clayer{position:absolute;will-change:transform,filter;
   transform-origin:50% 50%;pointer-events:none;backface-visibility:hidden}
#ceiling .clayer img{display:block;width:100%;height:100%}

/* vignette + a warm bloom at the eye. Siblings — see the note above. */
#ceiling .cbloom{position:absolute;inset:0;z-index:2;pointer-events:none;
   background:radial-gradient(60% 45% at 46% 46%,rgba(255,225,190,.10),rgba(0,0,0,0) 70%);
   mix-blend-mode:screen}
#ceiling .cgrade{position:absolute;inset:0;z-index:3;pointer-events:none;
   background:
     /* the main vignette — .90 at the corners is the principal lever */
     radial-gradient(115% 88% at 50% 50%,rgba(0,0,0,0) 28%,rgba(6,5,9,.62) 72%,rgba(4,3,7,.90) 100%),
     /* a second, tighter pass so the four corners fall off faster than the edge
        midpoints. That difference is what reads as a lens rather than a fade. */
     radial-gradient(78% 70% at 50% 50%,rgba(0,0,0,0) 55%,rgba(3,2,6,.55) 100%),
     linear-gradient(180deg,rgba(20,14,30,.26),rgba(0,0,0,0) 34%);
   mix-blend-mode:multiply}
/* A FLOOR UNDER THE COPY — AND THE COPY IS TALLER THAN THE FLOOR WAS.
   The bottom gradient alone was transparent above 48% of the frame. The copy
   block starts at 70% (measured: kicker at 70.2%, h2 at 66.7%, .sub at 47.6%,
   all of it in a 900px viewport), so the three largest things in the section
   — including the title — were sitting directly on unmodified fresco. It read
   as fine at any single scroll position and was luck rather than design: the
   ground under one line of that title measured 1.05:1 in one place and 13.9:1
   in another, because the dolly moves cloud through it. The most variable
   surface on the site was carrying the most important type on it.

   So there are two layers now, doing two different jobs:

   1 · THE POOL is the copy's own ground — an ellipse struck at the
       bottom-left where the copy actually lives, tall enough to reach over the
       kicker and feathered to nothing by 88% so it never shows an edge. It is
       off-centre on purpose: the copy column is left-aligned and the vortex's
       eye is right of centre, so the shadow falls where the words are and
       leaves the painting's subject alone.

   2 · THE FLOOR is the original full-width bottom wash, kept for .hand and
       .cue, which sit outside the pool's reach at the foot of the frame.

   Both in the same layered `background` rather than a second element: this is
   already a sibling of .cscene for the reason at the head of this file, and one
   more absolutely-positioned overlay for a job a comma can do would be a third
   thing to keep nailed to the viewport. */
#ceiling .cfloor{position:absolute;inset:0;z-index:4;pointer-events:none;
   background:
     radial-gradient(66% 60% at 19% 63%,#0b0a09cf 0%,#0b0a09a6 40%,#0b0a0952 70%,transparent 88%),
     linear-gradient(0deg,#0b0a09d9 0%,#0b0a0966 24%,transparent 48%)}

#ceiling .copy{z-index:20}
#ceiling .kicker{color:var(--gold)}
#ceiling .kicker .n{color:#efe7d7c4}
#ceiling h2{color:#f2ecdd}
#ceiling .sub{color:#efe7d7aa}
#ceiling .body{color:#efe7d7bb;max-width:34ch}
#ceiling .cue{color:#efe7d7c4}

/* the attribution, set as an engraved caption rather than body copy */
#ceiling .hand{margin-top:1.5rem;font-family:var(--mono);font-size:10px;
   letter-spacing:.16em;text-transform:uppercase;color:#efe7d799;line-height:1.7;
   max-width:26ch}
#ceiling .hand b{color:var(--gold);font-weight:400}


/* ==== sections/menu.css ==== */
/* ==========================================================================
   §5 · THE MENU — bill of fare
   markup: sections/menu.eta   ·   copy: 04-menu.json

   A card that rises into place as the runway scrolls. Two columns, leader dots
   between name and price — the typographic convention for a bill of fare, which
   is doing the work a heading would otherwise have to do.
   ========================================================================== */
#menu{--ground:#e7dcc7;background:#e7dcc7}
/* Desktop placement of the two relics. This used to be an inline style on the
   markup, which meant responsive.css could never move them — see menu.eta. */
#menu .relic:first-child{left:8%;top:20%;width:min(12vw,150px)}
#menu .relic:last-child{right:7%;bottom:16%;width:min(13vw,160px)}
#menu .relic img{width:100%;height:auto;mix-blend-mode:multiply}
/* Same correction as the visit plate: both terms measure the VIEWPORT, but the
   parent .copy is inset by --pad / --pad-r. At 1075px the 80vw term overtook the
   860px cap, the card grew past its container, and the leader-dot prices — the
   right edge of a bill of fare — overran the right inset. `100%` is the term
   that respects it. */
#menu .card{position:relative;z-index:20;width:min(80vw,860px,100%);margin:0 auto;
   transform:translateY(calc((1 - var(--p))*40px));opacity:calc(.62 + var(--p)*.38)}
#menu .head{margin-bottom:2.4rem}
/* The clamp MINIMUM comes down 2.4rem → 2rem for caps, and it is the floor that
   matters here rather than the vw term: .card is min(80vw,860px), so this
   heading's box tracks the viewport, and 6vw only overtakes 2rem above 533px.
   The floor therefore governs the whole 375–533px band, where "AND SOMETHING TO
   EAT." in caps needs 32px — 2.4rem (38.4px) wrapped the authored second line
   into a third. Below 375px responsive.css takes over with a smaller value
   again. Ceiling and vw term unchanged; this heading was never the problem on a
   wide screen. */
#menu h2{font-size:clamp(2rem,6vw,4.4rem)}
#menu .cols{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,6vw,5rem);text-align:left}
#menu .col h3{font-family:var(--mono);font-size:12px;letter-spacing:.3em;text-transform:uppercase;
   color:var(--oxblood);padding-bottom:.9rem;margin-bottom:1.1rem;border-bottom:1px solid var(--rule-2)}
#menu .item{display:flex;align-items:baseline;gap:.55rem;margin:.85rem 0}
#menu .item .nm{font-size:1.08rem;color:#1b2436d8}
#menu .item .dots{flex:1;border-bottom:1.5px dotted var(--rule-2);transform:translateY(-.28em)}
#menu .item .pr{font-family:var(--mono);font-size:.92rem;color:var(--ink-60);letter-spacing:.02em}

/* --------------------------------------------------------------------------
   THE BILL WRITES ITSELF — scroll reveal, CSS only, no module.

   §5b reveals its rows by dealing them up from below, one after another. This
   section is deliberately NOT that. Here the card is already in place (it rises
   as a whole, above) and what the runway does is DRAW the bill: each column's
   rule runs out from the left, then the leader dots stretch from the name
   toward the price, and the price lands last — the order a hand would write a
   line of a menu in. So both sections stagger, but one moves paper and this one
   moves ink, and the two read as different mechanics rather than one repeated.

   Nothing here is design. No size, weight, colour, grid or spacing is touched:
   every rule below animates a transform or an opacity that finishes at its
   authored value. --rp is the per-item progress, 0 → 1, driven off the stage's
   --p with a per-column head start (--c) and a per-row step (--i), so the left
   column is well down its list before the right one starts.

   The two constants are set against the LONGEST column, not against the count
   the section happens to ship with: at eight rows the left column's last row
   opens at .21 and the whole bill has settled by .57, so there is headroom for
   more drinks without the tail running off the end of the runway. If a column
   ever grows past about twenty rows, drop the .03 step rather than the .24
   column offset — the offset is what makes this read column by column.

   --p is seeded to 0 in tokens.css and pinned to 1 under Reduce Motion, so
   1 is the settled state and first paint is valid. Below 900px the columns
   stack but the chapter stays pinned, so --p is still live and the deal simply
   runs top to bottom — no override needed.
   -------------------------------------------------------------------------- */
#menu .item{--rp:clamp(0, calc((var(--p) - (var(--c,0) * .24 + var(--i,0) * .03)) / .3), 1)}
/* The column rule is drawn rather than faded: the real border goes transparent
   and an identical 1px line is scaled out from the left in its place. At
   --rp 1 the two are pixel-identical, so this changes when the rule appears
   and nothing about what it looks like. */
#menu .col h3{--rp:clamp(0, calc((var(--p) - var(--c,0) * .24) / .26), 1);
   position:relative;border-bottom-color:transparent}
#menu .col h3::after{content:"";position:absolute;left:0;right:0;bottom:-1px;
   border-bottom:1px solid var(--rule-2);
   transform:scaleX(var(--rp));transform-origin:left center}
/* Names ink in from the left. A 6px horizontal nudge, not §5b's vertical
   drop — same family of motion, different axis, which is the whole point. */
#menu .item .nm{opacity:var(--rp);
   transform:translateX(calc((1 - var(--rp)) * -6px))}
/* The leader stretches out of the name. transform-origin is left because the
   dots belong to the dish, not to the price. The translateY is the authored
   baseline correction from the rule above and has to be carried along. */
#menu .item .dots{transform:translateY(-.28em) scaleX(var(--rp));transform-origin:left center}
/* The price arrives once the leader has reached it — a second window that only
   opens over the back half of the row's own reveal. */
#menu .item .pr{opacity:clamp(0, calc((var(--rp) - .55) / .45), 1)}


/* ==== sections/hors.css ==== */
/* ==========================================================================
   §5b · FOOD FOR THE FINGERS — the hors d'œuvres board
   markup: sections/hors.eta   ·   copy: 05b-hors.json

   Reads as the second page of the bill of fare next door, on a ground half a
   step lighter than §5's so the boundary between them is a page turn rather
   than a seam. #menu is #e7dcc7; this is #efe6d2, which sits between it and
   --bone (#efe7d7) — the same family, decisively not the same field.

   THE SHAPE IS DIFFERENT FROM §5 ON PURPOSE. §5 is a centred card of one-line
   rows: name, leader dots, price. Eight dishes that each need a line of
   description cannot be that card — it would be sixteen lines of alternating
   weight down the middle of the screen. So this is a two-column BOARD, left
   aligned, with the head in its own column on desktop and the plates filling
   the other. Same leader-dot convention on each row, because that convention
   is what says "menu"; everything around it moves.

   No local colour, size or family is invented here. Everything is a token or a
   shared class from base/type.css — .kicker, h2, .body, .tag — with the same
   two departures §5 already makes: the column headings and the prices are the
   mono face at the sizes menu.css uses, so the two sections set numbers
   identically.
   ========================================================================== */
#hors{--ground:#efe6d2;background:#efe6d2}

/* ---- the relics ---------------------------------------------------------
   Placed HERE and not inline on the markup. An inline style beats every rule
   short of !important, which is how §5's mobile overrides spent a while being
   dead code that still looked correct in the source — see the note in
   menu.eta. Only --d stays inline there and here.

   Both are pushed well outside the board's own column: at 1440 the board is
   1180px in a viewport whose margins are 8vw, so there is roughly 60px of true
   margin at each side and these sit past it, reading in the corners of the
   frame rather than behind the type. */
#hors .relic{width:min(11vw,132px)}
/* BOTH SIT LOW, and the left one is the reason. The head column is the tallest
   thing on the left of this section but it does not fill it: the title, the
   label and four lines of body end around 55% of the stage, leaving the bottom
   half of that column empty. Placed at top:14% the relic was not in the margin
   at all — it was directly behind "FOOD FOR", 50px from the left edge at 1440,
   which is inside the title's own box. The empty half is where it goes. */
#hors .r-a{left:4%;bottom:14%}
#hors .r-b{right:4%;bottom:11%}
#hors .relic img{width:100%;height:auto;mix-blend-mode:multiply;opacity:.5}

/* ---- the board ----------------------------------------------------------
   width uses the same three-term min() as #menu .card, and for the same reason
   documented there: 100% is the term that respects the parent .copy's own
   insets rather than the raw viewport. Drop it and the prices — the right edge
   of a bill of fare — overrun the right inset in the 820–1180px band. */
#hors .board{position:relative;z-index:20;width:min(92vw,1180px,100%);margin:0 auto;
   display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr);
   gap:clamp(2rem,6vw,5.5rem);align-items:start}

#hors .head{align-self:start}
/* The head gives more width to the actual menu than to itself. Its title scale
   was solved against that narrower column at 901, 1180 and 1440px; 4.2vw keeps
   the authored two lines intact without making the left side heavier than the
   eight-item board. The 901–1050 band gets a second, smaller solution in
   responsive.css, where the menu needs still more of the available width. */
#hors .head h2{font-size:clamp(2rem,4.2vw,4rem)}

/* The French, under the English title. The display serif's italic is a real
   cut (fonts.css §B), so this is set in it rather than in --serif: the title
   above is the same family and the label reads as its subtitle rather than as
   a different voice. Not .sub — that class is sized for a sentence, and this
   is two words doing the work of a category heading. */
#hors .alt{font-family:var(--display);font-style:italic;font-size:clamp(1.25rem,2.1vw,1.8rem);
   color:var(--oxblood);margin-top:.85rem;letter-spacing:.01em}

#hors .head .body{max-width:34ch}

/* ---- the plates ---------------------------------------------------------
   An <ol> because the board has an order — it runs from the one-bite pick to
   the thing you eat with a spoon — and a screen reader saying "list, 8 items"
   is the correct description of a menu. list-style is off; the numbering is
   drawn below from the same counter, in the mono face, as a menu number rather
   than as a browser bullet. */
#hors .plates{list-style:none;margin:0;padding:0;counter-reset:plate}
/* Eight rows is the one number this section cannot change, so the vertical
   rhythm responds to viewport HEIGHT as well as width. At 900px tall this lands
   at the authored .78rem; on a 760px stage it tightens before either the kicker
   reaches the fixed chrome or the footnote reaches the cue. The short-viewport
   block in responsive.css makes the second, deliberate compression below
   800px. */
#hors .plate{counter-increment:plate;position:relative;
   padding:clamp(.4rem,.95vh,.78rem) 0 clamp(.4rem,.95vh,.78rem) 2.6rem;
   border-bottom:1px solid var(--rule)}
#hors .plate:first-child{border-top:1px solid var(--rule)}

/* The menu number. --ink-45 is the smallest-type token and clears AA on every
   ground on the page; do not lighten it to make these recede — if they need to
   recede they need to be smaller, not fainter. */
#hors .plate::before{content:counter(plate,decimal-leading-zero);position:absolute;
   left:0;top:1rem;font-family:var(--mono);font-size:10px;letter-spacing:.12em;
   color:var(--ink-45)}

/* THE STAGGER. Each row's own progress is the chapter's --p remapped so that
   row 0 finishes first and row 7 last: (p - i*STEP) / SPAN, clamped 0..1. The
   clamp is what makes eight rows deal in sequence out of one number, and it is
   arithmetic in the stylesheet — no JS, no IntersectionObserver, nothing that
   schedules a frame of its own (see the note at the top of main.js).

   --p is 0 until the engine's first tick, and tokens.css seeds it globally for
   exactly that reason. At 0 a row is 18px low at 0 opacity, which is the state
   the stagger starts from — correct on first paint, and correct with the
   engine still loading.

   SPAN .34 and STEP .055 together mean the last row starts at --p .385 and is
   settled by .725, so the whole board has finished dealing well before the
   runway ends and the reader is never scrolling into type that is still
   arriving. Under Reduce Motion the engine pins --p to 1, so every row lands
   at its settled state — the finished board, not an empty one.

   clamp() and not min/max chained: this is one value with a floor and a
   ceiling, and writing it as such is what makes the intent legible. */
#hors .plate{--rp:clamp(0, calc((var(--p) - var(--i) * .055) / .34), 1);
   opacity:var(--rp);
   transform:translateY(calc((1 - var(--rp)) * 18px))}

#hors .line{display:flex;align-items:baseline;gap:.6rem}
/* Same three values as #menu .item — name, leader, price are set identically
   in both sections, which is what makes them one menu in two parts. */
#hors .line .nm{font-size:1.08rem;color:#1b2436d8}
#hors .line .dots{flex:1;border-bottom:1.5px dotted var(--rule-2);transform:translateY(-.28em)}
#hors .line .pr{font-family:var(--mono);font-size:.92rem;color:var(--ink-60);letter-spacing:.02em}

/* The description. --ink-55 rather than the .body token's #1b2436c0: this is
   set at .95rem, and the type-scale note in tokens.css is explicit that small
   text takes the darker alphas. 4.86:1 on this ground. */
#hors .what{margin:.22rem 0 0;font-size:clamp(.85rem,1.55vh,.95rem);line-height:1.45;
   color:var(--ink-55);max-width:52ch}

/* THE FOOTNOTE IS A GRID ITEM, and left to itself auto-placement put it in
   column 1 — under the head, in the empty half of the left column, 700px away
   from the list it annotates and reading as an orphaned caption on the head
   instead. It belongs under the plates: grid-column 2 puts it there, and
   justify-self is not needed because the column is already its width.

   Explicitly placed rather than moved into .plates' own wrapper in the markup,
   because it is a note on the board and not the ninth item in an <ol> of eight
   dishes — the list's semantics are the reason it is an ordered list at all. */
#hors .tag{grid-column:2;margin-top:1.3rem;max-width:56ch}

/* ---- proximity ----------------------------------------------------------
   --near is 0..1, written per row by sections/hors.js from the pointer's
   distance to that row's centre. It is a REINFORCEMENT of :hover, not a
   replacement: the row under the cursor gets the full effect from :hover
   below, which needs no JavaScript, and --near only adds the falloff to its
   neighbours. With the module absent or failed, --near is unset, var()'s
   fallback is 0, and the section is a plain hover list — which is the whole
   reason it is written as a fallback rather than as a class the module adds.

   The properties that move are the number's colour and a small horizontal
   nudge. Nothing changes size: eight rows that each grow on approach is a list
   that reflows under the pointer, and reflow-on-hover is a layout thrash the
   reader can feel. transform and color only. */
#hors .plate{transition:opacity .3s ease}
#hors .plate .line,#hors .plate .what{
   transform:translateX(calc(var(--near,0) * 6px));
   transition:transform .35s cubic-bezier(.2,.7,.3,1)}
#hors .plate::before{
   color:color-mix(in srgb, var(--oxblood) calc(var(--near,0) * 100%), var(--ink-45));
   transition:color .35s ease}

/* The leader dots brighten toward the pointer — the one element in the row
   whose whole job is to be a texture, so it is the one that can carry the
   emphasis without any word on the page changing weight. --rule-2 → --rule-2
   at full strength is not a change, so this mixes toward the ink token the
   dots already sit beside. Non-text, so 3:1 is the bar and both ends clear it. */
#hors .plate .dots{
   border-bottom-color:color-mix(in srgb, var(--oxblood) calc(var(--near,0) * 55%), var(--rule-2));
   transition:border-color .35s ease}

/* :hover is the floor under all of the above and works with JS off. Scoped to
   a fine pointer: on a touch screen :hover latches on the last thing tapped
   and stays there, which would leave one row permanently emphasised. */
@media (hover:hover) and (pointer:fine){
  #hors .plate:hover .line,#hors .plate:hover .what{transform:translateX(8px)}
  #hors .plate:hover::before{color:var(--oxblood)}
  #hors .plate:hover .dots{border-bottom-color:var(--oxblood)}
}


/* ==== sections/intro.css ==== */
/* ==========================================================================
   §2 · WHAT WAS HERE — the state of the building, and the four years since
   markup: sections/intro.eta   ·   copy: 02-intro.json

   Static: no runway, no sticky stage. It is as tall as its own copy, giving the
   account of the work one plain screen between the menu and the moving night
   section.

   The three figures are decoration and carry empty alt. They are not a system,
   they are not named, and nothing on the page says which is which.
   ========================================================================== */
#intro{--ground:var(--bone-2);background:var(--bone-2);padding:16vh var(--frame-end) 14vh var(--frame-start);position:relative;overflow:hidden}
#intro .row{max-width:var(--content-max);margin:0 auto;display:grid;grid-template-columns:1fr 1fr;
   gap:clamp(2rem,7vw,6rem);align-items:center}
/* The collage leads on the left; the account of the work reads on the right.
   Source order stays copy-first for the stacked layout and for reading order. */
#intro .figs{order:-1}
#intro .said h2{font-size:clamp(2.4rem,5.2vw,4.4rem)}
#intro .said .body{max-width:38ch;font-size:1.1rem;margin-top:1.7rem}
#intro .said .body strong{font-weight:500;color:var(--ink)}

#intro .figs{position:relative;height:min(60vh,540px)}
/* POINTER PARALLAX WITH REAL DEPTH. The three figures are a collage, so they
   drift at three different rates under the cursor — the nearer they read (by
   z-index), the further they move: m3 is frontmost (z3 → 10px), m1 next
   (z2 → 7px), m2 hindmost (z1 → 4px). That splits a flat collage into layers
   the moment the mouse moves, the same trick the collection uses on its
   twenty-nine objects. The captions and copy do not move. */
#intro .figs .muse{position:absolute;mix-blend-mode:multiply;will-change:transform}
#intro .figs .m1{left:2%;top:4%;width:46%;z-index:2;
   transform:translate3d(calc(var(--mx,0)*7px),calc(var(--my,0)*7px),0)}
#intro .figs .m2{right:4%;top:0;width:40%;z-index:1;opacity:.9;
   transform:translate3d(calc(var(--mx,0)*4px),calc(var(--my,0)*4px),0)}
#intro .figs .m3{left:26%;bottom:-2%;width:42%;z-index:3;
   transform:translate3d(calc(var(--mx,0)*10px),calc(var(--my,0)*10px),0)}
#intro .figs .muse img{width:100%;height:auto;mix-blend-mode:multiply}

/* These captions overran the figure column when set nowrap — they are allowed
   to wrap and are held to the left half of it. */
#intro .carve{position:absolute;font-family:var(--mono);font-size:10px;letter-spacing:.16em;
   line-height:1.5;text-transform:uppercase;color:var(--ink-55);max-width:20ch}
#intro .carve b{color:var(--oxblood);font-weight:400}
#intro .c1{left:0;top:-1%}
#intro .c2{left:0;bottom:0}


/* ==== sections/night.css ==== */
/* ==========================================================================
   §3 · FIRE INSURANCE — the room's material logic, on the chapter that turns
   to night. markup: sections/night.eta   ·   copy: 06-night.json

   The heading stays inside the left half, where the scrim guarantees it has
   ground beneath it as the section darkens.

   The registry marks this section `dark: 0.6` — the chrome flips at --p 0.6,
   which is where the ground has actually gone dark enough to need it.
   ========================================================================== */
/* This is the one chapter whose ground CHANGES inside itself, so it declares
   both ends and the engine picks by the same --p threshold that flips the
   chrome (registry: dark 0.6). The .sky-night layer above fades between the
   two continuously; the chrome crosses in one step, at the point the section
   has already committed to night. */
#night{--ground:var(--bone);--ground-dark:var(--night)}
/* line-height and max-width are both re-set for caps, and for opposite reasons.
   Leading comes DOWN (1.04 → .92) on the same logic as §D: no descenders, so the
   1.04 that looked right under mixed case reads as a gap here. max-width goes UP
   (18ch → 21ch) because `ch` is the width of a zero and does not grow with the
   transform, while the letters do — 18ch of caps holds noticeably fewer words,
   which pushed this sentence from four lines to six. */
#night h2{font-size:clamp(2rem,4.2vw,3.4rem);line-height:.92;max-width:21ch}
#night .sub{max-width:26ch}
/* two paragraphs where there was none; the second is the smaller of the two so
   the eye finishes on the thesis rather than on the mechanism. */
#night .body{max-width:34ch;margin-top:1.2rem}
/* SIZE ONLY. The colour was hard-coded cream here, which is right for the
   bottom of the section and invisible at the top of it: this is the one chapter
   whose ground changes inside itself, and for the first 60% of its runway that
   ground is bone. The paragraph sat at 1.0:1 against it — not faint, absent —
   while the paragraph directly above it read normally, so the section appeared
   to be missing its last three lines rather than to have a contrast bug.
   Both paragraphs now take .body from type.css and invert together on `.ink`;
   only the two-step-down in weight, which was the actual intent, is kept. */
#night .body+.body{font-size:1rem;opacity:.86}
/* the copy column has grown from one line to five, so it starts higher and is
   bounded — otherwise it runs under the figure and off a short viewport. */
#night .copy.high{padding-top:10vh}
#night .copy>*{max-width:46ch}
/* The short-viewport rescale for this heading lives in base/responsive.css
   with every other media query — see the note at the head of that file. */

#night .sky-night{position:absolute;inset:0;background:
    radial-gradient(120% 90% at 62% 40%, #16203c 0%, var(--night) 70%);opacity:var(--p)}
#night #stars{position:absolute;inset:0;opacity:calc((var(--p) - .25)*1.6)}
#night #stars i{position:absolute;width:2px;height:2px;border-radius:50%;background:#f4ecd8;opacity:.9}

#night .rings .motion{transform:rotate(calc(var(--p)*40deg))}
#night .rings-a{--ax:62%;--ay:52%}
#night .hero-a{--ax:62%;--ay:53%}
#night .hero .motion{transform:translateY(calc(var(--p)*-4vh))}
#night .hero img{width:clamp(300px,30vw,440px);mix-blend-mode:multiply;transition:filter .8s}
/* the figure is ink on bone at the top of the section and cream on night at the
   bottom. `.ink` is this section's own darkness (see type.css) — `.flip` is the
   same state and is kept because it is what the chrome and #night's own JS
   read; either one firing is correct. body.dark is deliberately NOT here: it
   answers "what is behind the chrome", which is a different question once two
   sections share a phone screen. */
#night.flip .hero img, #night.ink .hero img{filter:invert(1) brightness(1.6)}

/* real constellation charts (cream on transparent): fade in as night falls */
#night .const{opacity:calc((var(--p) - .3)*1.4)}
#night .const img{width:100%;height:auto}

/* night reversal: ink relics become light on the dark sky */
#night.ink .stage, #night.ink .rings{color:#e9e0cd}

/* THE SCRIM STOPS SHORT OF THE FIXED CHROME, and the mask is how.

   Its job is stated above: guarantee ground under the copy in the left half.
   The copy starts about 90px down. The scrim ran `inset:0`, so it also held
   bone under the wordmark at y 26–62 — and the wordmark is the one element on
   screen that flips to cream at --p 0.6, on the same threshold as the sky.

   Measured across the runway at the mark's own box, chrome hidden — the ground
   the mark's plate has to sit on, before and after the mask:

     --p    was              now              Δ
     0.5    rgb(198,193,183) rgb(125,124,125)  73
     0.6    rgb(180,175,169) rgb(104,103,108)  76   ← the flip
     0.8    rgb(135,133,133) rgb( 58, 61, 72)  77
     1.0    rgb( 76, 78, 87) rgb( 13, 19, 37)  63

   The scrim was holding the top-left corner ~75 levels lighter than the rest
   of the sky for the whole chapter, so the one place the chrome asks its
   question was the one place the answer hadn't changed. It cost the wordmark
   1.85:1 at the flip (cream, on ground still bright enough to be bone's).

   The sky HAS committed to night by 0.6 — that is what the registry's
   `dark: 0.6` is about, and it is right. The scrim was the thing out of step,
   not the threshold.

   A hard `inset:62px 0 0 0` would draw a straight bone edge across the left
   quarter, which is exactly the visible rectangle this file spends its scrim on
   avoiding. A mask fades it in instead: nothing at the chrome band, full by the
   time the copy needs it, and no edge anywhere. Same reasoning as the ellipse
   masks in base/chrome.css — state the colour once, shape it separately.

   62 is the mark's bottom edge (top 26 + 36 tall). 108 is chosen against the
   copy, not against the mark: the kicker's box starts at y 90 and the heading
   at 122, so the heading clears the fade entirely and the kicker sits at 61% of
   it. That is not an assumption — sampled behind the hidden copy, the kicker's
   ground reads 12.63:1 at --p 0, 8.60 at 0.4 and 7.27 at 0.5, which is the last
   frame before the copy inverts to cream and stops needing bone at all. */
#night .scrim{position:absolute;inset:0;z-index:10;pointer-events:none;
  background:linear-gradient(90deg,var(--bone) 24%,#efe7d7bb 38%,transparent 56%);
  -webkit-mask-image:linear-gradient(180deg,transparent 62px,#000 108px);
  mask-image:linear-gradient(180deg,transparent 62px,#000 108px);
  opacity:calc(1 - var(--p)*.72)}


/* ==== sections/collection.css ==== */
/* ==========================================================================
   §4 · THE COLLECTION — dense parallax collage field
   markup: sections/collection.eta   ·   copy: 06-collection.json

   Twenty-nine objects, each with its own depth (--d), drift (--r), tilt and
   opacity, laid out in four tiers. .obj does the placing, .pmot does the
   moving — the same split as .anchor / .layer, and for the same reason: an
   element that centres itself must not also carry the parallax transform.
   ========================================================================== */
#collection{--ground:#ece0c9;background:#ece0c9}
#collection .field{position:absolute;inset:0;overflow:hidden;opacity:calc(.5 + var(--p)*.5)}
#collection .obj{position:absolute;left:var(--x);top:var(--y);width:var(--w);
   transform:translate(-50%,-50%);z-index:var(--z)}
#collection .pmot{will-change:transform;
   transform:translate(calc(var(--mx)*var(--d)*1px),
                        calc(var(--my)*var(--d)*1px + var(--p)*var(--r)*1vh))
             rotate(var(--tilt,0deg))}
#collection .obj img{width:100%;height:auto;mix-blend-mode:multiply;opacity:var(--o)}
#collection .scrim{position:absolute;inset:0;z-index:12;pointer-events:none;
   background:radial-gradient(56% 54% at 27% 50%, #ece0c9, #ece0c9b0 40%, transparent 74%)}
#collection .copy{position:absolute;top:50%;left:0;transform:translateY(-50%);z-index:20}
#collection h2{font-size:clamp(2.2rem,5.6vw,4.2rem)}

/* The named details. A rule per item and nothing else — the list is meant to
   read as an inventory, not as prose with bullets. No markers: a disc would
   make four sentences look like features. */
#collection .named{list-style:none;margin-top:1.9rem;max-width:40ch;
   border-top:1px solid var(--rule-2)}
#collection .named li{padding:.85rem 0;border-bottom:1px solid var(--rule-2);
   font-size:1.02rem;line-height:1.5;color:#1b2436c8}


/* ==== sections/origin.css ==== */
/* ==========================================================================
   §6 · THE NAME — two columns beside the figure, no stage.
   markup: sections/origin.eta   ·   copy: 02-origin.json

   This section follows the room details, once the reader has met the physical
   evidence of the building's influence. The two names remain separate: The Muse
   is the bar inside the Germania Building.
   ========================================================================== */
/* #f3edde separates the origin section from the denser collection before it and
   the night-ground sky plate after it. Two adjacent sections sharing a ground
   have no seam between them and read as one long section.

   #f3edde is not a new colour: it is one of the four creams the tokens header
   lists as AA-verified against every text token, and it is already the top stop
   of #contact's radial. It is the lightest of the three, which also suits the
   position — the page opens pale and gets progressively deeper.

   It is also the best of them here, measured rather than picked. Max channel
   delta against each neighbour, for every approved cream:

     #f3edde   7 vs hero · 23 vs intro     ← this one
     #ede2ce   9 vs hero ·  7 vs intro     (ties on the worst seam)
     #ece0c9  14 vs hero ·  5 vs intro
     #efe7d7   0 vs hero                   ← IS hero
     #e7dcc7                 0 vs intro    ← IS intro

   7 is the faintest seam on the page but not a new kind of faint: #history |
   #contact already ships at 9, and the four other cream-to-cream seams sit at
   14–23. The two grounds are flat fills, so those numbers are arithmetic, not
   samples — recompute them, don't re-eyeball them.

   If either neighbour moves, re-check this as a PAIR of seams, not one line. */
#origin{--ground:#f3edde;background:#f3edde;padding:17vh var(--frame-end) 15vh var(--frame-start);position:relative}
/* The portrait is a COLUMN, not a background. Putting that much black ink under
   the paragraphs would make the copy depend on a scrim; keeping it in the grid
   gives the figure a real place and keeps every line on the section ground.

   The art spans both rows: the arch becomes the visual spine on the left while
   heading and copy stay related on the right. At 1050px responsive.css stacks
   the three named areas before the nested copy grid gets squeezed. */
#origin .wrap{max-width:var(--content-max);margin:0 auto;display:grid;
   grid-template-columns:clamp(210px,22vw,300px) minmax(0,1fr);
   grid-template-areas:"art head" "art copy";
   column-gap:clamp(2.5rem,6vw,6rem);align-items:start}
#origin .head{grid-area:head}
#origin h2{font-size:clamp(2.4rem,5.4vw,4.2rem);margin-bottom:clamp(2.4rem,5vw,3.6rem)}
#origin .portrait{grid-area:art;align-self:center;margin:0}
/* The arch figure drifts with the pointer — the section's one woodcut, given
   the same living-under-the-cursor motion the menu relics and the star map
   have, so this section is not dead to the mouse while its neighbours move. The
   two columns of prose stay put; only the figure floats. */
#origin .portrait img{display:block;width:100%;height:auto;mix-blend-mode:multiply;opacity:.92;
   will-change:transform;transform:translate3d(calc(var(--mx,0)*7px),calc(var(--my,0)*7px),0)}
#origin .cols{grid-area:copy;display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,5vw,4.5rem)}
#origin p{font-size:1.06rem;line-height:1.66;color:var(--ink-70);margin-bottom:1.2rem;max-width:46ch}
#origin p:last-child{margin-bottom:0}
#origin p b{color:var(--ink);font-weight:500}
#origin .col:last-child p:last-child{color:var(--ink)}


/* ==== sections/sky1877.css ==== */
/* ==========================================================================
   §7 · 1877 — the brass plate in the floor
   markup: sections/sky1877.eta   ·   copy: 08-sky1877.json   ·   data: plate-1877.json

   THIS IS THE FLOOR. §2.5 is the ceiling. They are the same room in opposite
   directions, separated in the scroll so they never appear on screen together.

   What is underfoot is brass inlaid into parquet, recording the sky over 12th
   & Walnut in 1877. It is metal, not sky: warm, fixed, documentary, and it does
   NOT move — floors don't. The room moves
   across it, and the frame it looks through is the shape of the bar.

   Beside it is the instrument: the same sky right now, computed, never written.
   The plate says what the sky was; the readout says what it is.

   Not a disc. What you get standing in the bar is a RECTANGLE cut out of a sky
   that keeps going, so the star field is full-bleed and the circle's edge is
   never in frame. The room travels once — in, then back out — which is one
   piece of evidence that the sky continues past the frame without the section
   becoming a tour.

   Nothing about the astronomy is touched by any of this. Every position is the
   fitted projection; the travel only moves the window.
   ========================================================================== */

/* No overflow:hidden here — it makes this element a scroll container, which
   silently kills position:sticky on .sticky inside it and parked the whole star
   map off-screen. .sticky already clips its own contents. */
#sky1877{--ground:var(--night);position:relative;background:var(--night);color:var(--bone)}

/* the room: the whole frame, not an object centred inside it. It NEVER moves —
   it is the window. Scaling the room itself would shrink the crop rectangle and
   put a hard edge of stars in the middle of the night field; the zoom belongs
   to the sky behind it. */
#sky1877 .room{position:absolute;inset:0;overflow:hidden}

/* a vignette so the copy always has ground under it, whatever the sky is doing
   behind — the travel drags bright stars across the caption otherwise */
#sky1877 .vig{position:absolute;inset:0;z-index:3;pointer-events:none;
   background:radial-gradient(120% 90% at 62% 34%,transparent 34%,#0b102099 76%,#0b1020e0 100%),
              linear-gradient(0deg,#0b1020cc 0%,#0b102055 26%,transparent 52%)}

/* ---- the sky itself ----
   One SVG placed by the fitted projection. left/top/width/height are set by
   fitRoom() in js/sections/sky1877.js — the plate overflows the room on every
   side and is clipped by it.

   NO transition. The zoom is driven straight off scroll position every frame,
   so the sky moves exactly as much as the wheel turned and stops exactly when
   you stop. A transition here is what made it lurch: the scroll set a target
   and the sky travelled there on its own clock. */
#sky1877 .sky{position:absolute;overflow:visible;
   transform:translate3d(var(--dx,0px),var(--dy,0px),0) scale(var(--z,1));
   transform-origin:50% 50%;will-change:transform}

/* ---- the field arrives by magnitude ----
   --r runs 0→1 across the zoom. The brightest stars land first and the faint
   ones fill in behind them, which is the order your eye actually adapts to a
   night sky in. Real magnitudes, from the plate; the cascade is only the order
   of arrival, not the values.

   Radii are set in CSS, not as attributes, so they can track --kz — the
   scale-compensation for the live zoom. Without compensation the transform
   would scale a first-magnitude star 3.3× on the way in and it would stop being
   a star and start being a blob. Fully compensated, nothing changes size at all
   and the zoom reads as a flat spread. --kz splits the difference: apparent
   size grows as the SQUARE ROOT of the zoom, so stars swell about 1.8× while
   the field opens 3.3×. That is the ratio that feels like approach. */
#sky1877 .sky .sr{fill:#efe7d7;r:calc(.0030px*var(--kz,1));
   opacity:calc(var(--o,.1)*var(--r,0))}
#sky1877 .sky .st{fill:#efe7d7;opacity:0}
#sky1877 .sky .st.m0{r:calc(.0115px*var(--kz,1))}
#sky1877 .sky .st.m1{r:calc(.0082px*var(--kz,1))}
#sky1877 .sky .st.m2{r:calc(.0060px*var(--kz,1))}
#sky1877 .sky .st.m3{r:calc(.0042px*var(--kz,1))}
#sky1877 .sky .st.m4{r:calc(.0028px*var(--kz,1))}
#sky1877 .sky .st.m0{opacity:calc(.98*clamp(0,(var(--r,0) - .00)*7,1))}
#sky1877 .sky .st.m1{opacity:calc(.80*clamp(0,(var(--r,0) - .10)*7,1))}
#sky1877 .sky .st.m2{opacity:calc(.58*clamp(0,(var(--r,0) - .22)*7,1))}
#sky1877 .sky .st.m3{opacity:calc(.42*clamp(0,(var(--r,0) - .36)*7,1))}
#sky1877 .sky .st.m4{opacity:calc(.28*clamp(0,(var(--r,0) - .50)*7,1))}
#sky1877 .sky .m0{filter:drop-shadow(0 0 .012px #efe7d7)}

/* the brightest stars breathe. scale, not opacity — opacity is already carrying
   the magnitude cascade and the two would fight. */
@keyframes skytw{0%,100%{transform:scale(1)}50%{transform:scale(.68)}}
#sky1877 .sky .st.m0,#sky1877 .sky .st.m1{
   transform-box:fill-box;transform-origin:50% 50%;
   animation:skytw var(--td,5s) ease-in-out infinite;animation-delay:var(--tw,0s)}

/* depth: near planes swim further under the pointer than far ones. Values are
   in user units (1 = plate radius), scaled by --kz to stay constant.

   NO transition — same mistake as the zoom had. The pointer value is ALREADY
   eased in JS every frame; a .6s CSS transition on top of it is a second
   smoother running on its own clock, chasing a target that moves before it
   arrives. The two never agree, and the disagreement is the jerk. One easing,
   in JS, where the timing is known. */
#sky1877 .mg{will-change:transform}
#sky1877 .mg0{transform:translate(calc(var(--mx,0)*.020px*var(--kz,1)),calc(var(--my,0)*.016px*var(--kz,1)))}
#sky1877 .mg1{transform:translate(calc(var(--mx,0)*.015px*var(--kz,1)),calc(var(--my,0)*.012px*var(--kz,1)))}
#sky1877 .mg2{transform:translate(calc(var(--mx,0)*.010px*var(--kz,1)),calc(var(--my,0)*.008px*var(--kz,1)))}
#sky1877 .mg3{transform:translate(calc(var(--mx,0)*.006px*var(--kz,1)),calc(var(--my,0)*.005px*var(--kz,1)))}
#sky1877 .mg4{transform:translate(calc(var(--mx,0)*.003px*var(--kz,1)),calc(var(--my,0)*.002px*var(--kz,1)))}

/* ---- the figures ----
   Each one draws itself, but NOT on a timer and NOT when something arrives.
   Every figure carries its own threshold --t, and its draw progress is a pure
   function of --r, which is a pure function of scroll:

       drawn = clamp(0, (--r - --t) * 4, 1)

   So the line grows out of its own stars exactly as fast as you scroll,
   reverses if you scroll back, and freezes if you stop. Nothing has a clock of
   its own — that was the jerk.

   Thresholds run OUTSIDE-IN (computed per figure at render time): the
   constellations at the edge of the plate draw while they are still in frame,
   and the centre of the chart is the last thing to resolve, right as you
   arrive on top of it. */
#sky1877 .sky .cf{--d:clamp(0,(var(--r,0) - var(--t,0))*4,1);cursor:default}
/* a generous invisible target so the hairlines are actually hoverable */
#sky1877 .sky .cf .hit{fill:none;stroke:transparent;
   stroke-width:calc(.055*var(--kz,1));
   stroke-linecap:round;stroke-linejoin:round;pointer-events:stroke}
#sky1877 .sky .cf .ln{fill:none;stroke:#e9c46a;
   stroke-width:calc(.0022*var(--kz,1));
   stroke-linecap:round;stroke-linejoin:round;pointer-events:none;
   stroke-dasharray:var(--L,1);
   stroke-dashoffset:calc(var(--L,1)*(1 - var(--d)));
   opacity:calc(.40*var(--d));
   transition:opacity .3s ease,stroke-width .3s ease}
#sky1877 .sky .cf.hv .ln{
   opacity:calc(.95*var(--d));stroke-width:calc(.0042*var(--kz,1))}

/* Labels are OFF in the wide state and fade in as you come close. At full
   pull-back all sixteen names sit inside one screen height and collide into an
   unreadable mat. They are annotation, not the picture: the chart has to read
   as a chart first. --lab is driven off the zoom (0 wide → 1 close), so the
   names arrive only once there is room for them. Hover still overrides, at any
   zoom, so nothing is unreachable. */
#sky1877 .sky .cflabel{fill:#e9c46a;font-family:var(--mono);
   font-size:calc(.030px*var(--kz,1));letter-spacing:calc(.006px*var(--kz,1));
   text-transform:uppercase;text-anchor:middle;
   opacity:calc(.55*var(--lab,0)*clamp(0,(var(--d) - .72)*4,1));
   transition:opacity .3s ease;paint-order:stroke;
   stroke:#0b1020;stroke-width:calc(.010px*var(--kz,1))}
#sky1877 .sky .cf.hv .cflabel{opacity:.98}

/* ---- the copy ----
   1877 is the headline and stays put the whole way. It doesn't animate, shrink
   or swap: it's a heading, and it's the one fixed thing while the sky opens
   behind it. */
/* width:auto and padding:0 UNDO .copy, which this element also carries.
   .copy is `width:100%; padding:0 var(--pad-r) 0 var(--pad)` — written for a
   static block in the flow. On an absolutely positioned box with both `left`
   and `right` set, a definite width over-constrains it: the browser drops
   `right` and the caption becomes a full viewport wide starting at the left
   inset, i.e. hanging one --pad off the right edge. .copy's own padding then
   pushed the text back to exactly the viewport edge, which is why the standing
   line clipped at 390px. It was doing this at every width; the phone is only
   where the column got narrow enough to see it.
   Insets alone position this box. Nothing else should. */
#sky1877 .cap{position:absolute;left:var(--pad);right:var(--pad-r);bottom:8vh;z-index:5;
   width:auto;padding:0;pointer-events:none;text-shadow:0 2px 40px #0b1020}
/* the same h2 scale as every other section — this is a heading on the page, not
   a poster.

   `1877` is digits, so §D's text-transform passes straight over it — but the
   tracking still has to match, and it now does (+.015em, not the -.01em that was
   here). Instrument Serif's figures are LINING: they are drawn to cap height and
   sit in the same box as the caps every other title is now set in, so 1 and 7
   need the same air between them for this heading to read as one family with
   "GERMANIA." rather than a tighter-set exception. */
#sky1877 .yr{font-family:var(--display);font-weight:400;
   font-size:clamp(2.6rem,7.5vw,6rem);line-height:1;letter-spacing:.015em;
   margin:0;color:var(--bone);
   text-shadow:0 0 70px #0b1020,0 0 22px #0b1020cc}
/* and the site's .sub, in bone rather than ink */
#sky1877 .sub{font-family:var(--serif);font-style:italic;
   font-size:clamp(1.02rem,1.7vw,1.35rem);line-height:1.3;
   margin:.7rem 0 0;color:#efe7d7cc}
/* One standing line under the subtitle. It does not swap, fade or queue — no
   readout, no stop counter, no caption changing under you. */
#sky1877 .stop{margin-top:clamp(1rem,2.4vh,1.9rem);max-width:31rem}
#sky1877 .stop .ln{margin:0;font-size:1rem;line-height:1.5;color:#efe7d7cc}

/* ---- the instrument ----
   Every value comes from src/lib/astronomy.js at the real current moment; none
   is written by hand and none is placeholder. It sits in the OPPOSITE corner
   from the caption — the caption is the etching's own legend (bottom-left), the
   instrument is the room reading the live sky (top-right, under the header
   links).

   Deliberately small and quiet. This is a plaque on a wall, not a dashboard:
   you should be able to ignore it entirely, and it rewards the one person who
   stops to check whether the numbers are real. They are.

   It fades in with the section's own --p so it doesn't announce itself at the
   boundary — the sky arrives, then the instrument that measures it. */
#sky1877 .inst{position:absolute;right:var(--pad-r);top:clamp(88px,13vh,132px);
   z-index:6;pointer-events:none;text-align:right;
   font-family:var(--mono);font-size:9.5px;letter-spacing:.13em;
   line-height:1.9;text-transform:uppercase;color:#efe7d780;
   text-shadow:0 2px 30px #0b1020;
   opacity:calc(clamp(0,(var(--p,0) - .06)*6,1))}
#sky1877 .inst b{font-weight:400;color:#efe7d7c8}
/* the one live line — gold when the real sun is far enough down that the bar
   would be open, oxblood when it isn't. The hours are never written anywhere on
   this site; this is the only thing that states them, and it states them as an
   observation rather than a schedule. */
#sky1877 .inst .live{color:var(--gold);letter-spacing:.16em}
#sky1877 .inst .live.shut{color:#c8705c}
/* the plate's own epoch, set apart from the live block — 1877 is the one number
   here that is NOT now, and the rule makes that legible. */
#sky1877 .inst .plate{margin-bottom:.55rem;padding-bottom:.5rem;
   border-bottom:1px solid #efe7d72e;color:#efe7d766}


/* ==== sections/rooms.css ==== */
/* ==========================================================================
   §9 · THE ROOMS — twenty-eight, to follow. The one ink-ground section.
   markup: sections/rooms.eta   ·   copy: 09-rooms.json

   Registry marks it `dark: 0` — ink for its whole height, so the chrome is
   cream the entire time it owns the viewport.
   ========================================================================== */
#rooms{--ground:var(--ink);background:var(--ink);color:var(--bone);padding:17vh var(--frame-end) 17vh var(--frame-start)}
#rooms .wrap{max-width:var(--content-max);margin:0 auto;display:grid;grid-template-columns:1.1fr .9fr;
   gap:clamp(2rem,7vw,6rem);align-items:center}
#rooms .kicker{color:var(--gold)}
#rooms .kicker .n{color:#efe7d7c4}
/* 6vw → 4.8vw for caps. "TO STAY THE NIGHT." is the longer of the two authored
   lines, and this heading lives in the 1.1fr column of a two-column grid, so
   between the 900px breakpoint and about 1350px it is the vw term that governs,
   not the 4.6rem ceiling.

   4.8 is solved, not chosen: the widest size that fits was measured at eight
   viewports from 901 to 1600 and tracks the viewport almost exactly linearly at
   4.88–5.14vw, so anything at or below 4.88 holds everywhere. Above ~1350px the
   ceiling takes over and there is slack to spare. */
#rooms h2{color:#f4eedd;font-size:clamp(2.6rem,4.8vw,4.6rem)}
#rooms .body{color:#efe7d7c0;max-width:40ch;font-size:1.08rem}
#rooms .stats{margin-top:2.8rem;display:flex;gap:clamp(1.8rem,5vw,3.6rem);flex-wrap:wrap}
#rooms .stat .n{font-size:clamp(2.2rem,4vw,3rem);color:var(--gold);line-height:1}
#rooms .stat .l{margin-top:.6rem;font-family:var(--mono);font-size:10px;letter-spacing:.18em;
   text-transform:uppercase;color:#efe7d7c4}
#rooms .art{position:relative;display:flex;align-items:center;justify-content:center}
/* `height:auto` pairs with the width/height attributes in rooms.eta — see the
   note there, and the fuller one in history.eta. Without it the intrinsic
   height would be taken as a presentational hint and the muse would stretch. */
/* The building drifts with the pointer — this dark set-piece was one of the
   sections dead to the mouse between two that move (sky1877 above, contact
   below); the woodcut floating on --mx/--my closes that gap. The heading and
   stats stay fixed; only the art moves. */
#rooms .art img{width:min(90%,420px);height:auto;
   filter:invert(1) brightness(1.4);mix-blend-mode:screen;opacity:.9;
   will-change:transform;transform:translate3d(calc(var(--mx,0)*8px),calc(var(--my,0)*8px),0)}
#rooms .note{margin-top:2.4rem;font-family:var(--mono);font-size:10px;letter-spacing:.05em;
   color:#efe7d7c4;text-transform:uppercase}


/* ==== sections/facade.css ==== */
/* ==========================================================================
   §8b · THE ELEVATION — the facade comes apart, Germania stays
   markup: sections/facade.eta   ·   copy: 10b-facade.json   ·   js: facade.js
   geometry: public/data/facade-elements.json (derived, see tools/facade-elements.py)

   GROUND. #ece0c9, which is not a new colour — it is one of the four creams
   the tokens header lists as AA-verified against every text token, and it is
   already #collection's ground.

   It is measured for THIS pair of seams, per the note in origin.css:

     #rooms  is var(--ink) #1b2436 — max channel delta 209. Not a seam, a cut.
     #history is var(--bone) #efe7d7 — max channel delta 14 (0xc9 → 0xd7).

   14 sits inside the 14–23 band the page's other cream-to-cream seams already
   ship at, and #history | #contact ships at 9. The alternative creams score
   worse on the second seam: #f3edde is 7 vs history but that is FAINTER than
   the faintest currently on the page, and it is #origin's own ground four
   sections up. If either neighbour moves, recompute both numbers — the grounds
   are flat fills, so this is arithmetic, not a screenshot.

   NOT `dark`. The drawing is black ink on cream and the chrome sits on cream
   above it, exactly as on #collection.
   ========================================================================== */
#facade{--ground:#ece0c9}
#facade .sticky{background:#ece0c9}

/* The outlines. Out of flow and zero-sized, but NOT display:none — a clipPath
   inside a display:none subtree is not reliably resolvable, and a piece whose
   clip fails to resolve is not clipped at all, which is fifty copies of the
   whole elevation stacked on each other. */
#facade .fclips{position:absolute;width:0;height:0;overflow:hidden}

/* The drawing's box. Aspect is the artwork's own 1760×3577, declared here so
   the module can derive every measurement from the viewport height instead of
   reading a rect back off an element the camera is mid-scale on. If the source
   SVG is ever re-traced at another size, this and the `art` field in
   facade-elements.json are the two places that know it. */
#facade .fbuild{
  position:absolute;left:50%;top:50%;
  transform:translate3d(-50%,-50%,0);
  /* 84 and not 88: the shared .cue sits at bottom:28px, and the camera's
     opening drift pushes the drawing 2.5vh down — at 88 the shopfront and the
     cue were in the same 20px of screen, black type on black ink. */
  height:84vh;aspect-ratio:1760 / 3577;
  will-change:transform;
}

/* PERSPECTIVE LIVES ON .fwall, and it is load-bearing. A piece's motion out of
   the wall plane is a rotateY/rotateX and a translateZ and nothing else, and
   with no perspective on an ancestor those are a squash and a no-op.
   It is declared on the pieces' own parent rather than inherited down a
   `transform-style:preserve-3d` chain from .fbuild on purpose: every piece
   carries a clip-path, a clip flattens 3D exactly as a mask does, and a
   flattened chain would silently drop every translateZ.

   NO preserve-3d, which is also deliberate: the pieces are then composited in
   z-index order rather than sorted in 3D, so Germania stays in front of
   everything by declaring it once, below, instead of by out-running the
   pieces that are travelling toward the camera. */
#facade .fwall{
  position:absolute;inset:0;
  perspective:1900px;perspective-origin:50% 50%;
}

/* ONE ARCHITECTURAL ELEMENT — the segmental pediment, the finial, the name
   tablet, a corner pavilion, a string course, one window unit with its own
   hood and apron, a shopfront arch, the entrance.

   Its box is the element's own bounding box, and `clip-path` cuts it to the
   element's own outline, so what moves is the shape of the thing rather than a
   rectangle over it. Both come from public/data/facade-elements.json.

   Every piece paints the WHOLE drawing at the stage's size and shows only its
   own window onto it, so the browser rasterises the SVG once and all fifty
   share that one raster. The outlines carry no holes because they do not need
   any: the artwork is black on transparent, so every window opening and every
   white line inside a piece is transparent already. */
#facade .fpiece{
  position:absolute;
  background-image:url("/test-site-v0.3/assets/germania-facade-elevation-woodcut.svg");
  background-repeat:no-repeat;
  will-change:transform,opacity;
  backface-visibility:hidden;
}
/* NO mix-blend-mode here, unlike #origin .portrait img. That drawing needed it;
   this one is traced with no paper behind it, so multiply against the ground is
   a no-op on every transparent pixel and identity on every black one — blend
   groups composited every frame for a result no one can tell from normal. */

/* THE AEDICULE — Germania, her arch and her flanking pilasters — is one of the
   fifty pieces and is marked out only by what happens to it. It is the piece
   that does not leave.

   It is laid out at its FINAL on-screen size and scaled DOWN to sit in the
   wall, never up: the obvious way round — a small element scaled up on the way
   out — rasterises the SVG at the size it starts and then magnifies that
   bitmap, so she is at her softest at the exact moment she is largest and is
   the only thing on screen. The module writes that size, because it depends on
   the viewport; with no script she is a piece of the wall like any other and
   the percentages the server wrote are already right. */
#facade .fpiece[data-kind="germania"]{z-index:5}
/* The cut edge, once she is free of the wall. Driven from the module's own
   lift, so the shadow does not exist while she is still part of the facade — a
   plate with a drop shadow sitting flush in a wall reads as a sticker. */
#facade .fpiece[data-kind="germania"]::before{
  content:"";position:absolute;inset:0;
  box-shadow:0 2.5vh 7vh #1b243626;
  opacity:var(--lift,0);pointer-events:none;
}

/* ---- the copy, split around her ----
   She finishes centred, so the type goes either side of her rather than into
   one margin: "The building came apart." on the left, "She did not." on the
   right, and Germania is the full stop in the middle. --fgap is the channel she
   lands in, written by the module from her final width; the fallback is the
   width of the drawing at rest so an unscripted page still keeps the columns
   off the elevation. */
#facade .fcopy{
  position:absolute;inset:0;
  display:grid;align-items:center;
  grid-template-columns:1fr var(--fgap,44vh) 1fr;
  padding:0 var(--pad-r) 0 var(--pad);
  z-index:20;opacity:0;will-change:opacity;
  pointer-events:none;
}
#facade .fcol{transform:translateY(var(--cy,3.2vh))}
/* Placed EXPLICITLY either side of the channel. Auto-placement puts two
   children in columns 1 and 2 — and column 2 is the channel, so the paragraph
   landed on top of her. */
#facade .fcol-a{grid-column:1;justify-self:end;text-align:right;max-width:34ch;padding-right:2.4vw}
/* The heading is set against the ragged left edge the column creates, so where
   it breaks is visible; balanced, it breaks in the middle rather than leaving
   "APART." alone. The kicker is one label and is never broken — the column is
   ~480px at 1440 and the kicker is ~250, so nowrap cannot overflow here, and
   the narrow branch below drops it. */
#facade .fcol-a h2{text-wrap:balance}
#facade .fcol-a .kicker{white-space:nowrap}
#facade .fcol-b{grid-column:3;justify-self:start;max-width:38ch;padding-left:2.4vw}
/* The copy is faded in by the module, so with no script it would never arrive
   and the section would lose its words. Hand them back — an unscripted reader
   gets the elevation and the copy, just not in the order they were meant in. */
@media (scripting:none){
  #facade .fcopy{opacity:1}
  #facade .fcol{transform:none}
}
#facade .fcopy h2{font-size:clamp(1.9rem,3.4vw,3rem);line-height:1.06;margin-top:.4rem}
#facade .fcopy .sub{font-size:clamp(1.4rem,2.2vw,2rem)}
#facade .fcopy .body{max-width:none;margin-top:1.1rem}

/* ---- narrow ----
   Below 900 there are no margins to put anything in, so she takes the upper
   part of the screen and the copy stacks underneath her in one column. The
   module reads the same 900 breakpoint for her final size and resting place;
   the two must not drift apart. */
@media (max-width:900px){
  #facade .fbuild{height:60vh}
  #facade .fcopy{
    grid-template-columns:1fr;align-content:end;
    padding:0 var(--pad-r) 7vh var(--pad);
  }
  #facade .fcol-a,#facade .fcol-b{
    grid-column:1;justify-self:stretch;text-align:left;max-width:none;padding:0;
  }
  #facade .fcol-b{margin-top:.6rem}
  #facade .fcol-a .kicker{white-space:normal}
  #facade .fcopy h2{font-size:clamp(1.7rem,7vw,2.2rem)}
  #facade .fcopy .sub{font-size:clamp(1.3rem,5vw,1.6rem)}
  #facade .fcopy .body{font-size:1rem;line-height:1.55}
}


/* ==== sections/history.css ==== */
/* ==========================================================================
   §8 · THE BUILDING — real history; the one chapter that stops moving
   markup: sections/history.eta   ·   copy: 10-history.json

   Static. Everything before it moves; this one is a page of prose with a
   timeline, and it is last of the story beats because building history is
   background, not the pitch.

   The reader is buried at the foot of the section and rises out of it as you
   scroll deeper into the past. --hp (0..1) is written by the engine. She is
   uncaptioned and stays that way.
   ========================================================================== */
#history{--ground:var(--bone);background:var(--bone);padding:18vh var(--frame-end) 14vh var(--frame-start);position:relative;overflow:hidden}

#history .buried{position:absolute;left:50%;bottom:0;width:clamp(200px,22vw,330px);z-index:0;
   pointer-events:none;opacity:calc(var(--hp,0)*.22);
   transform:translate(-50%,calc(26% - var(--hp,0)*15%))}
#history .buried img{width:100%;height:auto;mix-blend-mode:multiply}

#history .intro,#history .building,#history .time,#history .src{position:relative;z-index:1}
#history .intro{max-width:var(--content-max);margin:0 auto 10vh;display:grid;grid-template-columns:1fr 1fr;
   gap:clamp(2rem,7vw,6rem);align-items:end}
/* All three clamp terms come down for caps (was 2.6rem/6vw/5rem). This is the
   only heading on the page living in a half-width grid column — 542px at 1440 —
   and "HAPPENED HERE FIRST." in caps is ~15% wider than the mixed case it
   replaced, so every term was overflowing that column and wrapping the authored
   two lines into three or four.

   Every term is solved against the box the h2 actually gets, not guessed and not
   measured against the grid column — the column is wider than the heading, since
   .lede and the gap take their share. The widest size that fits was measured at
   eight viewports from 901 to 1600: it tracks the viewport linearly at
   3.77–3.96vw, so 3.7vw holds everywhere, with 3.8rem as the ceiling it reaches
   around 1640px.

   The 2.1rem floor is set by the WORST case, 901px — one pixel above the
   breakpoint, where the two-column grid still applies and the heading box is at
   its narrowest 301px. 2.3rem overflowed there by 19px. Below 900 .intro
   collapses to a single column, the box roughly doubles and the floor has slack
   again, so this number is protecting a 1px-wide window: don't "simplify" it
   upward without re-measuring at exactly 901.

   NOTE the 6vw term was ALREADY overflowing this column before the switch to
   caps — mixed case wrapped to three lines at 1024–1200 too. Caps made it worse
   and this fixes both. */
#history .intro h2{font-size:clamp(2.1rem,3.7vw,3.8rem)}
#history .intro .lede{font-size:clamp(1.1rem,1.7vw,1.35rem);line-height:1.55;color:var(--ink-70)}
#history .intro .lede b{color:var(--ink);font-weight:500}

#history .building{max-width:var(--content-max);margin:0 auto 10vh;text-align:center}
/* This is the full façade woodcut, 3:2 rather than the portrait crop this rule
   used to size. Its height at 960px is almost the same as the old 560px portrait,
   so the chapter keeps its rhythm while the drawing gets the width it needs. */
/* `height:auto` IS NOT DECORATION HERE. The markup now carries width="4614"
   height="3069" so the browser can reserve the box before the lazy SVG loads
   (see the note in history.eta). Those attributes are also a presentational
   hint, so without this the drawing would render 3069px tall — the pair only
   works as a ratio when CSS sets one dimension and yields the other. */
/* PARALLAX ON A SECTION THAT CANNOT PIN. This chapter is deliberately static —
   it is a three-screen timeline and there is no 100vh stage to hold it. That is
   why it read as the one dead stretch after the long run of pinned set-pieces:
   nothing here moved except the buried reader. The fix is not to force a pin
   (the runway note in sections.js explains why a pin that isn't paid for makes
   the whole page feel cheaper) — it is to give the section depth AS IT SCROLLS.
   The engine already writes --hp here (0 as the top enters, 1 as the foot
   passes the top — see history.js); the façade rides it, drifting up and
   swelling a touch as you read deeper, the same motion language as the reader
   surfacing beneath it. Pointer parallax is on the FIGURE, not the img, so the
   two transforms never collide (the .layer/.motion split in chapter.css, done
   with the elements this section already has). Transform-only: no reflow, so
   the box the width/height pair reserves above is untouched. */
#history .building{transform:translate3d(calc(var(--mx,0)*5px),calc(var(--my,0)*4px),0)}
#history .building img{width:min(100%,960px);height:auto;margin:0 auto;mix-blend-mode:multiply;
   will-change:transform;
   transform:translateY(calc(var(--hp,0)*-58px)) scale(calc(1 + var(--hp,0)*.04))}
#history .building figcaption{margin-top:1.4rem;font-family:var(--mono);font-size:10.5px;
   letter-spacing:.12em;text-transform:uppercase;color:var(--ink-40)}

#history .time{max-width:820px;margin:0 auto;position:relative}
#history .time::before{content:"";position:absolute;left:0;top:.5rem;bottom:.5rem;width:1px;background:var(--rule)}
#history .ev{position:relative;padding:0 0 clamp(2.6rem,5vw,4rem) clamp(2.2rem,5vw,3.4rem)}
#history .ev::before{content:"";position:absolute;left:-3.5px;top:.55rem;width:8px;height:8px;
   border-radius:50%;background:var(--oxblood)}
#history .ev:last-child{padding-bottom:0}
#history .ev .yr{font-family:var(--mono);font-size:11px;letter-spacing:.2em;color:var(--oxblood);margin-bottom:.7rem}
#history .ev h3{font-size:clamp(1.35rem,2.6vw,1.9rem);margin-bottom:.7rem;font-weight:500;line-height:1.15}
#history .ev p{font-size:1.04rem;line-height:1.64;color:var(--ink-70);max-width:62ch}
#history .ev p em{font-style:italic;color:var(--ink)}
/* One line of verse in the timeline. Set small and italic, uncredited, no
   explanation offered. */
#history .ev .verse{margin-top:1.1rem;font-style:italic;font-size:1.02rem;
   color:var(--ink-60);max-width:62ch}
/* the figure in the music beat — uncaptioned, off to the side, small */
#history .ev.has-fig{padding-right:clamp(0px,16vw,210px)}
/* Faint pointer parallax so the timeline figures float a little under the
   cursor — the same living-under-the-pointer quality the menu relics and the
   collection have, and the second half of giving this static section depth.
   They drift further than the façade (8px vs 5) because they are smaller and
   read as nearer. #history has overflow:hidden, so any drift past the edge
   clips rather than adding a scrollbar. */
#history .ev-fig{position:absolute;right:0;top:-1.5rem;width:clamp(110px,14vw,190px);
   opacity:.5;pointer-events:none;will-change:transform;
   transform:translate3d(calc(var(--mx,0)*8px),calc(var(--my,0)*8px),0)}
#history .ev-fig img{width:100%;height:auto;mix-blend-mode:multiply}
/* The animated crane is a transparent video (see history.eta), so it needs
   nothing the SVG figures don't: the .ev-fig opacity and drift apply as they
   are, and there is deliberately NO mix-blend-mode here — .ev-fig's opacity
   and .time's z-index are both stacking contexts, so a blend on the video
   would only ever meet transparency, never the ground. Under Reduce Motion
   the video goes and the poster frame stands in. */
#history .ev-vid video{display:block;width:100%;height:auto}
#history .ev-vid .still{display:none}
@media (prefers-reduced-motion:reduce){
  #history .ev-vid video{display:none}
  #history .ev-vid .still{display:block}
}

#history .src{max-width:820px;margin:5vh auto 0;padding-left:clamp(2.2rem,5vw,3.4rem);
   font-family:var(--mono);font-size:10px;letter-spacing:.06em;color:var(--ink-40);
   text-transform:uppercase;line-height:1.7}


/* ==== sections/contact.css ==== */
/* ==========================================================================
   §10 · VISIT — engraved map, details & mailing list (the close)
   markup: sections/contact.eta + partials/signup.eta   ·   copy: 11-contact.json

   The plate rises into place as the runway scrolls, and everything inside it is
   drawn: the map is an SVG engraving, not a tile service, so this page loads no
   third-party script and hands nobody a visitor log.
   ========================================================================== */
#contact{--ground:#ede2ce;background:radial-gradient(120% 100% at 50% 0%, #f3edde 0%, #e7dcc7 72%)}
#contact .relic img{width:100%;height:auto;mix-blend-mode:multiply;opacity:.13}

/* Both terms of that min() are measured against the VIEWPORT, and the parent
   .copy is not the viewport — it is inset by --pad on the left and --pad-r on
   the right. So the plate could be wider than the space it was given, and below
   ~1200px it was: `margin:auto` resolves to zero when a box is over-constrained,
   so the surplus all went to the right and pushed the address, the heading and
   the signup button off centre and past the right inset. (While the chapter rail
   existed they landed under its links, which is how this was found.)

   `100%` is the third term, and it is the only one that knows about the parent's
   insets, because .copy already accounts for them. Nothing subtracts
   --rail-gutter here: that would deduct it twice and pull the plate off centre. */
#contact .plate{position:relative;z-index:20;
   width:min(94vw,1060px,100%);margin:0 auto;
   display:grid;grid-template-columns:1.08fr .92fr;
   background:#efe7d7;border:1.5px solid var(--ink);
   box-shadow:0 34px 70px -34px #1b243577, 0 2px 0 #1b243622;
   transform:translateY(calc((1 - var(--p))*36px));opacity:calc(.62 + var(--p)*.38)}
#contact .plate::before{content:"";position:absolute;inset:7px;border:1px solid var(--rule-2);
   pointer-events:none;z-index:3}
#contact .mapwrap{position:relative;border-right:1.5px solid var(--ink);min-width:0}

/* ---- the map, as a link -------------------------------------------------
   The anchor is the whole map cell, so the target is a 400px square rather than
   a line of text — no 44px minimum to think about, and no doubt about what is
   clickable. The 18px padding moved here off .mapwrap so the hit area includes
   the plate's inner margin instead of stopping at the drawing's edge.

   A FLEX COLUMN, CENTRED, RATHER THAN A BLOCK: the plate is a two-column grid
   and the details side is the taller of the two, so the map cell is stretched
   to match it. As a block the drawing sat at the top of that cell with a stripe
   of empty paper under it, which reads as the map having failed to fill its
   frame. Centred, the square drawing sits on the plate's optical middle.

   A DRAWN MAP THAT NAVIGATES HAS TO SAY SO. Nothing about an engraving suggests
   it is a control, so a hint sits under it in the site's mono and the whole
   plate lifts very slightly on hover — enough to answer "is this a button?",
   not enough to make the map look like one. */
#contact .maplink{display:flex;flex-direction:column;justify-content:center;
   height:100%;padding:18px;text-decoration:none;color:inherit;
   transition:background .35s ease}
#contact .maplink:hover{background:#1b24360a}
#contact .maplink:focus-visible{outline:2px solid var(--oxblood);outline-offset:-6px}
#contact .mapwrap svg{display:block;width:100%;height:auto;transition:transform .5s ease}
#contact .maplink:hover svg{transform:translateY(-2px)}
#contact .maphint{display:block;margin-top:.85rem;text-align:center;
   font-family:var(--mono);font-size:9.5px;letter-spacing:.22em;text-transform:uppercase;
   color:var(--ink-45);transition:color .35s}
#contact .maplink:hover .maphint{color:var(--oxblood)}

#contact .info{padding:clamp(1.8rem,3.4vw,3rem);display:flex;flex-direction:column;justify-content:center}
#contact .info .kicker{margin-bottom:.5rem}
#contact .info h2{font-size:clamp(2.1rem,4.4vw,3.3rem)}
#contact .rows{margin-top:1.6rem;border-top:1px solid var(--rule-2)}
#contact .row{display:grid;grid-template-columns:82px 1fr;gap:1rem;align-items:baseline;
   padding:.95rem 0;border-bottom:1px solid var(--rule-2)}
#contact .row .lbl{font-family:var(--mono);font-size:10px;letter-spacing:.22em;
   text-transform:uppercase;color:var(--oxblood)}
#contact .row .val{font-size:1.05rem;color:#1b2436d8;line-height:1.5}

/* The second half of "make the map active": the same destination as a plain
   text link, in the details column, for anyone who does not think to click a
   picture and for anyone reading this page as a list of links. Its own line
   rather than a phrase inside the address, so it is findable at a glance and
   cannot be mistaken for part of the street name. */
#contact .dirlink{display:inline-flex;align-items:center;min-height:44px;margin-top:.15rem;
   font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
   color:var(--ink-70);text-decoration:none;
   border-bottom:1px solid var(--rule-2);transition:color .35s,border-color .35s}
#contact .dirlink::after{content:"\2197";margin-left:.5em;font-size:1.1em;line-height:0}
#contact .dirlink:hover{color:var(--oxblood);border-bottom-color:currentColor}
#contact .dirlink:focus-visible{outline:2px solid var(--oxblood);outline-offset:3px}

/* ---- the signup, and the only joke on the page ----
   The crane and the mask are the two states of this form. Get it right and she
   releases the paper crane; mistype and she is holding a mask over her own
   face. Nothing is labelled; it pays off only for people who mistype. */
#contact .siglabel{display:block;margin-top:1.7rem;margin-bottom:.55rem;
   font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;
   color:var(--ink-55)}
#contact .signup{display:flex;gap:.55rem;flex-wrap:wrap;position:relative}
#contact .signup input{flex:1;font-family:var(--serif);font-size:1rem;padding:.72rem .95rem;min-width:170px;
   background:#f6f0e2;border:1px solid var(--rule-2);color:var(--ink);border-radius:2px}
#contact .signup input::placeholder{color:var(--ink-45)}
#contact .signup input:focus-visible,#contact .signup button:focus-visible{
   outline:2px solid var(--oxblood);outline-offset:2px}
#contact .signup input[aria-invalid="true"]{border-color:var(--oxblood)}
/* min-height 44, not the 36 the padding alone produced. The one button on the
   page that converts anything should not be the hardest thing on it to hit. */
#contact .signup button{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;
   padding:.72rem 1.3rem;min-height:44px;background:var(--oxblood);color:var(--bone);border:none;border-radius:2px;
   cursor:pointer;transition:filter .3s}
#contact .signup input{min-height:44px}
#contact .signup button:hover{filter:brightness(1.12)}

/* The crane or the mask, swapped by a class the SERVER writes. Whole figures
   rather than a square emblem, so this is sized off HEIGHT and sits on the
   baseline of the form — she stands next to it. Same multiply treatment as
   every other woodcut on the bone ground; no inversion, the source art is
   already ink. The two poses are not the same aspect ratio, which is fine
   precisely because height is what's pinned. */
#contact .mask{position:absolute;right:-4px;bottom:calc(100% - 2px);
   height:clamp(92px,11vw,132px);opacity:0;transform:translateY(8px) scale(.96);
   transform-origin:100% 100%;
   transition:opacity .25s ease,transform .25s ease;pointer-events:none}
#contact .mask img{height:100%;width:auto;display:block;mix-blend-mode:multiply}
#contact .signup.is-comedy .mask,#contact .signup.is-tragedy .mask{
   opacity:.9;transform:translateY(0) scale(1)}

/* the message is real text, not colour alone (WCAG 1.4.1) */
#contact .sigmsg{margin-top:.6rem;font-size:.94rem;color:var(--oxblood);min-height:1.2em}
#contact .sigmsg:empty{min-height:0}
/* success is not an error — say it in ink, not in the error colour */
#contact .signup.is-comedy ~ .sigmsg{color:var(--ink-70)}

/* Instagram + sister venues. Same mono register as the signoff, one step
   brighter so the links read as links. */
#contact .fmeta{margin-top:1.6rem;display:flex;flex-direction:column;gap:.1rem}
#contact .frow{display:flex;align-items:center;flex-wrap:wrap;gap:0 .85rem;
   font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase}
#contact .frow .lbl{color:var(--ink-45)}
/* 10px of ink, 44px of target — see the note on .topnav a. The underline is
   drawn on an inner span-height box rather than the padded one, so growing the
   hit area does not drop the rule away from the word. */
#contact .frow a{color:var(--ink-70);text-decoration:none;
   display:inline-flex;align-items:center;min-height:44px;
   box-shadow:inset 0 -1px 0 0 transparent;transition:color .35s}
#contact .frow a>span{border-bottom:1px solid var(--rule);padding-bottom:1px;
   transition:border-color .35s}
#contact .frow a:hover{color:var(--oxblood)}
#contact .frow a:hover>span{border-bottom-color:currentColor}
#contact .frow a:focus-visible{outline:2px solid var(--oxblood);outline-offset:-4px;border-radius:2px}
/* A social channel and its glyph. `.soc` is used by BOTH branches of the markup
   — the <a> when there is a URL and a <span> when there isn't — so the two
   states sit on exactly the same baseline and the row does not reflow when a
   missing address is filled in. (This replaced `.ig`, which named the one
   channel that existed rather than the shape it was.) */
#contact .frow .soc{display:inline-flex;align-items:center;gap:.45em}
#contact .frow .soc .ic{width:12px;height:12px;display:block;flex:none}
#contact .frow .soc.is-pending{color:var(--ink-45);min-height:44px}
#contact .frow .soc.is-pending .tk{margin-left:.15em}
#contact .frow .sep{color:var(--rule-2)}

#contact .signoff{margin-top:1.7rem;font-family:var(--mono);font-size:10px;letter-spacing:.2em;
   text-transform:uppercase;color:var(--ink-45)}

/* ---- map internals ---- */
#contact .map .blk{fill:url(#mapHatch);stroke:var(--ink);stroke-width:1}
#contact .map .park{fill:url(#mapDot);stroke:var(--ink);stroke-width:1}
#contact .map .canal{fill:none;stroke:var(--ink);stroke-width:8;opacity:.32;stroke-linecap:round}
#contact .map .canal-c{fill:none;stroke:var(--ink);stroke-width:1;opacity:.5;stroke-dasharray:2 4}
#contact .map .stlbl{font-family:var(--mono);font-size:8px;letter-spacing:.12em;fill:var(--ink-45)}
#contact .map .cface{font-family:var(--mono);font-size:9px;letter-spacing:.1em;fill:var(--oxblood)}
#contact .map .scl{font-family:var(--mono);font-size:8px;letter-spacing:.06em;fill:var(--ink-45)}
#contact .map .ctitle{font-family:var(--serif);font-style:italic;font-size:20px;fill:var(--ink)}
#contact .map .csub{font-family:var(--mono);font-size:8px;letter-spacing:.26em;fill:var(--ink-45)}
#contact .map .mkr{font-family:var(--mono);font-size:9px;letter-spacing:.1em;fill:var(--ink);font-weight:600}
#contact .map .mkr2{font-family:var(--serif);font-style:italic;font-size:9.5px;fill:var(--ink-60)}
#contact .map .pin{fill:var(--oxblood)}
#contact .map .pinring{fill:none;stroke:var(--oxblood);stroke-width:1.4;
   transform-box:fill-box;transform-origin:center;animation:pingp 3.4s ease-out infinite}
@keyframes pingp{0%{transform:scale(.45);opacity:.8}75%{opacity:0}100%{transform:scale(2.6);opacity:0}}


/* ==== sections/footer.css ==== */
/* ==========================================================================
   §11 · THE FOOT
   markup: sections/footer.eta   ·   copy: 12-footer.json

   The ground the scroll lands on. See the note in the markup for why this is a
   foot and not a second contact block.

   --ground is declared like every other section's because the fixed chrome
   borrows it (base/chrome.css); without it the wordmark and the header links
   would carry §10's cream ellipse down onto ink. The registry's `dark: 0` is
   what flips the chrome's own colour — this is what its backing plate is
   painted in.
   ========================================================================== */
#footer{--ground:var(--night);background:var(--night);color:var(--bone);
   padding:11vh var(--pad-r) 7vh var(--pad)}

/* Centred, and narrow. Every other section on the page is a left-aligned
   column because it is carrying an argument; this one is carrying a signature,
   and a signature sits under the middle of the page it signs. */
#footer .fwrap{max-width:44rem;margin:0 auto;text-align:center}

#footer .fmark img{width:66px;height:auto;
   /* the wordmark is drawn in ink for cream grounds — same inversion the fixed
      mark gets in .dark, and for the same reason */
   filter:invert(1) brightness(1.6);opacity:.92;
   /* The lightest pointer drift on the page (4px). The foot is a resting place
      and its authored motion is the hover arrow, not an ambience — but a
      signature that ignores the cursor entirely, alone among twelve sections,
      is the inconsistency that reads as jank. So: present, but the quietest. */
   will-change:transform;transform:translate3d(calc(var(--mx,0)*4px),calc(var(--my,0)*4px),0)}

#footer .fline{margin:1.7rem 0 0;font-family:var(--display);text-transform:uppercase;
   font-size:clamp(1.25rem,2.5vw,1.85rem);line-height:1.15;letter-spacing:.015em;
   color:#f2ecdd}
#footer .fwhere{margin:.85rem 0 0;font-family:var(--serif);font-style:italic;
   font-size:1.02rem;color:#efe7d7bb}


/* ==== base/responsive.css ==== */
/* ==========================================================================
   Z · RESPONSIVE & REDUCED MOTION

   MUST STAY LAST IN THE BUNDLE. Several rules here re-declare selectors from
   the section files at equal specificity and win only on source order. The
   concatenation order is src/lib/assets.js → CSS_ORDER, where this is the final
   entry; moving it breaks the small-screen layout silently, which is the worst
   kind of breakage because everything still renders.

   All media queries live here rather than beside the rules they override, so
   there is exactly one place to look when a breakpoint misbehaves.
   ========================================================================== */

/* ---- the rail gutter ----------------------------------------------------
   GONE, with the chapter rail it existed for. From 761px up this set
   --rail-gutter to 92px and then 114px, reserving a right-hand lane so the
   fixed rail did not land inside live text — the contact address and its CTA,
   the menu prices, the origin paragraph, the sky1877 star labels, all measured
   rather than eyeballed. With no rail there is nothing to clear, the token
   holds 0 at every width (tokens.css), and --pad-r / --frame-end collapse onto
   their left-hand partners. */

/* ---- short viewports ----------------------------------------------------
   §5b carries eight real rows inside a fixed-height stage. Width breakpoints
   cannot solve a 1440×700 laptop, so its second rhythm is keyed to HEIGHT:
   the explanatory paragraph yields first (the eight dish descriptions remain),
   then names and row padding tighten. At 901×700 the board is 528px tall with
   86px above it and 46px between its note and cue; at 1440×740 it is 583px,
   with 79px / 39px respectively. */
@media (max-height:800px){
  #hors .head .body{display:none}
  #hors .alt{margin-top:.5rem}
  #hors .line .nm{font-size:1rem}
  #hors .what{font-size:.83rem;line-height:1.4}
  #hors .plate{padding-top:.34rem;padding-bottom:.34rem}
  #hors .tag{margin-top:.9rem}
}

@media (max-height:760px){
  #night h2{font-size:clamp(1.7rem,3.4vw,2.5rem)}
  #night .copy.high{padding-top:7vh}
}

/* Below 700px every remaining half-rem matters. The dish descriptions stay —
   they are the useful content — while the title, labels and row air take the
   final compression. This holds the complete board at 901×600 and 1440×660;
   hiding descriptions would fit more easily but would turn unfamiliar names
   like Obatzda and Gilda into opaque labels, defeating the section. */
@media (max-height:700px) and (min-width:761px){
  #hors .head h2{font-size:clamp(1.7rem,3vw,2.1rem)}
  #hors .alt{font-size:1.1rem}
  #hors .line .nm{font-size:.94rem}
  #hors .what{font-size:.78rem;margin-top:.12rem}
  #hors .plate{padding-top:.2rem;padding-bottom:.2rem}
  #hors .tag{margin-top:.7rem}
}

/* ---- Germania portrait --------------------------------------------------
   Leaving the portrait beside the nested two-column copy squeezes each
   paragraph below a useful line length before the general 900px stack arrives.
   Stack the OUTER grid first; the two text columns can still hold until their
   own breakpoint below. (This band was tighter still when the rail took a
   114px lane out of it; the breakpoint is re-measured without one.) */
@media (max-width:1050px){
  #origin .wrap{grid-template-columns:1fr;grid-template-areas:"head" "art" "copy"}
  #origin .portrait{width:clamp(180px,34vw,250px);justify-self:center;
     margin:0 0 clamp(2.6rem,6vw,4rem)}
}

/* ---- §5b, the narrow two-column band ------------------------------------
   At 901–1050px the board is still in two columns inside a narrow viewport.
   Giving the head the desktop ratio leaves the menu column narrow
   enough for all eight descriptions to wrap together, adding roughly 140px to
   a fixed 100vh object. The menu gets 70% of the board here; the title comes
   down one step so its authored break remains exactly two lines in the smaller
   head. Measured at both ends of the band and at 760/900px viewport heights. */
@media (max-width:1050px) and (min-width:901px){
  #hors .board{grid-template-columns:minmax(0,.6fr) minmax(0,1.4fr);gap:2rem}
  #hors .head h2{font-size:clamp(1.9rem,3.4vw,2.4rem)}
  #hors .what{font-size:.86rem}
}

/* ---- tablet / narrow desktop -------------------------------------------- */
@media (max-width:900px){
  #intro .row,#history .intro,#rooms .wrap,#origin .cols{grid-template-columns:1fr}
  /* The desktop collage moves left with order:-1. Restore source order once the
     row stacks so the existing mobile sequence remains copy, then artwork. */
  #intro .figs{order:initial;height:52vh;margin-top:3vh}
  #rooms .art{order:-1}
  #rooms .art img{width:70%}
  #history .ev.has-fig{padding-right:0}
  #history .ev-fig{position:relative;right:auto;top:auto;margin-top:1.4rem;
     width:clamp(96px,26vw,150px)}

  /* §5b · The board stacks: head over plates, one column — the same 900px
     breakpoint every other two-column section on the page uses, and for the
     same reason. Below it the plates column is under 380px and the leader dots
     have no run left in them: name and price nearly touch, which is the one
     thing the convention exists to avoid.

     THE STAGGER HAS TO COMPRESS WITH IT, and that is the part that is not
     obvious from the layout change. At desktop the eight rows are ~78px each
     and the head sits beside them, so the list starts near the top of the
     stage and the deal has a third of the runway to play out in. Stacked, the
     head takes ~300px off the top and the rows tighten, so the foot of the
     list is at or below the fold: at the authored SPAN .34 / STEP .055 the
     last row is still arriving at --p .73, by which point the reader has
     scrolled past it and sees a row that is simply already there — having
     watched an empty space where it should have dealt.

     Half the step, slightly wider span: the whole board is settled by --p .5,
     while the stage is still pinned and the list is still on screen. Same
     gesture, played in the time a stacked layout actually gives it. */
  #hors .board{grid-template-columns:1fr;gap:2rem}
  #hors .head .body{max-width:none}
  /* one column, so the explicit grid-column:2 in hors.css would place this in a
     second column that no longer exists — which grid answers by creating one */
  #hors .tag{grid-column:1}
  /* Once stacked, the complete board is 835–857px tall before section padding.
     That only barely fits an 844–900px viewport and puts the kicker under the
     fixed chrome while the note crosses the cue. This is not a phone-only
     failure: 761–900px tablets use the same one-column board. Unpin at the same
     breakpoint that stacks it, preserve every dish in ordinary flow, and settle
     the stagger because --p only belongs to a sticky runway. */
  #hors{height:auto !important}
  #hors .sticky{position:static;height:auto;overflow:visible;padding:14vh 0 12vh}
  #hors .copy{position:relative}
  #hors .plate{--rp:1}
  #hors .what{max-width:none}
  #hors .relic{display:none}
  #hors .cue{display:none}
}

/* ---- phone -------------------------------------------------------------- */
@media (max-width:760px){
  :root{--pad:7vw}

  /* The chrome is fixed, so long copy scrolls underneath it. A scrim only hides
     that. Instead the chrome scrolls away with the page at this width — it is a
     320px screen and the three header links are reachable from the foot of the
     page. */
  .mark,.topnav{position:absolute}
  .mark img{width:44px}
  .topnav{top:30px;gap:1rem}
  .topnav a{font-size:9.5px;letter-spacing:.14em}

  .wordmark{width:74vw}
  #hero .germ{width:96vw;opacity:calc(.08 - var(--p)*.06)}

  #intro{padding-top:13vh}
  /* the two carve captions stacked on top of each other — flow them instead */
  #intro .figs{height:auto;display:flex;flex-direction:column;gap:1rem}
  #intro .figs .muse{position:relative;left:auto;right:auto;top:auto;bottom:auto;width:52%}
  #intro .figs .m2,#intro .figs .m3{align-self:flex-end;width:44%}
  #intro .carve{position:relative;left:auto;right:auto;top:auto;bottom:auto;max-width:none}

  /* §3 carried two words at this width and could let the figure sit anywhere.
     It now carries the thesis — a heading, a sub and two paragraphs — and at
     390px that column is the full width of the screen, so the figure was
     landing behind the body copy with both of them bone-coloured. She drops
     below the type and gives up some size; the copy takes the top of the
     screen and the stage takes the bottom. */
  #night .scrim{background:linear-gradient(180deg,transparent,var(--bone) 60%)}
  #night .hero img{width:44vw}
  #night .hero-a{--ay:74%}
  #night .rings-a{--ay:74%}
  #night .rings svg{width:78vw;height:auto}
  #night .copy.high{padding-top:9vh}
  #night h2{font-size:clamp(1.6rem,7vw,2.1rem);max-width:none}
  #night .body,#night .sub,#night .copy>*{max-width:none}
  /* a floor under the copy so the constellations never sit inside a line of
     text. Above .stage (z 1), below .copy (z 20). */
  #night .copy::before{content:"";position:absolute;inset:-6vh -7vw auto -7vw;
     height:calc(100% + 14vh);z-index:-1;pointer-events:none;
     background:linear-gradient(180deg,#0b1020e6 0%,#0b1020cc 62%,transparent 100%);
     opacity:var(--p)}

  /* §4's scrim is a radial centred at 27% — sized for the desktop layout, where
     the copy sits in the left third. Here the copy is full width and the named
     list has made it taller, so relics were reading through the middle of the
     sentences. Flip it to a vertical wash covering the copy band, matching the
     ground colour so the field only shows at the foot. */
  #collection .scrim{background:linear-gradient(180deg,#ece0c9 0%,#ece0c9f2 52%,#ece0c9c4 74%,transparent 100%)}
  #collection .named{max-width:none}

  /* §5 · The card is 740px tall on an 844px screen once the two columns stack,
     so it fills the frame — and the two relics were placed at 20%/16% from the
     edges, which at 390px is INSIDE it. The key sat across the first four
     drinks; the champagne bucket sat over the delicatessen price column and
     hid the very numbers the section exists to show.

     MOVING THEM DOES NOT WORK, and measuring is what proved it: the card
     leaves 39px at each side and ~52px below, so there is no corner on a phone
     that is outside the type. Any placement is a compromise between which
     words get covered.

     So the card OCCLUDES them instead. It takes the section's own ground
     colour — not a plate, the identical #e7dcc7 — which makes it opaque
     without reading as a new surface, and its existing z-index:20 does the
     rest. The relics keep their authored positions and still show in the
     margins and below the card, so the section keeps its atmosphere at the
     edges and loses none of its prices in the middle.

     (These overrides are only reachable at all because the geometry moved out
     of the inline style= on menu.eta. While it was inline it beat every rule
     here, so this whole block was dead code that read as a fix.) */
  #menu .card{background:#e7dcc7}
  #menu .relic{opacity:.55}

  #menu .cols{grid-template-columns:1fr;gap:1.8rem}
  /* the footnote landed on the scroll cue (2324–2346 against 2310–2334) — the
     card grew when the columns stacked and the cue is pinned 28px off the
     bottom. Give the card room to end above it. */
  #menu .card{padding-bottom:1.4rem}
  #menu .tag{margin-bottom:2.6rem}

  /* §5b is already unpinned at the 900px stack breakpoint above. Phones only
     adjust its row inset and use the section ground as the board's opaque field;
     the latter keeps any neighbouring section art from showing through during
     ordinary document flow. */
  #hors .board{background:#efe6d2;padding-bottom:1.2rem}
  #hors .plate{padding:.8rem 0 .8rem 2.2rem}
  #hors .tag{margin-bottom:0}

  #origin{padding-top:13vh}

  /* On a phone the whole plate fits the 320px width, so all sixteen names land
     in a third of the area they get on desktop and collide into an unreadable
     mat. The FIGURES still all draw — only the names go. The chart is the
     point; the labels are annotation. */
  #sky1877 .sky .cflabel{display:none}
  /* the standing line is 4 lines long at 320px, so the caption has to clear the
     scroll cue rather than sit on it */
  #sky1877 .cap{bottom:12vh;gap:.9rem}
  #sky1877 .cap h2,#sky1877 .cap .body{max-width:100%}
  #sky1877 .stop{margin-top:.8rem}
  #sky1877 .stop .ln{font-size:.92rem;line-height:1.45}
  /* The instrument keeps its live line and loses the coordinates. A phone has
     no room for five rows of mono in a corner, and LAT/LON is the part a reader
     can look up anywhere; whether the sky is open is not.
     It stays ABOVE the fold rather than moving to the bottom: the bottom of a
     phone screen already carries .cap (12vh) and .cue (28px), and a
     bottom-anchored readout landed on top of both — the live line printed
     straight through the standing line. The top of the plate is empty sky on a
     narrow viewport, so that is where it goes. */
  #sky1877 .inst{top:64px;right:var(--pad);left:var(--pad);
     font-size:8.5px;line-height:1.8}
  #sky1877 .inst .plate,#sky1877 .inst .coords{display:none}

  /* §9 · The three stats are 39 + 156 + 94 wide in a 335px row, so flex-wrap
     put 28 and 3 on one line and dropped 1877 alone underneath — which reads as
     two of them being a pair and the third being a footnote. They are three
     equal facts. A three-column grid states that at any width, and the middle
     label ("floors above the bar", the long one) is what forced the break. */
  #rooms .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:1.1rem;align-items:start}
  #rooms .stat .n{font-size:clamp(1.7rem,7vw,2.2rem)}
  #rooms .stat .l{font-size:9px;letter-spacing:.12em;line-height:1.45}

  #history .ev p{max-width:none}

  /* §10 · THE CHAPTER UNPINS HERE.
     Once the plate stacks to one column it is 1043px tall on an 844px screen,
     and a sticky stage is `height:100vh; overflow:hidden` — so the map was cut
     off at the top and the mailing-list form at the bottom, with no scroll
     position that showed either. The plate is the whole section; a frame it
     cannot fit inside is the wrong frame.

     So on a phone this stops being a pinned chapter and becomes an ordinary
     block that is as tall as its own content. The rise-into-place transform
     goes with it — it is driven by --p, which only a sticky stage is given, and
     a plate stuck at .32 opacity would be worse than no animation. Desktop is
     untouched.

     The !important is against page.eta's inline `style="height:190vh"`, which
     comes from the section registry. Registry data is not overridable from a
     stylesheet any other way, and the alternative — dropping `height` from the
     registry — would cost the desktop animation to fix a phone. */
  #contact{height:auto !important}
  #contact .sticky{position:static;height:auto;overflow:visible;padding:14vh 0 12vh}
  #contact .copy{position:static}
  #contact .plate{grid-template-columns:1fr;transform:none;opacity:1}
  #contact .mapwrap{border-right:none;border-bottom:1.5px solid var(--ink)}
  /* the two relics are placed against the sticky stage; with the stage now as
     tall as the plate they stretch down the whole section, so they are pinned
     to its corners and dimmed rather than left floating mid-copy. */
  #contact .relic{opacity:.5}
}

/* ---- narrow phones ------------------------------------------------------
   THE ONLY THING THIS BREAKPOINT DOES IS LOWER THREE CLAMP FLOORS FOR CAPS.

   Section titles are set in caps (§D), which are about 15% wider than the mixed
   case the sizes were originally chosen for. Every heading's vw term was solved
   against the box it actually gets and holds down to 320px — but three headings
   hit their clamp MINIMUM before that, and a floor does not scale, so below
   ~372px they overflowed and wrapped an authored two-line title into three.

   Measured at 320px, the widest that fits: menu 26px, history 30px, rooms 36px
   — set here as 1.6/1.85/2.2rem. Each is the longer authored line of a title
   that carries a <br> ("AND SOMETHING TO EAT.", "HAPPENED HERE FIRST.", "TO
   STAY THE NIGHT.").

   Scoped to 374px rather than folded into the floors themselves because the
   floors are also what the 375–760px band uses, and lowering them there would
   shrink titles on every ordinary phone to fix the smallest one. 374 sits just
   under the 375px iPhone SE/mini width, which measured clean.
   ------------------------------------------------------------------------- */
@media (max-width:374px){
  #menu h2{font-size:1.6rem}
  #history .intro h2{font-size:1.85rem}
  #rooms h2{font-size:2.2rem}
}

/* ---- reduced motion -----------------------------------------------------
   Everything stops. There was going to be one deliberate exception — an
   animation that made a point about motion — and it was cut, because a device
   that animates for people who asked for no animation is a WCAG 2.3.3 failure,
   not a joke.

   `.drift{animation:none}` alone left the map's ping and some drift instances
   still running, so the blanket rule is exhaustive and the two known stragglers
   are named explicitly after it.
   ------------------------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .drift,#contact .map .pinring{animation:none !important}

  /* §2.5 · the shells stop dead. NOT display:none and not a still frame of the
     animation — the nine bands are a partition of unity, so with every
     transform removed they recomposite to exactly the source photograph.
     Reduce Motion gets the fresco as a photograph, which is the correct
     destination of the idea rather than a frozen midpoint of the dolly. */
  #ceiling .clayer{transform:none !important;filter:none !important}
  #ceiling .cscene{transform:none !important}

  /* §7 · no zoom, no drift, no breathing: the finished chart, at rest. The JS
     pins --r to 1 and --z to Z0 so the whole plate is inscribed and every
     figure is already drawn. .sky KEEPS its transform — that transform IS the
     fit, and removing it would render the plate at 3.3×. */
  #sky1877 .mg{transform:none !important}
  #sky1877 .sky .st{animation:none !important}
  #sky1877 .sky .cf .ln{transition:none !important}
  /* Reduce Motion sits at the wide state permanently, where the names collide —
     so they stay off, exactly as they do at the wide end of the zoom. Hover
     still reveals them one at a time. */
  #sky1877 .sky .cf:not(.hv) .cflabel{opacity:0 !important}
}
