@font-face{font-family:'Asimov Print B';src:url('fonts/AsimovPrintB.otf') format('opentype');font-weight:400 900;font-style:normal;font-display:swap}
@font-face{font-family:'Asimov Print B';src:url('fonts/AsimovPrintBItal.otf') format('opentype');font-weight:400 900;font-style:italic;font-display:swap}
/* ============================================================
   BASE: the system every page shares, case pages included.
   Loaded before site.css (and before the case pages' inline styles),
   so it defines tokens and site-wide behaviour, and the page styles
   build on it.

   Three jobs, in the order Shrey set them (18 Sep 2026):
   personality, hierarchy, readability.
   ============================================================ */

:root{
  /* ======================================================================
     B0 (29 Oct): these names are now ALIASES into design/tokens.css, which
     is the single source of truth. Every value below is a var() reference,
     so the computed output is unchanged and nothing in the repo had to move.

     The aliases are the migration path, not the destination: as components
     adopt the token names the aliases shrink, and when the last one goes
     the old names go with it. Do not add a new alias; use the token.
     ====================================================================== */
  /* TYPE SCALE. Five sizes and nothing between them.
     Reset to his scale on 28 Oct (T5): 12 for caps labels only, then
     16, 20, 32, 56. H1 and H2 both top out at 56; H3 is 32; body is 20
     and --t-small is 16, which is the floor for body copy and buttons.
     --t-small was 14 and carried 129 elements on the homepage, so it was
     the body size in practice rather than a caption size. Headings step
     down on narrow screens; body and small never shrink. */
  --t-h1:var(--size-display);                          /* 56, the display step  */
  --t-h2:var(--size-section);                            /* 32, section headings  */
  --t-h3:var(--size-item);                         /* 20                    */
  --t-body:var(--size-item);                       /* 20 */
  --t-small:var(--size-body);                         /* 16, the body floor */
  --lh-tight:1.02; --lh-head:1.1; --lh-body:1.6;

  /* SPACING SCALE. Every gap is one of these. */
  --s-1:4px;  --s-2:var(--space-1);  --s-3:12px; --s-4:var(--space-2); --s-5:var(--space-3);
  --s-6:32px; --s-7:var(--space-4); --s-8:64px; --s-9:var(--space-5); --s-10:128px;

  /* PALETTE AND FAMILIES. The locked three colours and the three faces.
     Defined here, not in site.css, because the case pages load only this
     file and tw.css. */
  --cream:var(--c-cream); --deep:var(--c-cobalt); --fluoro:var(--c-tomato);
  --cream-rgb:var(--c-cream-rgb); --deep-rgb:var(--c-cobalt-rgb);
  --fd:var(--font-display);
  --fd-track:var(--font-display-track); /* Asimov Print B is already tight; Geist needed more */
  --fb:var(--font-body);
  --fu:var(--font-label);
  --pad:clamp(24px,4vw,48px); --max:var(--container);

  /* PAPER. A hand-drawn grid, not a ruled one (Shrey, 2 Oct 2026, with a
     reference image): 90px cells, lines that wobble between the crossings and
     are pinned at the tile edges so the 270px tile repeats without a seam.
     Two of every three lines sit lighter, the way a drawn grid does.
     Opacity is carried on the layer, not baked into the colour, so one number
     tunes the whole site. Keep it at or under .30: the test is that body copy
     over it reads exactly as it does on plain paper. */
  --paper:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22270%22%20height%3D%22270%22%20viewBox%3D%220%200%20270%20270%22%3E%3Cg%20fill%3D%22none%22%20stroke%3D%22%23173CA8%22%20stroke-width%3D%221.15%22%20stroke-linecap%3D%22round%22%3E%3Cpath%20d%3D%22M0.0%200.0%20Q-1.5%2022.5%20-1.1%2045.0%20Q-0.6%2067.5%200.1%2090.0%20Q-0.2%20112.5%20-0.5%20135.0%20Q0.3%20157.5%201.2%20180.0%20Q0.0%20202.5%200.5%20225.0%20Q0.6%20247.5%200.0%20270.0%22%20stroke-opacity%3D%220.30%22%2F%3E%3Cpath%20d%3D%22M90.0%200.0%20Q91.1%2022.5%2090.2%2045.0%20Q89.8%2067.5%2090.5%2090.0%20Q89.3%20112.5%2089.2%20135.0%20Q89.1%20157.5%2089.7%20180.0%20Q90.0%20202.5%2090.4%20225.0%20Q90.7%20247.5%2090.0%20270.0%22%20stroke-opacity%3D%220.22%22%2F%3E%3Cpath%20d%3D%22M180.0%200.0%20Q179.3%2022.5%20179.3%2045.0%20Q179.7%2067.5%20180.0%2090.0%20Q178.4%20112.5%20178.6%20135.0%20Q179.4%20157.5%20178.5%20180.0%20Q179.7%20202.5%20179.5%20225.0%20Q179.2%20247.5%20180.0%20270.0%22%20stroke-opacity%3D%220.30%22%2F%3E%3Cpath%20d%3D%22M0.0%200.0%20Q22.5%20-0.6%2045.0%20-0.7%20Q67.5%20-1.5%2090.0%20-0.3%20Q112.5%20-0.3%20135.0%201.9%20Q157.5%201.7%20180.0%201.8%20Q202.5%200.7%20225.0%20-0.6%20Q247.5%200.2%20270.0%200.0%22%20stroke-opacity%3D%220.30%22%2F%3E%3Cpath%20d%3D%22M0.0%2090.0%20Q22.5%2090.9%2045.0%2090.0%20Q67.5%2089.6%2090.0%2089.6%20Q112.5%2091.3%20135.0%2091.5%20Q157.5%2089.1%20180.0%2088.7%20Q202.5%2087.8%20225.0%2088.2%20Q247.5%2088.9%20270.0%2090.0%22%20stroke-opacity%3D%220.22%22%2F%3E%3Cpath%20d%3D%22M0.0%20180.0%20Q22.5%20180.0%2045.0%20181.5%20Q67.5%20180.4%2090.0%20179.8%20Q112.5%20180.7%20135.0%20181.0%20Q157.5%20179.2%20180.0%20178.6%20Q202.5%20180.6%20225.0%20181.6%20Q247.5%20181.1%20270.0%20180.0%22%20stroke-opacity%3D%220.30%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  --paper-size:270px 270px;
  /* the alpha is baked into the tile: keep the strongest line at or under .30 */
  /* MOTION. Three speeds, two curves. */
  --d-press:90ms;  --d-1:180ms;  --d-2:420ms;  --d-3:700ms;
  /* ONE curve, everywhere, at his instruction (25 Oct, block E). It was
     cubic-bezier(.16,1,.3,1), a hard decelerate that overshoots the eye on
     short moves; his is gentler at both ends and reads the same at 180ms
     and at 700ms, which is what "everywhere" has to mean when the same
     token drives a hover and a page reveal. The spring stays for a press
     release, which is the one place an overshoot is the point. */
  --e-out:cubic-bezier(.2,.7,.2,1);
  --e-spring:cubic-bezier(.34,1.56,.64,1);   /* the release overshoot */
}


/* ---------- PALETTE, RETRO (21 Sep 2026, Shrey: "a bit like Superman's") ----------
   The comic-book primaries, aged to newsprint so they sit on cream paper
   rather than shouting on white. Blue carries the type, red is the surface,
   yellow is the rare highlight. The ground was lifted from #EFE2BA to #F7F4EC on
   24 Sep 2026 at Shrey’s instruction: the same cream, taken towards white.
   Every pairing below improved, none regressed. Measured on #F7F4EC:
     --deep  #173CA8  cape blue      7.72  body type, rules, planes
     --accent #E8251F cape red       3.69  planes and type 24px and over ONLY
     --accent-type #B8141A           5.52  red as SMALL type
     --yellow #FFED10 Superman "Broom" yellow, trued to the source palette 23 Sep        ACCENT ONLY: marks, highlights, stars. Never a plane.
     --ink   #0D1633  print black         type on red or yellow planes
     --riso  #E00080  the star artefact's own ink; the star and nothing else
   Small type never sits directly on red (ink 3.55, cream 3.88 both fail):
   it gets a cream label under it. */
:root{
  --accent:#E8251F;
  --accent-type:#B8141A;
  --yellow:#FFED10;
  --ink:#0D1633;
  --riso:#E00080;
}
/* DARK (rebuilt 22 Sep 2026). The toggle used to flip a set of variables the
   home, work, me and contact pages never read, so it only recoloured the nav.
   Now the two base tokens swap: the cream ground becomes night blue and the
   blue type becomes paper, so every rule built on them follows. Red and
   yellow are unchanged. Measured on #101830: paper 13.71, red 3.94 (large
   type only, as in light), lifted red 6.29 for small accent type, yellow 10.86.
   Case pages keep their own scoped dark set in assets/tw/input.css. */
html[data-theme="dark"]{
  --accent-type:#FF6B5E;
  --paper:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22270%22%20height%3D%22270%22%20viewBox%3D%220%200%20270%20270%22%3E%3Cg%20fill%3D%22none%22%20stroke%3D%22%23EDE3C4%22%20stroke-width%3D%221.15%22%20stroke-linecap%3D%22round%22%3E%3Cpath%20d%3D%22M0.0%200.0%20Q-0.2%2022.5%20-1.7%2045.0%20Q-1.3%2067.5%200.9%2090.0%20Q0.3%20112.5%201.8%20135.0%20Q1.3%20157.5%201.9%20180.0%20Q0.8%20202.5%20-1.9%20225.0%20Q-1.4%20247.5%200.0%20270.0%22%20stroke-opacity%3D%220.18%22%2F%3E%3Cpath%20d%3D%22M90.0%200.0%20Q89.0%2022.5%2089.7%2045.0%20Q90.0%2067.5%2091.8%2090.0%20Q91.3%20112.5%2090.2%20135.0%20Q88.8%20157.5%2088.6%20180.0%20Q88.4%20202.5%2090.0%20225.0%20Q90.3%20247.5%2090.0%20270.0%22%20stroke-opacity%3D%220.13%22%2F%3E%3Cpath%20d%3D%22M180.0%200.0%20Q179.4%2022.5%20180.1%2045.0%20Q181.7%2067.5%20181.4%2090.0%20Q180.5%20112.5%20180.5%20135.0%20Q178.9%20157.5%20178.2%20180.0%20Q179.0%20202.5%20179.8%20225.0%20Q178.9%20247.5%20180.0%20270.0%22%20stroke-opacity%3D%220.18%22%2F%3E%3Cpath%20d%3D%22M0.0%200.0%20Q22.5%20-1.2%2045.0%20-1.5%20Q67.5%20-0.2%2090.0%201.1%20Q112.5%20-0.6%20135.0%20-1.5%20Q157.5%200.3%20180.0%201.3%20Q202.5%202.0%20225.0%200.6%20Q247.5%20-0.3%20270.0%200.0%22%20stroke-opacity%3D%220.18%22%2F%3E%3Cpath%20d%3D%22M0.0%2090.0%20Q22.5%2090.0%2045.0%2091.8%20Q67.5%2090.6%2090.0%2091.0%20Q112.5%2090.5%20135.0%2089.1%20Q157.5%2089.6%20180.0%2088.5%20Q202.5%2089.2%20225.0%2089.7%20Q247.5%2089.9%20270.0%2090.0%22%20stroke-opacity%3D%220.13%22%2F%3E%3Cpath%20d%3D%22M0.0%20180.0%20Q22.5%20181.4%2045.0%20181.1%20Q67.5%20180.2%2090.0%20179.8%20Q112.5%20181.6%20135.0%20181.8%20Q157.5%20180.9%20180.0%20181.7%20Q202.5%20180.3%20225.0%20179.6%20Q247.5%20180.3%20270.0%20180.0%22%20stroke-opacity%3D%220.18%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  --cream:#101830; --deep:#EDE3C4;
  --cream-rgb:16,24,48; --deep-rgb:237,227,196;
  color-scheme:dark;
}

/* ---------- navigation, every page ----------
   Phones used to get no navigation at all: the links were hidden under
   820px on ten of fifteen pages. The site is judged in under thirty
   seconds, so every route stays one tap away. Two rows on phones: name
   and theme toggle, then the four links at a full tap height. The html
   body prefix outranks the page styles, which load after this file. */
html body nav .links a{font-size:var(--t-small)}
@media (max-width:820px){
  html body nav{flex-wrap:wrap;row-gap:0;width:100%;max-width:100vw;box-sizing:border-box}
  html body nav .nav-end{display:contents}
  html body nav .theme-toggle{order:2}
  html body nav .links{display:flex;order:3;flex-basis:100%;min-width:0;gap:var(--s-4);justify-content:flex-start;flex-wrap:wrap}
  html body nav .links a{padding:var(--s-3) 0;letter-spacing:.02em}

}

/* ---------- focus: always visible, never the browser default ---------- */
:where(a,button,summary,[role="button"],[tabindex]):focus-visible{
  outline:2px solid var(--accent);outline-offset:3px}

/* ---------- press feedback ----------
   Every click answers. assets/feel.js springs the target itself (it
   composes with the transforms components already use); this handles
   the colour side of the state change and kills the grey tap flash. */
:where(a,button,summary,[role="button"]){-webkit-tap-highlight-color:transparent}
.is-pressed{filter:brightness(.94)}

/* the email address copies itself as well as opening mail, and says so */
.copied-note{display:inline-block;margin-left:var(--s-3);font-family:var(--fu);
  font-size:var(--t-small);letter-spacing:.04em;opacity:0;transform:translateY(4px);
  transition:opacity var(--d-1) var(--e-out),transform var(--d-2) var(--e-spring);vertical-align:middle}
.copied-note.on{opacity:.8;transform:none}

/* ---------- clip reveal, for section headlines ----------
   The line rises out of its own baseline. Added by script only (the
   hiding class is never in the markup), and every path ends visible. */
html.js-clip .clip{clip-path:inset(-10% -5% 100% -5%);transform:translateY(.35em);
  transition:clip-path var(--d-3) var(--e-out),transform var(--d-3) var(--e-out)}
html.js-clip .clip.in{clip-path:inset(-10% -5% -20% -5%);transform:none}

@media (prefers-reduced-motion:reduce){
  html.js-clip .clip{clip-path:none;transform:none;transition:none}
  .copied-note{transition:none}
}

/* ---------- small labels hold AA at 14px ----------
   These were faded to .5-.6 opacity when they were 9-10px texture. At the
   14px floor they are reading text, and .8 is the lowest that clears 4.5:1
   on both cream and ink. The hero discipline line had a fixed colour that
   did not follow the theme. */
html body :is(.phead .no,.pfoot .sig,.lab,.fbtn,.pm,.no){opacity:.82}
html body .fbtn[aria-pressed="true"]{opacity:1}
html body .proof .no{opacity:1}


/* ---------- interface guidelines pass (20 Sep 2026) ---------- */
.skip{position:fixed;left:var(--s-3);top:var(--s-3);z-index:200;padding:var(--s-3) var(--s-4);
  background:#0D1633;color:#F7F4EC;font-family:var(--fu);font-size:var(--t-small);
  text-decoration:none;transform:translateY(-160%);transition:transform var(--d-1) var(--e-out)}
.skip:focus-visible{transform:none}
:where(h1,h2,h3,.cta,.wk-title,.pg-title,.lt-cta){text-wrap:balance}
:where(p,li){text-wrap:pretty}
:where(a,button,summary,[role="button"]){touch-action:manipulation}
:where([id]){scroll-margin-top:96px}

/* ============================================================
   THE ISLAND (20 Sep 2026)
   One object at the top of every page. Expanded: name, links, theme. As the
   reader scrolls down it contracts to the name and a live readout (the
   section they are in, the song playing, the address just copied); scrolling
   up, hovering, tapping or tabbing into it opens it again. Navigation never
   leaves the screen, which is the point: the old bar hid itself on the way
   down, exactly when a visitor was deciding whether to get in touch.
   Clashes, marked: the veto list bans centred layouts and pills. The island
   is both by definition; picked on reachability. The page stays asymmetric.
   ============================================================ */
html body nav#nav{position:fixed;top:var(--s-3);left:50%;right:auto;bottom:auto;transform:translateX(-50%);
  display:grid;grid-template-columns:max-content max-content;align-items:center;gap:var(--s-4);width:var(--island-w,max-content);max-width:calc(100vw - var(--s-5));
  min-height:48px;box-sizing:border-box;padding:var(--s-1) var(--s-2) var(--s-1) var(--s-5);margin:0;
  background:#0D1633;color:#F7F4EC;border:1px solid rgba(247,244,236,.18);border-radius:999px;z-index:90;
  font-family:var(--fu);font-size:var(--t-small);letter-spacing:.08em;text-transform:uppercase;
  overflow:hidden;
  transition:width 620ms var(--e-spring),padding 620ms var(--e-spring),background-color var(--d-1) ease}
html[data-theme="dark"] body nav#nav{background:#173CA8}
html body nav#nav a{color:#F7F4EC;text-decoration:none}
/* The sm monogram, cropped from the three directions on the supplied
   artboard (2 Oct). It lives here, not in site.css, because the case pages
   load base.css and tw.css only: in site.css it rendered at its natural
   881px on those pages and ran off a phone screen. */
/* The mark sits at the type's own size (2 Oct feedback: the logo was bigger
   than the words beside it). 1em of the nav's font-size, which is --t-small,
   so the two scale together for ever. */
html body nav#nav .sig-logo{display:block;width:auto;height:1.05em;object-fit:contain}
html body nav#nav .sig{grid-area:1/1;font-family:var(--fu);font-size:var(--t-small);font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;white-space:nowrap;padding:var(--s-3) 0}
html body nav#nav .nav-end{grid-area:1/2;min-width:0;display:flex;align-items:center;white-space:nowrap;
  transition:opacity 260ms ease,transform 420ms var(--e-spring)}
html body nav#nav .links{display:flex;gap:var(--s-5)}
html body nav#nav .links a{padding:var(--s-3) 0;white-space:nowrap;min-width:24px;text-align:center}
/* 44, not 40: the one island control whose width can grow without
   changing the pill’s shape, because it is already square (D12). */
html body nav#nav .theme-toggle{width:44px;height:44px;margin-left:var(--s-3)}
.island-now{grid-area:1/2;min-width:0;display:flex;align-items:center;gap:var(--s-2);opacity:0;white-space:nowrap;pointer-events:none;
  font-family:var(--fu);font-size:var(--t-small);letter-spacing:.04em;
  transition:opacity 260ms ease,transform 420ms var(--e-spring);transform:translateY(4px)}
.inow-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--accent-type)}
nav#nav.is-live .inow-dot{animation:inow 1.2s ease-in-out infinite}
@keyframes inow{50%{opacity:.35}}
html body nav#nav.is-compact .nav-end{opacity:0;transform:none;pointer-events:none;transition:opacity 140ms ease}
html body nav#nav.is-compact .island-now{opacity:1;transform:none;pointer-events:auto;transition:opacity 220ms ease 320ms}
html body nav#nav.is-compact{cursor:pointer;padding-right:var(--s-4)}
html body nav#nav.is-compact .island-now{justify-self:start}
html body nav#nav.is-compact:is(:hover,:focus-within) .nav-end{opacity:1;transform:none;pointer-events:auto}
html body nav#nav.is-compact:is(:hover,:focus-within) .island-now{opacity:0;transform:none;transition:opacity 120ms ease}
html body nav#nav.is-compact:is(:hover,:focus-within){padding-right:var(--s-2)}
/* the way down tightens to the logo alone; the way up names the section again */
html body nav#nav.is-tight:not(:hover):not(:focus-within) .island-now{opacity:0;transform:none;transition:opacity 120ms ease}
html body nav#nav.is-tight:not(:hover):not(:focus-within){padding-right:var(--s-2)}
@media (max-width:820px){
  html body nav#nav{display:flex;left:var(--s-3);right:var(--s-3);width:auto;max-width:none;transform:none;flex-wrap:wrap;
    row-gap:0;border-radius:24px;padding:var(--s-1) var(--s-2) var(--s-1) var(--s-4)}
  html body nav#nav .nav-end{display:contents}
  html body nav#nav .island-now{order:1;flex:1;min-width:0}
  html body nav#nav .theme-toggle{order:2;margin-left:auto}
  html body nav#nav .links{order:3;flex-basis:100%;gap:var(--s-4);max-height:56px;overflow:hidden;
    transition:max-height var(--d-2) var(--e-spring),opacity var(--d-1) ease}
  html body nav#nav.is-compact .links{max-height:0;opacity:0;margin:0}
  html body nav#nav.is-compact:focus-within .links{max-height:56px;opacity:1}
  html body nav#nav.is-compact .island-now{max-width:none}
  html body nav#nav.is-compact .theme-toggle{display:none}
}
@media (prefers-reduced-motion:reduce){
  html body nav#nav,html body nav#nav .nav-end,.island-now,html body nav#nav .links{transition:none}
  html body nav#nav{width:auto}
  nav#nav.is-live .inow-dot{animation:none}
}

/* ---------- safe gutters (21 Sep 2026) ----------
   Copy was reaching the window edge. Every page container now keeps a gutter
   that also clears a notched phone's curved corners, and the fixed furniture
   (island, record) sits inside the same safe area. */html body .wrap,html body .desk-surface{
  padding-left:max(var(--pad),env(safe-area-inset-left,0px));
  padding-right:max(var(--pad),env(safe-area-inset-right,0px))}
@media (max-width:820px){
  html body nav#nav{left:max(var(--s-3),env(safe-area-inset-left,0px));
    right:max(var(--s-3),env(safe-area-inset-right,0px))}
}
html body .vinyl{right:max(clamp(.8rem,2vw,1.6rem),env(safe-area-inset-right,0px));
  bottom:max(clamp(.8rem,2vw,1.6rem),env(safe-area-inset-bottom,0px))}

/* ============================================================
   TYPE (23 Sep 2026, Reference Book row 07, marked Take)
   Headings    Geist Black (900), with a pressed-ink edge (see INK below)
   Body        Inter
   Annotation  Geist Light, one weight for every label, caption and nav item
   All three are OFL via Google Fonts, so the Milligram trial licence problem
   is gone. 'Geist Light' is an alias of the same Geist file pinned to 300, so
   every rule that sets a label's family gets the light cut without a weight.
   ============================================================ */
@font-face{font-family:'Geist Light';font-style:normal;font-weight:300;font-display:swap;
  src:url(https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwcGFU.woff2) format('woff2')}
body{font-synthesis:none}

/* headings get the display tracking; the dither that used to sit here was
   removed 21 Sep 2026 at Shrey's instruction */
:where(main,.mag,footer) :is(h1,h2):not(.hero-name),.wk-title{letter-spacing:var(--fd-track)}

/* ---------- shared across every page (moved here 21 Sep 2026 so the case
   pages, which do not load site.css, get the same) ---------- */
/* ---------- CROSS-PAGE TRANSITION ----------
   The two skins meet at a navigation, so the switch must not snap.
   View Transitions cross-fades document to document where supported and
   is inert everywhere else, which is the right kind of enhancement: the
   site is not relying on it to be usable. 260ms, matching the enter
   timing used everywhere else. */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation-duration:260ms;
  animation-timing-function:cubic-bezier(.16,1,.3,1)}
@media(prefers-reduced-motion:reduce){
  @view-transition{navigation:none}
}
/* island links: a rule draws under the one you are on or over */
html body nav#nav .links a{position:relative;text-decoration:none}
/* [WORK] becomes WORK with a rule under it: the brackets fade as the line
   draws, and come back when the pointer leaves. One gesture, two states. */
html body nav#nav .links a i{font-style:normal;display:inline-block;
  transition:opacity 180ms var(--e-out),transform 180ms var(--e-out)}
html body nav#nav .links a:is(:hover,:focus-visible,[aria-current="page"]) i{opacity:0}
html body nav#nav .links a:is(:hover,:focus-visible,[aria-current="page"]) i:first-of-type{transform:translateX(4px)}
html body nav#nav .links a:is(:hover,:focus-visible,[aria-current="page"]) i:last-of-type{transform:translateX(-4px)}
html body nav#nav .links a::after{content:"";position:absolute;left:.42em;right:100%;bottom:var(--s-2);height:2px;background:var(--accent);transition:right 220ms var(--e-out)}
html body nav#nav .links a:is(:hover,:focus-visible,[aria-current="page"])::after{right:.42em}
@media (prefers-reduced-motion:reduce){html body nav#nav .links a::after{transition:none}}

/* ---------- INK (Reference Book row 07) ----------
   Display headings read hard-pressed: a faint roughened edge and a hair of
   ink spread, from an SVG filter site.js adds to every page. Subtle on
   purpose; the words stay crisp at reading distance. */
:where(h1:not(.hero-name),h2,.said,.cta){filter:url(#ink)}
@media (forced-colors:active){:where(h1,h2,.cta){filter:none}}

/* ---------- ASSET PLACEHOLDERS (Reference Book rule 2) ----------
   Every missing icon, logo or image holds its exact slot as a labelled box,
   The slot is blank on the page (7 Oct): a ruled box awaiting an image reads
   as a layout, and the same box with a name and a ticket number in it reads as
   a bug to everyone except the person holding the ticket. Which slot is which
   lives in data-slot and in FILL-IN.md. */
.ph{display:grid;place-content:center;gap:2px;box-sizing:border-box;padding:6px;text-align:center;
  background:repeating-linear-gradient(135deg,#FCFAF4 0 8px,#F7F4EC 8px 16px);color:#0D1633;
  border:var(--line) dashed #0D1633;font-family:var(--fu);font-size:11px;line-height:1.25;letter-spacing:.04em;text-transform:uppercase}
/* ---------- THE GAP REGISTER (7 Oct 2026) ----------
   Every missing thing on the site is now one component with one treatment and
   one code, G01 upward in page order, matched line for line by FILL-IN.md.
   Four different treatments were in use before this (a dashed box on /work and
   /contact, a dashed box in the magazine, a dashed utility box on the case
   pages, and two dead icon buttons in the contact row), so a visitor met the
   same absence looking four different ways and read it as four kinds of broken.
   The treatment is the red left rule the system already uses for a trade-off
   and a pull quote, which reads as an editor's margin note rather than a hole.
   Lives in base.css because the case pages load only base.css and tw.css. */
.slot{display:block;max-width:32em;margin:var(--s-5) 0;border-left:3px solid var(--fluoro);
  padding:var(--s-2) 0 var(--s-2) var(--s-4);font-family:var(--fu);font-size:var(--t-small);
  line-height:1.6;letter-spacing:.01em}
.slot b{font-family:var(--fd);font-weight:900;font-size:1em;letter-spacing:.08em;
  color:var(--accent-type);margin-right:.6em}

/* ---------- THE STAR LOADER (13 Oct 2026, at his instruction) ----------
   The site's own mark, drawn rather than spun: the stroke writes itself on,
   the fill lands, and the whole thing steps rather than glides because that is
   the idiom the ransom tiles already animate on. It lives in base.css because
   every route carries a loading page now, not just home. */
/* The injected loading page is scoped to its own id and carries everything it
   needs, because the case pages load only base.css and tw.css and the richer
   home loader lives in site.css. Self-contained beats moved: nothing of home's
   changes. */
#sm-page-loader{position:fixed;inset:0;z-index:120;background:var(--cream);
  display:grid;place-items:center;transition:clip-path 620ms cubic-bezier(.16,1,.3,1)}
#sm-page-loader.gone{clip-path:inset(0 0 100% 0);pointer-events:none}
#sm-page-loader .lgrid{display:grid;place-items:center;gap:var(--s-4)}
#sm-page-loader .lrule{width:min(42vw,280px);height:2px;background:var(--deep);
  transform:scaleX(0);transform-origin:0 50%}
#sm-page-loader .lrule.draw{animation:lrule-draw 520ms cubic-bezier(.16,1,.3,1) forwards}
@keyframes lrule-draw{to{transform:scaleX(1)}}

.lstar{display:block;width:clamp(56px,7vw,96px);height:auto;margin:0 auto var(--s-5);
  fill:var(--fluoro);opacity:0}
.lstar.spin{animation:lstar-in 520ms steps(6,end) forwards}
@keyframes lstar-in{
  from{opacity:0;transform:rotate(-70deg) scale(.5)}
  to{opacity:1;transform:rotate(0deg) scale(1)}
}
@media (prefers-reduced-motion:reduce){.lstar{opacity:1}.lstar.spin{animation:none}}

/* ---------- THE GRID FADES IN (12 Oct 2026, at his instruction) ----------
   The drawn grid was a flat texture at one strength everywhere. It now starts
   nearly absent at the top of the page and comes up as the visitor scrolls, so
   the paper asserts itself the further in they get. --paper-in is written by
   site.js from the scroll position, 0 at the top and 1 by the second screen.
   It is a custom property on :root and the only thing reading it is the ground
   layer, so the cost is one variable write per frame and no relayout. */
:root{--paper-in:0}
body::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background-image:var(--paper);background-size:var(--paper-size);
  /* Dialled back 13 Oct, at his instruction: the grid was reaching opacity 1
     and reading as squared paper rather than as texture. The ceiling is .58
     now, and the floor .16. The SVG itself is untouched, so the strongest
     stroke is still .30 of this, which is the rule in CLAUDE.md.
     It also no longer runs the whole page: the flat blocks below paint over
     it, so the grid is the texture of SOME sections rather than of the site. */
  opacity:calc(.16 + var(--paper-in) * .42);transition:opacity 120ms linear}
@media (prefers-reduced-motion:reduce){body::before{opacity:1;transition:none}}

.vh{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ============================================================
   THE MOBILE NAV (20 Oct 2026). His note: "the mobile navigation is a
   failure, with a frustrating scroll experience." He is right, and the
   cause was specific rather than cosmetic.

   The island has three shapes and the OPEN one is reached by :hover. A
   touch screen has no hover, so the moment feel.js added .is-compact on the
   first scroll down, the links row was clipped to max-height 0 and there was
   no gesture left anywhere to reopen it. Below the fold, on a phone, the
   site had NO NAVIGATION AT ALL.

   The links were never hidden by opacity, which is why they read as present
   in the DOM and in a class check: they were clipped to ZERO HEIGHT inside
   an overflow:hidden pill. Found by measuring the box, not by reading the
   rules.

   Below 820px the island does not shrink. The three shapes exist to buy
   horizontal room on a wide screen; on a phone the links already wrap to
   their own row and there is nothing to save. Stable beats clever when the
   clever state cannot be exited.

   THIS BLOCK IS LAST IN THE FILE ON PURPOSE. The first attempt sat at line
   110 and lost to a rule at 239, which is the third time source order has
   silently beaten a fix in this repo.
   ============================================================ */
@media (max-width:820px){
  html body nav#nav.is-tight,
  html body nav#nav.is-compact{width:100%;padding-right:var(--s-4);cursor:auto}
  html body nav#nav.is-tight .links,
  html body nav#nav.is-compact .links,
  html body nav#nav .links{max-height:none;opacity:1;pointer-events:auto;
    transform:none;display:flex;overflow:visible;margin:0}
  html body nav#nav.is-tight .nav-end,
  html body nav#nav.is-compact .nav-end{opacity:1;pointer-events:auto;transform:none}
  /* the section read-out belongs to the wide island; on a phone it would
     push the links onto a third row to repeat what the heading says */
  html body nav#nav .island-now{display:none}
  /* the pill has to be able to grow to two rows */
  html body nav#nav{height:auto;overflow:visible}
}

/* ---------- block E: the shared element, and the case reading bar ----------
   The poster named "work-hero" on the index expands into the case hero of
   the page it opens. 350ms, his number. The name is put on the clicked
   plate by site.js and lives permanently on the case image, because a
   view-transition-name has to be unique in a document and there is only
   ever one hero here. */
.casefig img{view-transition-name:work-hero}
::view-transition-old(work-hero),::view-transition-new(work-hero){
  animation-duration:350ms;animation-timing-function:cubic-bezier(.2,.7,.2,1)}

/* 2px, scroll-linked, NO easing: it is an instrument, and one that lags
   what it measures is worse than none. Transform only, so it never costs a
   layout on a page built to be read. */
.case-rail{position:fixed;left:0;top:0;z-index:70;width:100%;height:2px;
  background:var(--fluoro);transform:scaleX(0);transform-origin:0 50%;
  will-change:transform;pointer-events:none;
  /* NO easing, explicitly. It was picking up transition:all from a global
     rule and easing behind the scroll, which is the one thing an
     instrument must not do. */
  transition:none}
@media (prefers-reduced-motion:reduce){
  ::view-transition-old(work-hero),::view-transition-new(work-hero){animation:none}
}

/* ---------- THE LOADER CLEARS ITSELF (27 Oct) ----------
   Found while testing the hero failsafe with scripts disabled: the whole
   site was a blank cream screen. .loader is position:fixed, inset:0,
   z-index:120, and the ONLY thing that ever moved it was a script adding
   .gone. If home.js or site.js does not run, is blocked, or throws before
   that line, every route on this site is an empty page.

   That is the real version of the risk this file already records twice:
   the hero copy stranded at opacity 0 for four days in October. The same
   failure one layer up takes the entire page, not one block.

   So the loader now clears itself in CSS, with no script involved. The
   script still clears it sooner and with the wipe; this only catches the
   case where nothing runs. Both end in the same state, so they cannot
   disagree. Applies to the home loader and the one site.js injects. */
@keyframes loader-clear{to{clip-path:inset(0 0 100% 0);visibility:hidden}}
.loader,#sm-loader{animation:loader-clear 1ms linear 2.2s forwards}
@media (prefers-reduced-motion:reduce){
  /* it is already display:none under reduced motion, so nothing to clear */
  .loader,#sm-loader{animation:none}
}


/* H6 (28 Oct): the scale STEPS, it does not slide. A clamp lands on
   whatever the viewport makes it - 29, 44 and 46 at 1280 - and none
   of those is in his 12/16/20/32/56. One breakpoint, one step down,
   so at any width the page uses at most five sizes. */
@media (max-width:760px){
  :root{--t-h1:2rem; --t-h2:1.25rem;}
}
