/* GENERATED by tools/build_site.py -- DO NOT EDIT THIS FILE.
   Every build overwrites it. Edit the CSS string in tools/build_site.py
   and re-run the build. */
/* Above Black — site styles.
   Palette is the GAME's palette, not a new one: the night sky the town sits under, the
   diner's neon orange, the sign's teal, the bone-cream of the checkerboard. A visitor
   should recognise the screenshots as belonging to the page around them. */
:root{
  /* ── THEME TOKENS ──────────────────────────────────────────────────────────────
     Two schemes, both Justin's: DARK is teal + white + black (the night the town sits
     under, the Flying Saucer sign) and LIGHT is orange + amber + white (the Desert Night
     palette, the diner booths, the mesas at noon).

     The variable NAMES stay the palette names they always were -- --night, --ink, --teal --
     rather than being renamed to --bg/--fg. That is deliberate: every rule in this
     stylesheet already uses them, and a rename would touch several hundred declarations to
     achieve nothing a reader can see. Read them as ROLES: --night is "the page ground",
     --ink is "text on that ground", --teal is "the accent". In light mode the ground is
     bone-white and the accent is burnt orange, and every component follows without knowing.

     FIRST PAINT: the default comes from prefers-color-scheme, so a visitor's very first
     frame already matches their OS. An explicit choice is stamped onto <html data-theme>
     by js/site.js, which runs after first paint -- so someone who picks AGAINST their OS
     setting gets one frame of the other theme. The alternative is an inline <script> in
     <head>, and this site's CSP has no 'unsafe-inline' on purpose. One frame is the
     cheaper price. */
  --night:#0a0d14;
  --panel:#111621;
  --panel-2:#161d2b;
  --ink:#e9e2d0;          /* the diner's cream */
  --dim:#8b93a3;
  --rule:#222a3a;
  --rule-2:#39465e;
  --teal:#57c4b8;         /* the Flying Saucer sign */
  --orange:#e8834a;       /* the booths */
  --rust:#ae4923;
  --amber:#e0a32e;        /* the mark's warning rule -- Above Black's, not the studio's */
  --mesa:#7d3d2c;
  --pink:#ff3fa4;
  --ok:#5fd08a;
  --warn:#f0b342;
  --concept:#e8834a;
  --panel-3:#1b2231;
  --shadow:0 18px 50px -22px rgba(0,0,0,.85);
  /* The bar and the footer baked their own translucent black, so a token could not
     flip them and the header stayed night-dark on a bone page with unreadable brand
     text. Literals are where a theme quietly fails. */
  --bar-bg:rgba(10,13,20,.82);
  --foot-bg:rgba(0,0,0,.25);
  /* 1240, not more. The three auto-fit grids gain a column at ~1322px of CONTENT width;
     1240 puts content at 1225, below all three, so every grid keeps its current column
     count at every viewport with no media query. Widening is only safe because of
     --measure below: the prose column is a FIXED grid track and the rail is 1fr, so the
     extra 120px goes into the RAIL rather than into the gutter. Without the rail this
     change would make the complaint worse, not better. */
  --wrap:1240px;

  /* The prose column, in PIXELS, and it must not be a `ch` value. A custom property
     holding a font-relative unit resolves at the element that USES it -- so 66ch would be
     587px on a <p> and 1138px on a 32px <h2>, which is exactly why the page currently has
     six different right edges instead of two. 600px is within 13px of today's measure:
     this does NOT widen the prose, which was never the problem. */
  --measure:600px;
  --read:720px;
  --rail-gap:clamp(40px,4vw,76px);

  /* The one room screenshot that sits BESIDE prose rather than above it. 480 is the
     native width of every -thumb.jpg in web/shots/, so the 1240-and-up band renders it
     1:1; the 1080-1239 band narrows it to 360 because 600 + gap + media has to fit
     inside main and it does not fit at 480 down there. Five numbers now have to agree
     -- --wrap, --measure, --rail-gap, --media and the 1240 breakpoint -- and nothing
     will tell you when one of them moves. */
  --media:480px;
  --bar-h:63px;   /* .bar: 12px padding x2 + the 38px toggle + 1px border. */
  color-scheme:dark;
}

/* LIGHT: orange, amber, white. Applied by OS preference first... */
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]){
    --night:#faf7f0; --panel:#fffdf8; --panel-2:#f4efe3; --ink:#241a12; --dim:#6f5f4d;
    --rule:#e0d5c2; --rule-2:#c3b498; --teal:#b8541b; --orange:#c2621f; --rust:#9c3f1b; --amber:#c98a10;
    --mesa:#8a4a2c; --pink:#c2185b; --ok:#2f7d4f; --warn:#a06a05;
    --concept:#9c3f1b; --panel-3:#efe9dc;
    --shadow:0 16px 42px -24px rgba(70,45,20,.42);
    --bar-bg:rgba(250,247,240,.86); --foot-bg:rgba(120,90,50,.07);
    color-scheme:light;
  }
}
/* ...then overridden in BOTH directions by an explicit choice, which must win over the
   media query above -- that is why these are separate blocks and not one. */
:root[data-theme="light"]{
  --night:#faf7f0; --panel:#fffdf8; --panel-2:#f4efe3; --ink:#241a12; --dim:#6f5f4d;
  --rule:#e0d5c2; --rule-2:#c3b498; --teal:#b8541b; --orange:#c2621f; --rust:#9c3f1b; --amber:#c98a10;
  --mesa:#8a4a2c; --pink:#c2185b; --ok:#2f7d4f; --warn:#a06a05;
    --concept:#9c3f1b; --panel-3:#efe9dc;
  --shadow:0 16px 42px -24px rgba(70,45,20,.42);
  --bar-bg:rgba(250,247,240,.86); --foot-bg:rgba(120,90,50,.07);
  color-scheme:light;
}
:root[data-theme="dark"]{
  --night:#0a0d14; --panel:#111621; --panel-2:#161d2b; --ink:#e9e2d0; --dim:#8b93a3;
  --rule:#222a3a; --rule-2:#39465e; --teal:#57c4b8; --orange:#e8834a; --rust:#ae4923; --amber:#e0a32e;
  --mesa:#7d3d2c; --pink:#ff3fa4; --ok:#5fd08a; --warn:#f0b342;
  --concept:#e8834a; --panel-3:#1b2231;
  --shadow:0 18px 50px -22px rgba(0,0,0,.85);
  --bar-bg:rgba(10,13,20,.82); --foot-bg:rgba(0,0,0,.25);
  color-scheme:dark;
}


/* ── THE HEX BOARD ───────────────────────────────────────────────────────────────
   Third attempt, and the first two were wrong in opposite directions. Squares
   flipping ON TOP flipped the text with them. Then opaque hexagons scaling in read
   as a colour wash rather than as tiles. Justin: "the hexagons should be a faint
   background feature and they are like tiles on a board that are fliping over and
   the other side has the other color."

   So the lattice is PERMANENT and FAINT -- a quiet texture the page always has,
   about 3.5% ink, which you notice as a surface rather than as a pattern. On a
   theme change those same tiles turn over, and the far side is the other scheme.
   Nothing appears from nowhere and nothing washes over the page; the board that was
   already there simply flips.

   Everything sits at z-index:-1, behind every word and image. The page ground lives
   on <html>, not <body>, because a body background propagates to the root canvas and
   the canvas paints in front of negative z-index -- which is what made the first
   hexagons invisible. */

/* The permanent texture. mask-image rather than a coloured SVG, so the tint is a
   CSS token and follows the theme instead of being baked into a data URI.
   This is also the FALLBACK for the flip field below: no JS, reduced motion, or a
   browser without color-mix and this is what the page keeps, unchanged. */
.hextex{
  position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:var(--ink); opacity:.035;
  -webkit-mask-image:var(--hexmask); mask-image:var(--hexmask);
  -webkit-mask-size:56px 97px; mask-size:56px 97px;
  transition:opacity 500ms ease;
}
/* Once the tile field exists it IS the ground, so the flat texture would double it. */
body.hexlive .hextex{display:none}

/* ── the flip field ───────────────────────────────────────────────────────────────
   A CANVAS, after the DOM version was measured at 20fps.

   The DOM build put clip-path on 2,688 elements, and a clipped element that is animating
   cannot be cached as a composited layer -- so the browser re-rasterised every clip on
   every frame. Measured on production: 50ms median frames during a flip, one frame of
   1008ms. Removing clip-path alone took it to a locked 60fps, while halving the elements
   or dropping the 3D bought 4fps each. The shape was the entire cost, not the count and
   not the third dimension.

   Canvas has no clipping and no layers: one element, hexagons as paths, and the "flip" is
   a horizontal scale of |cos(angle)| with the face colour swapping as it crosses zero --
   which is exactly what a rotateY looks like, without asking the compositor for anything.

   Colours live in JS rather than here because a canvas cannot read CSS custom properties
   per shape; they are transcribed from the same export. */
#hexfield{
  /* -2, not -1: the reading scrim below sits between the honeycomb and the content. */
  position:fixed; inset:0; z-index:-2; pointer-events:none;
  width:100%; height:100%; display:block;
}

/* ── THE READING SCRIM ───────────────────────────────────────────────────────────────
   Justin, 2026-08-11: "With the text overlaying the grid now. these are a little
   difficult to read now. can we add something behind the text blocks to help?"

   He is right, and the shape of the fix comes from his own screenshot: the honeycomb
   looks GOOD in the margins and only competes where words sit on it. So this quiets the
   field behind the reading column and leaves the edges alone, rather than turning the
   grout down -- which he has looked at and locked.

   A scrim rather than a panel behind each text block, for two reasons. A panel per block
   turns a page into a stack of boxes and fights the editorial rail we just built; and it
   would need a fill opaque enough to hide a lattice, which is a card whether or not it
   has a border. One quiet plane behind everything reads as paper.

   MASKED AT BOTH EDGES so it has no vertical seam. A hard-edged column of slightly
   different ground running the height of the page is more distracting than the texture it
   is covering -- the eye finds edges before it finds contrast.

   Wider than --wrap by 120px so the fade lands in the gutter rather than on the text.
   Fixed, so scrolling never moves it relative to the tiles behind it. */
:root{
  --scrim:rgba(10,13,20,.80);
}
:root[data-theme="light"], :root:not([data-theme="dark"]){ --scrim:rgba(250,247,240,.84) }
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){ --scrim:rgba(10,13,20,.80) } }
:root[data-theme="dark"]{ --scrim:rgba(10,13,20,.80) }

/* THE FADE LIVES IN THE GUTTER, NEVER ON THE TEXT. First version was a fixed 1360px plane
   with a 6% fade, which put the end of the fade almost exactly on the content edge --
   Justin: "I feel it butts up to the content a little too tight." The percentage was
   measured against the plane, so it had no idea where the words were.

   Now the plane is the full viewport and the fade length is derived from the actual
   gutter: --gut is the space outside the content column, and the fade is capped at 190px
   so a very wide monitor gets a soft edge rather than a 400px smear. At 1900px that means
   the scrim is FULLY opaque from 190px in, while the text does not start until 337 --
   ~147px of clear ground before a single word. As the window narrows the fade narrows
   with it and stays in the margin, so it can never eat into the column at any width. */
body.hexlive::before{
  content:""; position:fixed; z-index:-1; inset:0; pointer-events:none;
  --gut:max(0px, calc((100vw - var(--wrap)) / 2));
  /* 60% OF THE GUTTER, not all of it. min(gut, 190px) was still wrong below ~1620px: once
     the gutter is narrower than the cap, the fade eats the whole thing and lands right on
     the text again -- the same complaint, just at a narrower window, which is the width
     Justin's screenshot was taken at. Spending only 60% always leaves the last 40% fully
     opaque, so there is clear ground before the first word at EVERY width:
       1900 -> 140px clear   1600 -> 72px   1400 -> 32px   1280 -> 8px */
  --fade:min(calc(var(--gut) * 0.6), 190px);
  background:var(--scrim);
  transition:background-color 180ms linear;
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--fade),
    #000 calc(100% - var(--fade)),transparent 100%);
  mask-image:linear-gradient(90deg,transparent 0,#000 var(--fade),
    #000 calc(100% - var(--fade)),transparent 100%);
}
/* Narrow screens have no margin to fade in: --gut is already 0 there, so this only
   silences the mask entirely rather than relying on it resolving to zero. */
@media (max-width:760px){
  body.hexlive::before{-webkit-mask-image:none; mask-image:none}
}

/* THE INK CROSSFADES FASTER THAN THE TILES, ON PURPOSE. If body colour transitions over
   the same duration, then mid-flip you have half-turned ink over a field that is half dark
   faces and half light -- text effectively disappears for a few hundred milliseconds. It is
   invisible at both endpoints, which is exactly why it would have shipped. That ceiling
   still holds below -- what changed is that the short crossfade now WAITS its turn instead
   of running immediately. */

/* ── THE CONTENT CHANGES WHEN THE WAVE REACHES IT ────────────────────────────────────
   Justin, 2026-08-11: "when the hexagon flip they take a while to change, but the rest of
   the content is instant. Can we find a way to match them up on their transitions?"

   The fix is not to slow the content to the tiles' ~2s, and not to speed the tiles up.
   The tiles do not change all at once either -- they change in a SWEEP, and a single
   duration cannot match a sweep, it can only average it. So each block takes the SAME
   delay the tile behind it took, and the recolour travels across the page with the
   flip instead of racing it or trailing it.

   --td is written per element by the flip, from the same pattern function that staggers
   the tiles. 260ms per element, not 1200: a tile has to physically turn, a background
   only has to change colour, and matching the durations would smear each block long after
   its own tiles had settled. What has to match is WHEN, not how long.

   Only concrete properties can transition -- custom properties cannot -- so these list
   background-color / color / border-color rather than relying on the token swap.

   ONLY ONE LEVEL MAY ANIMATE `color`, which is the whole reason these lists are split the
   way they are. colour inherits: if a container crossfades its colour AND a paragraph
   inside it does too, the paragraph spends the sweep chasing a value that is itself moving,
   and lands somewhere neither of them intended. So body snaps (transition:none), the TEXT
   rule animates leaves, and the SURFACE rule animates containers WITHOUT colour. The two
   selector lists are deliberately disjoint -- an element matching both would keep only the
   later `transition`, silently dropping the earlier one's properties. */
body.hexlive{transition:none}
/* Outside main and always on screen, so a positional delay would only look broken. */
body.hexlive .bar, body.hexlive .foot{transition:color 220ms linear,
  background-color 220ms linear, border-color 220ms linear}

/* text leaves -- colour, plus the wash on the pills that carry one */
body.hexlive main :is(h1,h2,h3,h4,h5,h6,p,li,figcaption,blockquote,summary,
                      a:not(.btn),.st,.mark,.num,.n,.rolepill,.qcount,.plate span){
  transition:color 260ms linear var(--td,0ms),
             background-color 260ms linear var(--td,0ms);
}
/* surfaces -- no colour, so nothing inside them is chasing a moving target */
body.hexlive main :is(.card,.region,.panel,.note,.plate,.shot,.sheetblk){
  transition:background-color 260ms linear var(--td,0ms),
             border-color 260ms linear var(--td,0ms);
}
/* a button is both: its own text is a direct child, and nothing under it transitions.
   A .tab is the same shape -- its label is a direct child, it carries its own colour and a
   hover fill, and nothing beneath it animates -- so it joins the button rather than getting
   a fourth list. It cannot go in the TEXT list above: that one does not animate
   border-color, and the selected tab's teal underline is a border. */
body.hexlive main :is(.btn,.tab){transition:background-color 260ms linear var(--td,0ms),
  border-color 260ms linear var(--td,0ms), color 260ms linear var(--td,0ms)}

@media (prefers-reduced-motion:reduce){
  body.hexlive *{transition:none !important}
}

@media (prefers-reduced-motion:reduce){
  /* No field at all: the flat texture stays and the theme just changes. */
  #hexfield{display:none}
  body.hexlive .hextex{display:block}
}
:root{
  --hexmask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='97'%3E%3Cg fill='none' stroke='%23fff' stroke-width='2'%3E%3Cpath d='M28 1 L54 16 L54 46 L28 61 L2 46 L2 16 Z'/%3E%3Cpath d='M0 49 L26 64 L26 94 L0 109'/%3E%3Cpath d='M56 49 L30 64 L30 94 L56 109'/%3E%3C/g%3E%3C/svg%3E");
}

/* The theme changes by FADE. There was a hexagon flip here -- the same lattice as the
   texture above, tiles turning over to reveal the other scheme -- and Justin's call after
   living with it: "It doesn't look that great for the switch. We can just do the fade for
   now instead." He is right that it was doing a lot of work for a control you press twice a
   day. The faint lattice STAYS, because that was a separate ask and it is still the page's
   surface; only the turning-over goes.

   A slightly longer fade than the old 620ms, because with nothing else happening the
   crossing is the whole effect and it should feel deliberate rather than abrupt. */
html{background:var(--night); transition:background-color 700ms ease}
body{background:transparent}

@media (prefers-reduced-motion: reduce){
  html{transition:none}
}

.themetoggle{
  background:none; border:1px solid var(--rule); color:var(--dim); cursor:pointer;
  border-radius:7px; padding:0; width:38px; height:38px; display:grid; place-items:center;
  font:inherit; line-height:1; flex:none;
}
.themetoggle:hover{color:var(--ink); border-color:var(--teal)}
.themetoggle svg{width:18px; height:18px; display:block; fill:currentColor}
.themetoggle .sun{display:none}
:root[data-theme="light"] .themetoggle .sun,
:root:not([data-theme="dark"]) .themetoggle .sun{display:block}
:root[data-theme="light"] .themetoggle .moon,
:root:not([data-theme="dark"]) .themetoggle .moon{display:none}
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) .themetoggle .sun{display:none}
  :root:not([data-theme="light"]) .themetoggle .moon{display:block}
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; color:var(--ink);
  font:16.5px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  -webkit-font-smoothing:antialiased;
  background-image:
    radial-gradient(ellipse 70% 45% at 50% -5%, rgba(87,196,184,.09), transparent 70%),
    radial-gradient(ellipse 60% 40% at 50% 105%, rgba(232,131,74,.07), transparent 70%);
  background-attachment:fixed;
  display:flex; flex-direction:column; min-height:100vh;
}
img{max-width:100%; height:auto; display:block}
a{color:var(--teal)}
a:focus-visible,button:focus-visible,input:focus-visible,summary:focus-visible{
  outline:2px solid var(--teal); outline-offset:3px; border-radius:3px;
}
.skip{position:absolute; left:-9999px}
.skip:focus{left:12px; top:12px; background:var(--panel); padding:10px 14px; z-index:50}

/* ── chrome ── */
/* flex-wrap:wrap AT EVERY WIDTH, NOT JUST ON PHONES, AND THIS IS A BUG FIX.
   Measured 2026-08-15 at an 881 px viewport, signed in as superadmin: the bar wanted 993 px
   and got 881, so it did what a nowrap flex row does -- it OVERFLOWED, and took the whole
   document with it (documentElement.scrollWidth 993). The page scrolled sideways and the
   account chip was cut off by the window edge.

   It is width-dependent AND role-dependent, which is why it went unseen: a signed-out
   visitor has four nav links and one button and fits in half the space. Only staff carry
   Editor + Tests + Cast + Blockout, so only staff can overflow, and the person with all
   four pills is the person who built it.

   Wrapping is the content-driven fix. A hard breakpoint would have to be set for the
   worst case (~1040 px) and would then collapse an ordinary visitor's nav into a hamburger
   at a width where it fits fine. Wrap asks the actual content: a visitor keeps one row at
   any width, staff get a second row only when the pills genuinely do not fit, and nobody
   gets a horizontally scrolling page. row-gap is small so a wrapped bar grows by a line
   rather than by a block. */
.bar{
  position:sticky; top:0; z-index:30; display:flex; align-items:center;
  flex-wrap:wrap; column-gap:22px; row-gap:8px;
  padding:12px max(20px,calc((100vw - var(--wrap))/2));
  background:var(--bar-bg); backdrop-filter:blur(10px);
  border-bottom:1px solid var(--rule);
}
.brand{display:flex; align-items:center; gap:11px; text-decoration:none; color:var(--ink); font-weight:600;
  letter-spacing:.14em; font-size:14px; white-space:nowrap}
.brand b{color:var(--teal); font-weight:600}
/* .brand .mark, NOT .mark. The status pills in .state carry the same class name, and this
   30x30 box was silently winning on width -- so "Not yet" was crammed into a 30px square
   and spilled out of its own pill, while "In" happened to fit and looked fine. A name
   shared by a 30px logo and a text label is the collision; scoping it is the fix. */
.brand .mark{width:30px; height:30px; flex:0 0 auto}
.bar nav{display:flex; gap:20px; margin-left:auto; font-size:14.5px}
/* ── ONE COLOUR PER NAV ITEM ───────────────────────────────────────────────────────
   Justin, 2026-08-12: "you are making the colors of these buttons in the navbar orange,
   can we make a color for each button that is in the palette of the website please?"

   Every colour below is an existing site token, so both themes are already handled -- each
   token carries a light and a dark value, and a hardcoded hex would have looked correct in
   one theme and wrong in the other.

   RESTING STATE IS STILL --dim FOR ALL OF THEM. Seven saturated colours sitting in a row
   at full strength is a toolbar that competes with the page it sits above. The colour is
   the HOVER and the CURRENT-PAGE state, which is where it does work: it tells you which
   item you are on and which one you are about to hit, and it is the same colour both
   times so the two states agree. */
.bar nav a{color:var(--dim); text-decoration:none; --nv:var(--ink)}
.bar nav a:hover,.bar nav a[aria-current]{color:var(--nv)}
.bar nav a[aria-current]{text-decoration:underline; text-underline-offset:6px;
  text-decoration-thickness:2px; text-decoration-color:var(--nv)}
/* SCOPED TO .bar nav, AND THAT IS NOT DECORATION. `.bar nav a` is (0,1,2) and a bare
   `.nv-home` is (0,1,0), so the base rule's --nv:var(--ink) beat every per-item value and
   all four links resolved to the same colour. It looked like the tokens were wrong; the
   cascade was. Matching the base selector's specificity is what makes the later rule win.
   Same shape as the .col.wide bug in the .idx block -- worth recognising on sight. */
.bar nav a.nv-home   {--nv:var(--teal)}
.bar nav a.nv-route  {--nv:var(--amber)}
.bar nav a.nv-about  {--nv:var(--ok)}
.bar nav a.nv-recruit{--nv:var(--pink)}

/* The two admin links were both orange, which made them read as one control. They keep
   the pill, because they are a different KIND of thing from the nav -- staff-only -- but
   they no longer look like each other. */
.adminlink{font-size:14.5px; color:var(--nv,var(--orange)); text-decoration:none;
  border:1px solid color-mix(in srgb,var(--nv,var(--orange)) 40%,transparent);
  padding:5px 10px; border-radius:6px; white-space:nowrap}
.adminlink:hover{background:color-mix(in srgb,var(--nv,var(--orange)) 14%,transparent)}
#adminlink{--nv:var(--orange)}

/* --rust, NOT --concept. --concept is an alias, and it aliases to a colour already spoken
   for in BOTH themes: it is #e8834a in dark, which is exactly --orange (the Editor link),
   and #9c3f1b in light, which is exactly --rust. So Editor and Cast rendered identical on a
   dark screen -- the same "they are all orange" Justin asked to fix, fixed for the four
   public links and missed for the three admin ones, because the check only measured the
   four. --rust is distinct from --orange in both themes. */
#castlink {--nv:var(--rust)}
/* --ok, the one green in the set. Every other pill is in the orange family (--orange,
   --warn, --rust) and a fourth of those would have re-made the "they are all orange"
   complaint that produced this block in the first place. Its only other use is /about's
   hover, which is a text link at the far end of the bar in a state you have to reach for
   -- not something anyone will confuse with a standing pill. */
#blockoutlink{--nv:var(--ok)}
/* --mesa, the last token not already spoken for by a pill or a nav item. */
#atlaslink{--nv:var(--mesa)}

/* ── PLAY GAME: the only filled control in the bar ──────────────────────────────────
   Justin, 2026-08-15: "can we add a dedicated button in the navbar for Play Game, that is
   the blueish color".

   --teal IS the blueish one, and it is the ONLY cool token in the set. Worth knowing
   before anyone "fixes" this: the light theme is entirely warm desert daylight, so --teal
   resolves to #57c4b8 on dark and #b8541b on light. Naming the token rather than the
   colour is what stops this being a blue rectangle sitting on a cream page in light mode.

   FILLED, where everything else in the bar is text or an outline. The bar already carried
   four nav links, three staff pills and a Sign in box; an eighth outlined thing would have
   weighed the same as the seven around it. A solid ground says "press this" without
   spending another colour. */
.playbtn{font:600 13.5px/1 ui-sans-serif,system-ui,sans-serif; letter-spacing:.01em;
  color:var(--night); background:var(--teal); border:1px solid var(--teal);
  border-radius:6px; padding:8px 14px; white-space:nowrap; text-decoration:none; flex:none}
.playbtn:hover{background:color-mix(in srgb,var(--teal) 84%,var(--ink));
  border-color:color-mix(in srgb,var(--teal) 84%,var(--ink))}
.playbtn:focus-visible{outline:2px solid var(--teal); outline-offset:2px}
.who{display:flex; align-items:center; gap:9px; font-size:13.5px; color:var(--dim)}
/* An author `display` beats the hidden attribute's UA rule, so .who stayed a live flex
   box while signed out. It was invisible only because the JS also empties it -- one
   line of belt to go with those braces. */
.who[hidden]{display:none}
.who img{width:26px; height:26px; border-radius:50%; border:1px solid var(--rule)}
.who a{color:var(--dim); text-decoration:none; border-bottom:1px solid var(--rule)}
.who a:hover{color:var(--ink)}

/* ── mobile nav ──
   The bar carries five things (brand, links, Editor, theme, account) and a phone has
   room for about three. Rather than shrink them all, the three that are NAVIGATION fold
   onto a second row of the same flex container and collapse behind a hamburger.

   flex-wrap + flex-basis:100%, NOT an absolutely-positioned drawer. The bar is sticky
   with a blurred background, so letting it grow means the panel inherits that ground and
   pushes the page down instead of floating over it -- no z-index, no scroll-lock, and
   nothing to mis-measure when the address bar hides on iOS. */
.navtoggle{
  background:none; border:1px solid var(--rule); color:var(--dim); cursor:pointer;
  border-radius:7px; padding:0; width:38px; height:38px; display:none; place-items:center;
  font:inherit; line-height:1; flex:none;
}
.navtoggle:hover{color:var(--ink); border-color:var(--teal)}
.navtoggle svg{width:20px; height:20px; display:block; fill:none;
  stroke:currentColor; stroke-width:1.9; stroke-linecap:round}
.navtoggle .close{display:none}
/* Sits between the nav and the theme toggle, and reads as a control rather than another
   nav link -- it is the only thing in the bar that changes state. Swaps to "Account" when a
   session exists, which is also how a signed-in person finds the delete buttons. */
.navauth{font:600 13.5px/1 ui-sans-serif,system-ui,sans-serif; letter-spacing:.01em;
  color:var(--ink);
  border:1px solid var(--rule-2); border-radius:6px; padding:8px 13px; white-space:nowrap;
  text-decoration:none; background:var(--panel-2)}
.navauth:hover{border-color:var(--teal); color:var(--teal)}
.navauth:focus-visible{outline:2px solid var(--teal); outline-offset:2px}
@media (max-width:860px){
  /* A fourth item in the bar wrapped it onto a second row and took the mobile header from
     about 60px to 126. It behaves like the nav links instead: hidden until the hamburger is
     open, then a full-width row inside the menu. */
  .navauth{display:none}
  /* :not([hidden]), for the reason the .adminlink rule below already carries: an author
     `display` beats the hidden attribute's UA rule, so without this the Sign in button
     reappears for signed-in people the moment they open the menu -- which is the exact
     duplicate control this was changed to remove, coming back on one breakpoint only.
     order:8 rather than 3 puts it last in the open menu; 3 now belongs to .themetoggle. */
  .bar[data-nav="open"] .navauth:not([hidden]){display:block; order:8; width:100%;
    text-align:center; padding:11px 13px}
}
.bar[data-nav="open"] .navtoggle .open{display:none}
.bar[data-nav="open"] .navtoggle .close{display:block}

@media (max-width:760px){
  /* Breakpoint duplicated in the MOBILE NAV block of the JS below -- it closes the menu
     on the way back up to desktop, where the button that would reopen it no longer
     exists. Change both. */
  .bar{flex-wrap:wrap; gap:12px; column-gap:14px}
  .brand{order:1}
  /* PLAY STAYS ON THE TOP ROW, where Sign in does not. .navauth folds into the hamburger
     at 860px because it is a destination; this is the thing the site exists to offer, and
     burying the primary action behind a menu button on the one device most likely to be
     handed to somebody is the wrong trade. It takes the margin-left:auto that used to sit
     on .themetoggle, and everything after it shifts one place. */
  /* "Play", not "Play Game", and this is arithmetic rather than taste. At 375 px the bar
     has 335 px of usable width and the four top-row items wanted 363: brand 153, button 92,
     theme 38, hamburger 38, three 14 px gaps. So the hamburger wrapped to a second row and
     the header went from ~60 px to ~110 -- the exact failure the .navauth block above was
     written to fix, re-made by adding a fifth thing to the bar. Dropping one word takes the
     button to ~56 px and the row to 321, which fits with room to spare. Measured in the
     browser, not estimated; the first two guesses at this both still wrapped. */
  .pb-x{display:none}
  /* 44 px TALL, WHICH IT WAS NOT. Measured at the 375x812 preset the Play button was
     52x32 -- the most-pressed control on the site, 32 px high, with the theme toggle 13 px
     to its right. A thumb that misses flips the site into dark mode, which reads as the
     site being flaky rather than as a miss. web/shell.html already sets min-height:44px on
     both of its in-game buttons, so the standard was known and simply had not been applied
     up here.
     HEIGHT ONLY, ON PURPOSE. The comment above this block is arithmetic: at 375 px the top
     row has 335 px and wants 321, and the first two attempts at that layout wrapped it onto
     a second row. Widening the two 38 px icon buttons to 44 spends 12 of the 14 px of slack
     and is not a change to make without a browser in front of you. Growing DOWNWARD costs
     the row nothing -- it is 62 px tall already -- so all three get the height and the two
     icons keep their width. */
  .playbtn{order:2; margin-left:auto; padding:8px 12px;
    min-height:44px; display:grid; place-items:center}
  .themetoggle{order:3; height:44px}
  .navtoggle{order:4; display:grid; height:44px}
  .bar nav{order:5; flex-basis:100%; margin-left:0; display:none;
    flex-direction:column; gap:0; font-size:16px}
  .adminlink{order:6; flex-basis:100%; display:none; text-align:center; padding:11px 10px}
  .who{order:7; flex-basis:100%; display:none; justify-content:center; padding-bottom:6px}
  .bar[data-nav="open"] nav{display:flex}
  /* :not([hidden]) or the Editor link and the account block reappear for everyone the
     moment the menu opens -- `display` would override the attribute that hides them. */
  .bar[data-nav="open"] .adminlink:not([hidden]){display:block}
  .bar[data-nav="open"] .who:not([hidden]){display:flex}
  /* ~44px of tap target, which is the whole reason for doing this at all. */
  .bar nav a{padding:13px 2px; border-top:1px solid var(--rule)}
}

main{flex:1 0 auto; padding:0 max(20px,calc((100vw - var(--wrap))/2))}
/* ── WIDE MODE ────────────────────────────────────────────────────────────────────
   Justin, 2026-08-15: "we have the page limited to a max width size, we should have a
   feature that we can expand that or allow it to go wider."

   --wrap is 1240 because that is what the PROSE pages want, and the note above it explains
   the three auto-fit grids it was measured against. That reasoning is about reading, and a
   room 60 tiles across is not reading -- the editor was drawing Rachel into a 1225px column
   with a third of the monitor sitting empty beside it.

   So the cap becomes a toggle rather than a smaller number: body.wide lets the page use the
   window, minus a gutter. It is opt-in and remembered, because the narrow measure is still
   right for every page that is words. `.wide` was already being emitted by shell() with
   nothing behind it -- this is the CSS that flag was always describing.

   The editor's canvas re-fits on its own: it watches its stage with a ResizeObserver, so
   the room grows into the new width the moment the class lands. */
body.wide{--wrap:min(2600px, calc(100vw - 40px))}
/* WIDE MEANS THE WINDOW, FOR THE EDITOR. Justin, 2026-08-15: "When I click wide, can we
   have the tool in the editor go full size inside the browser window?"
   The --wrap toggle above is the right answer for a page of words -- it still caps at
   2600px, because a 4000px line of prose is unreadable however wide the monitor is. A tool
   is not prose: every pixel it gets is more room, and the cap was leaving a visible margin
   on a wide screen for no benefit. So the panel breaks out of the wrapper entirely rather
   than the wrapper growing, which leaves every other page's measure exactly as it was.
   The canvas re-fits itself -- it watches its stage with a ResizeObserver. */
body.wide .gb-panel{
  width:calc(100vw - 16px); max-width:none;
  margin-left:calc(50% - 50vw + 8px); margin-right:calc(50% - 50vw + 8px)}
section{padding:64px 0; border-bottom:1px solid var(--rule)}
section:last-child{border-bottom:0}
.eyebrow{
  font:600 11.5px/1 ui-monospace,SFMono-Regular,Menlo,monospace; letter-spacing:.24em;
  text-transform:uppercase; color:var(--teal); margin:0 0 16px;
}
/* NO max-width. It was 20ch, which is a sensible cap for a display line in the abstract and
   wrong here: the hero column is already ~540px, so 20ch broke this sentence over FOUR lines
   and left a ragged gap beside it while the paragraph underneath ran the full column.
   Justin, 2026-08-12: "the subtitle or what is in bold is truncated a little, can we let it
   extend out to the same width as the body below it please."
   The column is the measure. text-wrap:balance still evens the lines out, which is what the
   20ch was really reaching for -- a cap sets a width, balance sets a SHAPE, and shape was
   the thing wanted. */
.tagline{font-size:clamp(21px,3.2vw,28px); line-height:1.18; letter-spacing:-.015em;
  margin:0 0 16px; text-wrap:balance; font-weight:600}
h1{font-size:clamp(34px,6.2vw,60px); line-height:1.03; letter-spacing:-.02em; margin:0 0 20px; text-wrap:balance}
h2{font-size:clamp(24px,3.4vw,32px); line-height:1.15; letter-spacing:-.015em; margin:0 0 14px; text-wrap:balance}
h3{font-size:19px; margin:0 0 8px}
p{max-width:66ch; margin:0 0 15px}
.lede{font-size:19.5px; color:var(--ink)}
.dim{color:var(--dim)}
.tiny{font-size:13.5px; color:var(--dim)}
hr{border:0; border-top:1px solid var(--rule); margin:34px 0}

/* A BLOCK AND ITS ACTION BUTTONS, SIDE BY SIDE.
   Justin, 2026-08-12: "the box shortened so its half of the width and we can place the
   buttons to the right so we have 2 columns for that little part."

   Its own container rather than .duo-a/.duo-b, because this section already spends its grid
   on a heading and a four-card row -- adding two more pinned children would put them in the
   same row as the cards and collide. A self-contained pair cannot fight a grid it is not
   part of, which is the same reason .duo names its halves instead of counting children.

   Centred on the cross axis so two lines of buttons sit against the middle of a four-line
   note rather than hanging off its top. Below 1080 it is one column and the buttons return
   to their old place under the note, which is where they want to be on a phone anyway. */
.actionpair{display:grid; gap:20px; margin-top:26px}
.actionpair > *{margin:0}
.actionpair .note{margin:0}
@media (min-width:1080px){
  .actionpair{grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:var(--rail-gap);
    align-items:center}
}

/* The box behind the .poc pitch. Panel ground rather than a border, so it reads as one
   object holding a claim and its screenshot rather than two things that happen to be
   adjacent -- which is what "solid background box" is asking for. The ledger underneath
   stays INSIDE the box but visually separate, because it is the audit OF that pitch. */
section.split.idx.media.poc{background:var(--panel); border:1px solid var(--rule);
  border-radius:12px; padding:32px 26px}
section.split.idx.media.poc > .shot{margin:0}
@media (min-width:1080px){ section.split.idx.media.poc{padding:38px 40px} }

/* ── the editorial rail ──────────────────────────────────────────────────────────
   THE DEFECT, in Justin's words: "not taking full advantage of the width... it feels
   truncated and pushed to the left." A 1105px h2 sat directly on a 587px paragraph,
   leaving ~518px empty to the right of every block of prose. The eye reads the heading's
   right edge as where the column ends, then watches the prose fall short of it.

   Widening the prose was never available: 1105px is roughly 120 characters against a
   readable ceiling near 75. So the space gets USED instead. The eyebrow and heading move
   into a left rail, the prose keeps its measure in a right column, and the media bands
   keep the full width. 549 + 76 + 600 = 1225 exactly at a 1900px viewport, so there is no
   leftover strip anywhere on the line and nothing can read as truncation.

   OPT-IN via class="split", deliberately. The CSS needs a .rail wrapper in the markup, so
   the class is the contract between the two; a blanket section rule would half-apply to
   any section nobody had wrapped yet.

   TWO WRAPPERS, NOT BARE SIBLINGS -- load-bearing, not tidiness. With bare siblings every
   paragraph is its own grid row, so row 1 is sized by the RAIL (~130px) rather than by
   paragraph 1 (~82px), and a ~48px hole opens in the prose column between the first and
   second paragraph. One rail item + one prose item = one row = nothing to distribute.

   PLACEMENT IS THE API: direct children span both columns by default, so .cards, .gal,
   .concepts, .zones, .row and <hr> need no class and keep the full width they have today.
   Both wrappers repeat -- a section can carry several rail/col pairs, and a sticky rail is
   bounded by its own grid area, so a heading follows its own prose and releases when the
   next band starts. */
@media (min-width:1080px){
  main > section.split{
    display:grid;
    grid-template-columns:minmax(0,1fr) minmax(0,var(--measure));
    column-gap:var(--rail-gap);
    /* Rows are spaced by the margins the elements already carry (.cards brings its own
       26px). A row-gap here would double it. */
    row-gap:0;
    /* LOAD-BEARING twice over: `start` is what leaves the rail shorter than its grid
       area, and that leftover space is what position:sticky travels in. Change this to
       stretch and the rail silently stops moving. */
    align-items:start;
  }
  main > section.split > *{grid-column:1 / -1}
  /* NOT STICKY. The rail was pinned and rode down beside its own prose, which sounded good
     and read badly: Justin, 2026-08-11, "those headers when they scroll go over content.
     It doesn't look of flow that well." A grid item's sticky constraint is its grid area,
     so a heading in a tall row travels the whole row -- and once a section carries several
     rail/col pairs, or a card band lands mid-section, it arrives on top of them.
     The alignment was the fix he asked for; the pinning was a flourish nobody asked for
     and it cost the thing the alignment bought. Static, and no escape hatch to maintain. */
  main > section.split > .rail{grid-column:1 / -1; min-width:0;
    max-width:var(--read); margin-inline:auto}
  main > section.split > .col{grid-column:1 / -1; min-width:0;
    max-width:var(--read); margin-inline:auto}
  /* The In / Not yet list is structured rows rather than prose, so it reads fine wider than
     a paragraph does and looked especially stranded at the reading measure. */
  main > section.split > .col > .state{max-width:none}

  /* .col.wide -- FULL WIDTH, ONE COLUMN.
     This was two newspaper columns from 2026-08-11 to 2026-08-12, and both states came from
     Justin looking at the real page. First: "instead of it being that long, can you utilize
     that whole width for that section? It'll make the text feel easier to read." Then, after
     seeing it: "Each section of this page can we make it a single column and the paragraph
     reaches the full width please. The columns breaks up the text in an awkward method."

     He is right, and the reason is specific to THIS content rather than to columns. The
     Majestic 12 section is short blocks under h3 subheadings, not continuous prose. Every
     subheading forces a column break, so a two-line paragraph strands its last sentence at
     the top of the right column with a gap under it -- and a reader who reaches the bottom
     of the left column has to guess whether to go right or down. Newspaper columns need a
     continuous river of text to work, and this is a stack of short sections.

     So the long measure is accepted deliberately: about 140 characters a line is past the
     comfortable band, and it is still better than a layout where the reading ORDER is
     ambiguous. Line length costs a little on every line; a broken reading order costs the
     reader the thread. */
  main > section.split > .col.wide{
    grid-column:1 / -1;
    max-width:none;
    /* The rail's `> :last-child{margin-bottom:0}` above assumes the prose sits BESIDE the
       heading, where the zeroed margin is correct. A wide column sits UNDER it instead, so
       without this the first line of text touches the h2 with zero space between them. */
    margin-top:16px;
  }
  /* THE CAVEAT UNDER #regions SPANS BOTH COLUMNS, BECAUSE IT IS ABOUT BOTH.
     Justin, 2026-08-12: "the part that says nine of these ten don't exist yet. Can that go
     full width under the ten region portion. I feel it could look more complete." It was
     inside the prose .col, so it read as a footnote to the paragraph above it -- but it
     qualifies the route board just as much, and the board is the other column.

     SINGLE column, unlike .col.wide, and that is a judgement about LENGTH not a principle.
     Two columns is right for the Majestic 12 block because it has enough text to fill both.
     This has four sentences: split, it put three lines in the left column and ONE in the
     right, which reads as a mistake rather than as a layout. Justin, 2026-08-12: "Instead
     of the text in the red box being two columns. Can we make the text a single column for
     that section?" Long measure is the lesser cost on a four-sentence caveat someone reads
     once; a ragged one-line orphan column is visible every time. */
  main > section.split > .warn{
    grid-column:1 / -1;
    max-width:none;
    margin:26px 0 0;
  }

  /* A LONE .col IN AN .idx SECTION IS A PAGE HEADER, NOT A TEXT COLUMN.
     Justin, 2026-08-12, about /route: "Can the title on this page stretch the full width
     too". Its header is a single .col with nothing beside it, and the blanket
     `.idx > .col{max-width:var(--measure)}` capped it at 600px -- so a 50px headline broke
     across FOUR lines with half the wrap empty to its right. The cap is right when
     something shares the row (a figure, a card grid); with nothing beside it there is
     nothing to leave room for.

     `:only-child` says exactly that and nothing more, so it cannot reach a .col that has a
     figure or a rail as a sibling -- and any future solo header gets the same treatment
     without needing to know this rule exists. The standfirst under the h1 keeps its
     measure: the HEADLINE wanting the width does not mean the paragraph does. */
  main > section.split.idx > .col:only-child{max-width:none}
  main > section.split.idx > .col:only-child > :is(p,ul,ol):not(.eyebrow){
    max-width:var(--measure)}

  main > section.split > .col.wide > p{max-width:none; margin:0 0 15px}
  /* The standfirst keeps a measure even though the body no longer does -- it is a single
     short line under the h1 and reads as part of the heading, not as body text. */
  main > section.split > .col.wide > .dim:first-child{
    margin:0 0 20px; max-width:var(--measure)}

  /* Grid items do not margin-collapse, so the last child's bottom margin would add to the
     next row's top margin. Zeroing keeps the spacing identical to today. */
  main > section.split > :is(.rail,.col) > :last-child{margin-bottom:0}

  /* The TRACK is the measure now; a per-element cap would only re-introduce a ragged
     right edge inside the column. .tiny is excluded on purpose -- 600px of 13.5px text is
     ~80ch, and small print sitting shorter than the body reads as small print rather than
     as an accident. */
  main > section.split > .col > :is(p,ul,ol,blockquote,.note):not(.tiny){max-width:none}
  main > section.split > .col > .note p{max-width:none}

  /* 60px in a 549px rail runs to four lines on /recruit. Rail-scoped only, so no heading
     anywhere else changes size. */
  main > section.split > .rail > h1{font-size:clamp(32px,3.4vw,50px); line-height:1.05}

  /* Pre-existing wart, fixed while we are here: .concepts holds FOUR images in a
     3-column auto-fit grid, so it renders 3+1 with an orphan at every desktop width
     today. Two columns of ~603px roughly doubles the concept art. Shares the rail's
     breakpoint rather than inventing a second one. */
  /* Three across at desktop, which is what makes the band arithmetic work: the lead card
     takes region 01 out of the grid, so the remaining nine are exactly three clean rows. */
  .regions{grid-template-columns:repeat(3,minmax(0,1fr))}
  /* The lead goes side-by-side: it is the only card carrying a real screenshot and it
     should not be squeezed to a card's width. */
  .region.lead{display:grid; grid-template-columns:minmax(0,1.2fr) minmax(0,1fr)}
  .region.lead > img{height:100%; object-fit:cover; border-bottom:0;
    border-right:1px solid var(--rule)}
  .region.lead .body{padding:26px 28px}
}

/* ── the index rhythm: seven shapes, one invariant ───────────────────────────────
   FOUR ROUNDS OF THE SAME COMPLAINT, and the pattern in them is the answer. Round 1:
   "not taking full advantage of the width... it feels truncated and pushed to the left"
   -- we built the rail. Round 2: "I am not a fan of this two column style layout" -- we
   stacked it. Round 3: "sections with data on one side it feels awkward" -- we centred
   it at 720. Round 4: "All these sections, you just centered. it doesn't flow well."

   Every one of those was ONE treatment applied to EVERY section. The width was never the
   defect. The defect is that seven sections in a row have the same silhouette -- eyebrow,
   heading, a slab of prose, sometimes cards -- so there is nothing to look at between the
   walls of text. This block therefore does not add a fifth column rule. It gives sections
   DIFFERENT SHAPES, and one rule they all obey:

       EVERY SECTION PUTS SOMETHING AT THE RIGHT EDGE OF THE WRAP.

   Media satisfies it once (the room-2 still beside section 5). Structure satisfies it
   everywhere else: a breakout quote, a four-up hairline voices band, the region grid that
   already worked, a two-column status ledger, a full-wrap fine-print panel, a display-size
   closing headline over a full-wrap roster. Prose goes back to the LEFT at the measure --
   which is where round 1 complained about it -- and round 1 is answered by the other half:
   something always reaches the far edge on the same line or the next one.

   NOTHING ENFORCES THIS BUT THIS COMMENT. Add a section with a heading, three paragraphs
   and no device and that section -- only that one -- gets the round-1 look back, which is
   harder to notice than the whole page being wrong.

   SCOPED TO .idx. /privacy and /terms still use bare .split and keep the centred column
   they have today -- they are documents, section.legal already centres them, and the defect
   is milder there. /about and /recruit opted IN on 2026-08-12; the block immediately below
   is what that needed. Because the rail block above survives intact, so do .col.wide (the
   Majestic 12 two-column set), .col > .state{max-width:none}, .rail > h1, .regions and
   .region.lead. */
@media (min-width:1080px){
  main > section.split.idx > .rail{max-width:var(--read);    margin-inline:0}
  main > section.split.idx > .col {max-width:var(--measure); margin-inline:0}
}

/* ── /about and /recruit opt in ───────────────────────────────────────────────
   Justin, 2026-08-12: "the about page needs a layout reformatting. Its all text and its
   difficult to follow", and the same for /recruit.

   THESE TWO PAGES DO NOT GET THE INDEX INVARIANT ABOVE. That answered a different defect --
   seven sections in a row with one silhouette -- and copying it here would be the fifth
   "one treatment everywhere", only with more shapes in it. /about was ONE section pretending
   to be seven, using <hr> as a fake divider, so it could carry neither a pace nor a device
   and its six .rail headings shrank to fit and floated mid-line with ~490px of nothing
   either side. /recruit was two genuinely different asks -- "work on this" and "be a
   character in it" -- wearing identical clothes.

   THE FIX IS REAL SECTIONS plus a device each that already exists, and one thing borrowed
   from nothing on this site: every /about section states its ANSWER next to its own heading,
   in the rail. That is what "difficult to follow" actually asks for -- you should not have to
   read a paragraph to find out what the paragraph is going to tell you. The page is now
   legible at heading level alone and every paragraph under it is optional.

   Four rules follow. No new component, nothing added to either theme-sweep list. */
@media (min-width:1080px){
  /* THE ONE THAT BITES, AND IT IS NOT GLUE. `main > section.split.idx > .col` (just above)
     and `main > section.split > .col.wide` (:727) are both specificity (0,3,2), and the .idx
     block is declared LATER, so it wins the tie. Opting /about into .idx -- the single class
     that left-anchors its headings -- would therefore clamp the Majestic 12 block from
     full-wrap two-column back to 600px while `columns:2` survived, giving two ~280px columns
     and roughly DOUBLING the height of the worst wall on the site WHILE APPEARING TO IMPROVE
     THE PAGE. Restated here rather than reordered above, because .col.wide has to keep
     winning whether or not its section is opted in -- and if the cascade ever resolved the
     other way this is a no-op, so it is correct to ship either way. */
  main > section.split.idx > .col.wide{max-width:none; margin-inline:0}

  /* A .media section carries its heading INSIDE .col; there is no .rail on one, because a
     rail spans 1 / -1 and would land on top of the prose that .media explicitly places in
     row 1, column 1 (see the grid-row:1 note below). That is the established markup and both
     new media sections follow it. The consequence is that /about's h1, now inside a .col,
     picks up the global clamp(34px,6.2vw,60px) instead of the rail's. 60px of headline in a
     600px track next to a 480px picture is the picture losing an argument it should not be
     having. Same values as `.rail > h1`, same reason, same scope -- no other h1 changes. */
  main > section.split.idx.media > .col > h1{
    font-size:clamp(32px,3.4vw,50px); line-height:1.05}
}

/* SIGNPOSTS INSIDE THE MAJESTIC 12 BLOCK. Four of its eleven paragraphs came out as
   duplicates of other blocks on the same page, taking it from 777 words to ~490 -- but 490
   words of two-column newspaper with nothing in it is still a wall, only a shorter one, and
   the return sweep to the top of column two is exactly where a reader gives up. There is no
   honest picture for a 1952 briefing paper, and a room screenshot beside it would be
   decoration with nothing behind it, so it gets HEADINGS instead: three h3s spanning both
   columns, which is the move `.fineprint .sheetblk > h2{column-span:all}` already makes and
   already ships. One slab becomes three labelled runs.

   Unscoped on purpose. Below 1080 .col.wide is not a multi-column container at all, so
   column-span is inert and the same three headings are simply headings -- which is what the
   phone needed anyway, and is why this needs no media query of its own. */
main > section.split > .col.wide > h3{margin:26px 0 10px; font-size:17px}
main > section.split > .col.wide > h3:first-of-type{margin-top:0}

/* THE FOUR CLAIM TAGS on /about are a list that has been written as prose since the page
   shipped. .state is the right component and its pill track is not: 78px is "NOT YET" at 11px
   monospace, and CORROBORATED is ~105px, so it would collide with its own sentence at every
   width from 520 up. 130px is the .roster key column -- the same job, already sized on this
   site. Gated at 521 so the existing `max-width:520px` collapse to one column still wins on a
   phone; .state.tags li is (0,3,1) and would otherwise beat it silently.

   THE PILL IS RE-COLOURED, AND THAT IS NOT COSMETIC. --ok and --dim mean "in" and "not yet"
   everywhere else on this site. These four are a taxonomy, not a build status, and borrowing
   either colour for them is the kind of wrong that looks completely fine. --teal is the
   existing marker accent (.eyebrow, links, the .pull rule) and says marker without saying
   shipped-or-unshipped. */
@media (min-width:521px){
  .state.tags li{grid-template-columns:130px minmax(0,1fr)}
}
.state.tags .mark{background:color-mix(in srgb,var(--teal) 16%,transparent);
  color:var(--teal)}

/* /recruit's pitch panel becomes the media aside, and NOTHING is needed to size it: .panel's
   own 520px cap is wider than the track it now sits in, so the grid item stretches to the
   track and the cap never binds. That is the whole argument for reusing .panel rather than
   inventing anything -- the fill, border, radius, shadow and its theme-sweep registration
   at :482 all come along already done. It also closes the widest hole on the site, a 520px
   panel sitting alone in a 1225px wrap.

   The only thing that gives is padding: 600 + gap + media does not fit inside main until
   1240, so --media is 360 in the band below, and 28px either side of a five-field form
   leaves 304px of input. Padding is the cheapest thing in that arithmetic to spend. */
@media (min-width:1080px) and (max-width:1239px){
  .panel.aside{padding:22px 20px}
}

/* PACE. A flat 64px on every section is half of why seven sections read as one texture,
   and it is the one lever none of the four prior rounds touched. Density runs opposite to
   width: the airy sections breathe, the picture band gets out of the way of its own
   pictures, the fine print compresses. */
.s-air{padding:92px 0}
.s-tight{padding:52px 0}
.s-show{padding:44px 0}
@media (max-width:760px){ .s-air{padding:64px 0} }

/* ── the one media section ──────────────────────────────────────────────────────
   The hero already has a screenshot on its right; section 5 gets one on its LEFT, so the
   two media beats alternate rather than repeat. room2-thumb is the ONLY thumbnail that
   earns a placement anywhere on this page: "Saving. You sleep in room 2" is already a row
   in the list beside it, so the picture is captioned by copy that was there first. The
   other five stay unused on purpose -- all six rooms are already on this page at full size
   in the gallery, and a second crop of the same room is decoration with a rationale.

   TWO BANDS, because the arithmetic has a floor: 600 + gap + media must fit inside main.
   At 1080 that leaves room for 360; from 1240 up the native 480 fits. The slack lands in
   the GAP (space-between), not in a track, so both blocks stay pinned to their own edge of
   the wrap at every width -- ~145px of air between them at 1440, which is what using the
   width looks like and is not the same thing as 518px of nothing to the right of
   everything. DOM order is prose-then-figure, so a phone and a screen reader get the text
   first while the desktop gets the picture on the left. */
@media (min-width:1080px){
  main > section.split.idx.media{
    --media:360px;
    grid-template-columns:minmax(0,var(--measure)) minmax(0,var(--media));
    justify-content:space-between;
  }
  /* grid-row:1 IS LOAD-BEARING, NOT TIDINESS. The prose comes first in the DOM and, in the
     .left variant, is placed in column 2. Grid auto-placement is sparse by default: it never
     goes BACKWARDS to fill an earlier column, so the figure could not take column 1 of row 1
     and dropped to row 2 instead. Measured before this line existed: the text started 64px
     into the section and the picture started at 385 -- 321px of nothing above it, on a
     grid whose align-items was already `start`, which is why `start` looked like the
     problem and was not. Naming the row for both children is what actually pairs them.
     Everything after them (the status ledger) still auto-places onto row 2. */
  main > section.split.idx.media > .col       {grid-column:1; grid-row:1; max-width:none}
  main > section.split.idx.media > .aside     {grid-column:2; grid-row:1}
  /* ANY LATER .col IN A MEDIA SECTION IS NOT THE PAIRED ONE.
     The rule above has no positional scope, so it placed EVERY .col in the section at
     row 1 column 1. #regions on the homepage closes with a second .col, and that second
     one was dragged up on top of the heading -- two paragraphs rendering through each
     other, desktop only, because below 1080 the grid does not exist. It was live for one
     deploy and Justin caught it in a screenshot.

     `.col ~ .col` means "a .col preceded by another .col", which is exactly "not the
     first one" without depending on :has(), :first-of-type, or the order of unrelated
     siblings. Sending them back to 1 / -1 auto is what the rest of the section already
     gets from `main > section.split > *`. */
  main > section.split.idx.media > .col ~ .col{grid-column:1 / -1; grid-row:auto;
    max-width:var(--measure)}
  /* .left swaps the TRACKS as well as the placement. Placing alone would put the 600px
     prose in the 480px track and the 480px figure in the 600px one -- both blocks still
     touch their own edge, so it looks plausible and is silently wrong. */
  /* .duo -- TWO BALANCED COLUMNS UNDER A FULL-WIDTH HEADING.
     The third distinct answer to "use the whole width", and it exists because the other two
     do not fit every case. `.media` is prose + a 360px picture: deliberately lopsided,
     because a screenshot has a natural size and prose has a natural measure. `.col.wide` is
     ONE block of prose flowed into two newspaper columns, which needs enough text to fill
     both. `.duo` is for two DIFFERENT blocks that each want half the wrap -- a pull-quote
     beside its commentary, prose beside a reference table.

     Justin, 2026-08-12, on #subject: "can you make the quote box on the left, and the other
     text information that is under to the right so its a two column section" -- and, on the
     claims tags: "Can we move the unverified and corroborated stuff to the right of it so
     we can be utilizing all the space most efficiently".

     EXPLICIT CLASSES, NOT :nth-child. `main > section.split.idx.media > .col` had no
     positional scope and dragged a later .col onto row 1, on top of a heading, live for a
     deploy. Naming the two halves means adding, removing or reordering a sibling cannot
     silently repoint the layout. Everything not named still auto-places full width, so the
     heading above and the card grid below need no rules at all. */
  main > section.split.idx.duo{
    grid-template-columns:minmax(0,1fr) minmax(0,1fr);
    column-gap:var(--rail-gap);
  }
  /* THE HEADING NEEDS A GAP UNDER IT, AND THIS IS THE SECOND TIME.
     `> :is(.rail,.col) > :last-child{margin-bottom:0}` is correct when the prose sits
     BESIDE the heading, which is the normal .idx layout -- the zeroed margin is what keeps
     the two columns top-aligned. Under .duo the content sits UNDERNEATH, so that zero left
     the h2 touching both the quote box and the first line of prose. Justin, 2026-08-12:
     "The heading on this section needs proper spacing for the content below it."
     .col.wide already carries this exact fix, in the comment directly above, and I wrote
     .duo without applying it -- which is the argument for putting it on the RAIL here
     rather than on each half, so a third variant inherits the gap instead of rediscovering
     the bug. Grid items do not margin-collapse, so this is the whole spacing. */
  main > section.split.idx.duo > .rail  {grid-column:1 / -1; max-width:none; margin-bottom:26px}
  main > section.split.idx.duo > .duo-a {grid-column:1; grid-row:2; max-width:none; margin-top:0}
  main > section.split.idx.duo > .duo-b {grid-column:2; grid-row:2; max-width:none; margin-top:0}
  /* Half the wrap is ~590px, which is already the measure -- so the prose keeps its line
     length and simply stops leaving the other half empty. Nothing inside needs a cap. */
  main > section.split.idx.duo > :is(.duo-a,.duo-b) > :is(p,ul,ol){max-width:none}
  main > section.split.idx.duo > .duo-b.state{margin-top:0}

  /* .form-lead -- A FORM IS NOT A SCREENSHOT, SO IT DOES NOT GET A SCREENSHOT'S COLUMN.
     .media sizes its second column with --media: 360px, chosen for a picture, which has a
     natural size and is happy to be small. A form has a natural WIDTH instead -- its labels,
     its inputs and its longest placeholder all want room, and every pixel it does not get
     comes out of the input someone has to type into. Justin, 2026-08-12: "The [prose] can be
     a little more narrow and allows the form box to be a little wider."
     The prose gives up 80px and the form gains 160. It stays a two-column layout with the
     prose still above its measure, so nothing about reading changes. */
  main > section.split.idx.media.form-lead{--measure:500px; --media:600px}
  /* .panel carries its own max-width:520px, set for a panel sitting in a normal .media
     column. Widening the grid track alone does nothing while that cap stands -- measured:
     the track went to 540 and the panel stayed at 520. */
  main > section.split.idx.media.form-lead > .panel.aside{max-width:none}

  /* .poc -- THE PROOF-OF-CONCEPT SECTION: a box around the pitch, the picture centred
     against it, and a ledger that divides evenly.
     Justin, 2026-08-12: "Maybe the top portion has a solid background box and align the
     screenshot to the middle of the text for the two columns. The In and Not Yet, Can we
     add an 8th or find a way for that section to feel more aligned please."

     The picture was top-aligned because .media pins both children to grid-row 1 and lets
     them start together -- right when a caption sits under a picture beside a heading, and
     wrong here, where a 270px screenshot sat beside 300px of prose and floated. Only THIS
     section centres them, because only here are the two halves the same kind of thing: a
     claim and the evidence for it. */
  main > section.split.idx.media.poc > :is(.col,.aside){align-self:center}

  main > section.split.idx.media.left{
    grid-template-columns:minmax(0,var(--media)) minmax(0,var(--measure));
  }
  main > section.split.idx.media.left > .col  {grid-column:2; grid-row:1}
  main > section.split.idx.media.left > .aside{grid-column:1; grid-row:1}
}
@media (min-width:1240px){ main > section.split.idx.media{--media:480px} }
/* PLACEMENT ONLY. It ships as class="shot aside", so the border, radius, shadow and black
   fill all come from .shot -- already themed, already in the sweep list. .shot.aside and
   not .aside because .shot{margin:0} is declared LATER in this file and would win a tie. */
.shot.aside{max-width:var(--media); margin:26px 0 0}
@media (min-width:1080px){ .shot.aside{margin:0} }

/* ── section 2: the quote stops being a box inside a column ─────────────────────
   A 64ch bordered card inside a 600px column is a small thing inside a small thing. Full
   wrap it becomes the first horizontal break on the page -- and it now lands BEFORE the
   prose, which makes section 2 the only section that leads with its object instead of with
   a paragraph. That is what breaks the section-2 / section-3 seam: without it both
   sections open eyebrow, heading, slab, which is the same repetition one zoom level down.

   The rules run the whole wrap; the sentence does not. A full-width block with 34ch in it
   and space to the right would be the original complaint rebuilt at component scale. And
   no second track for the cite: a citation pushed into a right-hand column is
   heading-beside-content again, in miniature, and that shape has been rejected three
   times. Chris's one-line reply folds in here as .after rather than trailing the section
   as a stray fourth voice. */
@media (min-width:1080px){
  .pull.breakout{max-width:none; margin:26px 0 30px; padding:30px 34px}
  .pull.breakout p{font-size:23px; line-height:1.4; max-width:46ch}
  .pull.breakout .after{font-size:15px; max-width:46ch}
}
.pull .after{margin:14px 0 0; font-size:14.5px; color:var(--dim)}

/* ── section 2: four voices, not four cards ─────────────────────────────────────
   Section 3's region cards are filled, bordered, image-on-top, three across. Two grids of
   that object on one page IS the sameness this whole block exists to kill, so these four
   lose the box entirely: a hairline, a mono key, a line, a sentence, four across. Same
   words, different component. .card itself is untouched, so /about and /recruit keep the
   card they have. */
.cards.voices{grid-template-columns:repeat(4,minmax(0,1fr)); gap:22px 30px; margin-top:34px}
.cards.voices .card{background:none; border:0; border-top:1px solid var(--rule-2);
  border-radius:0; padding:16px 0 0}
@media (max-width:1000px){ .cards.voices{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:620px){ .cards.voices{grid-template-columns:1fr; gap:0} }

/* ── section 5: the status list IS the right edge ───────────────────────────────
   Seven rows across 1240 is ~150 characters a line, which is the one change guaranteed to
   make it harder to read. Two columns put every row back near the measure, reach the far
   edge, and halve the height. Same move .col.wide already makes on /about, same reason. */
@media (min-width:1080px){
  main > section.split.idx .state.ledger{max-width:none; columns:2;
    column-gap:var(--rail-gap)}
  /* A row is itself a 78px + 1fr grid. Without this a pill can end one column and its own
     sentence start the next. */
  main > section.split.idx .state.ledger li{break-inside:avoid;
    -webkit-column-break-inside:avoid}
  /* :last-child's rule was removed because it was the end of the list. In two columns item
     4 is the visual end of column 1 and keeps its rule, so item 7 needs its back or the
     two columns terminate on different lines. */
  main > section.split.idx .state.ledger li:last-child{border-bottom:1px solid var(--rule)}
}

/* ── section 6: the wall becomes fine print ─────────────────────────────────────
   Eight paragraphs of consent and telemetry with no image, no card and nothing to look at
   -- the only true wall on the page. There is no honest picture for it: a room screenshot
   next to Google OAuth scopes is decoration with nothing behind it, which is how you
   re-ship the problem in a new costume. So it gets the treatment that is TRUE. This is
   fine print, and looking like fine print is honest here rather than a failure. The
   section stops being page ground and becomes ONE PANEL the width of the wrap -- the only
   solid surface that size on the site, and unmistakable against anything above it. The
   PANEL reaches the right edge; the text inside stays near the measure.

   THE PANEL IS VISIBLE AT EVERY WIDTH; only the columns wait for 860. A column-only fix
   gated at 1080 leaves the worst wall on the page a wall exactly where the page is most
   likely to be read. The border, the fill and the padding are what carry the silhouette on
   a phone.

   Worth saying out loud rather than shipping quietly: boxing this copy makes it look less
   important, and that is the intent. The Google-scope disclosure stays on this document
   because a consumer OAuth homepage should carry it -- it is lower and quieter, not gone. */
main > section.split.idx.fineprint > .sheetblk{
  background:var(--panel); border:1px solid var(--rule-2); border-radius:11px;
  padding:34px 38px; box-shadow:var(--shadow); max-width:none; margin-inline:0;
}
.fineprint .sheetblk h2{font-size:22px; letter-spacing:-.01em}
.fineprint .sheetblk p{max-width:none}
@media (min-width:860px){
  main > section.split.idx.fineprint > .sheetblk{columns:2; column-gap:var(--rail-gap);
    orphans:3; widows:3}
  .fineprint .sheetblk > p{margin:0 0 15px}
  /* Both headings, the eyebrow and the closing note run the full panel, so it reads
     heading -> two columns -> heading -> two columns -> note bar, instead of starting
     mid-thought at the top of column 2. */
  .fineprint .sheetblk > :is(h2,.eyebrow,.note){column-span:all}
  .fineprint .sheetblk > h2{margin:26px 0 14px}
  .fineprint .sheetblk > h2:first-of-type{margin-top:0}
  .fineprint .sheetblk > .note{margin-bottom:0}
}
@media (max-width:600px){
  main > section.split.idx.fineprint > .sheetblk{padding:22px 20px}
}

/* ── section 7: the closer ──────────────────────────────────────────────────────
   The last thing on the page and the actual ask, so it gets the loudest silhouette: the
   headline is released from the measure and set at display size -- the only type this big
   outside the h1 -- over a short paragraph, which reads as a sign-off rather than as a
   stranded column. text-wrap:balance is already on h2 globally and does the breaking. */
main > section.split.idx.closer > .rail{max-width:none}
main > section.split.idx.closer h2{font-size:clamp(28px,4vw,44px); line-height:1.06;
  letter-spacing:-.02em}

/* THE ROSTER -- the second card grid, removed. Four boxes under a heading is the same
   object section 2 used, and two card bands on one page is the mechanical reason the
   bottom read as a repeat. Same four roles, same words, as full-width hairline rows: mono
   key, the line, the sentence. Nothing else on the page has this shape, and .card survives
   untouched for /about and /recruit. Deliberately NO images here: there is no screenshot
   that means "writer", and giving these four pictures purely so every section has one is
   the same failure as giving every section the same column. */
.roster{list-style:none; margin:32px 0 0; padding:0; border-top:1px solid var(--rule-2)}
.roster li{display:grid; grid-template-columns:minmax(0,1fr); gap:5px 26px; padding:16px 0;
  border-bottom:1px solid var(--rule); align-items:baseline; max-width:none}
.roster .k{font:600 11px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;
  letter-spacing:.2em; text-transform:uppercase; color:var(--orange)}
.roster h3{margin:0; font-size:17px; line-height:1.25; color:var(--ink);
  letter-spacing:-.01em}
.roster p{margin:0; max-width:none; font-size:15px; line-height:1.55; color:var(--dim)}
@media (min-width:860px){
  .roster li{grid-template-columns:130px minmax(0,.85fr) minmax(0,1.7fr)}
}

/* ── hero ── */
.hero{padding:74px 0 54px; border-bottom:1px solid var(--rule)}
.hero-grid{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.15fr); gap:46px; align-items:center}
@media (max-width:900px){ .hero-grid{grid-template-columns:1fr; gap:32px} }
.shot{
  /* <figure>'s UA default is `margin:1em 40px`, and this rule never overrode it. Every
     screenshot on the site -- hero, the six-shot gallery, the four concepts -- has been
     80px narrower than its own cell, which on a 375px phone is 24% of the screen. */
  margin:0;
  border:1px solid var(--rule); border-radius:8px; overflow:hidden; background:#000;
  box-shadow:var(--shadow);
}
.shot img{image-rendering:pixelated; width:100%}
.shot figcaption{font-size:13px; color:var(--dim); padding:10px 13px; background:var(--panel);
  border-top:1px solid var(--rule)}

/* ── the ten regions ────────────────────────────────────────────────────────────
   Replaces the .zones chip row and the four-image .concepts grid. Justin, 2026-08-11:
   "we have 10 regions, you show a few previews, can we make sure to do all 10 regions on
   the site... It feels like the copy on the page is still centered around the 6 rooms."

   THE HONESTY PROBLEM IS THE LAYOUT PROBLEM, so they are solved together. Nine of the ten
   do not exist, and a disclaimer paragraph asks the reader to hold that in mind for the
   length of a grid. A STATUS ON EVERY CARD does not: you cannot read a card without
   reading what it is. The .warn paragraph survives, but its job shrinks from carrying the
   whole guard rail to defining three words.

   THE ARITHMETIC IS THE STRUCTURE. Region 01 is the only one that exists, so it is pulled
   out as a full-width lead card and the other nine sit as three bands of three.
   1 + 3 + 3 + 3 = 10 -- no orphan at desktop or tablet, one column below 660px. The old
   .concepts grid held four images in a three-up and rendered 3+1 at every desktop width.

   .plate IS SOLID, NOT TRANSPARENT -- and as of 2026-08-12 nothing renders one, because every
   region has art. Kept because the next region will be named before it is drawn, and an empty
   cell reads as broken, so the blank has to be designed. Same for the unused .st.named pill.
   And everything
   here that touches the honeycomb uses --panel-3 and --rule-2, because a bare 1px --rule
   hairline floating on the lattice is exactly the .btn.ghost failure and adding four more
   is how you re-ship it. */
.band{display:flex; align-items:baseline; gap:8px 15px; flex-wrap:wrap; margin:40px 0 0}
.band .n{font:700 11px/1.6 ui-monospace,Menlo,monospace; letter-spacing:.18em;
  text-transform:uppercase; color:var(--concept); flex:0 0 auto}
.band h3{margin:0; flex:0 0 auto}
.band p{margin:0; font-size:14px; color:var(--dim); flex:1 1 340px; min-width:0; max-width:none}

.regions{display:grid; gap:18px; grid-template-columns:repeat(auto-fit,minmax(270px,1fr));
  margin:18px 0 0}
.region{background:var(--panel); border:1px solid var(--rule-2); border-radius:9px;
  overflow:hidden; display:flex; flex-direction:column; min-width:0}
.region.lead{margin:18px 0 0}
.region > img{width:100%; height:auto; display:block; image-rendering:pixelated;
  background:#000; border-bottom:1px solid var(--rule)}
.region .body{padding:16px 18px 18px; display:flex; flex-direction:column; gap:9px; flex:1 1 auto}
.region .body > *{margin:0; max-width:none}
.region .hd{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.region .num{font:700 12px/1 ui-monospace,Menlo,monospace; letter-spacing:.16em;
  color:var(--concept)}
.region h4{font-size:17.5px; line-height:1.2; color:var(--ink); letter-spacing:-.01em}
.region p{font-size:14.5px; line-height:1.5; color:var(--dim)}
/* THE OPEN QUESTION IS THE CARD'S PAYOFF, so it gets the same place on every card.
   margin-top:auto pushes it to the bottom of the flex column, which means a card with two
   lines of body and a card with five put their question on the same baseline instead of
   letting it float wherever the copy happened to end. The rule above it turns it from a
   fourth paragraph into a labelled segment, and the label is set in the same uppercase mono
   as .num and the .st pills so it reads as part of the existing system rather than a new
   invention. Cards that say "Not designed yet" and cards that say "Open question" are then
   visibly the same component carrying different words, which is the honest relationship --
   they are different states of one thing, not two different kinds of card. */
.region .q{color:var(--ink); margin-top:auto; padding-top:12px;
  border-top:1px solid var(--rule-2)}
.region .q b{display:block; font:700 10.5px/1.7 ui-monospace,Menlo,monospace;
  letter-spacing:.14em; text-transform:uppercase; color:var(--concept); margin-bottom:1px}

/* Same recipe as .state .mark, because a region's status and the In / Not yet pill are the
   same object doing the same job on two surfaces. The wash does the work; no bare border,
   which would resolve to currentColor and read differently from .mark. */
.st{font:700 11px/1.5 ui-monospace,Menlo,monospace; letter-spacing:.1em; text-transform:uppercase;
  padding:2px 8px; border-radius:4px; white-space:nowrap; flex:0 0 auto}
.st.playable{color:var(--ok);      background:color-mix(in srgb,var(--ok) 12%,transparent)}
.st.concept {color:var(--concept); background:color-mix(in srgb,var(--concept) 12%,transparent)}
.st.named   {color:var(--ink);     background:color-mix(in srgb,var(--dim) 22%,transparent)}

.plate{aspect-ratio:880/503; background:var(--panel-3); border-bottom:1px solid var(--rule-2);
  display:grid; place-items:center; position:relative}
.plate::after{content:""; position:absolute; inset:13px; border:2px dashed var(--rule-2);
  border-radius:5px}
.plate span{font:700 46px/1 ui-monospace,Menlo,monospace; letter-spacing:.1em; color:var(--rule-2)}

@media (min-width:660px) and (max-width:1079px){
  /* auto-fit at minmax(270px) gives two columns from ~558px, so a band of three would
     render 2+1 through this whole range. Force three. */
  .regions{grid-template-columns:repeat(3,minmax(0,1fr))}
  .region h4{font-size:16px}
  .region p{font-size:13.5px}
}

/* ── buttons ── */
.row{display:flex; flex-wrap:wrap; gap:12px; align-items:center; margin-top:26px}
.btn{
  display:inline-flex; align-items:center; gap:9px; cursor:pointer;
  font:600 15px/1 ui-sans-serif,system-ui,sans-serif; letter-spacing:.02em;
  padding:14px 22px; border-radius:7px; border:1px solid var(--rule);
  background:var(--panel-2); color:var(--ink); text-decoration:none;
  transition:transform .12s ease, background .16s ease, border-color .16s ease;
}
.btn:hover{background:#1d2637; border-color:#2f3a4f; transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.primary{background:var(--teal); color:#04231f; border-color:var(--teal)}
.btn.primary:hover{background:#6ad4c8; border-color:#6ad4c8}
/* NOT TRANSPARENT ANY MORE. A ghost button worked when the page behind it was a flat
   colour: a 1px --rule border was the only edge in that area, so it read. The background
   is now a honeycomb whose grout lines are the same weight and a similar value, and the
   button's outline became one more line among hundreds -- Justin, on the light theme:
   "it is difficult to see the border with the background."

   A secondary button should be quieter than primary, not invisible against texture. So it
   keeps a real fill and earns the distinction from --primary through COLOUR instead: same
   ground as the base .btn, a stronger border, and none of the teal. The rule this
   establishes: on a textured ground, hierarchy comes from fill and weight, never from
   transparency -- there is nothing behind an element here that is quiet enough to show
   through. */
.btn.ghost{background:var(--panel-2); border-color:var(--rule-2)}
.btn.ghost:hover{border-color:var(--teal)}
.btn[disabled]{opacity:.5; cursor:not-allowed; transform:none}
.btn svg{width:18px; height:18px; flex:0 0 auto}
@media (prefers-reduced-motion:reduce){ *{transition:none !important; animation:none !important} }

/* ── pull quote ── */
.pull{margin:24px 0; padding:20px 24px; background:var(--panel); border:1px solid var(--rule);
  border-left:3px solid var(--teal); border-radius:0 8px 8px 0; max-width:64ch}
.pull p{font-size:18px; line-height:1.5; margin:0; color:var(--ink)}
.pull cite{display:block; margin-top:10px; font-style:normal; font-size:14px; color:var(--dim)}

/* ── state list ── */
.state{list-style:none; padding:0; margin:22px 0 0; max-width:66ch}
/* A GRID, not a flex row. The status column is a fixed 78px so every pill starts and every
   sentence starts on the same two verticals -- with flex, a 2-character "In" and a 7-
   character "Not yet" set two different text indents and the list read as ragged. 78px is
   "NOT YET" at 11px monospace with .1em tracking plus its padding, with room to spare. */
.state li{display:grid; grid-template-columns:78px minmax(0,1fr); gap:14px; padding:11px 0;
  border-bottom:1px solid var(--rule); font-size:15.5px; align-items:baseline}
.state li:last-child{border-bottom:0}
.state .mark{width:auto; height:auto; justify-self:start; text-align:center;
  font:700 11px/1.5 ui-monospace,Menlo,monospace; letter-spacing:.1em;
  padding:3px 8px; border-radius:4px; text-transform:uppercase; white-space:nowrap}
@media (max-width:520px){
  /* Below this the 78px column costs more than the alignment buys: the pill goes above
     its own line rather than squeezing the sentence into a third of the screen. */
  .state li{grid-template-columns:1fr; gap:7px}
}
.state .yes{background:color-mix(in srgb,var(--ok) 18%,transparent); color:var(--ok)}
.state .no{background:color-mix(in srgb,var(--dim) 22%,transparent); color:var(--dim)}
.state b{color:var(--ink); font-weight:600}
.state span.t{color:var(--dim)}

/* THE SOURCE BLOCKS ON /tests. Only used there, but the CSS lives here because
   web/css/site.css is generated by this file -- hand-editing the stylesheet works right up
   until the next build silently discards it. */
.src{background:var(--night); border:1px solid var(--rule); border-radius:8px;
  padding:14px 16px; overflow:auto; max-height:340px; margin:14px 0 28px;
  font:12px/1.5 ui-monospace,Menlo,Consolas,monospace; color:var(--dim);
  white-space:pre; -webkit-overflow-scrolling:touch}
.src::selection{background:var(--orange); color:var(--night)}

/* ── THE GLOBE, on /route ─────────────────────────────────────────────────────────
   SVG rather than WebGL: the whole world at this size is 660 points, a library would be
   six hundred kilobytes to save forty lines of trigonometry, and script-src 'self' blocks
   one anyway. Lives here rather than in site.css because site.css is GENERATED by this
   file -- a hand-edit to the stylesheet survives exactly until the next build. */
.globewrap{display:grid; gap:30px; grid-template-columns:1fr; margin:34px 0 0}
@media (min-width:1080px){
  .globewrap{grid-template-columns:minmax(0,1fr) minmax(0,360px); align-items:start}
}
.globe-stage{background:var(--night); border:1px solid var(--rule); border-radius:12px;
  overflow:hidden}
.globe-svg{display:block; width:100%; height:auto; touch-action:none; cursor:grab}
.globe-svg.dragging{cursor:grabbing}
.globe-svg:focus-visible{outline:2px solid var(--orange); outline-offset:-2px}
.g-sphere{fill:#12202f}
.g-grat{fill:none; stroke:var(--ink); stroke-width:.6; opacity:.18}
.g-land{fill:#5d6b4a; fill-rule:evenodd; stroke:#8f9a6e; stroke-width:.9;
  stroke-linejoin:round; stroke-linecap:round}
.g-rim{fill:none; stroke:var(--orange); stroke-width:1.4; opacity:.55}
.g-route{fill:none; stroke:var(--teal); stroke-width:2.4; stroke-linecap:round;
  stroke-linejoin:round; opacity:.95}
/* ── the Moon, as a second globe ── */
.g-craters{fill:#7d8791; fill-opacity:.5; stroke:#5b636c; stroke-width:.8;
  fill-rule:evenodd}
.globe-svg.is-moon .g-sphere{fill:#9aa3ad}
.globe-svg.is-moon .g-rim{stroke:var(--dim); opacity:.7}
.g-earth{fill:#2f6ea8; stroke:#8fc4e8; stroke-width:2}
.g-earth.hidden{display:none}
.gm{cursor:pointer}
.gm-hit{fill:transparent}
.gm-dot{fill:var(--orange); stroke:var(--night); stroke-width:2}
/* ONE LABEL AT A TIME. Justin, 2026-08-16: "the labels are difficult to read around
   Nevada/Utah area. Do we remove the labels, but when you click on it, a little box shows up
   with the area and some information about the area?"
   The box he describes already exists -- the readout beside the globe, which every pin fills
   in when selected. What was missing was getting the permanent labels out of its way: Rachel,
   Area 51 and S-4 sit inside forty miles of each other and Skinwalker is the next state over,
   so at this scale four names print on top of one another and none of them can be read.
   A name now appears for the pin you are pointing at or have chosen, and nowhere else. The
   dots stay visible always -- an unlabelled dot invites the click that names it, whereas
   removing the labels outright would leave a globe of anonymous points. */
.gm-label{fill:var(--ink); font:600 13px ui-monospace,Menlo,Consolas,monospace;
  paint-order:stroke; stroke:var(--night); stroke-width:4px; stroke-linejoin:round;
  opacity:0; transition:opacity .12s ease}
.gm:hover .gm-label{opacity:1}
@media (prefers-reduced-motion: reduce){ .gm-label{transition:none} }
.gm.on .gm-dot{fill:var(--teal); r:7}
.gm.on .gm-label{fill:var(--teal); opacity:1}
.gm.behind{display:none}
.gm:focus-visible .gm-dot{stroke:var(--teal); stroke-width:3}
.gm:focus{outline:none}

.globe-side .readout{background:var(--panel); border:1px solid var(--rule);
  border-radius:11px; overflow:hidden}
.globe-side .rhead{display:flex; align-items:center; gap:12px; padding:14px 16px;
  border-bottom:1px solid var(--rule)}
.globe-side .rhead .big{font:700 22px ui-monospace,Menlo,monospace; color:var(--orange)}
.globe-side .rhead h2{margin:0; font-size:19px}
.globe-side .rhead .tag{margin-left:auto}
.globe-side .rsub{margin:14px 16px 0; font:11px ui-monospace,Menlo,monospace;
  letter-spacing:.14em; text-transform:uppercase; color:var(--dim)}
.globe-side #g-note{margin:8px 16px 16px; font-size:14.5px; color:var(--dim);
  max-width:none}
.rlist{display:grid; gap:6px; grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
  margin-top:12px}
.rlist button{display:flex; gap:9px; align-items:baseline; text-align:left; cursor:pointer;
  background:var(--panel); border:1px solid var(--rule); border-radius:8px;
  padding:8px 11px; color:inherit; font:inherit; font-size:14px; transition:.15s ease}
.rlist button b{font:700 12px ui-monospace,Menlo,monospace; color:var(--dim)}
.rlist button:hover{border-color:var(--rule-2); background:var(--panel-2)}
.rlist button[aria-pressed="true"]{border-color:var(--orange); background:#1a1710}
.rlist button[aria-pressed="true"] b{color:var(--orange)}
.rlist button:focus{outline:none}
.rlist button:focus-visible{box-shadow:0 0 0 2px var(--teal)}

/* ── THE ZOOM MODAL ───────────────────────────────────────────────────────────────
   Justin, 2026-08-12: "can you make it so if you click on it, it opens up in a modal so
   its easier to read?" The route board is 1920x1080 and renders at 480 in the aside, so
   most of what is on it is unreadable until it is bigger.

   <dialog> rather than a hand-rolled overlay: the browser gives focus trapping, Escape to
   close, inert background and the top layer for free, and every one of those is a thing
   people get wrong by hand. */
.zoomable .zoombtn{display:block; width:100%; padding:0; border:0; background:none;
  cursor:zoom-in; border-radius:8px; overflow:hidden}
.zoomable .zoombtn img{display:block; width:100%; height:auto}
.zoomable .zoombtn:focus{outline:none}
.zoomable .zoombtn:focus-visible{box-shadow:0 0 0 2px var(--teal)}
.zoomhint{color:var(--orange)}

dialog.zoom{border:0; padding:0; background:none; max-width:100vw; max-height:100vh;
  width:100%; height:100%}
dialog.zoom::backdrop{background:rgba(6,9,14,.94)}
.zoomwrap{display:flex; flex-direction:column; height:100%; padding:14px; gap:10px}
.zoomtop{display:flex; align-items:center; gap:12px; flex:0 0 auto}
.zoomtop p{margin:0; font:11px ui-monospace,Menlo,monospace; letter-spacing:.14em;
  text-transform:uppercase; color:var(--dim)}
.zoomclose{margin-left:auto}
/* FIT THE VIEWPORT, DO NOT SCROLL IT. Justin, 2026-08-12: "when you open it in on the
   main page or the route page that it geos full screen width so it doesn't overflow".
   The board is 1920px wide and the first version rendered it at native size inside a
   scroll box -- so on anything narrower than 1920 it overflowed and you dragged it around
   to read it, which is the opposite of what a zoom modal is for.
   width:100% with height:auto fills the available width and cannot overflow; the
   max-height keeps a very wide, short screen from pushing the image past the fold. */
.zoomscroll{flex:1 1 auto; overflow:auto; border:1px solid var(--rule);
  border-radius:10px; background:var(--night); display:grid; place-items:center;
  padding:8px}
.zoomscroll img{display:block; image-rendering:pixelated; width:100%; max-width:100%;
  height:auto; max-height:100%; object-fit:contain}

/* ── /cast, the admin contact sheet ── */
.cast{display:grid; gap:16px; grid-template-columns:repeat(auto-fill,minmax(210px,1fr));
  margin-top:22px}
.ch{margin:0; background:var(--panel); border:1px solid var(--rule); border-radius:10px;
  overflow:hidden}
.ch img{display:block; width:100%; height:auto; image-rendering:pixelated;
  background:var(--panel-2)}
/* THE WHOLE CARD IS THE BUTTON. The card carries the picture, the name and the two status
   chips and nothing else -- everything it used to list lives in the dialog now, so the
   click target is the card rather than a link inside it. */
.cbtn{display:block; width:100%; padding:0 0 12px; border:0; background:none; text-align:left;
  color:inherit; font:inherit; cursor:pointer}
.cbtn:focus{outline:none}
.cbtn:focus-visible{box-shadow:inset 0 0 0 2px var(--teal)}
.cbtn:hover img{filter:brightness(1.07)}
.ch .cx{display:block; margin:11px 13px 0; font-size:15px; line-height:1.3; font-weight:600}
.ch .cst{display:flex; margin:6px 13px 0; align-items:center; gap:8px; flex-wrap:wrap}
.cdetail{display:none}
/* The failure count and the reasons. "off model" is not one thing: Clint fails on palette
   alone and IS the reference the format was measured from, Chris fails on five. Badging
   both the same way throws away the only distinction that decides what to do next. */
.ch .stn{font:10.5px ui-monospace,Menlo,monospace; color:var(--dim)}
.ch .cf{margin:7px 0 0; padding:0 0 0 15px; font:11px/1.5 ui-monospace,Menlo,monospace;
  color:var(--dim)}
.ch .cf li{margin:1px 0}
.ch .cn{margin:7px 0 0; font-size:13px; color:var(--dim)}
/* NOT .roster. That class was already taken by the recruit list on the home page --
   a <ul> of four "Testers / Writers / Artists / Designers" rows -- and a second
   `.roster{display:grid; grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}`
   defined 294 lines later won it, turning that list into four one-word-wide columns with
   the headings overlapping the body text. Live, on the home page, for one deploy.

   The irony is exact: hours earlier the editor's `.bar` had to be renamed for colliding
   with the site header's `.bar`, and the lesson did not transfer to the class I was
   writing at the time. Cast-page classes are prefixed now. */
.castgrid{display:grid; gap:12px; grid-template-columns:repeat(auto-fill,minmax(165px,1fr));
  margin-top:22px}
.rr{margin:0; background:var(--panel); border:1px solid var(--rule); border-radius:8px;
  overflow:hidden}
.rr img{display:block; width:100%; height:auto; image-rendering:pixelated}
.rr .cx{display:block; margin:7px 9px 0; font-size:12.5px; line-height:1.3; font-weight:600}
.rr .cst{display:flex; margin:5px 9px 0; gap:6px; flex-wrap:wrap}
/* DESIGN CARDS -- uniform by construction, not by luck. A design card carries two metric
   chips where a roster card carries one status pill, and names run from "Josh" to "Man in
   Black", so left alone the footers land at different heights and the row reads broken even
   though every card is individually right. Flex column + margin-top:auto on the footer puts
   all the slack in one place and the grid's equal-height rows do the rest. */
.rr.dsn{display:flex; flex-direction:column; height:100%}
.rr.dsn .cbtn{display:flex; flex-direction:column; height:100%; text-align:left}
.rr.dsn .cst{margin-top:auto; padding-bottom:9px}
.dm{font:600 11px/1.2 ui-monospace,Menlo,monospace; font-variant-numeric:tabular-nums;
  display:flex; flex-direction:column; align-items:center; gap:1px; padding:3px 6px;
  border:1px solid var(--rule); border-radius:5px; background:var(--panel-2); color:var(--ok)}
.dm.bad{color:var(--warn)}
.dm i{font:400 8px/1 ui-monospace,Menlo,monospace; font-style:normal; letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim)}
/* The four passes in the modal, and the same four at sprite size below them. */
.dvs{display:flex; gap:10px; flex-wrap:wrap; margin:8px 0 4px}
.dv{margin:0; width:132px; padding:0; background:none; border:0; cursor:pointer;
  text-align:left; font:inherit; display:block}
.dv img{display:block; width:100%; height:auto; border:1px solid var(--rule);
  border-radius:6px; image-rendering:pixelated}
.dv:hover img{border-color:var(--dim)}
.dv:focus-visible{outline:2px solid var(--teal); outline-offset:3px; border-radius:8px}
/* The chosen one, unmistakably. A 1px border shift is not enough to answer "which did I
   pick" from across the grid, which is the only question this control exists to answer. */
.dv.pick img{border-color:var(--teal); box-shadow:0 0 0 3px var(--teal)}
.dv .dvc{display:block; font:400 10px/1.4 ui-monospace,Menlo,monospace; color:var(--dim);
  margin-top:5px}
.dv .dvc b{display:block; font-weight:600; text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink)}
.dv.pick .dvc b{color:var(--teal)}
.dvnote{font:400 11px/1.5 ui-monospace,Menlo,monospace; color:var(--dim); margin:8px 0 0;
  min-height:1.5em}
/* Three states, three colours, because "saved for everyone" and "saved in this browser"
   are different facts and a single neutral line lets the weaker one read as the stronger. */
.dvnote.ok{color:var(--ok)}
.dvnote.warn{color:var(--warn)}
.dvcopy{margin-top:6px}
.dcs{display:flex; gap:10px; flex-wrap:wrap; margin:8px 0 4px}
.dc{margin:0}
.dc img{display:block; height:150px; width:auto; border:1px solid var(--rule);
  border-radius:5px; image-rendering:pixelated}
.dc figcaption{font:600 10px/1.4 ui-monospace,Menlo,monospace; text-transform:uppercase;
  letter-spacing:.06em; color:var(--dim); margin-top:4px; text-align:center}
/* The roster renders a character ALREADY has a sprite for, hung off that character's card
   rather than listed again under a heading that says the sheet is still to be drawn.
   Prefixed cx- like the rest of the cast page, for the .roster reason two comments up. */
.cxrs{display:flex; gap:6px; flex-wrap:wrap; padding:0 10px 10px}
.cxrs:empty{display:none}
.cxr{width:52px; padding:0; border:1px solid var(--rule); border-radius:6px; background:none;
  overflow:hidden; cursor:zoom-in}
.cxr img{display:block; width:100%; height:auto}
.cxr:focus-visible{outline:none; box-shadow:0 0 0 2px var(--teal)}
/* ── the character dialog ──
   Everything the compact card stopped showing. It is a <dialog>, so the focus trap, Escape
   and the inert background are the platform's job rather than this file's. */
.cdlg{width:min(680px,92vw); max-height:88vh; padding:0; border:1px solid var(--rule);
  border-radius:12px; background:var(--panel); color:var(--ink); overflow:auto}
.cdlg::backdrop{background:rgba(0,0,0,.62)}
.cdlgx{position:sticky; top:0; float:right; margin:8px 8px 0; width:34px; height:34px;
  border:1px solid var(--rule); border-radius:8px; background:var(--panel-2);
  color:var(--ink); font-size:20px; line-height:1; cursor:pointer; z-index:2}
.cdlgx:focus-visible{outline:none; box-shadow:0 0 0 2px var(--teal)}
#castdlgbody{padding:18px 22px 24px}
#castdlgbody h3{margin:0 0 8px; font-size:22px}
#castdlgbody h4{margin:20px 0 8px; font-size:12px; letter-spacing:.09em;
  text-transform:uppercase; color:var(--dim)}
.cdsheet{display:block; width:100%; max-width:420px; height:auto; margin:0 auto;
  image-rendering:pixelated; background:var(--panel-2); border:1px solid var(--rule);
  border-radius:8px}
.cdt{width:100%; margin:16px 0 0; border-collapse:collapse;
  font:12.5px ui-monospace,Menlo,monospace; font-variant-numeric:tabular-nums}
.cdt th{width:42%; padding:5px 8px 5px 0; text-align:left; font-weight:400; color:var(--dim);
  vertical-align:top}
.cdt td{padding:5px 0; border-bottom:1px solid var(--rule)}
.cdl{display:flex; gap:8px; flex-wrap:wrap; margin:0}
.cdl .btn{font-size:12.5px; padding:6px 11px}
.csb{margin:0; padding:16px 22px 20px; border-top:1px solid var(--rule)}
.csbn{margin:0 0 11px; font-size:12.5px; color:var(--dim)}
.csb input[type=file]{display:block; width:100%; margin:0 0 9px; font:inherit; font-size:13px}
.csb input[type=text]{display:block; width:100%; margin:0 0 9px; padding:8px 10px;
  border:1px solid var(--rule); border-radius:7px; background:var(--panel);
  color:var(--ink); font:inherit; font-size:13px}
.csbd{margin:0 0 9px; font:12px ui-monospace,Menlo,monospace; color:var(--dim);
  font-variant-numeric:tabular-nums; min-height:1.1em}
.csba{display:flex; gap:9px; align-items:flex-start; margin:0 0 11px; font-size:12.5px;
  line-height:1.5; color:var(--dim)}
.csba input{margin-top:3px; flex:0 0 auto}
.csba em{font-style:normal; opacity:.7}
.csbt{display:flex; gap:6px; align-items:center; flex-wrap:wrap; margin:6px 0 0}
.csbt select{padding:4px 8px; border:1px solid var(--rule); border-radius:6px;
  background:var(--panel); color:var(--ink); font:inherit; font-size:11.5px}
/* ── palettes ── */
.palgrid2{display:grid; gap:16px; grid-template-columns:repeat(auto-fill,minmax(330px,1fr));
  margin-top:22px}
/* UNIFORM CARDS. Justin, 2026-08-13: "Can we make sure these cards follow the same
   formatting? ... We need to make these all the same so they look uniform."
   Above Black 32 has four rows of swatches and cast_8 carries a note, so the cards had
   different heights and their download rows sat at different levels -- a ragged grid.
   The card is a flex COLUMN at full row height and the footer is pushed to the bottom
   with margin-top:auto, so every download row lines up whatever the card holds. */
.pal{display:flex; flex-direction:column; height:100%; margin:0; padding:14px 15px 13px;
  background:var(--panel); border:1px solid var(--rule); border-radius:10px}
.palhead{display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  margin:0 0 11px}
.palhead b{font-size:15px}
.pcount{font:11px ui-monospace,Menlo,monospace; color:var(--dim)}
/* THE SPLIT HE ASKED FOR: squares on the left, values on the right, inside the card. */
.palbody{display:grid; grid-template-columns:1fr 118px; gap:13px; align-items:start}
.pswatches{display:grid; grid-template-columns:repeat(var(--cols,8),1fr); gap:3px;
  align-content:start}   /* swatches hug the top; the slack goes to the footer */
.pswatch{aspect-ratio:1; min-width:0; padding:0; border:1px solid rgba(0,0,0,.28);
  border-radius:3px; cursor:pointer}
.pswatch:focus{outline:none}
.pswatch:focus-visible,.pswatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--ink),
  0 0 0 4px var(--teal)}
.palinfo{display:flex; flex-direction:column; min-width:0}
.palinfo .pcopy{margin-top:auto}
.pchip{height:46px; border:1px solid var(--rule); border-radius:7px}
.phex{margin:7px 0 6px; font:13px ui-monospace,Menlo,monospace; letter-spacing:.02em}
.pdl{display:grid; grid-template-columns:auto 1fr; gap:2px 7px; margin:0 0 9px;
  font:11px ui-monospace,Menlo,monospace}
.pdl dt{color:var(--dim)}
.pdl dd{margin:0; overflow-wrap:anywhere}
.pcopy{font-size:11.5px; padding:4px 9px; width:100%}
.pnote{margin:11px 0 0; font-size:12px; color:var(--dim); line-height:1.5}
.pdl2{display:flex; gap:6px; flex-wrap:wrap; margin:11px 0 0; padding-top:11px;
  border-top:1px solid var(--rule)}
.pal .pdl2{margin-top:auto}          /* the footer sits on the floor of every card */
.pdl2 .btn{font-size:11.5px; padding:5px 9px}
@media (max-width:430px){ .palbody{grid-template-columns:1fr} }
.crd{margin-top:18px}
.agr{margin:14px 0; padding:14px 18px; border-left:3px solid var(--teal);
  background:var(--panel); border-radius:0 8px 8px 0}
.agr p{margin:0; font-size:15px; line-height:1.6}
.crdr{margin:0 0 7px; font-size:15px}
.crdr span{color:var(--dim); font-size:13.5px}
.csbq{margin:12px 0 0}
.csbr{display:flex; gap:9px; align-items:center; flex-wrap:wrap; padding:8px 0;
  border-top:1px solid var(--rule); font-size:12.5px}
.csbr time{color:var(--dim); font:11.5px ui-monospace,Menlo,monospace}
.csbr .btn{font-size:11.5px; padding:4px 9px}
.cmv{margin:0; padding:16px 22px 20px; border-top:1px solid var(--rule);
  background:var(--panel-2)}
.cmvh{margin:0 0 9px; font:12px ui-monospace,Menlo,monospace; letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim)}
.cmvb{display:flex; gap:8px; flex-wrap:wrap}
.cmvb button[disabled]{opacity:.45; cursor:default}
.cmvm{margin:10px 0 0; font-size:13px; min-height:1.2em}
.cmvm.ok{color:var(--teal)}
.cmvm.err{color:#ff8f8f}
.cmvn{margin:8px 0 0; font-size:12px; color:var(--dim)}
/* The chip a card wears once a decision overrides its folder. It has to be visible on the
   CARD, not only in the dialog -- an override nobody can see from the grid is exactly the
   drift this design accepted, so it does not get to be silent. */
.cmoved{display:block; margin:5px 13px 0; font:11px ui-monospace,Menlo,monospace;
  color:var(--dim)}
.cdr{display:flex; gap:10px; flex-wrap:wrap}
.cdr img{width:132px; height:auto; border:1px solid var(--rule); border-radius:8px}

/* ── cards ── */
/* The concept-art guard rail. This is the only place on the site showing something that does
   not exist, so the label is styled to be impossible to skim past -- a recruiting artifact
   that lets a nice picture imply shipped content is how you lose the people you are courting
   on the day they find out. */
.warn{border-left:3px solid var(--rust); background:rgba(174,73,35,.10); padding:13px 16px;
  border-radius:0 7px 7px 0; margin:18px 0}
.tag{border:1px solid var(--rust); color:var(--rust); border-radius:4px; padding:1px 7px;
  font-size:11px; text-transform:uppercase; letter-spacing:.09em; margin-left:8px}
.cards{display:grid; gap:18px; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); margin-top:26px}
.card{background:var(--panel); border:1px solid var(--rule); border-radius:9px; padding:22px}
/* THE CARDS ON /recruit ARE h2, NOT h3, AND THAT IS AN ACCESSIBILITY FIX RATHER THAN A
   STYLE ONE. That section's heading is the page h1, so its four cards are the next level
   down -- writing them as h3 skipped a level and a screen reader tabbing by heading went
   h1 straight to h3 with nothing in between. Same size as the h3 they replace, because
   nothing about the design was wrong; only the outline was. Kept next to the h3 rule so
   the pair stays obvious. */
.card h2{font-size:19px; margin:0 0 8px; letter-spacing:0; line-height:1.3}
.card :is(h2,h3){color:var(--ink)}

/* SAME FIX, SAME REASON, /signin and /account -- and /recruit's pitch panel. Each of those
   is one h1 with a panel under it, so the panel's heading is level two. All three were h3,
   so heading navigation went h1 straight to h3.

   THIS RULE HAS TO LIVE HERE, IN THE GENERATOR. web/css/site.css is WRITTEN by this file on
   every build (see main(), "css/site.css"), so a hand-edit to the .css survives exactly
   until the next build and then vanishes without a word. That is how the first attempt at
   this fix disappeared: edited the output, rebuilt, and the deployed h2 rendered at the
   global clamp(24px,3.4vw,32px) instead of 19px. Nothing errored; the rule was simply gone.
   The tell was measuring the live page rather than trusting the edit. */
.panel > h2{font-size:19px; margin:0 0 8px; letter-spacing:0; line-height:1.3}
.panel > h2.mt0{margin-top:0}
.card p{font-size:14.5px; color:var(--dim); margin:0}
.card .k{font:600 11px/1 ui-monospace,Menlo,monospace; letter-spacing:.2em; text-transform:uppercase;
  color:var(--orange); display:block; margin-bottom:11px}

/* ── gallery ── */
.gal{display:grid; gap:16px; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); margin-top:26px}

/* ── panels / forms ── */
.panel{background:var(--panel); border:1px solid var(--rule); border-radius:10px; padding:28px;
  max-width:520px; box-shadow:var(--shadow)}
.panel.wide{max-width:760px}

/* /signin is a TASK page, not an editorial one: there is no media band to break out with,
   so there is nothing to compose the space with and the rail would be an empty column. It
   was also the worst instance of the complaint -- a 520px .panel with no auto margin sat
   flush left with ~585px of void beside it. Centring the SECTION rather than the panel is
   what avoids a left / left / centre / left zigzag down the page.

   /account LEFT THIS RULE on 2026-08-12. Justin: "its pretty narrow compared to the rest of
   the website. We need to apply the same rules to this page." He is right, and the reason is
   that the page stopped being a task: it now carries a tab strip and three tables, one of
   them seven columns wide, and 680px is a measure for a sign-in form. This is the same
   complaint page_tests() already answered ("The width of the testing page should match the
   width of the other pages") and it gets the same answer -- `split idx` on the 1240px grid,
   not a wider number here, because /signin still wants 680 and this rule is what gives it. */
main > section.form{max-width:680px; margin-inline:auto}
main > section.form :is(.panel,.narrow){max-width:none}
.field{display:block; margin:0 0 16px}
.field span{display:block; font-size:13px; color:var(--dim); margin-bottom:6px}
/* EVERY CONTROL, NOT JUST input. Justin, 2026-08-12: "A couple of the fields do not feel
   part of the site."

   The rule said `.field input` and the pitch form on /recruit has a <select> and two
   <textarea>s, so three of its five controls were rendering as raw browser defaults --
   monospace, three lines tall, no padding, a native border. The inputs beside them were
   styled, which is what made it obvious something was wrong rather than plain.

   A textarea also needs font-family said out loud: it does NOT inherit, it defaults to
   monospace, and that alone is most of why those two boxes looked foreign. */
.field :is(input,select,textarea){
  width:100%; padding:12px 13px; border-radius:7px; border:1px solid var(--rule);
  background:var(--night); color:var(--ink);
  font:15px/1.5 ui-sans-serif,system-ui,sans-serif;
}
.field textarea{min-height:96px; resize:vertical}
.field :is(input,select,textarea):hover{border-color:var(--rule-2)}
.field :is(input,select,textarea)::placeholder{color:var(--dim); opacity:1}

/* The select needs its native chrome removed before it can match, and then it needs an
   arrow drawn back on -- appearance:none takes the arrow with it. Inline SVG in a data URI
   because the CSP blocks external images and this must not become a request. */
.field select{
  appearance:none; -webkit-appearance:none; padding-right:38px; cursor:pointer;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%238b93a3' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 13px center;
  background-size:12px 8px;
}
/* ── the two ways out ─────────────────────────────────────────────────────────────────
   A native <dialog>, so the modal behaviour, the focus trap and Escape all come from the
   browser rather than from code that has to be maintained. ::backdrop is styled rather than
   left default because the default is a flat black wash that looks like a bug on this page.
   .danger is the only place on the site that uses --pink at full strength -- these are the
   two controls that cannot be undone, and they should not look like the Play button. */
.sheet{max-width:min(520px,calc(100vw - 32px)); border:1px solid var(--rule-2);
  border-radius:10px; background:var(--panel); color:var(--ink); padding:24px 26px}
.sheet::backdrop{background:rgba(4,7,12,.72)}
.sheet h3{margin:0 0 12px; font-size:20px; letter-spacing:-.01em}
.sheet p{font-size:14.5px; line-height:1.55; color:var(--dim); margin:0 0 11px; max-width:none}
.sheet p b{color:var(--ink)}
.sheet .row{margin-top:18px}
.btn.danger{border-color:var(--pink); color:var(--pink)}
.btn.primary.danger{background:var(--pink); border-color:var(--pink); color:#fff}
.btn.danger:hover{filter:brightness(1.1)}
.btn[disabled]{opacity:.55; pointer-events:none}

/* ── THE WORLD MAP (/atlas) ────────────────────────────────────────────────────────
   A canvas of absolutely-positioned boxes over an SVG of lines. Boxes are DOM rather than
   canvas because they need to be dragged, hit-tested and read by a screen reader, and none
   of that is free on a <canvas>. The lines are SVG because they need to be clickable to
   delete, which is the one thing an ::after border cannot be. */
.at-bar{display:flex; flex-wrap:wrap; gap:9px; margin:18px 0 8px}
/* TALLER THAN IT WAS, AND THE ROOM IS EARNED. The world is 648 by 470 tiles -- taller in
   proportion than any browser window -- so the fit is always limited by HEIGHT and every
   pixel of height is a pixel of map, while the extra width just sits empty. At 560 px the
   whole world came out at about one pixel per tile, which is a map you can locate things on
   and not one you can see. This is the page whose entire job is to be looked at; the height
   is what it is looking through. */
.at-wrap{position:relative; height:min(78vh,780px); margin:10px 0 18px; overflow:hidden;
  background:var(--panel); border:1px solid var(--rule); border-radius:10px;
  touch-action:none; cursor:grab}
.at-wrap:active{cursor:grabbing}
/* THE VOID GOES DARK WITH THE MAP, AND THIS IS A MEASUREMENT RATHER THAN A TASTE.
   Tiles mode fixes its ink: names are #fff5e6 ringed in #1a0d08, chosen -- correctly -- to
   survive both a pale quay and a dark wall UNDERNEATH THEM. That reasoning holds only where
   there IS map under the words, and often there is not. A name is printed below its region on
   the 9-tile halo, and a two-line name is 26 px tall where the halo is about 14: measured on
   the real page at the opening fit, seven of the twenty-one visible names overhang their own
   ground and three of them sit half off it (AREA 51 -- INSIDE THE MOUNTAIN, SAN DIEGO -- THE
   WATERFRONT, SKINWALKER RANCH). Whatever they overhang onto is this background.
   In the dark theme that is --panel #111621 and the ink reads at 16.8:1. In the light theme it
   is #fffdf8 and the SAME ink reads at 1.06:1 -- invisible except for its own shadow, which is
   the one-theme-ink-on-the-other-theme's-ground failure exactly.
   So the void stops following the theme, for the same reason the landmass already does not:
   it is the surface the map's own ink is judged against, so it belongs to the map. The frame
   around it still follows the theme, because the frame belongs to the page. */
.at-wrap.tiles{background:#15100c}
/* THE WORLD ITSELF. Sized in JS from the wrap's own box times the device pixel ratio, so a
   1px-per-tile bitmap blown up 6x lands on whole device pixels instead of being resampled
   into porridge. image-rendering is belt and braces with the context's imageSmoothingEnabled
   -- Safari has historically honoured one and not the other. */
.at-canvas{position:absolute; inset:0; width:100%; height:100%;
  image-rendering:pixelated; image-rendering:crisp-edges}
.at-svg{position:absolute; inset:0; width:100%; height:100%}
.at-layer{position:absolute; inset:0}
.at-room{position:absolute; box-sizing:border-box; border:1.5px solid var(--teal);
  background:color-mix(in srgb,var(--teal) 16%,transparent); border-radius:4px;
  cursor:move; user-select:none; z-index:1}
/* ── WHEN THE MAP IS THE REAL THING ────────────────────────────────────────────────
   A tinted rectangle over a room's own tiles is a colour wash over the picture you came to
   look at, so in tiles mode the box stops being the region and becomes the HANDLE for it:
   no fill, a hairline edge that says where a room ends, and every interactive state kept.
   The edge is not decoration -- two rooms of the same zone meeting flush read as one room
   without it, and knowing where a region STOPS is most of what this view answers.

   `.sel` and `.linking` are untouched here on purpose: the ring is a box-shadow outside the
   border and linking is the border colour plus an inside fill, so both still read against
   tiles, and the pair that had to stay distinguishable still is. */
/* THE EDGE IS QUIET AT REST AND LOUD UNDER THE CURSOR. A hairline at 34 % fought the tiles
   -- twenty-three pale rectangles ruled over a picture whose whole point is that it has no
   rectangles in it. At 16 % it separates a room from its own ground without being the first
   thing you see, and hover puts the full teal back the moment you go looking for a handle,
   which is the only moment the edge has a job to do.

   It does not follow the theme, and it should not: it sits on the map, which does not follow
   the theme either. White at low alpha over the game's own dirt reads the same in both. */
.at-wrap.tiles .at-room{background:transparent; border-color:rgba(255,255,255,.16);
  border-width:1px; border-radius:0}
.at-wrap.tiles .at-room:hover{border-color:#5fd8c8; border-width:1.5px}
/* The tile count is what made fit-the-text honest -- the box may have grown, the number
   never lies. In tiles mode the box IS the grid, so the number is answering a question
   nobody can ask any more, and it is printing over the map to do it. */
.at-wrap.tiles .at-dim{display:none}
.at-wrap.tiles .at-room.route{border-color:rgba(242,173,60,.7);
  background:transparent}
.at-wrap.tiles .at-room.linking{border-color:#6ee39a; background:rgba(110,227,154,.3)}
/* A PROPOSED REGION HAS NO TILES, because it has no blockout yet -- so it keeps the dashed
   box it always had. That difference is information: it is the one shape on this map that is
   an idea rather than a place. */
.at-wrap.tiles .at-room.added{background:rgba(242,173,60,.22);
  border-color:#f2ad3c; border-style:dashed; border-width:1.5px}
/* The links stand back over tiles -- they are annotation on a picture now, not the picture.
   Except the red ones, which are the entire reason this page exists and never fade. */
.at-wrap.tiles .at-link{opacity:.42}
.at-wrap.tiles .at-link:hover{opacity:1}
.at-wrap.tiles .at-link.bad{opacity:1}
/* Names sit ON the map, so they need to survive both a pale quay and a dark wall underneath.
   A ring of ground-coloured shadow does that in either theme without a plate behind the
   words, which is what would start hiding the map to label it. */
.at-wrap.tiles .at-label{color:#fff5e6;
  text-shadow:0 0 3px #1a0d08,0 0 7px #1a0d08,0 1px 2px #1a0d08,0 -1px 2px #1a0d08}
.at-wrap.tiles .at-label.route{color:#ffd79a}
/* ── THE SELECTION RING BELONGS TO THE MAP, NOT TO THE THEME ───────────────────────
   The base rule is `.at-room.sel{box-shadow:0 0 0 2px var(--ink)}`, and on the page that is
   right. On the map it is the 1.06:1 bug wearing a different hat: --ink in the light theme
   is #241a12, a dark brown ring drawn on #15100c ground, and the ring is how you know what
   Remove is aimed at. Restated here for the map, which does not follow the theme.

   The hover edge, the route edge, the half-drawn-connector fill and the proposed-region box
   had the same fault and are fixed AT THEIR OWN RULES above rather than overridden here --
   two rules for one selector is one fact in two places, and the one that loses is invisible.
   Every value is measured by tools/verify_atlas_ink.py against the real extremes of the
   ground it lands on. */
.at-wrap.tiles .at-room.sel{box-shadow:0 0 0 2px #f4e8d4}

/* ── THE DIAGRAM ON TOP (Map: places and roads) ────────────────────────────────────
   Justin, 2026-08-16, still holding the Kanto map: "The atlas looks good but I was hoping to
   make it the points and lines with the terrain in the background."

   The previous pass took Kanto's GROUND and stopped. Kanto is two layers, and the ground is
   the one you do not read the map by: painted terrain says where you are, and a DIAGRAM over
   it -- a marker per town, a thick line per route, a badge on the line -- says how the places
   connect. Without the second layer this page was a landmass with names on it.

   So the terrain stays exactly as it was and gets knocked back (the scrim is in paintWorld,
   because it has to composite over a canvas), and this is the ink that goes on top.

   THE ONE RULE THAT MAKES THE INK WORK: EVERY MARK OWNS ITS OWN GROUND. A line is a bright
   core inside a dark casing; a label is text on a plate; a badge is dark text on a shield.
   That is not decoration -- it is the answer to the trap that already bit this page once,
   where names measured 1.06:1 because they were drawn straight onto whatever happened to be
   underneath. Ink-on-terrain cannot be measured, because the terrain is 400 different
   colours; ink-on-its-own-plate is one number, and it is the same number everywhere on the
   map, in both themes, on and off the landmass. Read the numbers in
   tools/verify_atlas_ink.py -- it computes them from this stylesheet and the real zone
   atlases, so they cannot drift from what ships.

   NO NINTENDO GREEN, and no new palette either: teal is already what this page calls a
   region and amber is already what it calls a road, on /atlas and on /blockout both. The
   diagram is the same vocabulary at a legible weight. */
.at-wrap.diagram{background:#15100c}
/* The room box stops being the thing you look at and becomes the thing you grab: the marker
   is the region now. The edge is kept at a whisper because knowing where a place STOPS is
   still worth something, and comes back under the cursor when you go looking for a handle. */
.at-wrap.diagram .at-room{border-color:rgba(255,255,255,.09)}
.at-wrap.diagram .at-room.route{border-color:rgba(242,173,60,.20)}
/* Restated after the .route rule, which matches at the same weight and would otherwise win
   on source order -- so hovering a ROAD would leave it at its resting whisper. */
.at-wrap.diagram .at-room:hover{border-color:#5fd8c8; border-width:1.5px}
/* A MARKER PER PLACE, and a fixed size in screen pixels -- deliberately not the room's size.
   Kanto's diamonds are all the same however big the town is, and that is the "one visual
   weight" this map was already reaching for: nothing is emphasised for having a big grid.
   pointer-events:none so the marker never swallows a drag aimed at the region under it. */
.at-mark{position:absolute; width:11px; height:11px; pointer-events:none; z-index:3;
  transform:translate(-50%,-50%) rotate(45deg);
  background:#5fd8c8; border:2px solid #0e0a06}
/* A PROPOSED REGION IS AN IDEA, NOT A PLACE -- the same distinction the dashed box makes. */
.at-mark.added{background:#f2ad3c; border-style:dashed}
/* PULLED IN ALONG ITS TRUE BEARING: Antarctica, and the Moon, which is not on this planet.
   The second ring is the break in a paper chart, said on the marker as well as on the box. */
.at-mark.offscale{box-shadow:0 0 0 2px #15100c,0 0 0 4px rgba(244,232,212,.6)}
/* THE NAME SITS ON ITS OWN GROUND. Not a taste: a plate is the only version of this whose
   contrast is a number rather than a hope, because a label here can be over the palest quay,
   over void, or off the landmass entirely, and it has to read on all three. */
.at-wrap.diagram .at-label{color:#f4e8d4; z-index:4; text-shadow:none;
  background:rgba(14,10,6,.82); border:1px solid rgba(244,232,212,.20);
  border-radius:3px; padding:2px 6px; font-size:10.5px}
/* THE ROAD'S SHIELD, sitting on the line it names, the way Kanto badges 01/02/03. Dark on
   amber rather than amber on dark, so it reads as a sign rather than as another label --
   and so the two kinds of thing on this map are never the same shape. */
.at-wrap.diagram .at-label.route{color:#17110a; background:#f2ad3c; border-color:#0e0a06;
  font-weight:700; font-size:10px; letter-spacing:.06em; white-space:nowrap;
  padding:1px 5px}
/* THE CASING. Every line is drawn twice -- this underneath, wider and near-black, then the
   colour on top. It is what lets one ink read over both the palest quay and the void: over
   dark ground the bright core carries the line, over pale ground the dark rim does, and the
   core-against-its-own-casing ratio never changes whatever is underneath. */
.at-case{fill:none; stroke:#0e0a06; stroke-linecap:round; pointer-events:none}
/* A CROSSING -- a ride you take, not a road you walk. Steel rather than amber because it is
   not a road, and knocked a step back from white because these are the longest lines on the
   map: Skinwalker to the Pentagon and both legs to Antarctica cross the whole board, and at
   full white they were the loudest thing on a map whose subject is the roads. Still 3.6:1
   against the palest ground it crosses, so it is quieter without being faint. */
.at-wrap.diagram .at-link{opacity:1; stroke:#bcc8d6; stroke-width:2.5}
.at-wrap.diagram .at-link:hover{stroke:#ffffff; stroke-width:3.5}
/* A ROAD IS THE HEAVIEST THING ON THE MAP, because in Kanto the routes are what you read it
   by. It runs town -> road -> town as one thick amber run with the shield at the bend. */
.at-wrap.diagram .at-link.road{stroke:#f2ad3c; stroke-width:5}
.at-wrap.diagram .at-link.road:hover{stroke:#ffc76a; stroke-width:6}
.at-wrap.diagram .at-link.vert{stroke:#f2ad3c; stroke-dasharray:2 5; stroke-width:2.5}
.at-wrap.diagram .at-link.conn{stroke:#ff5fb4; stroke-dasharray:3 5; stroke-width:3}
/* Red still means one thing and one thing only, and it is still never faded. Last in the
   block so it out-orders the road weight above it at equal specificity. */
.at-wrap.diagram .at-link.bad{stroke:#ff4d63; stroke-width:4}
/* Labels float OVER the map rather than inside their box -- see the draw code for why the box
   cannot resize to its text. pointer-events:none so a label never swallows a drag aimed at
   the box under it, which is the one thing that would make this worse than clipping. */
.at-label[hidden]{display:none}
.at-label{position:absolute; transform:translate(-50%,-50%); pointer-events:none; z-index:2;
  font:600 11px/1.2 ui-sans-serif,system-ui,sans-serif; letter-spacing:.02em;
  color:var(--ink); text-align:center; text-shadow:0 1px 3px var(--night),0 0 6px var(--night);
  white-space:normal; overflow-wrap:anywhere}
/* --warn, NOT --amber, AND THIS IS THE ONE MODE THAT FOLLOWS THE THEME. Both map modes
   override this rule, so it is reached only in plain-boxes mode -- where the ground IS the
   page panel, so the ink has to work on cream as well as on night. --amber measured 2.9:1 on
   #fffdf8: the same "one theme's ink on the other theme's ground" failure as the 1.06:1
   labels, in the one mode nobody re-checked because it looked fine in the dark. --warn is
   the amber family's readable end -- 4.53:1 light, 9.68:1 dark -- so routes stay amber and
   the words stay legible. Pinned by tools/verify_atlas_ink.py. */
.at-label.route{color:var(--warn); font-size:10px}
.at-dim{position:absolute; transform:translate(-50%,-50%); pointer-events:none; z-index:2;
  font:10px/1 ui-sans-serif,system-ui,sans-serif; color:var(--dim);
  font-variant-numeric:tabular-nums; text-shadow:0 1px 3px var(--night)}
/* A proposed region is not a real one yet, and should not be mistaken for one at a glance. */
.at-room.route{border-color:var(--amber); border-width:1px;
  background:color-mix(in srgb,var(--amber) 10%,transparent)}
.at-room.route b{color:var(--amber); font-size:10px}
.at-room.added{border-style:dashed; border-color:var(--amber);
  background:color-mix(in srgb,var(--amber) 14%,transparent)}
/* PULLED IN ALONG ITS TRUE BEARING because it is much further away than the board can show
   -- Antarctica, and the Moon, which is not on this planet at all. The double outline is the
   break in a paper chart: the direction is honest and the distance is not, and the map has
   to say so rather than quietly compress one leg. */
.at-room.offscale{outline:1px dashed var(--dim); outline-offset:3px}
/* THESE TWO STACK, DELIBERATELY, because they answer different questions. The ring means
   "Remove is aimed here"; the green fill means "the connector you are drawing starts here".
   Since 2026-08-16 a click in draw-connectors mode sets both, so the first box of a pair
   wears both marks at once -- which is the truth about it, and the reason they are kept on
   separate visual channels: the ring is a box-shadow OUTSIDE the border, the linking state
   is the border colour and the fill INSIDE it, so neither can hide the other. Had selection
   also been expressed as a border colour the two would have collapsed into one
   indistinguishable state, and not being able to tell what Remove was aimed at is the whole
   bug this pairing was introduced to end. */
.at-room.sel{box-shadow:0 0 0 2px var(--ink)}
.at-room.linking{border-color:var(--ok); background:color-mix(in srgb,var(--ok) 22%,transparent)}
/* `.at-road` USED TO BE HERE AND WAS DEAD -- three rules for a class nothing on the site has
   ever emitted, left behind when routes became rooms rather than lines ("ROUTES STAY AS
   ROOMS", world_seed). Harmless while it was merely unused; not harmless now that the
   diagram draws a real road as `.at-link.road`, because the next person to change how a road
   looks would find two plausible rules and edit the one that does nothing. Deleted rather
   than kept: a rule that cannot fire is a comment that lies. */
.at-link{stroke:var(--dim); stroke-width:2; cursor:pointer}
.at-link:hover{stroke:var(--ink); stroke-width:3}
.at-link.locked{stroke-dasharray:6 5}
/* A lift or a stairwell. Drawn differently because the two rooms it joins are on top of each
   other rather than beside each other, so its length on this map means nothing. */
.at-link.vert{stroke:var(--amber); stroke-dasharray:2 4}
/* A CONNECTOR -- a hole between two rooms that are not neighbours. Its own colour because it
   is its own kind of thing: not a road you walk and not a ride you take, but a way through
   that exists because nobody would expect it. */
.at-link.conn{stroke:var(--pink); stroke-dasharray:3 5; stroke-width:2.5; opacity:.85}
/* Red is reserved for the one thing this page exists to surface: a door whose edge disagrees
   with where its neighbour actually is. Nothing else on the page is allowed to be red. */
.at-link.bad{stroke:var(--pink); stroke-width:3}
#at-issues ul{margin:6px 0 0; padding-left:20px}
#at-issues li{font-size:14px; margin:3px 0; color:var(--dim)}
#at-issues b{color:var(--ink)}

/* ── THE AUDIO BENCH, TWO COLUMNS (/blockout) ──────────────────────────────────────
   Justin, 2026-08-18: "for the playlist style, it takes up the full width, We probably can
   make it half the width, but then place the soundfx on the right column next to the
   playlist."

   THE REASON IS NOT TIDINESS, IT IS THAT THE TWO ARE USED TOGETHER. Judging whether a
   footstep sits right against the Rachel theme means hearing them near each other, and
   stacked vertically the board is off screen while the music plays -- so the one comparison
   this page exists to make costs a scroll every single time, which is the kind of friction
   that ends with it not being made.

   1240 IS THE BREAKPOINT THIS STYLESHEET ALREADY HAS, not a number invented for this block:
   it is --wrap, and the --media rule two hundred lines up switches on it too. The tightest
   case is a viewport of exactly 1240, and it was MEASURED there rather than computed: each
   half comes out 568px, not the 575 the arithmetic promises, because a classic scrollbar
   takes 15px off the layout width before --wrap ever sees it. At 568 the soundboard's
   auto-fill grid (178px minimum, 10px gap) still settles at THREE columns -- 3x178+2x10 =
   554 fits with 14px to spare, four would need 742. Three across reads as an instrument;
   two would read as a list. So the margin is real but thin, and 178 is the number to watch:
   raise the pad minimum past 182 and the narrow column silently becomes a list -- a cliff
   MEASURED in the browser at a 568px column rather than reasoned from the 14px of slack:
   182 still lays out three, 183 drops to two.

   BELOW IT, ONE COLUMN IN SOURCE ORDER -- playlist first, board second, which is what the
   page already was. Nothing is reordered, so the visual order cannot disagree with the tab
   order, which is the usual price of doing this with `order` instead of with markup. */
.bench{display:grid; align-items:start; column-gap:var(--rail-gap)}
.bench-half{min-width:0}
@media (min-width:1240px){
  /* Equal halves rather than auto/1fr: the two panels are peers being compared, and a grid
     that sizes them by their contents would make the wider one look like the subject. */
  .bench{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
}

/* ── THE PLAYLIST PLAYER (/blockout) ───────────────────────────────────────────────
   Justin, 2026-08-15: "can we make it look like a playlist player."

   It replaced a grid of nineteen native <audio> controls. Those were the right call while
   this was a listening bench for one person -- the browser's own control already has a
   scrubber, a volume and a keyboard, for free. What it could not do is be a PLAYLIST: no
   running order, no idea what is playing, and nothing stopping all nineteen at once.

   One transport at the top, one row per track, and the row that is playing says so. */
.pl{background:var(--panel); border:1px solid var(--rule); border-radius:12px;
  padding:14px 14px 8px; margin:14px 0 26px}
.pl-bar{display:flex; align-items:center; gap:12px}
.pl-meta{min-width:0; flex:1 1 auto; display:flex; flex-direction:column; gap:2px}
/* The title can be a long slot name in a narrow column; it truncates rather than pushing
   the transport buttons off the end of the row. */
.pl-meta b{color:var(--ink); font-size:15px; line-height:1.2;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pl-meta span{color:var(--dim); font-size:12.5px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pl-b{display:grid; place-items:center; width:38px; height:38px; flex:0 0 auto;
  background:none; color:var(--ink); border:1px solid var(--rule); border-radius:50%;
  font-size:15px; line-height:1; cursor:pointer; padding:0}
.pl-b:hover{border-color:var(--teal); color:var(--teal)}
.pl-b:disabled, .pl-b[aria-disabled="true"]{opacity:.4; cursor:default;
  border-color:var(--rule); color:var(--dim)}
.pl-b[aria-disabled="true"]:hover{border-color:var(--rule); color:var(--dim)}
/* The global `button:focus-visible` rule sets border-radius:3px, and being (0,1,1) it
   out-specifies `.pl-b` at (0,1,0) -- so a round transport button squared off the moment a
   keyboard user reached it. Restated here at matching specificity. */
.pl-b:focus-visible{border-radius:50%}
.pl-main{width:46px; height:46px; background:var(--teal); border-color:var(--teal);
  color:var(--night); font-size:17px}
.pl-main:hover{background:var(--teal); color:var(--night); filter:brightness(1.08)}
/* The glyph is nudged because a triangle's optical centre is left of its bounding box;
   a pause bar is symmetrical and must not inherit the nudge. */
.pl-gly{display:block; transform:translateX(1px)}
.pl-gly.on{transform:none}

.pl-seekrow{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin:12px 2px 4px}
.pl-time{color:var(--dim); font-size:12px; font-variant-numeric:tabular-nums;
  min-width:38px; flex:0 0 auto}
.pl-time:last-of-type{text-align:right}
.pl-seek{flex:1 1 auto; min-width:0}
.pl-vol{display:flex; align-items:center; gap:6px; flex:0 0 auto; color:var(--dim);
  font-size:13px}
.pl-vol input{width:74px}
input.pl-seek, .pl-vol input{-webkit-appearance:none; appearance:none; height:4px;
  border-radius:3px; background:var(--rule); cursor:pointer}
input.pl-seek:disabled{cursor:default; opacity:.5}
input.pl-seek::-webkit-slider-thumb, .pl-vol input::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none; width:13px; height:13px; border-radius:50%;
  background:var(--teal); border:none; cursor:pointer}
input.pl-seek::-moz-range-thumb, .pl-vol input::-moz-range-thumb{
  width:13px; height:13px; border-radius:50%; background:var(--teal); border:none;
  cursor:pointer}
.pl-msg{min-height:1.1em; margin:2px 2px 0; font-size:13px; color:var(--dim)}
.pl-msg.err{color:var(--pink)}

.pl-list{list-style:none; margin:6px 0 0; padding:0; border-top:1px solid var(--rule)}
/* THE ROW IS THE <li> NOW, NOT THE BUTTON. Justin, 2026-08-18: "Can we add the download
   button back to each of the tracks" -- and a link cannot live inside a button, so the two
   controls are siblings and the <li> is what holds them side by side.

   THE RULE AND THE HOVER MOVED UP WITH IT, which is not tidying. Left on .pl-row they would
   describe a button that is now narrower than the row: the hairline would stop short of the
   right edge and the hover tint would leave an untinted notch behind the download button, on
   every row, at every width. A separator between two rows is a fact about the LIST; it
   belongs to the thing that spans it. */
.pl-item{display:flex; align-items:center; border-bottom:1px solid var(--rule)}
.pl-item:hover{background:color-mix(in srgb,var(--teal) 8%,transparent)}
.pl-item:hover .pl-row{color:var(--ink)}
.pl-row{display:flex; align-items:center; gap:12px; flex:1 1 auto; min-width:0;
  padding:9px 6px; background:none; border:0; color:var(--dim);
  font:inherit; text-align:left; cursor:pointer}
/* GHOST UNTIL YOU REACH FOR IT. Twenty-one filled buttons down the right-hand edge would be
   the loudest thing in a panel whose subject is the track names; the border is transparent
   rather than absent so the tile does not grow by 2px on hover and shove the row sideways.
   38px like .pl-b would crowd a 9px-padded row, so 34 -- still over the 24px AA minimum for
   a pointer target that has a 40px-tall row of slop around it. */
.pl-dl{display:grid; place-items:center; width:34px; height:34px; flex:0 0 auto;
  margin:0 2px 0 4px; color:var(--dim); border:1px solid transparent; border-radius:50%;
  text-decoration:none}
.pl-dl:hover{color:var(--teal); border-color:var(--teal)}
/* The same trap .pl-b and .sfx-pad each carry a note about, and this is its third sighting:
   the global `a:focus-visible` sets border-radius:3px at (0,1,1), which out-specifies
   .pl-dl at (0,1,0) -- so a round button squares off the moment a keyboard user reaches it.
   Restated at matching specificity. */
.pl-dl:focus-visible{border-radius:50%}
.pl-dl svg{display:block; width:17px; height:17px}
/* NO `opacity` ON THE RUNNING ORDER. var(--dim) on var(--panel) measures 6.0:1 light and
   5.9:1 dark, which is fine; knocking it back to .65 dropped it to 2.85:1 and 3.19:1, both
   under the 4.5:1 floor for 11.5px text. Stacking opacity on an already-dim token is how a
   pre-checked colour stops being pre-checked -- if something needs to recede further than
   --dim, it wants a dimmer token, not a multiplier. */
.pl-n{font-size:11.5px; font-variant-numeric:tabular-nums; width:20px; flex:0 0 auto}
/* The playing indicator occupies its slot at all times, so a row does not shift sideways
   by a few pixels the moment it starts -- which is exactly when you are looking at it. */
.pl-i{width:9px; height:9px; flex:0 0 auto; border-radius:50%; background:transparent}
.pl-t{min-width:0; flex:1 1 auto; display:flex; flex-direction:column; gap:1px}
.pl-t b{color:inherit; font-size:14px; font-weight:600; line-height:1.25;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pl-t em{font-style:normal; font-size:12px; color:var(--dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pl-d{font-size:12px; color:var(--dim); font-variant-numeric:tabular-nums; flex:0 0 auto}
.pl-row[aria-current="true"]{color:var(--ink)}
.pl-row[aria-current="true"] b{color:var(--teal)}
.pl-row[aria-current="true"] .pl-i{background:var(--teal)}
.pl-row[aria-current="true"] .pl-i.beat{animation:pl-beat 1.6s ease-in-out infinite}
@keyframes pl-beat{0%,100%{opacity:1}50%{opacity:.35}}
@media (prefers-reduced-motion:reduce){.pl-row .pl-i.beat{animation:none}}
@media (max-width:520px){
  /* THE VOLUME WRAPS, IT DOES NOT VANISH. It used to be display:none here, on the reasoning
     that a phone has hardware volume -- but this player SETS au.volume from a stored value,
     and hardware volume cannot lift a software gain of zero. Hiding it also took it out of
     the tab order, so a muted player had no control on screen and none reachable by
     keyboard, and nothing on the page explaining the silence. It gets its own line instead. */
  .pl-vol{flex:1 0 100%; justify-content:flex-end; margin-top:2px}
  .pl-vol input{width:60%}
  .pl-d{display:none}
}

/* ── THE SOUNDBOARD (/blockout) ────────────────────────────────────────────────────
   A SOUNDBOARD IS FOR PRESSING, WHICH IS WHY IT IS NOT THE PLAYLIST AGAIN. The music
   bench above is a running order: one thing at a time, in sequence, with a scrubber,
   because a four-minute cue is something you sit through. Twenty-eight clips between
   0.09 s and 3.6 s are the opposite shape -- you want them all visible at once and you
   want the gap between deciding to hear one and hearing it to be nothing. So: pads, not
   rows; a grid, not a list; no transport, no scrubber, no running order.

   IT SCALED FROM ELEVEN TO TWENTY-EIGHT WITHOUT A BREAKPOINT, which is the payment for
   auto-fill: six groups now reflow independently and the only thing that changed in this
   block was the arithmetic in the comments.

   THE PAD IS THE WHOLE TARGET, the same call the room cards below make. There is one
   verb here and the tile is a much more forgiving hit area than a control inside it.

   The grid is auto-fill from a minimum, so the two groups reflow independently and
   neither needs a breakpoint deciding how many columns it gets. */
.sfx-grid{display:grid; gap:10px; margin:10px 0 22px;
  grid-template-columns:repeat(auto-fill,minmax(178px,1fr))}
.sfx-h{margin:20px 0 2px; font-size:13px; letter-spacing:.07em; text-transform:uppercase;
  color:var(--dim)}
.sfx-h span{color:var(--dim); text-transform:none; letter-spacing:0; font-size:12.5px}
.sfx-pad{display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  position:relative; width:100%; padding:12px 14px 11px; text-align:left; font:inherit;
  background:var(--panel-2); border:1px solid var(--rule); border-radius:10px;
  color:var(--dim); cursor:pointer; transition:border-color .12s,transform .08s}
.sfx-pad:hover{border-color:var(--teal); transform:translateY(-1px)}
/* The same trap `.pl-b` carries a note about, two hundred lines up: the global
   `button:focus-visible` rule sets `border-radius:3px`, and at (0,1,1) it out-specifies
   `.sfx-pad` at (0,1,0). Measured by tabbing into the grid -- the computed radius went
   10px -> 3px -- so every pad squared off the moment a keyboard user reached it, one after
   another, across the whole grid. Restated here at matching specificity. */
.sfx-pad:focus-visible{border-radius:10px}
/* PRESSED, VISIBLY. A clip can be shorter than the time it takes to look down at what you
   just hit -- dialogue_advance is 131 ms -- so the pad flashes on press and holds the flash
   for a fixed 180 ms rather than for the length of the sound. Tying the feedback to the
   audio would make the shortest sounds, the ones you most need confirmation of, the ones
   that confirm least. */
/* A RING, NOT A TINT. The first version washed the tile with 14% teal and recoloured the
   name to match, which looked good and failed WCAG in both themes: measured on the light
   palette the name came out 3.54:1 and the where-line 4.48:1 against the tinted ground, and
   4.19:1 on the dark one -- all under the 4.5:1 floor for text this size. A press lasts 180
   ms and cannot be a moment when the tile becomes harder to read. The ring changes nothing
   behind the text, so every contrast on the pad is the resting one, which passes. */
.sfx-pad.hit{border-color:var(--teal);
  box-shadow:0 0 0 2px color-mix(in srgb,var(--teal) 55%,transparent)}
/* padding-right is here and not in a second `.sfx-name` rule further down: the build fails
   on a bare class defined twice, and rightly -- that is how /cast's `.roster` grid silently
   reflowed the home page's recruit list. The key chip is absolutely positioned, so the name
   has to leave room for it itself. */
.sfx-name{font:600 14px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--ink);
  padding-right:26px}
.sfx-where{font-size:12.5px; color:var(--dim); line-height:1.35}
/* margin-top:auto, not a fixed gap. Grid rows stretch every tile to the tallest in the
   row, and one pad here genuinely needs two lines of text -- so without this, five pads
   carry their footer halfway up with dead space beneath and the row reads as a mistake.
   Pushed to the bottom instead, the durations and gains line up ACROSS the row, which is
   the comparison the numbers are there for. */
.sfx-foot{display:flex; align-items:baseline; gap:8px; margin-top:auto; padding-top:6px;
  font-size:11.5px; color:var(--dim); font-variant-numeric:tabular-nums}
/* The duration is the number this page exists to put in front of you, so it is the one
   thing in the footer that is not dimmed away. */
.sfx-len{color:var(--ink); font-weight:600}
.sfx-gain{opacity:.8}
/* Matches the editor's key chips exactly -- same font, same surface, same reasoning about
   --rule being too faint on the dark theme to carry a box on its own. Absolute, so a long
   slot name wraps under it instead of shoving it off the tile. */
.sfx-pad kbd{position:absolute; top:9px; right:10px;
  font:600 11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
  background:color-mix(in srgb, var(--ink) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--ink) 26%, transparent);
  color:var(--ink); border-radius:4px; padding:1px 5px}
/* A second slot pointing at this same clip. Stated in words, undecorated -- see the note in
   _pad() for why this one is not flagged. */
.sfx-also{font-size:11.5px; color:var(--dim); margin-top:3px}
/* PINK, NOT AMBER, AND THE REASON IS CONTRAST BEFORE IT IS TASTE. --amber measures 2.57:1
   on the light panel -- unreadable -- against 7.59:1 on the dark one, which is exactly the
   trap of picking a colour while looking at one theme. Of the eight palette tokens tried,
   only --pink and --dim clear 4.5:1 on BOTH grounds. Pink is also already the site's "this
   is the thing to look at" colour (/atlas reserves it for a door that disagrees with its
   neighbour), and both things wearing it here -- an unwired pad among wired neighbours, a
   file that is not on disk -- are that. Amber is spoken for on this very page: a route card.

   WHAT IT DELIBERATELY NO LONGER MARKS: a clip nothing calls. That was the right flag when
   every clip in the build was in the game and one exception meant something; seventeen
   candidates arrived on 2026-08-16 and made "uncalled" the ordinary state, at which point
   seventeen pink tiles would have spent the colour and left nothing to spend it on. The
   heading says it once for the whole group instead. A colour that marks a third of the page
   is not a warning, it is a theme. */
.sfx-dead{color:var(--pink)}
.sfx-pad.gone{cursor:not-allowed; border-style:dashed; opacity:.75}
.sfx-pad.gone:hover{border-color:var(--rule); transform:none}
.sfx-pad.gone .sfx-name{color:var(--pink)}
.sfx-bar{display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin:12px 0 2px}
.sfx-msg{min-height:1.1em; margin:6px 2px 0; font-size:13px; color:var(--dim)}
.sfx-msg.err{color:var(--pink)}
@media (prefers-reduced-motion:reduce){
  .sfx-pad{transition:none}
  .sfx-pad:hover{transform:none}
}

/* ── THE GREYBOX INDEX (/blockout) ─────────────────────────────────────────────────
   A CARD GRID, not a list of rows. Justin, 2026-08-15: "Can we create a grid on that page
   inside of the rows? I feel it would look better if we have a grid card set."

   auto-fill with a minimum rather than a fixed column count, so the same markup is four
   across on a monitor and one on a phone without a breakpoint deciding for it. Each card is
   the whole link -- the entire tile is the target, which is a bigger and more forgiving hit
   area than a button inside a row, and there is only one verb on this page anyway. */
.bo{display:grid; gap:12px; margin:16px 0 30px;
  grid-template-columns:repeat(auto-fill,minmax(210px,1fr))}
.bo-card{display:flex; flex-direction:column; gap:3px; padding:13px 15px 14px;
  background:var(--panel); border:1px solid var(--rule); border-radius:10px;
  text-decoration:none; position:relative; transition:border-color .12s,transform .12s}
.bo-card:hover{border-color:var(--teal); transform:translateY(-2px)}
.bo-card b{color:var(--ink); font-size:14.5px; line-height:1.25}
.bo-note{color:var(--dim); font-size:13px}
.bo-dims{color:var(--dim); font-size:11.5px; font-variant-numeric:tabular-nums;
  margin-top:5px; opacity:.75}
/* The number is the card's identity, so it sits where an eye lands first and never wraps
   into the title. Routes carry a letter (1a) and are amber, matching /atlas -- the same two
   kinds of thing coloured the same way on both pages. */
.bo-code{font:600 12px/1 ui-sans-serif,system-ui,sans-serif; letter-spacing:.08em;
  color:var(--teal); margin-bottom:2px}
.bo-card.route{border-style:dashed}
.bo-card.route:hover{border-color:var(--amber)}
.bo-card.route .bo-code, .bo-card.route b{color:var(--amber)}
/* Starting the game is a different KIND of thing from dropping into region seven, so it does
   not sit in the grid pretending to be one of them. */
.bo-start{display:flex; flex-direction:column; gap:3px; padding:15px 18px; margin-top:18px;
  background:color-mix(in srgb,var(--teal) 14%,var(--panel));
  border:1px solid var(--teal); border-radius:10px; text-decoration:none}
.bo-start:hover{background:color-mix(in srgb,var(--teal) 22%,var(--panel))}
.bo-start b{color:var(--ink); font-size:16px}
.bo-start .bo-code{font-size:14px}

/* ── the greybox editor ───────────────────────────────────────────────────────────────
   Justin, 2026-08-15: "I think if I am just able to edit a block at a time, it might make
   it less daunting" and "Maybe a sidebar that pops out and you can select the tools and or
   sprites. That way its easily accessible."

   THE SIDEBAR IS FIXED AND SLIDES, rather than being a column that reflows the canvas.
   A column would resize the room every time the tools opened, and the room resizing under
   your cursor mid-edit is how you paint the wrong tile. The canvas never moves. */
/* ONE BORDERED PANEL, so the tool has an edge and everything inside it is obviously part
   of the same object. Before this the controls, the room and the legend were three unrelated
   things stacked down a prose page. */
.gb-panel{border:1px solid var(--rule); border-radius:12px; background:var(--panel);
  margin:16px 0 10px; overflow:hidden}
.gb-panel > * + *{border-top:1px solid var(--rule)}
.gb-wrap{display:block; position:relative; margin:16px 0 8px}
.gb-bar{display:flex; flex-wrap:wrap; gap:9px; align-items:center; padding:12px 14px;
  margin:0}
/* A hairline between "which tool is in my hand" and "open a dialog". Two kinds of button
   sitting in one undifferentiated row is why the bar read as clutter. */
.gb-sep{width:1px; align-self:stretch; background:var(--rule); margin:0 3px}

/* ── the inspector ──────────────────────────────────────────────────────────────
   Directly under the bar, which is where Justin drew the box: what you have selected
   belongs next to the tool that is about to act on it, not past the canvas. */
.gb-insp{display:flex; align-items:center; gap:10px; padding:9px 14px;
  background:color-mix(in srgb,var(--night) 45%,var(--panel))}
.gb-chip{flex:0 0 auto; font-size:10.5px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--dim); border:1px solid var(--rule); border-radius:20px; padding:3px 10px;
  white-space:nowrap}
.gb-chip.door{color:var(--pink); border-color:color-mix(in srgb,var(--pink) 55%,var(--rule))}
.gb-chip.object{color:var(--teal); border-color:color-mix(in srgb,var(--teal) 55%,var(--rule))}
.gb-inspname{flex:1 1 auto; min-width:0; background:transparent; color:var(--ink);
  border:1px solid transparent; border-radius:7px; padding:6px 9px; font:inherit;
  font-size:14px}
.gb-inspname:hover:not(:disabled){border-color:var(--rule)}
.gb-inspname:focus{border-color:var(--teal); outline:none;
  background:color-mix(in srgb,var(--night) 70%,transparent)}
.gb-inspname:disabled{color:var(--dim); font-style:italic}
.gb-inspmeta{flex:0 0 auto; color:var(--dim); font-size:12px;
  font-variant-numeric:tabular-nums; white-space:nowrap}

/* ── the room, with the legend beside it rather than flying over it ───────────── */
/* The stage gets a real height so the room can be measured against something. Tall enough
   to be worth editing in, short enough that the toolbar above and the actions below stay on
   screen with it -- the whole point of the panel is that you can see all three at once. */
.gb-body{display:flex; align-items:stretch; min-height:0; height:min(74vh, 860px)}
.gb-body > .gb-stage{flex:1 1 auto; min-width:0; border:0; border-radius:0;
  background:var(--night)}
.gb-foot{display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px; padding:10px 14px}
.gb-foot .gb-report{margin:0}
.gb-foot .msg{margin:0; flex:1 1 240px; min-width:0}
.gb-acts{display:flex; gap:8px; margin-left:auto}
.gb-legendbtn{display:none}
.gb-ver{margin-left:auto; font:600 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
  letter-spacing:.04em; color:var(--dim); padding:6px 9px; border-radius:6px;
  border:1px solid var(--rule); white-space:nowrap; cursor:help}
.gb-ver.bad{color:#f0b429; border-color:#f0b429}
/* READ-ONLY IS VISIBLE, not just enforced. The canvas keeps its normal colours -- dimming
   the thing you are trying to compare would defeat the comparison -- so the signal is on the
   frame around it instead. */
.gb-tray{display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:0 0 9px}
.gb-traylabel{font:600 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;
  letter-spacing:.06em; text-transform:uppercase; color:var(--dim)}
.gb-traylist{display:flex; gap:6px; flex-wrap:wrap}
/* A parked door looks like the pink the compiler paints doors, so the shelf reads as the
   same kind of object the canvas is showing. */
.gb-trayitem{background:color-mix(in srgb, var(--pink, #a4265e) 22%, var(--panel));
  border:1px solid color-mix(in srgb, var(--pink, #a4265e) 60%, transparent);
  color:var(--ink); border-radius:7px; padding:5px 10px; font:inherit; font-size:12.5px;
  cursor:pointer}
.gb-trayitem:hover{border-color:var(--teal)}
.gb-readonly .gb-stage{outline:2px dashed color-mix(in srgb, var(--amber) 70%, transparent);
  outline-offset:3px}
.gb-readonly #gb-view{border-color:var(--amber); color:var(--amber)}
.gb-bar select{background:var(--panel); color:var(--ink); border:1px solid var(--rule);
  border-radius:8px; padding:8px 11px; font:inherit; font-size:14px; max-width:100%}
.gb-btn{background:var(--panel); color:var(--ink); border:1px solid var(--rule);
  border-radius:8px; padding:8px 13px; font:inherit; font-size:13.5px; cursor:pointer}
.gb-btn:hover{border-color:var(--teal)}
.gb-btn.key{background:color-mix(in srgb,var(--teal) 16%,var(--panel)); border-color:var(--teal)}
.gb-stage{background:var(--panel); border:1px solid var(--rule); border-radius:10px;
  padding:4px; overflow:auto}
.gb-panel .gb-stage{padding:8px}
/* pixelated: the canvas is a grid of flat squares and the browser smoothing them turns a
   one-tile edit into a smudge you cannot line up against its neighbours. */
#gb-canvas{display:block; margin:0 auto; image-rendering:pixelated; touch-action:none;
  cursor:crosshair; max-width:100%}
.gb-report{color:var(--dim); font-size:13px; margin:9px 2px 0;
  font-variant-numeric:tabular-nums}
.gb-warn{color:var(--pink)}

/* DOCKED, not floating. Justin: "the legend and Brush, feels so off to the side." It was
   literally off the side -- a fixed panel that slid in over the room and covered the thing
   you were about to paint. As a column inside the panel it is part of the tool, and the
   canvas simply gets the width that is left. */
.gb-side{flex:0 0 218px; background:var(--panel); border-left:1px solid var(--rule);
  padding:12px 12px 16px; overflow-y:auto; min-height:0}
.gb-side h3{font-size:12px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--dim); margin:0 0 9px}
.gb-side h3 + h3{margin-top:20px}
.gb-palette{display:flex; flex-direction:column; gap:4px}
.gb-swatch{display:flex; align-items:center; gap:9px; width:100%; padding:6px 8px;
  background:none; border:1px solid transparent; border-radius:7px; cursor:pointer;
  color:var(--dim); font:inherit; font-size:13px; text-align:left}
.gb-swatch:hover{border-color:var(--rule); color:var(--ink)}
.gb-swatch.on{border-color:var(--teal); color:var(--ink);
  background:color-mix(in srgb,var(--teal) 12%,transparent)}
.gb-swatch i{width:19px; height:19px; border-radius:4px; flex:0 0 auto;
  border:1px solid rgba(255,255,255,.18)}
.gb-swatch .gb-tag{margin-left:auto; font-size:10.5px; letter-spacing:.06em;
  color:var(--dim)}
/* The legend's group headings. Small and quiet -- they separate the list, they are not
   competing with it for attention. */
.gb-group{font-size:10.5px; letter-spacing:.09em; text-transform:uppercase; color:var(--dim);
  margin:14px 0 5px; font-weight:600}
.gb-group:first-child{margin-top:0}
.gb-readonly{display:flex; align-items:center; gap:9px; margin:0; padding:6px 8px;
  color:var(--dim); font-size:12px; line-height:1.35}
.gb-readonly i{width:19px; height:19px; border-radius:4px; flex:0 0 auto;
  border:1px solid rgba(255,255,255,.18)}
/* The tool strip. Segmented rather than three loose buttons, so it reads as "one of these
   is in your hand" instead of "three things you can press". */
.gb-tools{display:flex; gap:0; flex:0 0 auto}
.gb-tools .gb-tool{border-radius:0; margin:0}
.gb-tools .gb-tool:first-child{border-radius:8px 0 0 8px}
.gb-tools .gb-tool:last-child{border-radius:0 8px 8px 0}
.gb-tools .gb-tool + .gb-tool{border-left:0}
.gb-tool.on{background:color-mix(in srgb,var(--teal) 18%,var(--panel));
  border-color:var(--teal); color:var(--ink)}
.gb-tool.on kbd{opacity:1; background:color-mix(in srgb, var(--teal) 26%, transparent);
  border-color:color-mix(in srgb, var(--teal) 52%, transparent)}

/* The size dialog. A real <dialog>, so the backdrop, Escape and the focus trap are the
   browser's job rather than three more things to get wrong. */
.gb-dlg{background:var(--panel); color:var(--ink); border:1px solid var(--rule);
  border-radius:12px; padding:20px 22px; max-width:min(460px,92vw)}
.gb-dlg::backdrop{background:rgba(0,0,0,.55)}
.gb-dlg h3{margin:0 0 8px; font-size:16px; color:var(--ink)}
.gb-dlgnote{color:var(--dim); font-size:13px; line-height:1.45; margin:0 0 14px}
.gb-dlgnote code{font-size:12px}
.gb-edges{display:flex; flex-direction:column; align-items:center; gap:8px}
.gb-edges-mid{display:flex; align-items:center; gap:12px}
.gb-edges label{display:flex; flex-direction:column; align-items:center; gap:3px;
  font-size:11.5px; letter-spacing:.06em; text-transform:uppercase; color:var(--dim)}
.gb-edges input{width:68px; text-align:center; background:var(--night); color:var(--ink);
  border:1px solid var(--rule); border-radius:7px; padding:6px 4px; font:inherit;
  font-size:14px; font-variant-numeric:tabular-nums}
.gb-edges-size{min-width:96px; text-align:center; color:var(--ink); font-size:15px;
  font-variant-numeric:tabular-nums}
.gb-dlgwarn{min-height:2.6em; margin:12px 0 0; font-size:12.5px; color:var(--dim);
  line-height:1.4}
.gb-dlgwarn.bad{color:var(--pink)}
.gb-dlgrow{display:flex; gap:8px; justify-content:flex-end; margin-top:14px}
.gb-dlgrow .gb-btn{margin:0}
.gb-field{display:flex; flex-direction:column; gap:5px; font-size:11.5px; letter-spacing:.06em;
  text-transform:uppercase; color:var(--dim); margin:4px 0 12px}
.gb-field input{background:var(--night); color:var(--ink); border:1px solid var(--rule);
  border-radius:7px; padding:9px 11px; font:inherit; font-size:14px; text-transform:none;
  letter-spacing:0}
.gb-field input:focus{border-color:var(--teal); outline:none}
/* THE KEY CHIP, LEGIBLE ON BOTH GROUNDS. Justin, 2026-08-15: "the box around the shortcut
   item is still a little too dark on the dark mode."
   It was a 70%-opacity glyph inside a --rule border, and --rule on the dark theme is barely
   above the panel it sits on -- so the box read as a smudge and the letter inside it as grey
   on grey. Given its own surface and full opacity it stops depending on the border to be
   seen at all, which is what was failing. */
.gb-btn kbd, .gb-tool kbd{
  font:600 11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; opacity:.92;
  background:color-mix(in srgb, var(--ink) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--ink) 26%, transparent);
  color:var(--ink); border-radius:4px; padding:1px 5px; margin-left:6px}
.gb-side .gb-btn{width:100%; margin-top:7px}
/* The tools button rides above the sidebar so it can also close it. */
#gb-toggle{position:fixed; right:14px; bottom:14px; z-index:41}
/* On a phone the room needs the whole width, so the legend goes back to being a drawer --
   the same control, a different shape, rather than a second implementation. */
@media (max-width:760px){
  .gb-body{display:block}
  .gb-legendbtn{display:inline-flex; margin:0 14px 12px}
  .gb-side{position:fixed; top:0; right:0; bottom:0; width:min(80vw,236px); z-index:40;
    max-height:none; transform:translateX(100%); transition:transform .16s ease;
    box-shadow:-14px 0 30px rgba(0,0,0,.35)}
  .gb-side.open{transform:none}
}

.note{border-left:3px solid var(--orange); background:var(--panel); padding:15px 18px;
  border-radius:0 7px 7px 0; margin:22px 0; font-size:14.5px; color:var(--dim)}
.note b{color:var(--ink); font-weight:600}
.msg{margin-top:14px; font-size:14px; min-height:1.2em}
.msg.err{color:var(--pink)} .msg.ok{color:var(--ok)}

/* ── stats ── */
.stats{display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); margin:22px 0}
.stat{background:var(--panel); border:1px solid var(--rule); border-radius:9px; padding:17px}
.stat b{display:block; font-size:27px; line-height:1.1; font-variant-numeric:tabular-nums}
.stat span{font-size:12px; color:var(--dim); letter-spacing:.1em; text-transform:uppercase}

/* ── /account's tab strip ─────────────────────────────────────────────────────────
   Justin, 2026-08-12: "I would like to see tabs for different things... any forms and items
   I should be able to review in my account page as an Admin tab maybe?"

   .pane HAS NO `display`, AND THAT IS THE WHOLE TRICK. The panes are shown and hidden with
   the `hidden` attribute, which works because it is the UA's `[hidden]{display:none}` doing
   it -- and a UA rule loses to ANY author rule on the same element. Give .pane a
   `display:block` (or grid, or flex) here and every pane renders at once, stacked, on a page
   whose script is working perfectly. Nothing errors. Padding only.

   FLEX-WRAP, NOT overflow-x. Four tabs on a 360px phone do not fit; wrapping to two rows
   shows all four, and a scrolling strip hides the last one off the right edge where nobody
   scrolls to look for it. */
.tabs{display:flex; flex-wrap:wrap; gap:4px; border-bottom:1px solid var(--rule);
  margin:26px 0 0}
.tab{appearance:none; -webkit-appearance:none; background:none; border:0;
  border-bottom:2px solid transparent; margin-bottom:-1px; color:var(--dim);
  font:600 15px/1 ui-sans-serif,system-ui,sans-serif; padding:12px 14px; cursor:pointer;
  border-radius:7px 7px 0 0}
.tab:hover{color:var(--ink); background:var(--panel-2)}
.tab[aria-selected="true"]{color:var(--ink); border-bottom-color:var(--teal)}
.pane{padding-top:8px}
@media (max-width:520px){ .tab{padding:11px 10px; font-size:14px} }

/* ── /account#docs, the design docs rendered for somebody who is not at the machine ──
   The picker wraps rather than scrolls: eleven buttons on a 390px phone is three rows, and
   a horizontal scroller hides half the list behind a gesture nobody knows is available. */
.docnav{display:flex; flex-wrap:wrap; gap:6px; margin:14px 0 10px}
.docpick[aria-current="true"]{border-color:var(--teal); color:var(--ink)}
/* THE MEASURE MOVES OFF THE CONTAINER AND ONTO THE PROSE.

   Justin, 2026-08-18, on a 1920px screen: "For the docs section, we we be using the full
   width of the container for the text? Feels like its half a column."

   Measured before touching anything, at 1920: the pane is 1225px, .docnav above it is
   1225px, and .docbody was 640px. 52%. He is describing the picker and the text having
   different right edges with nothing in between, and that reads as a broken rule rather
   than as a margin -- which is exactly what it was for the half of this content that is
   not prose.

   THE OLD COMMENT WAS RIGHT ABOUT PROSE AND WRONG ABOUT THE DOCUMENTS. A 1225px line is
   ~150 characters and nobody finds the start of the next one. But these are not articles:
   24 tables across 8 docs, 592 <code> spans, 44-character Drive ids, and file paths. A
   table has no measure. Its columns are independent, and a Drive id is one atomic string
   that cannot be rewrapped, so squeezing SOURCES.md's ten tables into 640px bought the
   prose nothing and cost every table half its room.

   So the cap moves from the box to the paragraphs inside it. Prose, lists and headings get
   var(--read); tables, code blocks and rules take the container. That split is already the
   site's own answer one section up -- `main > section.split > .col > .state{max-width:none}`
   exists because "structured rows read fine wider than a paragraph does and looked
   especially stranded at the reading measure." Same content shape, same call.

   var(--read), NOT A NEW NUMBER AND NOT `ch`. 720px is the token this site already uses for
   prose that is alone in a column with no rail beside it, which is what the docs pane is;
   inventing an 800 here would be a sixth number that has to agree with the other five and
   nothing to tell you when one moves. It is also WIDER than the 640 it replaces, so no line
   of prose got shorter -- 640 -> 720 is ~78 -> ~88 characters, still inside the band. And a
   `ch` value is the bug the --measure comment describes: it resolves at the element that
   uses it, so 72ch on a 24px h3 is 931px and on a p is 640, which is six right edges. */
.docbody{margin-top:6px;
  /* THE DOCS PANE GETS ITS OWN MEASURE, wider than the site's. Justin, 2026-08-18, looking at
     /account#docs on a wide screen: "we we be using the full width of the container for the
     text? Feels like its half a column." He was right about the symptom -- --read is 720px and
     the pane measures ~1225px, so the picker buttons spanned the container while the prose
     stopped just past halfway, which reads as a bug rather than as a choice.

     NOT width:100%, though, and the reason is why the cap exists at all: a 1900px line of body
     text is genuinely hard to read, because the eye loses its place on the return sweep. What
     is wrong here is the VALUE, not the idea.

     min() rather than a flat number so it fills a 1225px pane and still stops short of a
     1900px one. Tables, code and images are untouched by this -- the rule below caps only
     direct prose children, deliberately, which is the half that already worked. */
  --read:min(100%, 1120px)}
/* DIRECT CHILDREN ONLY. A <p> inside a <blockquote>, or a nested <ul>, is already inside a
   capped ancestor -- capping it again does nothing, and capping a <td>'s paragraph would
   put the measure back inside the tables this rule exists to free. */
.docbody > :is(p,ul,ol,blockquote,h3,h4,h5,h6){max-width:var(--read)}
.docbody h3{font-size:24px; margin:0 0 14px; line-height:1.25}
.docbody h4{font-size:19px; margin:30px 0 10px; line-height:1.3}
.docbody h5,.docbody h6{font-size:16px; margin:22px 0 8px; color:var(--ink)}
.docbody p{margin:0 0 14px}
/* THE SELF-DESCRIPTION EVERY FRAGMENT NOW CARRIES (DOC_STAMP, in write_docs). It is here
   for the /account pane; the same sentence is the first line of the bare fragment served at
   /docs/<slug>, where there is no stylesheet at all and it simply reads as a sentence. That
   is the point of it being TEXT rather than a styled banner -- the reader it was written for
   is the one who arrived with no page around it. */
.docbody .docstamp{max-width:var(--read); margin:0 0 22px; padding:8px 12px;
  border:1px solid var(--rule); border-radius:4px; background:var(--panel-2);
  font-size:13px; color:var(--dim)}
.docbody ul,.docbody ol{margin:0 0 14px; padding-left:22px}
.docbody li{margin:0 0 6px}
.docbody hr{border:0; border-top:1px solid var(--rule); margin:26px 0}
.docbody blockquote{margin:0 0 16px; padding:2px 0 2px 14px;
  border-left:3px solid var(--rule); color:var(--dim)}
.docbody blockquote p{margin:0}
/* Long identifiers and Drive ids are the reason for the wrap: a 33-character file id in a
   table cell pushes the page sideways on a phone, and this content is not ours to rewrap. */
.docbody code{overflow-wrap:anywhere}
.docbody pre{overflow-x:auto; background:var(--panel-2); border:1px solid var(--rule);
  border-radius:8px; padding:12px; margin:0 0 16px}
.docbody pre code{overflow-wrap:normal; white-space:pre}
.docbody .scroll-x{margin:0 0 18px}
.doclink{text-decoration:underline}

/* ── /cast#quantised, the before/after sheets ──────────────────────────────────────
   TWO VIEWS, ON PURPOSE, because one cannot do both jobs. Fit-to-width answers "did the
   whole character survive"; it cannot answer "are the eyes still there", because the sheet
   is 1066px wide and a phone is ~390 CSS px. The 1:1 view is the judgement: the image at its
   own pixel size in a scroll box, so the 14x face crop on that sheet is genuinely at 14x
   and every source pixel is a square you can see.

   image-rendering:pixelated on BOTH. Without it the browser smooths on the way down and the
   comparison becomes a comparison of two blurs -- which is the failure this art pipeline
   has already paid for once. */
.qz{margin:0 0 26px}
/* max-width, NOT width. `width:100%` scaled these sheets UP: the pane is 1225px on a 1440
   screen and the sheets are 1066, so every one was drawn at 1.149x with image-rendering
   :pixelated -- which does not blur, it ROUNDS, so roughly one source pixel in seven became
   two pixels wide and the rest stayed one. On a before/after comparison of pixel art that is
   the worst possible artifact, because it is indistinguishable from the quantiser having
   moved a pixel, and it appears on the BEFORE half too. Same class of defect as a wrong
   width attribute, which the QUANTISE manifest already refuses to build with, and it was
   invisible while the only sheet was 978px on a narrower pane.
   Capping at natural size means a wide screen shows 1:1 and the scroll box below becomes
   redundant there -- which is the honest outcome, not a loss. Narrow screens still fit.

   AND NO `width` DECLARATION AT ALL, which the first attempt at this got wrong. Writing
   `width:auto` fixed the upscale and silently broke the other half: a CSS `width` overrides
   the HTML width/height attributes, so the reserved box disappeared and every sheet measured
   2px until it loaded -- i.e. the page-jump those attributes exist to prevent, reintroduced
   by the fix for the stretch. With no width rule the attributes supply the intrinsic size
   before the bytes arrive, and max-width caps it after. */
.qzfit{display:block; max-width:100%; height:auto; image-rendering:pixelated;
  border:1px solid var(--rule); border-radius:10px; background:var(--panel-2)}
.qzbox{overflow:auto; max-height:78vh; border:1px solid var(--rule); border-radius:10px;
  background:var(--panel-2); -webkit-overflow-scrolling:touch}
.qzbox img{display:block; image-rendering:pixelated; max-width:none}
.qzrow{display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:10px 0}

/* The identity block sits ABOVE the strip, full width, because it is true on every tab --
   and because as a .panel it would have been a 760px card alone in a 1225px wrap, which is
   exactly the hole the 680px form rule above used to close. */
.idhead{display:flex; justify-content:space-between; align-items:center; gap:16px;
  flex-wrap:wrap; padding-bottom:20px; border-bottom:1px solid var(--rule)}
.idhead h2{font-size:21px; margin:0; letter-spacing:0; line-height:1.3}

/* Two tokens, one shape. .rolepill answers "what am I here?" -- the page never said before
   -- and labels the kind of each submission. .qcount is the Review badge, and it only ever
   counts work that can actually be CLEARED (pitches still 'new', edits still 'pending'):
   notes and feedback have no status column, so counting them would give the tab a badge
   that can never reach zero. */
/* The five places a submission can be, coloured so the table scans without being read.
   Semantic colour, kept separate from the site accent: green is a ruling, not a brand. */
.sent-wait{border-color:var(--dim); color:var(--dim)}
.sent-yes{border-color:#3f9d5a; color:#3f9d5a}
.sent-part{border-color:#f0b429; color:#f0b429}
.sent-no{border-color:#c2544d; color:#c2544d}
.sent-off{border-color:var(--rule); color:var(--dim)}
.rolepill{display:inline-block; font:600 11px/1 ui-monospace,Menlo,monospace;
  letter-spacing:.14em; text-transform:uppercase; padding:6px 10px; border-radius:999px;
  color:var(--teal); background:color-mix(in srgb,var(--teal) 16%,transparent);
  white-space:nowrap}
.qcount{display:inline-block; margin-left:7px; font:600 11px/1 ui-monospace,Menlo,monospace;
  padding:3px 6px; border-radius:999px; background:var(--orange); color:var(--night)}
.subrow{margin:8px 0 0; font-size:13.5px; color:var(--dim); display:grid; gap:4px;
  max-width:70ch}

/* ── legal ── */
/* section.legal, NOT .legal -- page_account puts class="legal" on two <table>s for the
   th/td skin, and a bare .legal rule gives those tables a document width. A policy IS a
   document, so it centres, which is the same treatment .doc already gets: its h2s are
   clause headings rather than section titles, and there is no media to break out with. */
section.legal{max-width:74ch; margin-inline:auto}
.legal h2{margin-top:36px; font-size:22px}
.legal ul{padding-left:20px; color:var(--dim)} .legal li{margin:7px 0}
/* `.legal table` AND `table.legal`, because the class is used BOTH ways and only one of
   them was covered. /privacy and /terms put .legal on the SECTION and the table inside it;
   /account puts .legal on the TABLE ITSELF, to borrow the cell styling without the 74ch
   section measure. The descendant selector matched the first and not the second, so every
   table on /account fell back to `.tbl-wide{min-width:560px}` -- 560px of table sitting in a
   1225px pane, which is what Justin saw: "we are not utilizing the full width of the
   container for the accounts."
   The cell rules below are `.legal th` / `.legal td`, which match descendants either way,
   so the borders and padding looked right and only the WIDTH was wrong. That is why it read
   as a layout choice rather than a broken rule. */
:is(.legal table, table.legal){border-collapse:collapse; width:100%; margin:18px 0;
  font-size:14.5px}
.legal th,.legal td{text-align:left; padding:9px 12px 9px 0; border-bottom:1px solid var(--rule);
  vertical-align:top}
.legal th{color:var(--dim); font:600 11px/1 ui-monospace,Menlo,monospace; letter-spacing:.14em;
  text-transform:uppercase}
.updated{font-size:13px; color:var(--dim)}

/* ── utilities ──
   These exist because the site ships under `style-src 'self'` with no unsafe-inline, so a
   `style="margin-top:18px"` attribute is BLOCKED, not merely inelegant. Measured on the
   live site: the CSP console named every one of them. A class is the only way to say this. */
.m0{margin:0}
.mt0{margin-top:0}
.mt-m{margin-top:18px}
.mt-l{margin-top:34px}
.mt-xl{margin-top:38px}
.mb-m{margin-bottom:20px}
.narrow{max-width:520px}
.sub-line{margin:2px 0 0}
.wide-btn{width:100%; justify-content:center}
.scroll-x{overflow-x:auto}
.tbl-wide{min-width:560px}
.id-row{display:flex; gap:16px; align-items:center}
.avatar{border-radius:50%; border:1px solid var(--rule)}

/* ── footer ── */
.foot{border-top:1px solid var(--rule); margin-top:20px; background:var(--foot-bg)}
.foot-in{padding:26px max(20px,calc((100vw - var(--wrap))/2)); display:flex; gap:22px;
  justify-content:space-between; flex-wrap:wrap; align-items:center}
.foot nav{display:flex; gap:18px; font-size:13.5px}
.foot a{color:var(--dim); text-decoration:none}
.foot a:hover{color:var(--ink)}
.foot p{margin:0; max-width:62ch}
.copyline{flex-basis:100%; order:3; font-size:12.5px; opacity:.72}

/* ── the 404 document ── */
.doc{
  background:#f2ece0; color:#15130f; max-width:720px; margin:36px auto; padding:52px 48px;
  border-radius:3px; box-shadow:var(--shadow); position:relative;
  font:15.5px/1.8 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
.doc h1{font-size:19px; letter-spacing:.22em; margin:0 0 4px; color:#15130f}
.doc .sub{font-size:12.5px; letter-spacing:.2em; color:#6b6355; margin:0 0 30px}
.doc .stamp{
  position:absolute; top:26px; right:-8px; background:#b4241f; color:#fff; font-weight:700;
  font-size:12px; letter-spacing:.28em; padding:7px 16px; transform:rotate(3deg);
}
.doc .blk{background:#15130f; color:transparent; user-select:none; border-radius:1px}
/* .row is `display:flex` and this override only ever set margin, so the four memo
   paragraphs on /404 have been rendering as gapped flex chunks with the trailing "." as
   its own flex item. Unrelated to the width work; found while scoping it. */
.doc .row{display:block; margin:0 0 13px}
.doc .home{margin-top:34px; padding-top:20px; border-top:1px solid #d6cdba; font-size:14px}
.doc .home a{color:#0f5f57}
@media (max-width:600px){ .doc{padding:34px 24px} }

/* Generated from art/palette/*.gpl -- one class per colour, because the site ships style-src 'self' and an inline style attribute fails the build. */
.sw-010101{background:#010101}
.sw-051a2f{background:#051a2f}
.sw-0d1b25{background:#0d1b25}
.sw-0e192c{background:#0e192c}
.sw-0f1c21{background:#0f1c21}
.sw-0f231c{background:#0f231c}
.sw-140607{background:#140607}
.sw-15251e{background:#15251e}
.sw-152b4b{background:#152b4b}
.sw-19ffff{background:#19ffff}
.sw-1a031f{background:#1a031f}
.sw-1eaecc{background:#1eaecc}
.sw-230d0d{background:#230d0d}
.sw-245475{background:#245475}
.sw-2a2b35{background:#2a2b35}
.sw-2c4559{background:#2c4559}
.sw-2fdcd5{background:#2fdcd5}
.sw-321616{background:#321616}
.sw-326187{background:#326187}
.sw-33275d{background:#33275d}
.sw-34a681{background:#34a681}
.sw-3a515f{background:#3a515f}
.sw-3b3f48{background:#3b3f48}
.sw-3b526a{background:#3b526a}
.sw-3d2e00{background:#3d2e00}
.sw-3e505b{background:#3e505b}
.sw-3e7946{background:#3e7946}
.sw-3f4346{background:#3f4346}
.sw-40211a{background:#40211a}
.sw-4391c6{background:#4391c6}
.sw-466e8d{background:#466e8d}
.sw-494549{background:#494549}
.sw-4a1331{background:#4a1331}
.sw-4f6f92{background:#4f6f92}
.sw-503e94{background:#503e94}
.sw-53564f{background:#53564f}
.sw-542c21{background:#542c21}
.sw-555351{background:#555351}
.sw-555651{background:#555651}
.sw-561b02{background:#561b02}
.sw-572f3a{background:#572f3a}
.sw-5a1f05{background:#5a1f05}
.sw-5b1903{background:#5b1903}
.sw-5ba5be{background:#5ba5be}
.sw-5f4926{background:#5f4926}
.sw-5f80a3{background:#5f80a3}
.sw-617274{background:#617274}
.sw-642e27{background:#642e27}
.sw-646a6f{background:#646a6f}
.sw-678ca2{background:#678ca2}
.sw-68804f{background:#68804f}
.sw-69332c{background:#69332c}
.sw-6b96b6{background:#6b96b6}
.sw-6d8a9c{background:#6d8a9c}
.sw-734c3d{background:#734c3d}
.sw-73928c{background:#73928c}
.sw-7393b4{background:#7393b4}
.sw-7663be{background:#7663be}
.sw-772a0e{background:#772a0e}
.sw-775b00{background:#775b00}
.sw-795c58{background:#795c58}
.sw-804b33{background:#804b33}
.sw-81380c{background:#81380c}
.sw-843e1e{background:#843e1e}
.sw-87db80{background:#87db80}
.sw-8a9197{background:#8a9197}
.sw-8b9ea5{background:#8b9ea5}
.sw-8f6a0a{background:#8f6a0a}
.sw-914726{background:#914726}
.sw-917c65{background:#917c65}
.sw-933627{background:#933627}
.sw-949e99{background:#949e99}
.sw-959d9c{background:#959d9c}
.sw-97753c{background:#97753c}
.sw-995643{background:#995643}
.sw-99c4e0{background:#99c4e0}
.sw-99c6d5{background:#99c6d5}
.sw-9a9581{background:#9a9581}
.sw-9aa1a7{background:#9aa1a7}
.sw-9b9481{background:#9b9481}
.sw-9d4325{background:#9d4325}
.sw-9fbbd0{background:#9fbbd0}
.sw-a4917d{background:#a4917d}
.sw-a79ad5{background:#a79ad5}
.sw-aa753b{background:#aa753b}
.sw-aa8b79{background:#aa8b79}
.sw-adc1cd{background:#adc1cd}
.sw-ae4923{background:#ae4923}
.sw-af9378{background:#af9378}
.sw-b0c0c9{background:#b0c0c9}
.sw-b28700{background:#b28700}
.sw-b36356{background:#b36356}
.sw-b45c3e{background:#b45c3e}
.sw-b4b8bc{background:#b4b8bc}
.sw-b86047{background:#b86047}
.sw-b8ac92{background:#b8ac92}
.sw-b97a48{background:#b97a48}
.sw-ba6841{background:#ba6841}
.sw-ba7459{background:#ba7459}
.sw-bac2c5{background:#bac2c5}
.sw-baddcd{background:#baddcd}
.sw-c09c61{background:#c09c61}
.sw-c15b31{background:#c15b31}
.sw-c4c5b6{background:#c4c5b6}
.sw-ccf4f8{background:#ccf4f8}
.sw-d0935f{background:#d0935f}
.sw-d39683{background:#d39683}
.sw-d42c32{background:#d42c32}
.sw-d6bf99{background:#d6bf99}
.sw-db9f38{background:#db9f38}
.sw-dcb57d{background:#dcb57d}
.sw-ddba8b{background:#ddba8b}
.sw-e07e34{background:#e07e34}
.sw-e19f6d{background:#e19f6d}
.sw-e1a96c{background:#e1a96c}
.sw-e1d1a4{background:#e1d1a4}
.sw-e3721f{background:#e3721f}
.sw-e4732f{background:#e4732f}
.sw-e4955a{background:#e4955a}
.sw-e4bd86{background:#e4bd86}
.sw-e5d8c5{background:#e5d8c5}
.sw-e6d8a8{background:#e6d8a8}
.sw-e86f3c{background:#e86f3c}
.sw-eba458{background:#eba458}
.sw-ecb300{background:#ecb300}
.sw-eddfbe{background:#eddfbe}
.sw-efb51b{background:#efb51b}
.sw-f16326{background:#f16326}
.sw-f18c51{background:#f18c51}
.sw-f3b197{background:#f3b197}
.sw-f3b677{background:#f3b677}
.sw-f5d17b{background:#f5d17b}
.sw-f5e5b9{background:#f5e5b9}
.sw-f6d783{background:#f6d783}
.sw-f7fefe{background:#f7fefe}
.sw-f98d5d{background:#f98d5d}
.sw-fad0b6{background:#fad0b6}
.sw-fafaf9{background:#fafaf9}
.sw-fc6a02{background:#fc6a02}
.sw-fce9aa{background:#fce9aa}
.sw-fcfcf4{background:#fcfcf4}
.sw-fcfdf9{background:#fcfdf9}
.sw-fcfefc{background:#fcfefc}
.sw-fdf5d6{background:#fdf5d6}
.sw-fdfaf7{background:#fdfaf7}
.sw-ff19d0{background:#ff19d0}
.sw-ff2fa0{background:#ff2fa0}
.sw-ffcb27{background:#ffcb27}
.sw-ffd962{background:#ffd962}
.sw-ffe79c{background:#ffe79c}
.sw-fff5d6{background:#fff5d6}
.pc1{--cols:1}
.pc2{--cols:2}
.pc3{--cols:3}
.pc4{--cols:4}
.pc5{--cols:5}
.pc6{--cols:6}
.pc7{--cols:7}
.pc8{--cols:8}
