/* =========================================================================
   cinema.css — the COMPOSITION layer.

   site.css settled the MATERIAL of this site: how a panel catches light, how
   many steps there are between the brightest ink and the quietest, how the
   scrim shapes the owner's artwork instead of washing it out. It did not
   settle the SHAPE of the page, and that is what the owner was looking at
   when he said the site was "so bad… where are the animations, where are the
   backgrounds": a headline, a lede, two buttons, and a square CROP of his own
   artwork fenced in a bordered box on the right, over a near-black slab. The
   material was right. The composition was a landing-page template.

   THE FAILURE THIS SHEET REPLACES, precisely:
     1. THE PICTURE WAS A THUMBNAIL. The owner's banner rendered at 430px max
        inside a rounded rectangle, with the right half of the hero empty
        below it. His artwork is a cinematic montage; it was being used as a
        product photo.
     2. THE HERO WAS A BAND, NOT A SCREEN. 58px of padding, a two-column
        grid, and the next section began. A film title card has ONE subject
        and holds the viewport; this held about a third of it.
     3. EVERY INNER PAGE STARTED MID-SENTENCE. A small label and a heading,
        then straight into cards. Twelve pages with no sense of arrival.
     4. THE LIVE STATUS BLOCK — the only genuinely live thing on the landing
        page — looked like a form. Three grey figures in a row.
     5. NO RHYTHM. One 64px gap between every section regardless of what the
        section was, so the page had no idea which of its parts mattered.

   RULES THIS SHEET KEEPS
     - NO COLOUR OF ITS OWN, exactly like site.css. Every value below is a
       role token from theme.css or a color-mix() of one, so a pale jungle
       island and a near-black ember island get the same composition and never
       the same face. A test refuses this file if a raw #hex or rgba() appears.
     - NO NEW MARKUP CONTRACTS. Element ids and data-* hooks are load-bearing
       (site.js/status.js/map.js/vault.js/shop.mjs select on them). The only
       markup this wave adds is two SEMANTIC CLASSES — .hero-copy and
       .page-head — both purely additive, so no existing selector can miss.
     - IT LEAVES ROOM FOR ITS NEIGHBOURS. window.wcBackdrop owns the living
       artwork behind .night; window.wcMotion drives reveals off the class
       names below; window.wcAudio injects a toggle into the nav and the live
       bar. Nothing here paints a competing background image, and nothing here
       animates beyond a hover/press affordance.
   ========================================================================= */


/* =========================================================================
   1. THE COMPOSED LAYER — rhythm, rules and depth, mixed from owner tokens.

   site.css composes the MATERIAL constants (--veil-*, --ink-*, --lift-*,
   --grain). These are the SPATIAL ones: how far apart two sections stand,
   how hard a hairline has to be to survive a true-black ground, and how far
   the one live panel on the page stands off it.
   ========================================================================= */
:root {
  /* ---- the spacing ladder ----
     One 64px gap between everything is not a rhythm, it is an absence of
     one. Three steps, all viewport-aware, so a 390px phone is dense and a
     1920px desktop breathes without either being re-typed:
        --band        between two major sections
        --band-tight  between a section and its own sub-blocks
        --head-air    above a page's opening, under the sticky nav */
  --band:       clamp(44px, 5.0vw, 84px);
  --band-tight: clamp(26px, 3.0vw, 44px);
  --head-air:   clamp(16px, 1.6vw, 26px);

  /* ---- hairlines that survive a true black ----
     ★ A 1px RULE IS NOT FAINT BECAUSE IT IS THIN, IT IS FAINT BECAUSE IT IS
       THE WRONG COLOUR. site.css's --edge-hair is the owner's edge at 42%
       toward transparent, which reads on --surface and disappears on
       --bg-deep — so every divider that sat on the page ground rather than
       on a panel was invisible, and the page lost its structure. These are
       the same edge carried further, and they are used on the GROUND;
       --edge-hair stays correct on a panel. Nothing here is thicker than
       1px: on a 2x display that is two device pixels, which is plenty. */
  --rule-hair:      color-mix(in srgb, var(--edge) 66%, transparent);
  --rule-hair-soft: color-mix(in srgb, var(--edge) 34%, transparent);

  /* ---- the bezel: a machined ring, not a border ----
     A border is one flat stroke. A bezel is a hairline PLUS a lit top and a
     sunk bottom, which is what makes an instrument look milled rather than
     drawn. Used on the live status block and nowhere else, because it is the
     one panel that is allowed to look like hardware. */
  --bezel: 0 0 0 1px var(--edge-hair) inset,
           0 1px 0 var(--edge-top) inset,
           0 -1px 0 var(--edge-sink) inset;

  /* ---- one step above --lift-2 ----
     site.css's ladder tops out at --lift-2 for the status card. The
     instrument now stands higher again: a longer, softer throw so it reads
     as an object ON the page rather than a region OF it. */
  --lift-3: 0 1px 0 var(--edge-top) inset,
            0 -1px 0 var(--edge-sink) inset,
            0 2px 6px var(--edge-sink),
            0 54px 96px -38px var(--veil-deep),
            0 0 0 1px var(--edge-hair);

  /* ---- measures ----
     A lede at 52ch is a paragraph. A lede at 44ch under a 90px headline is a
     caption, which is what a title card wants. Body copy stays wider. */
  --measure-lede: 44ch;
  --measure-body: 58ch;
}


/* =========================================================================
   2. THE HERO — a title card, not a two-column landing template.

   ★ THE DECISION, AND WHY IT IS THIS ONE.

   Three compositions were on the table: full-bleed art behind an asymmetric
   type column; a framed plate that breaks its frame; a foreground/background
   split with the headline between depth planes. The third one wins here, and
   the reason is the NEIGHBOUR: window.wcBackdrop is turning the owner's
   montage into a living, parallaxing, ember-lit scene behind .night. A
   second full-bleed image in the hero would be the same picture twice, at
   two different scales, fighting. So the page gets THREE PLANES instead:

        plane 0   the living backdrop (someone else's file, full bleed)
        plane 1   the owner's banner as a PLATE — large, bled off the right
                  edge of the viewport, feathered on every edge that stays
                  on screen
        plane 2   the type column, sitting IN FRONT of the plate and
                  crossing its left edge

   That crossing is the whole point. A picture inside a border is an
   illustration OF the page; a picture the headline stands in front of is the
   page's subject. It also deletes the empty half — the right side is now
   floor-to-ceiling artwork rather than 430px of art above 300px of nothing.

   The plate deliberately runs PAST the viewport edge. An image that ends
   inside the frame is a thumbnail no matter how large it is; an image that
   leaves the frame is a scene the frame is a window onto.
   ========================================================================= */
.hero {
  /* Held to a screen, not a band. min-height rather than height: an owner
     with a six-line headline grows the hero instead of overflowing it.

     ★★ CUT, AND THE REASON IS A MEASUREMENT AGAINST THE BENCHMARK THE OWNER
        NAMED. This was `clamp(420px, 64vh, 720px)`, which put the live status
        instrument — the only thing on this page that is actually reading the
        server — 887px down a 1000px window at 1280. Arkadia reaches real data
        at about 270px. A title card that holds two thirds of the first screen
        is a title card on a page nobody scrolls.
        52vh instead of 64vh, and a 520px ceiling instead of 720px: still a
        held screen rather than a band (the failure this section exists to fix
        stays fixed), and the instrument now lands inside the first screen. */
  min-height: clamp(340px, 52vh, 520px);
  display: flex;
  /* OPTICALLY CENTRED, WHICH IS NOT MATHEMATICALLY CENTRED. A block of type
     placed at the exact vertical middle of a tall band reads as sitting LOW,
     because the eye weights the mass of the headline rather than the box
     around it. The asymmetric padding lifts the column by roughly a third of
     a line; the first cut was mathematically centred and left a dead 250px
     strip between the nav and the kicker. */
  align-items: center;
  padding-block: clamp(18px, 2vw, 32px) clamp(30px, 3.6vw, 60px);
  /* ★ NO `overflow: clip` HERE, AND THAT IS A CORRECTION, NOT AN OVERSIGHT.
     The first cut carried one "to be safe about scrollbars". .hero is also
     .wrap, so its box ends at --maxw — clipping there cut the bled plate off
     at exactly the wrap edge and drew the hard vertical seam the bleed exists
     to avoid. The plate looked boxed again, 160px further right. Containing
     the bleed is body's job (site.css: `body { overflow-x: hidden }`) and it
     already does it. */
}

/* ONE ROW, TWO OVERLAPPING PLANES.
   The copy occupies the left of the grid; the plate spans the WHOLE grid and
   is pushed to its right edge. Because they share a single grid cell range
   they are free to overlap, and z-index decides who is in front. This is why
   the art keeps its place in the DOM: a screen reader still meets the
   headline, the lede and the buttons before the picture. */
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto;
  align-items: center;
  gap: 0;
  width: 100%;
}
/* ★ THE COPY PLANE IS MATCHED STRUCTURALLY, NOT BY ITS CLASS, AND THAT IS
     DELIBERATE. This wave adds `.hero-copy` to the two heroes it owns, but a
     hero is shared markup: a page authored next week (or by the agent writing
     the voice page in this same tree right now) will copy the pattern and may
     not carry the class. If the copy plane were matched by class alone, that
     page's text would sit BEHIND the plate — a silent, total regression that
     no test for "the fragment still parses" could ever catch. So the rule
     matches "the child of .hero-grid that is not the art", and the class is
     an additional, explicit hook for window.wcMotion and for the tests. */
.hero-copy,
.hero-grid > :not(.hero-art) {
  grid-area: 1 / 1;
  position: relative;
  z-index: 2;
  /* ★ THE CAP IS IN PIXELS, NOT ch, AND THAT COST ONE ITERATION.
     `min(60%, 30ch)` looked right and was wrong: `ch` on this element
     resolves against the element's OWN font-size, which is body copy at
     16px — not against the 90px display face inside it. The column came out
     at ~250px and broke a seven-word headline over FIVE lines, one or two
     words each. A measure for display type has to be set in the same units
     the layout is in.

     ★★ WIDER, BECAUSE THE PLATE IS NOW OPTIONAL AND ON THIS OWNER IT IS OFF.
        58% was the width that left room for the plate beside it. theme.py now
        emits `--hero-art-display: none` for an owner who sets
        `theme.hero_banner: false`, and Jurassic Land sets exactly that: his
        crest became the SITE LOGO in the nav and his montage is the backdrop,
        so there is deliberately no second picture in the hero. At 58% that
        left the right 42% of the hero empty for no reason.
        66% is the width that works BOTH ways rather than a width that needs
        to know which owner it is on: with a plate, the headline crosses
        further into it — which is what the crossing is FOR, and the scrim on
        .hero-art::after already defends it on every palette; without one, the
        type column reads as a deliberate left-weighted title over the
        artwork instead of a column that ran out. */
  max-width: min(66%, 760px);
}

/* ---- the type ladder ----
   ★ OPTICAL SCALE, NOT A LINEAR ONE. site.css already tightens tracking as
     size grows, which is right. What it did not do was let the headline get
     big enough to BE a title. It topped out at --fs-6 (72px on this owner) in
     a 46px-gutter two-column grid, so the largest type on the site was
     roughly the size of a large paragraph heading. The ceiling here is
     derived from the owner's own --fs-6 rather than typed, so an owner who
     sets a smaller type scale gets a proportionally smaller title card and
     nobody inherits this owner's 90 pixels. */
.hero h1 {
  font-size: clamp(38px, 6.9vw, calc(var(--fs-6) * 1.26));
  line-height: .94;
  letter-spacing: -.035em;
  text-wrap: balance;
}

/* THE KICKER IS A RULE THAT MEANS SOMETHING.
   It was an accent dash and a word. It is now a measured rule that runs from
   the left margin, stops at the label, and resumes as a hairline that travels
   to the end of the type column — so the kicker states the width of the
   column the headline is about to fill. The trailing rule is drawn on a
   pseudo-element that carries no text, so nothing is announced. */
.hero .kicker {
  gap: 14px;
  margin-bottom: clamp(14px, 1.8vw, 24px);
}
.hero .kicker::before { width: clamp(28px, 4vw, 54px); }
.hero .kicker::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  min-width: 0;
  background: linear-gradient(90deg, var(--rule-hair), transparent);
}

.hero .lede {
  margin-top: clamp(16px, 1.6vw, 24px);
  max-width: var(--measure-lede);
  font-size: clamp(16px, 1.12vw, 18.5px);
  line-height: 1.6;
  color: var(--ink-2);
}

/* The CTA row sits on a hairline that runs the width of the type column: the
   base the title card stands on. Without it the buttons floated in the middle
   of a large empty rectangle, which is most of what "flat" meant. */
.hero-cta {
  margin-top: clamp(22px, 2.6vw, 38px);
  padding-top: clamp(20px, 2.2vw, 32px);
  border-top: 1px solid var(--rule-hair-soft);
  gap: 12px;
}

/* ---- plane 1: THE PLATE ----
   Spans the full grid, pinned right, and pushed PAST the wrap and past the
   viewport edge. `calc(50% - 50vw)` is exactly the wrap's own side margin, so
   the plate lands flush on the viewport; the extra 4vw is the part that
   leaves the screen. */
.hero-art {
  /* ONE FEATHER, DECLARED ONCE. The picture and the scrim that defends the
     text crossing it must dissolve on exactly the same curve — see the
     ::after rule for the seam that appears the moment they do not.

     ★ TWO LINEAR MASKS INTERSECTED, NOT ONE RADIAL, AND A PALE OWNER IS WHY.
       The first cut used a single radial. A radial cannot be opaque through
       the middle of a box AND fully transparent at that box's top and bottom
       unless its vertical radius is small enough to squash the subject — so
       it wasn't, and the plate still had ~60% opacity where its own box
       ended. On the ember island that seam was invisible: a 60%-opaque dark
       picture against a near-black page is a dark page. Rendered against a
       PALE jungle palette the identical CSS drew a hard black horizontal line
       across the hero. One sheet, one owner fine and one owner broken, which
       is the whole reason a second palette gets rendered.

       Two linear masks intersect instead, and each edge is now stated
       explicitly rather than falling out of an ellipse:
         horizontal — transparent at the left, solid from 26% rightward, so
                      the right edge stays at full strength and leaves the
                      viewport;
         vertical   — transparent at both the top and the bottom, solid
                      through 12%..86%, which is inside the skull on this
                      owner's banner and is empty field on a wider one.
       `mask-composite: intersect` is the standard spelling and
       `-webkit-mask-composite: source-in` the WebKit one; both are written,
       because with neither of them multiple mask layers ADD and the plate
       becomes the hard rectangle this whole block exists to avoid.

     ★ AND A NOTE PAID FOR IN FOUR RENDERS: A COMMENT THAT CLOSES EARLY IN
       THIS SHEET DELETES THE DECLARATION UNDER IT, SILENTLY.
       While retuning these ramps, an edit left this block with one comment
       opener and two closers. CSS comments do not nest: the first closer
       ended the comment, the remaining prose was parsed as declarations, and
       the error recovery that follows swallowed the whole --plate-mask line.
       The property was then simply UNDEFINED. Every var() reading it fell
       back to nothing, the plate and its scrim both lost their masks, and the
       hero rendered as exactly the hard rectangle this comment is about. The
       file still read correctly to a human, a grep still found the
       declaration, the assembler still shipped, and every text-level test
       still passed.
       It was found by giving the var() a FALLBACK and watching the fallback
       render, which is the cheap way to ask a browser whether a token is
       defined at all.
       TWO gates now guard the sheet, in
       tests/frontend/test_cinema_composition.py: delimiters must balance,
       AND every line of the comment-stripped sheet must still look like CSS.
       The second one is the real gate — the first passes happily when a
       closer is written INSIDE a comment, which is its own way to end one
       early, and is why no comment in this file spells those two characters
       out. */
  /* ★ A GENTLE EDGE FEATHER, NOT A WASH ACROSS A THIRD OF THE PICTURE.
     This was `transparent 0%, black 32%` horizontally and `black 16% / 84%`
     vertically — a third of the plate's left side faded out. Correct for an
     abstract plate that has to dissolve into the page; wrong for a logo,
     where it dimmed the leading letter and the left of the arch and read as
     a rendering fault rather than a blend. The feather now only softens the
     last few percent at each edge, which is enough to stop a hard rectangle
     without eating the subject. */
  --plate-mask:
    linear-gradient(90deg, transparent 0%, black 9%, black 96%, transparent 100%),
    linear-gradient(180deg, transparent 0%, black 7%, black 93%, transparent 100%);

  grid-area: 1 / 1;
  position: relative;
  z-index: 0;
  justify-self: end;
  align-self: center;
  /* ★ THE PLATE'S HEIGHT IS THE HERO'S HEIGHT, so this number is a
     LAYOUT number, not a taste one. The banner is square, so the width
     set here is also how tall the hero becomes: 52vw looked right and
     made an 832px-tall plate on a 1000px-tall window, pushing the live
     status card off the first screen and leaving 230px of slack above
     the headline. 46vw keeps the picture the subject and keeps the hero
     inside one screen on a 1280x800 laptop. */
  width: clamp(330px, 43vw, 700px);
  max-width: none;

  /* ★★ THE PLATE NO LONGER BLEEDS OFF THE EDGE, BECAUSE A BANNER IS USUALLY
       A LOGO AND A LOGO CANNOT BE CROPPED.
       This was `margin-right: calc(50% - 50vw - 4vw)` — a deliberate bleed
       past the right edge of the page, which is a good instinct for an
       abstract plate: it makes the frame feel bigger than the window. It is
       the wrong instinct the moment the banner contains TYPE. The first
       owner to put a real crest here had his wordmark sliced vertically down
       the middle of JURASSIC and LAND, off the side of the screen — the site
       cropping the server's own name.
       The bleed cannot be conditional on "does this image contain words",
       because that is not a question CSS can ask and not one an owner should
       have to answer. So the safe reading wins for everyone: the banner is
       shown WHOLE, inside the frame, and the sense of scale comes from the
       backdrop behind it instead — which is genuinely full-bleed and has no
       words in it. A small negative margin keeps it reaching toward the edge
       without ever reaching past it.
       ★ Zero, not "a small negative". I tried
       `min(0px, calc(50% - 50vw + 2vw))` first and it still clipped the crest
       at 1600px, because a percentage margin resolves against the CONTAINING
       BLOCK's width, not the viewport — so the expression was never the
       distance I was reasoning about. A number that has to be re-derived per
       viewport to stay safe is not a safe number. The plate ends where the
       reading column ends. */
  margin-right: 0;
  pointer-events: none;

  /* ★ THE PLATE STEPS BACK, BECAUSE THE BACKDROP ARRIVED AND IT IS THE SAME
       PICTURE. This wave designed the plate as the hero's subject while the
       page behind it was a near-black slab. window.wcBackdrop then landed and
       made the owner's montage a living full-bleed scene — and the montage's
       own centrepiece is the fossil skull, which is exactly what banner.svg
       is a crop of. Shot together for the first time, the hero had TWO
       skulls at two scales, about 400px apart. That is the failure this
       sheet's own hero comment says it exists to avoid; the difference is
       that the duplicate now arrives from the backdrop rather than from
       here, so it is this layer that has to give way.
       The plate keeps its bleed, its feather and its meaning in the DOM (it
       is the owner's art and it carries real alt text), but it drops to a
       depth cue: enough to light the right of the frame and give the type
       something to stand in front of, not enough to be a second portrait.
       Dropping it to 34% opacity was tried first and was not enough: the
       banner's subject is BONE-WHITE on near-black, so a third of it is still
       plainly a second skull. The picture twice is worse than the picture
       once, so the plate leaves the composition entirely.

       IT STAYS IN THE MARKUP. The img, its real alt text and its content key
       are untouched, so this is one line to reverse for an owner whose banner
       is genuinely a different picture from their backdrop — and nothing in
       assemble.py, content.json or the schema has to change either way.
       ★ THIS RULE IS CONDITIONAL ON THE BACKDROP BEING LIVE. If
       window.wcBackdrop is ever switched off for an owner, the hero loses its
       art with it and this line has to come out. Recorded for the integrator
       rather than left as a surprise.

       ★★ AND THAT IS EXACTLY WHAT HAPPENED, ONE HOUR LATER. The owner
       replaced his banner with a real brand CREST — a tyrannosaur roaring
       through a stone gate over the wordmark — which is not a crop of the
       backdrop and shares nothing with it. The duplicate this rule was
       written to kill no longer exists, so the plate comes back and the
       reasoning above is kept as the record of WHY it may need to leave
       again for the next owner whose banner is their backdrop.

       The condition is therefore stated as a condition, not as a verdict.
       ★ THE KEY IS `theme.hero_banner` (boolean, default TRUE) — an earlier
       draft of this comment named `theme.banner_is_backdrop`, which does not
       exist. That is worse than no comment: an owner reading it would set a
       key nothing reads and silently keep the hero plate they wanted gone.
       `"hero_banner": false` in owner.json makes theme.py emit
       `--hero-art-display: none`; anything else, including absence, gives
       `block`. The literal fallback keeps this rule correct even against a
       theme.css generated before the token existed.
       The default is SHOW, because an owner who has never heard of the key
       should get their banner. Jurassic Land sets it false: the owner's
       banner is his brand crest and he asked for it in the nav, with his
       backdrop left as the one picture on the page. */
  display: var(--hero-art-display, block);
}

/* ★ THE FEATHER IS OFF-CENTRE, AND THAT IS THE FIX.
     site.css feathered the plate with a radial centred at 50% — correct for a
     picture standing alone in a box, wrong for one that leaves the frame,
     because it dissolved the very edge that is supposed to run off screen
     while leaving a hard edge nowhere. The centre moves right: the left, top
     and bottom edges dissolve into the ground, the right edge stays at full
     strength and exits the viewport. Radius and border are dropped entirely —
     a bled plate has no corners to round. */
.hero-art img {
  width: 100%;
  height: auto;
  border-radius: 0;
  -webkit-mask-image: var(--plate-mask);
          mask-image: var(--plate-mask);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* The ember bloom behind the plate. site.css had it at inset:-14% with a 16px
   blur, which on a 430px thumbnail was a glow and on a 780px plate is a
   smudge. Wider, much softer, and pushed toward the plate's own centre of
   mass so the light appears to come off the artwork rather than off a box. */
.hero-art::before {
  inset: -22% -14% -18% -6%;
  filter: blur(46px);
  opacity: .8;
}

/* ★ THE CROSSING HAS TO BE DEFENDED, ON EVERY OWNER'S ARTWORK.
     The headline crosses the plate's left edge. On THIS owner that edge is
     dark cracked rock and the ink would survive unaided — but the same markup
     ships to an island whose banner is a pale jungle, and there the same
     crossing is white text on white leaves. So a SHAPED band sits over the
     plate's left third: the same argument .night makes at page scale, made
     again locally. It starts and ends fully transparent, so it can never draw
     an edge of its own on top of a feathered picture, and it is mixed from
     --bg-deep so it deepens the owner's OWN black rather than greying the
     artwork toward a neutral nobody chose. */
.hero-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent 0%,
    var(--veil-strong) 11%,
    var(--veil-mid) 26%,
    var(--veil-soft) 40%,
    transparent 56%);
  pointer-events: none;
  /* ★ THE SCRIM WEARS THE PICTURE'S OWN FEATHER.
       Without this it is an un-masked rectangle sitting on a feathered image,
       so where the picture had dissolved to nothing the veil was still at
       full strength and drew a hard horizontal line across the hero — a
       visible seam under the plate, which is precisely the boxed-in look the
       bleed exists to remove. Shot, seen, fixed. */
  -webkit-mask-image: var(--plate-mask);
          mask-image: var(--plate-mask);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* ★ THE HERO'S TEXT SHADOW COMES OFF THE OWNER'S OWN GROUND.
   theme.py emits `--shadow-text: 0 1px 3px rgba(0,0,0,.85), 0 0 8px
   rgba(0,0,0,.7)` — a BLACK glow, hardcoded, identical for every palette it
   generates. site.css spends it on .hero h1 and .hero .lede to lift them off
   the artwork, and on a near-black island that is exactly right. Rendered
   against a pale jungle palette the same declaration puts a black halo around
   already-dark ink and the masthead reads as smudged.

   Mixed from --bg-deep instead, so the glow is whatever the owner's own floor
   is: it deepens on an ember island and becomes a soft knockout on a pale
   one — which is the correct behaviour in both directions, because the job of
   the shadow is to separate the words from the picture, not to be dark.

   (The token itself is theme.py's to fix and theme.py is not this wave's to
   edit; it is reported to the integrator. This override is scoped to the two
   hero elements so nothing else changes.) */
.hero h1,
.hero .lede {
  text-shadow: 0 1px 2px var(--veil-deep), 0 0 12px var(--veil-strong);
}


/* =========================================================================
   3. THE PAGE HEAD — every inner page gets an opening.

   Eleven pages opened with a 11.5px label and a 34px heading, four pixels
   under the nav, and then went straight into a card grid. There was no
   arrival: every page looked like the middle of a page.

   The head is the SAME element it always was — the first .sec-head, the one
   holding h1.page-h1. It gains one additive class and nothing else, so every
   existing selector, every heading level and every i18n hook is untouched.
   ========================================================================= */
/* ★ MATCHED BOTH WAYS, FOR THE SAME REASON THE HERO IS.
     `.page-head` is the explicit class this wave adds. `:has(> .page-h1)` is
     the structural truth it encodes — "the section head that carries this
     page's h1" — and it means a page added later, or one being written in
     this tree right now, gets its opening without anybody remembering to add
     a class. site.css already depends on :has() (.status-figs), so this is a
     dependency the sheet family carries, not a new one. */
/* ★★ THE OPENING IS AN OPENING, NOT A SECOND HERO. A CORRECTION, MEASURED.
     This block was written to fix eleven pages that "started mid-sentence",
     and it over-corrected: measured on the boards page at 1600, the head was
     243px tall and its bottom edge sat 406px down the document, with the
     first real card at ~500px and the first grid at 718px. The benchmark the
     owner named reaches real data at ~270px. An inner page is not a landing
     page — the reader arrived by clicking the page's own name in the nav, so
     they already know where they are, and every pixel spent telling them
     again is a pixel taken off the thing they came for.

     Four numbers changed and nothing structural did. The chapter rule, the
     accent segment, the wash, the deck and the type ladder are all exactly as
     they were; they are simply not surrounded by 180px of air any more.
       --head-air        3.2vw/52 -> 1.6vw/26   (the gap under the nav)
       padding-bottom    2.6vw/34 -> 1.4vw/18   (head to chapter rule)
       margin-bottom     3.2vw/44 -> 1.8vw/24   (chapter rule to content)
       main's own top    var(--band) -> 0       (see the rule below)
     Measured after: 243px -> 148px on the same page at the same width, and
     the head's bottom edge lands at 253px. */
:is(.page-head, .sec-head:has(> .page-h1)) {
  position: relative;
  padding-top: var(--head-air);
  padding-bottom: clamp(13px, 1.4vw, 18px);
  margin-bottom: clamp(16px, 1.8vw, 24px);
  /* THE CHAPTER RULE. A full-measure hairline with a short accent segment at
     its left: the page states where it starts and whose site it is on. The
     accent is spent as a MARK, which is the one job site.css reserves it
     for. */
  border-bottom: 1px solid var(--rule-hair);
}
:is(.page-head, .sec-head:has(> .page-h1))::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: clamp(56px, 7vw, 96px);
  height: 2px;
  border-radius: var(--r-xs);
  background: var(--rule-accent);
}

/* THE SENSE OF PLACE, WITHOUT A NEW ASSET.
   A watermark of the owner's mark was the obvious move and is the wrong one:
   an owner whose mark is a raster with no alpha ships a grey square (the same
   trap site.css documents for .eyebrow::before). A soft wash in the owner's
   ACCENT costs no asset, cannot render as a broken image, and reads on every
   palette — the head sits in its own pool of the owner's light. It is drawn
   behind the text, bleeds past the gutter, and is inert. */
:is(.page-head, .sec-head:has(> .page-h1))::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* ★ THE WASH BLEEDS TO THE VIEWPORT AND DIES INSIDE ITS OWN BOX.
       First cut: `inset` one gutter, radial at `72% 130% at 0% 42%`. A radial
       whose radii are LARGER than the box it is painted in never reaches its
       transparent stop before the edge, so it terminated in four hard lines —
       a visible glowing RECTANGLE sitting behind every page head on eleven
       pages. Shot on the vault and casino pages, seen, fixed.
       Two changes, both required: the box now spans the whole viewport so it
       has no vertical edge on screen at all, and the radii (40%/72%) are
       small enough that the gradient is fully transparent well before the top
       and bottom edges are reached. */
  left: calc(50% - 50vw);
  right: calc(50% - 50vw);
  top: 0;
  bottom: 0;
  background: radial-gradient(40% 72% at 15% 50%,
    var(--accent-wash) 0%, transparent 66%);
  pointer-events: none;
}

/* .page-h1 is only ever the page's own h1, on every fragment that has one, so
   it is styled directly rather than through the head. */
.page-h1 {
  font-size: clamp(31px, 4.6vw, calc(var(--fs-6) * .72));
  line-height: 1.02;
  letter-spacing: -.028em;
  text-wrap: balance;
}
:is(.page-head, .sec-head:has(> .page-h1)) .eyebrow { margin-bottom: clamp(11px, 1.3vw, 16px); }
/* The lede under a page head takes the reading measure, not the 62ch
   site.css gives an ordinary section head — it is being read directly under
   a 46px heading and wants to look like a deck, not a paragraph. */
:is(.page-head, .sec-head:has(> .page-h1)) > p:not(.eyebrow) {
  max-width: var(--measure-body);
  margin-top: clamp(10px, 1.2vw, 15px);
  font-size: clamp(15px, 1.05vw, 17px);
  color: var(--ink-2);
}

/* The map page owns its whole viewport under the nav and has no page head by
   design (its h1 is the map's name, inside the layer panel). Nothing to do
   here — recorded so the next reader does not go looking for the twelfth. */


/* =========================================================================
   4. SURFACE CRAFT — depth, not borders.

   site.css already gave a card a lit top edge, a tooled grain and a real
   shadow. What it did not give it was a REACTION: every card on the site was
   the same panel whether or not you were pointing at it, so a grid of six
   tiles read as wallpaper. And .sec-head — the heading over each grid — had
   no rule of its own, so a long page was one column of text at four sizes.
   ========================================================================= */

/* A section heading now carries a hairline the width of the measure, so the
   eye can count the sections on a long page without reading them. Drawn on
   the head itself rather than as a border on the section, so a head that is
   the first thing in a hidden panel does not leave a stray line behind. */
.sec-head:not(.page-head):not(:has(> .page-h1)) {
  padding-bottom: clamp(12px, 1.4vw, 18px);
  border-bottom: 1px solid var(--rule-hair-soft);
  margin-bottom: clamp(18px, 2.2vw, 28px);
}
.sec-head:not(.page-head):not(:has(> .page-h1)) > p:not(.eyebrow) { max-width: var(--measure-body); }

/* THE CARD REACTS. A 1px lift and a warmed edge on hover, and the lit top
   edge brightens — the same three strokes site.css already draws, moved one
   step up the ladder. Transitions are short and property-scoped: this sheet
   does not own motion, and a card that eases for 400ms would fight the
   reveal window.wcMotion is about to run on the same element. */
.card {
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.card:hover {
  transform: translateY(-2px);
  border-color: var(--border-2);
  box-shadow: var(--lift-2);
}
.card::before { transition: opacity .18s ease; opacity: .75; }
.card:hover::before { opacity: 1; }

/* A card that is a LINK gets the affordance; a card that is a container must
   not pretend to be one. Keyboard users get the same treatment as the mouse,
   which is the half of this that hover-only styling always forgets. */
.card:focus-within {
  border-color: var(--border-2);
  box-shadow: var(--lift-2);
}

/* The grids were 15px apart at every width — tight enough at 390 to look
   cramped and at 1920 to look like a spreadsheet. */
.grid { gap: clamp(13px, 1.3vw, 22px); }


/* =========================================================================
   5. THE INSTRUMENT — the live status block.

   ★ THIS IS THE MOST IMPORTANT THING ON THE LANDING PAGE AND IT LOOKED LIKE
     A FORM. A flat panel, a label, three grey numbers at 26px, a sentence.
     Nothing about it said "this is reading the server right now" — which is
     the single fact the whole page exists to deliver.

     It is now built like a panel meter: a machined bezel, a state-coloured
     rail across the top, a sunken readout header, big tabular figures in
     milled wells separated by hairlines, and a sunken caption strip.

   ★ AND IT LIGHTS WITH THE SERVER, WITHOUT ONE LINE OF NEW JAVASCRIPT.
     status.js already writes exactly one thing about state into the DOM:
     `dot.className = 'dot ' + cls`, where cls is on/off/wait. :has() reads
     that existing class off the existing element and drives the whole panel
     from it. No new hook, no new id, no new event — so this cannot rot when
     status.js changes, because it consumes the contract status.js already
     publishes.

     The default is the accent rather than a signal colour: before the first
     poll answers, the honest statement is "this is the live panel", not
     "this is good" or "this is bad". */
.status {
  --instr: var(--accent);
  --instr-glow: var(--accent-glow);
  --instr-halo: var(--accent-wash-2);

  padding: 0;                       /* every child now owns its own inset */
  overflow: hidden;                 /* the sunken strips stop at the radius */
  box-shadow: var(--lift-3), var(--bezel);
  border-color: var(--edge);
}
.status:has(.dot.on)   { --instr: var(--ok);   --instr-glow: var(--ok-glow);   --instr-halo: var(--ok-halo); }
.status:has(.dot.off)  { --instr: var(--bad);  --instr-glow: var(--bad-glow);  --instr-halo: var(--bad-halo); }
.status:has(.dot.wait) { --instr: var(--warn); --instr-glow: var(--warn-glow); --instr-halo: var(--warn-halo); }

/* THE RAIL. site.css's .status::before is the 1px chisel above the panel;
   this is a second, state-coloured stroke inside it. Two strokes, two jobs:
   one says which way the light falls, one says what the server is doing. */
.status::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  background: linear-gradient(90deg,
    var(--instr), color-mix(in srgb, var(--instr) 30%, transparent) 62%, transparent);
  box-shadow: 0 0 12px var(--instr-glow);
}

/* THE READOUT HEADER, sunk below the face of the panel. */
.status-top {
  margin: 0;
  padding: clamp(15px, 1.5vw, 19px) clamp(17px, 1.7vw, 23px);
  background: var(--surface-sink);
  border-bottom: 1px solid var(--edge-hair);
  gap: 11px;
}
.status .dot {
  width: 10px; height: 10px;
  box-shadow: 0 0 0 4px var(--instr-halo), 0 0 14px var(--instr-glow);
}
.status-label {
  font-size: 14px;
  letter-spacing: .03em;
}
/* The source tag is machined: mono, boxed, and carrying the state colour, so
   "live" and "no feed" are legible as a state at a glance rather than as two
   pieces of small grey text. */
.status-src {
  color: var(--instr);
  border-color: color-mix(in srgb, var(--instr) 42%, transparent);
  background: color-mix(in srgb, var(--instr) 9%, transparent);
  border-radius: var(--r-xs);
  padding: 5px 9px;
}

/* THE GAUGE CLUSTER. Hairlines between the wells rather than gaps, so the
   three figures read as one instrument rather than three cards. The rule is
   drawn with a border on the well and killed on the first, which survives a
   figure being hidden — see the [hidden] guard below. */
.status-figs {
  gap: 0;
  padding: 0;
}
.status .fig {
  padding: clamp(17px, 2vw, 26px) clamp(16px, 1.7vw, 22px);
  border-left: 1px solid var(--edge-hair);
}
.status .fig:first-child { border-left: 0; }

/* ★ A FIGURE THAT IS HIDDEN MUST STAY HIDDEN.
     status.js sets `fig.hidden = true` when the payload carries no map name.
     `hidden` is a display:none the moment any rule sets display on the same
     element — this sheet does not, but the guard is written down anyway,
     because the map figure disappearing is exactly the kind of silent
     regression that a display rule added here in six months would cause.
     site.css records the same trap for .nav-links. */
.status .fig[hidden] { display: none; }

/* The numbers were 26px, which on a 1236px measure is a caption. They are the
   payload — the one live reading on the page — so they take display scale and
   the mono face's tabular figures, which stop the count-up window.wcMotion is
   about to run from jittering the layout on every frame. */
.status .fig b {
  font-size: clamp(28px, 3vw, 44px);
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--ink-1);
}
.status .fig span {
  margin-top: 9px;
  color: var(--ink-3);
  letter-spacing: .15em;
}

/* THE CAPTION STRIP, sunk like the header so the panel closes the way it
   opened. */
.status-note {
  margin: 0;
  padding: clamp(13px, 1.3vw, 16px) clamp(17px, 1.7vw, 23px);
  background: var(--surface-sink);
  border-top: 1px solid var(--edge-hair);
  color: var(--ink-3);
  font-size: 13px;
}


/* =========================================================================
   6. RHYTHM — sections, and the footer that closes them.
   ========================================================================= */
.section { padding-block: var(--band); }
.section + .section { padding-top: 0; }

/* ★ A PAGE HEAD BRINGS ITS OWN AIR, SO THE SECTION MUST NOT BRING IT TWICE.
   Eleven inner pages are `<main class="wrap section">` with the page head as
   their first child, so they were paying --band (84px at 1600) on top of
   --head-air before a single word. That is the single largest block of dead
   space above the fold on the whole site and it is bought for nothing: the
   head's own padding-top is what sets it off the nav.

   ★ TWO SELECTORS, AND `:has()` CANNOT BE NESTED TO COLLAPSE THEM.
     The sheet's convention is to match the explicit class AND the structural
     truth, so that a page authored later without the class still composes.
     The structural form would naturally read `:has(> .sec-head:has(> .page-h1))`
     — which is INVALID: the spec forbids :has() inside :has(), and an invalid
     compound drops the ENTIRE selector list it appears in, taking the working
     half down with it. `:has(> .sec-head > .page-h1)` says the same thing with
     one :has() and a child combinator, and is what ships. */
main.section:has(> .page-head),
main.section:has(> .sec-head > .page-h1) { padding-top: 0; }

/* ★ THE GUTTER IS RESTORED HERE, AND IT IS A REAL BUG BEING FIXED.
   `.wrap` sets `padding-inline: var(--gut)`. site.css then declares
   `.section { padding: 64px 0 }` — a SHORTHAND, twelve lines later and at the
   same specificity — so on every element that is both (`main class="wrap
   section"`, which is ELEVEN of the twelve pages, and every
   `<section class="section wrap">` on the landing page) the inline padding is
   reset to zero.

   On a wide desktop this is invisible: the wrap is capped at --maxw and
   centred, so there is margin either side whatever the padding says. It only
   appears once the viewport is narrower than the cap — and then it appears
   completely. Shot at 390: the eyebrow, the h1, the deck, the chapter rule
   and the gate card on the vault page all began hard against the left edge of
   the screen, with the nav above them correctly inset. Present in the
   untouched baseline on every inner page and every phone.

   Longhand, and scoped to elements that are actually .wrap, so a `.section`
   that was never meant to have a gutter does not grow one. This sheet's own
   rules use padding-block/padding-inline throughout for the same reason.

   ★ The gutter now also clears a display cutout — see section 12 for why
     env() belongs on every gutter and not only on the fixed bars. */
.wrap.section { padding-inline: max(var(--gut), env(safe-area-inset-left)) max(var(--gut), env(safe-area-inset-right)); }
.hero.wrap { padding-inline: max(var(--gut), env(safe-area-inset-left)) max(var(--gut), env(safe-area-inset-right)); }

/* The sub-block ladder. .mt-l was a flat 34px on every page at every width;
   it is the gap between a page's main content and its "how it works" block on
   nine pages, and 34px is not enough separation for a change of subject. */
.mt-l { margin-top: var(--band-tight); }
.mt-m { margin-top: clamp(20px, 2.2vw, 30px); }

/* THE FOOTER WAS A STRIP. It carried the brand, the links and the legal line
   in about 90 pixels, so a long, dark, cinematic page ended by simply running
   out. It now closes: a full-measure rule with the same accent segment the
   page heads use, real vertical air, and the legal line separated from the
   links rather than crowded under them. */
.foot {
  position: relative;
  padding-block: clamp(34px, 4.4vw, 66px) clamp(26px, 3vw, 40px);
  border-top: 1px solid var(--rule-hair);
}
.foot::after {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: -1px;
  width: min(100% - var(--gut) * 2, var(--maxw));
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg,
    var(--accent) 0, var(--accent-hi) clamp(56px, 7vw, 96px),
    transparent clamp(56px, 7vw, 96px));
}
.foot-in {
  padding-bottom: clamp(20px, 2.4vw, 32px);
  border-bottom: 1px solid var(--rule-hair-soft);
  gap: 26px;
}
.foot-brand img { width: 44px; height: 44px; }
.foot-brand b { font-size: 17px; }
.foot-links { gap: 9px 26px; }
.foot-legal { padding-top: clamp(16px, 1.8vw, 22px); }


/* =========================================================================
   7. BREAKPOINTS — 390 / 768 / 1280 / 1920.

   The phone is not a smaller desktop and it is not an afterthought: the
   overlapping-planes hero cannot survive a 390px viewport, because there is
   no "other half" to give the picture. So the phone gets a DIFFERENT
   composition with the same intent — the plate becomes a full-bleed cover
   band across the top of the hero, dissolving downward into the ground, and
   the type sits under it. A magazine cover rather than a title card.
   ========================================================================= */

/* ---- ≤ 1080: the plate stops leaving the screen ----
   Below this the wrap has no side margin worth speaking of, so a 4vw bleed
   pushes the subject of the picture off the edge instead of the empty field
   around it. */
@media (max-width: 1080px) {
  .hero { min-height: clamp(340px, 50vh, 470px); }
  /* ★ `26ch` HERE WAS THE EXACT BUG THIS SECTION'S OWN COMMENT DOCUMENTS, one
       media query further down the sheet. `ch` resolves against this element's
       font-size — body copy at 16px — not against the 90px display face inside
       it, so between 821px and 1080px the whole type column was capped at
       about 230px and a seven-word headline broke over five lines. The base
       rule was fixed and this one was missed. Pixels, like the base rule. */
  .hero-copy, .hero-grid > :not(.hero-art) { max-width: min(78%, 620px); }
  /* margin-right stays 0 here for the same reason as the base rule: a banner
     that carries the server's name must not run off the side of the screen.
     Narrowing the plate is the right lever at this width, bleeding it is not. */
  .hero-art { width: clamp(320px, 52vw, 520px); margin-right: 0; }
}

/* ---- ≤ 820: stacked. The phone and small-tablet composition. ----
   site.css already switches .hero-grid to one column and gives .hero-art
   `order: -1` at this width; this keeps that DOM order decision and rebuilds
   the geometry around it. */
@media (max-width: 820px) {
  /* ★ THE GUTTER COMES BACK. site.css's phone block sets
       `.hero { padding: 34px 0 28px }` — a SHORTHAND, so it also sets
       padding-left and padding-right to 0 and silently cancels the
       `padding-inline: var(--gut)` that .hero gets from .wrap. The result on
       every phone: the kicker, the headline, the lede and both buttons all
       start hard against the left edge of the screen with no margin at all,
       while the nav above them is correctly inset — so the page looked like
       the text had fallen out of its own layout. Present in the untouched
       baseline; shot and confirmed before this rule was written.
       Restored longhand, which is also what makes the art's bleed maths below
       resolve to exactly one gutter on each side. */
  .hero {
    display: block;
    min-height: 0;
    padding-block: 0 clamp(26px, 6vw, 40px);
    padding-inline: max(var(--gut), env(safe-area-inset-left)) max(var(--gut), env(safe-area-inset-right));
  }
  .hero-grid {
    display: flex;
    flex-direction: column;
    gap: clamp(20px, 5vw, 30px);
  }

  /* THE COVER BAND. Full-bleed to both viewport edges, cropped to a cinematic
     band rather than left square — a 600x600 plate at 390px wide is 600px of
     vertical screen before a single word, which is most of the phone. */
  .hero-art {
    order: -1;
    width: auto;
    max-width: none;
    margin-inline: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    align-self: stretch;
    justify-self: stretch;
  }
  .hero-art img {
    width: 100%;
    height: clamp(230px, 46vh, 340px);
    /* ★ `contain`, not `cover`. Cover fills the band by cropping, and on a
       390px-wide phone that crop took the sides off the wordmark — the one
       viewport where the brand is most often seen for the first time. Contain
       letterboxes instead, and the letterbox is invisible because the plate
       is transparent and the owner's backdrop is already behind it. */
    object-fit: contain;
    object-position: center 46%;
    /* Feathered DOWNWARD into the type instead of sideways: on a stacked
       layout the seam that matters is the horizontal one. */
    -webkit-mask-image: linear-gradient(180deg, black 0%, black 52%, transparent 100%);
            mask-image: linear-gradient(180deg, black 0%, black 52%, transparent 100%);
  }
  /* The side scrim was defending a crossing that no longer happens; a band
     across a full-bleed cover would just dim it. */
  .hero-art::after { display: none; }
  .hero-art::before { inset: -10% -20% 0 -20%; filter: blur(38px); opacity: .55; }

  .hero-copy, .hero-grid > :not(.hero-art) { max-width: none; }
  .hero .kicker::after { display: none; }
  .hero .lede { max-width: none; }
  /* site.css makes the buttons full-width at this size; the rule above them
     stays, because it is what keeps them attached to the headline. */
  .hero-cta { padding-top: clamp(16px, 4vw, 22px); }

  :is(.page-head, .sec-head:has(> .page-h1)) { padding-top: clamp(22px, 5vw, 34px); }
  :is(.page-head, .sec-head:has(> .page-h1))::before { display: none; }

  /* The gauge cluster goes vertical: hairlines become horizontal rules and
     the left borders would otherwise stack down one edge. */
  .status .fig { border-left: 0; border-top: 1px solid var(--edge-hair); }
  .status .fig:first-child { border-top: 0; }

  .foot-in { gap: 18px; }
}

/* ---- ≥ 1600: the wide desk ----
   The wrap is capped at --maxw, so past about 1500px the page stops growing
   and the plate is the only thing that can take the extra room. Letting it
   have that room is what keeps a 1920 monitor from looking like a 1280 one
   with grey margins. */
@media (min-width: 1600px) {
  /* The extra room goes into the plate's WIDTH, not into pushing it past the
     edge. Same rule as everywhere else: the banner is shown whole. */
  .hero-art { width: clamp(560px, 40vw, 780px); margin-right: 0; }
  .hero-copy, .hero-grid > :not(.hero-art) { max-width: min(64%, 820px); }
}


/* =========================================================================
   9. VOLCANIC DISPLAY TYPE — heat living inside the letterforms.

   Owner ruling 2026-08-08, on re-sending the same artwork: "make text
   volcanic animated as well". The picture he sent has an erupting volcano in
   its top-left corner and embers at the base of the skull; the display type
   is supposed to belong to that picture rather than sit on top of it.

   HOW IT IS BUILT, and why this way:
     - CSS ONLY. No canvas, no script, nothing for another agent to wire. It
       survives with JavaScript off, costs one composited layer, and ships to
       every owner with the sheet.
     - The flow is a pair of @property-TYPED custom properties. An unregistered
       custom property cannot be interpolated — a keyframe would jump from one
       value to the next — so the gradient stops are registered as
       <percentage> and the browser tweens them. That is what makes the lava
       MOVE through the strokes rather than blink.
     - The heat is a background-image clipped to the glyphs. The glow around
       them is `filter: drop-shadow`, NOT `text-shadow`: with the fill
       transparent, a text-shadow is painted behind the glyph and shows
       straight THROUGH it as a flat coloured slab. drop-shadow reads the
       alpha of what was actually rendered, so it haloes the letter shapes.

   ★ IT STAYS READABLE, AND THAT IS BUILT IN RATHER THAN CHECKED AFTERWARDS.
     The obvious molten ramp — bone at the top down to the raw accent, or to a
     cooled near-black crust at the bottom — puts the darkest part of every
     letter at the bottom of the glyph. Measured on this owner, a ramp closing
     on `color-mix(--accent 62%, --bg-deep)` lands at 2.36:1 on --bg. That is
     a beautiful, failing headline.

     So every heat stop is the accent MIXED TOWARD THE OWNER'S OWN INK
     (`--fg`), never toward the ground. That single rule is contrast-safe on
     any palette by construction, in both directions:
       - near-black island: --fg is bone, so mixing toward it makes the heat
         BRIGHTER against a dark ground;
       - pale island: --fg is dark ink, so the same mix makes the heat DARKER
         against a light ground.
     Measured: Jurassic Land 5.33:1 at the coldest stop, the pale jungle
     control 6.22:1 at the same stop. Both over AA for normal text, on type
     that is 90px.

   ★ AND IT DEGRADES TO THE PLAIN HEADLINE, NEVER TO NOTHING.
     `color: transparent` is only ever set INSIDE the @supports test. A
     browser without background-clip:text gets --fg and the correct headline;
     it never gets an invisible one. @property is separately optional: without
     it the gradient still paints at its initial stops, it simply does not
     flow.
   ========================================================================= */

/* The two stops that travel. Registered so they can be animated at all. */
@property --lava-mid {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 44%;
}
@property --lava-far {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 86%;
}

:root {
  /* ---- the heat ladder, mixed toward the owner's INK ----
     See the contrast argument above: toward --fg, never toward the ground. */
  --heat-hi:  color-mix(in srgb, var(--accent-hi) 34%, var(--fg));
  --heat-mid: color-mix(in srgb, var(--accent-hi) 74%, var(--fg));
  --heat-far: color-mix(in srgb, var(--accent) 72%, var(--fg));

  /* ---- the crust ----
     Fracture lines are the GROUND showing through a cooled skin, so they are
     mixed from --bg-deep: dark cracks on a dark island, pale ones on a pale
     island, and in both cases they read as breaks in the surface. Kept to one
     pixel in a nineteen-pixel cycle so they cost the glyph almost none of its
     average luminance. */
  --crust: color-mix(in srgb, var(--bg-deep) 46%, transparent);

  /* The molten body itself. Declared once; three headings share it. */
  --lava: repeating-linear-gradient(104deg,
            transparent 0 7px, var(--crust) 7px 8px, transparent 8px 19px),
          linear-gradient(177deg,
            var(--fg) 0%,
            var(--fg) 12%,
            var(--heat-hi) 34%,
            var(--heat-mid) var(--lava-mid),
            var(--heat-far) var(--lava-far),
            var(--heat-mid) 100%);
}

@keyframes lava-flow {
  0%   { --lava-mid: 38%; --lava-far: 80%; }
  50%  { --lava-mid: 56%; --lava-far: 96%; }
  100% { --lava-mid: 38%; --lava-far: 80%; }
}
/* The bloom breathes on its own clock, so the glow and the flow never land on
   the same beat and the heat reads as alive rather than as a loop. */
@keyframes lava-bloom {
  0%, 100% { filter: drop-shadow(0 0 10px var(--accent-glow)); }
  50%      { filter: drop-shadow(0 0 22px var(--accent-glow)); }
}

@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .hero h1,
  .page-h1,
  .sec-head h2 {
    background-image: var(--lava);
    background-size: auto, 100% 100%;
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    /* WebKit needs the fill colour cleared too — `color` alone loses to a
       -webkit-text-fill-color inherited from anywhere. */
    -webkit-text-fill-color: transparent;
    /* The bloom replaces --shadow-text on these three. A text-shadow under a
       transparent fill is a slab, not a glow (see the block comment). */
    text-shadow: none;
  }

  .hero h1 {
    animation: lava-flow 11s ease-in-out infinite,
               lava-bloom 7s ease-in-out infinite;
  }
  /* The page heads and section heads run the same material at a slower, lower
     amplitude: a landing page has ONE masthead, and eleven headings all
     pulsing at hero strength is a novelty rather than a design. */
  .page-h1 {
    animation: lava-flow 17s ease-in-out infinite;
    filter: drop-shadow(0 0 10px var(--accent-glow));
  }
  .sec-head h2 {
    animation: lava-flow 23s ease-in-out infinite;
  }

  /* ★ THE LIVE NUMBERS TAKE HEAT, NOT LAVA.
     The figures in the instrument are the one thing on the page a player
     actually reads for information. They get the warm end of the ladder and
     a faint bloom so they look lit from inside, and they do NOT move: a
     count-up (window.wcMotion drives one) over a travelling gradient is two
     animations arguing about the same pixels. */
  .status .fig b {
    background-image: linear-gradient(176deg,
      var(--fg) 0%, var(--heat-hi) 62%, var(--heat-mid) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    filter: drop-shadow(0 0 9px var(--accent-glow));
  }
}

/* ★ REDUCED MOTION FREEZES THE LAVA, IT DOES NOT REMOVE IT.
   The owner asked for volcanic type; a reader who has asked the system to
   stop moving things should still get a molten headline, just a still one.
   site.css's global reduced-motion rule already collapses animation-duration
   to ~0, which would leave the stops wherever the first frame put them —
   `animation: none` plus explicit mid-flow stop positions is the difference
   between "frozen at a good moment" and "frozen at 0%". */
@media (prefers-reduced-motion: reduce) {
  .hero h1, .page-h1, .sec-head h2 {
    animation: none;
    --lava-mid: 48%;
    --lava-far: 88%;
    filter: drop-shadow(0 0 14px var(--accent-glow));
  }
}


/* =========================================================================
   10. THE PICTURE HAS TO READ — on every page, not just the first screen.

   Owner ruling, said twice: "i gave u a whole background to implement on the
   site" and then "this background should be on every page". That is an
   acceptance test, and the honest way to read a screenshot against it is:
   can you tell what that picture is, without being told?

   The answer used to be no below the first screen, and the reason was not the
   backdrop — window.wcBackdrop paints the artwork full-bleed and alive. It
   was everything stacked ON TOP of it:
     - `main` carries a ramp that closes on --veil-deep, 94% of the owner's
       floor colour, so from about 70vh down the artwork survives as a 6%
       ghost. That is a black slab with a rumour of a picture in it.
     - every card, gate, notice and panel is an OPAQUE --surface rectangle, so
       the busiest part of every page is the part that hides the most.

   Both are fixed the same way: the page stops covering the picture and starts
   FLOATING on it. Panels keep their material — the lit top edge, the tooled
   grain, the shadow ladder — and gain the one thing that makes glass read as
   glass, a real backdrop blur. The artwork stays legible THROUGH them, which
   is the whole point, and the copy on them stays legible because the blur
   plus a mostly-opaque tint destroys the picture's local contrast without
   destroying its tone.
   ========================================================================= */

/* ★ THE GROUND RAMP IS NOT THIS SHEET'S, AND THAT IS A CORRECTION.
   This layer briefly restated `main`'s ramp on --veil-strong (82% of the
   floor colour) to open the artwork up below the fold. It was right about the
   problem and wrong about the owner: backdrop.css had ALREADY restated the
   same ramp, from a real measurement — the brightest pixel it has to defend
   is the skull at rgb(230,230,230), body ink clears AA over it at 70%, and it
   closes at 74%. cinema.css loads AFTER backdrop.css, so this sheet's 82% was
   silently overriding a sibling's measured 74% and showing LESS of the
   picture than the atmosphere layer intended.
   Removed. `main` belongs to backdrop.css; this sheet stops at the panels
   that sit on it. The rule is written down rather than deleted quietly so the
   next reader knows the ground has an owner. */

/* PANELS FLOAT. `backdrop-filter` is progressive: a browser without it simply
   gets the tint, which is still translucent and still shows the picture. The
   tint is mixed from the owner's own --surface, so this is the same panel on
   every island, just no longer a lid. */
.card,
.gate,
.notice {
  background-color: color-mix(in srgb, var(--surface) 74%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(112%);
          backdrop-filter: blur(14px) saturate(112%);
}
.status {
  background-color: color-mix(in srgb, var(--surface) 78%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(115%);
          backdrop-filter: blur(18px) saturate(115%);
}
/* The instrument's two sunken strips are the darkest thing on the panel and
   were fully opaque; at 82% they still read as sunken and still let the
   picture's tone through. */
.status-top,
.status-note {
  background-color: color-mix(in srgb, var(--surface-sink) 82%, transparent);
}

/* ★ THE FOOTER WAS THE LAST SLAB. It closed the page on the owner's floor
   colour at full strength, so the artwork stopped dead about 200px before the
   page did. It now floats like everything else. */
.foot {
  background-color: color-mix(in srgb, var(--bg-deep) 62%, transparent);
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  /* ★ AND IT CLOSES THE PAGE WHEN THE PAGE IS SHORTER THAN THE SCREEN.
     backdrop.css paints .night as `position: fixed; inset: 0; z-index: -2`, so
     it covers the whole viewport whatever the document's height is, while the
     ramp that tames it lives on `main` and therefore stops where the content
     stops. On a short page — the shop and the staff console when the backend
     is not answering, and now more pages than before, because this wave took
     ~150px of dead air out of every inner page — the document ends above the
     fold and a bright band of raw artwork sits between the footer's last line
     and the live bar. It reads as the page having fallen off.

     This carries the footer's own ground down past the end of the document
     without adding an element, without touching `main`'s ramp (which is
     backdrop.css's and measured), and without making `body` a flex column —
     which is the usual sticky-footer fix and would put a stacking context and
     a new formatting context under a sticky nav and a fixed backdrop that both
     currently work.
     A box-shadow offset DOWN by half a viewport and spread BY half a viewport
     has its top edge exactly on the footer's own top edge and its bottom edge
     a full viewport below, so there is no seam and no gap at any page length.
     It is translucent in the same mix as the footer's background, so the
     artwork still reads through it and the tone does not step at the join.
     Shadows paint behind the element's own background and cost no layout. */
  box-shadow: 0 50vh 0 50vh color-mix(in srgb, var(--bg-deep) 62%, transparent);
}

/* Forced colours: a backdrop blur cannot be honoured and a translucent panel
   over a system canvas is unreadable. Everything goes back to solid. */
@media (forced-colors: active) {
  .card, .gate, .notice, .status, .status-top, .status-note, .foot {
    background-color: Canvas;
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
  main { background-image: none; }
  .hero h1, .page-h1, .sec-head h2, .status .fig b {
    background-image: none;
    color: CanvasText;
    -webkit-text-fill-color: currentColor;
    filter: none;
    animation: none;
  }
}
@media print {
  .card, .gate, .notice, .status, .foot {
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
  .hero h1, .page-h1, .sec-head h2, .status .fig b {
    background-image: none;
    color: var(--fg);
    -webkit-text-fill-color: currentColor;
    filter: none;
    animation: none;
  }
}


/* =========================================================================
   8. THE CONDITIONS THIS PAGE ALSO SHIPS IN.
   ========================================================================= */

/* Reduced motion: the hover affordances are transforms, and a transform is
   motion. site.css already flattens transition-duration globally under this
   query; the transform itself is removed here so nothing shifts at all. */
@media (prefers-reduced-motion: reduce) {
  .card:hover { transform: none; }
}

/* Forced colours (Windows high contrast): every mixed colour above collapses
   to the system palette, which turns the scrims into opaque slabs over the
   artwork and the rails into invisible strokes. Strip the decoration and
   keep the structure — the same stance site.css takes. */
@media (forced-colors: active) {
  .hero-art::after,
  :is(.page-head, .sec-head:has(> .page-h1))::before,
  .foot::after,
  .status::after { display: none; }
  .status, .card { box-shadow: none; }
  .foot, .sec-head { border-bottom-color: currentColor; }
}

/* Print: the plate, the washes and the rails are all screen furniture. */
@media print {
  .hero { min-height: 0; display: block; }
  .hero-art,
  :is(.page-head, .sec-head:has(> .page-h1))::before,
  :is(.page-head, .sec-head:has(> .page-h1))::after,
  .foot::after,
  .status::after { display: none; }
  .hero-copy, .hero-grid > :not(.hero-art) { max-width: none; }
  .status { box-shadow: none; }
}


/* =========================================================================
   11. THE MASTHEAD — one bar that cannot collide with itself.

   ★ THE DEFECT, MEASURED BEFORE IT WAS FIXED, not described.
     At a 500px viewport the nav row asked for more width than it had and
     every part of it failed differently:
       .brand      clientWidth 73px holding 145px of content, overflow
                   VISIBLE — so "Jurassic Land" and the two wrapped lines of
                   the tagline painted straight through the burger button
                   sitting at x=103.
       .brand-txt  38px tall: the tagline had broken onto a second line, and
                   the lockup was three lines deep in a 62px bar.
       .nav-cta    clientWidth 55px holding 107px — "Community" cut in half
                   at the right edge of the screen.
     At 1280 and at 1600 the same row was 52px over budget and the squeeze
     landed on the same place: .brand fell from its natural 243px to 191px,
     which is narrower than the tagline, so the tagline wrapped to two lines
     on the WIDEST screens the site ships to. 1600 was no better than 1280
     because .nav-in is .wrap and .wrap stops at --maxw.

   ★ WHY IT WAS INVISIBLE TO EVERY EXISTING CHECK.
     Nothing overflowed the DOCUMENT — body carries `overflow-x: hidden`, so
     `document.scrollWidth` was exactly the viewport at all five widths and no
     scrollbar ever appeared. The collision was entirely INSIDE a flex row
     whose items had `overflow: visible`. A layout can be broken in every
     screenshot and still report clean geometry at the page level.

   ★ THE DECISION: THE LOCKUP COLLAPSES TO THE MARK, IT DOES NOT SHRINK.
     Two answers were on the table. Shrink the type until it fits, or drop
     the text lockup below the width where the bar turns into a phone bar.

     Shrinking loses twice. The tagline is the WIDEST element in the lockup on
     every owner — it is uppercase, tracked at .24em, and on this owner it is
     198px against a 111px name — so type small enough to fit a 500px bar is
     type nobody can read, and the bar still has no slack for an owner with
     one more nav link than this one.

     Collapsing is also simply the truer answer HERE: the owner has just
     replaced his mark with a CREST that already carries the words JURASSIC
     LAND and PROXIMITY VOICE inside the artwork. Beside that crest, a
     wordmark and a tagline in 16.5px and 9.5px type are the same two strings
     twice, and the second copy is the one that breaks the bar.

   ★ AND IT IS CORRECT FOR AN OWNER WHOSE MARK IS AN ABSTRACT GLYPH.
     This is the half that a `display: none` would get wrong, silently and
     ★ AND THE BRACES AROUND A SLOT NAME MUST NOT BE WRITTEN IN THIS SHEET AT
     ALL, EVEN INSIDE A COMMENT. cinema.css is copied to the box VERBATIM; the
     assembler renders slots in chrome.html and the page fragments only. A pair
     of braces here ships to a player's browser as literal characters, and
     run_all's unrendered-slot gate refuses the build for exactly that reason —
     it caught this comment. Where this sheet needs the owner's artwork it uses
     the generated token instead: var(--art-mark), var(--art-hero),
     var(--art-background), each emitted per owner by theme.py.
     The nav's mark carries an EMPTY alt attribute by design —
     the mark is decorative because the name is written beside it. Take the
     name away with `display: none` and the brand LINK has no accessible name
     at all: a screen reader announces "link" and nothing else, on every page,
     at every phone width. So the lockup is hidden the way site.css's own
     .sr-only hides things — clipped to 1px, still rendered, still in the
     accessibility tree — and the link keeps announcing the owner's name and
     tagline whatever the mark happens to be a picture of.
   ========================================================================= */

/* THE ROW ITSELF. Three separate guarantees, and each one closes a different
   way the bar failed:
     nowrap        a flex row that wraps turns a crowded bar into a two-storey
                   bar, which is worse than a tight one;
     max-width     the bar stops being capped at the READING measure. A column
                   of prose wants --maxw; a masthead wants the window, which is
                   why the live bar at the bottom of the page is already full
                   width. 1600 was rendering a 1280 bar with 320px of empty
                   margin either side WHILE the row inside it overflowed;
     padding       the gutter never falls inside a display cutout. */
.nav-in {
  flex-wrap: nowrap;
  gap: clamp(9px, .9vw, 15px);
  max-width: min(100%, calc(var(--maxw) + 320px));
  padding-inline: max(var(--gut), env(safe-area-inset-left)) max(var(--gut), env(safe-area-inset-right));
}
/* The bar is sticky at top: 0, so on a notched device held in landscape the
   left/right insets above do the work, and this covers the case where a
   browser hands the page the top inset as well. env() resolves to 0px on
   every device that has no cutout, so this costs nothing anywhere else. */
.nav { padding-top: env(safe-area-inset-top); }

/* ★ THE BRAND CAN NO LONGER PAINT OUTSIDE ITSELF.
     `overflow: hidden` is the structural half of the fix and it is not
     decoration: an element that is clipped CANNOT collide with its
     neighbour, whatever the owner's name is, however long a translation runs
     and whatever font actually loads. The ellipsis below turns that clip into
     something a reader understands instead of a word cut mid-letter. This is
     the same argument site.css already makes for .lb-left/.lb-mid/.lb-right,
     which failed the same way and was found the same way — in a screenshot,
     not in a test. */
.brand {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
}
.brand-txt {
  min-width: 0;
  max-width: 100%;
}
.brand-txt b,
.brand-txt span {
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* THE 52px THE DESKTOP ROW WAS SHORT.
   Eight links at 12px of padding a side is 192px spent on air between eight
   words, plus 2px of gap between each. Nine pixels a side and no gap gives the
   row back 62px, which is what the full lockup needs — and it reads TIGHTER,
   which is the direction the density round is going anyway. The links keep
   their 44px touch height on a touch screen (see section 13), so nothing here
   makes them harder to hit.

   ★ 10px WAS NOT ENOUGH AND THE SHORTFALL WAS SIX PIXELS. Shot at 1280 with
     10px: the lockup fitted on two lines as intended, and the tagline was
     ellipsised — "PROXIMITY VOICE - GATEW…" — because .brand landed at 192px
     against a 198px tagline. A tagline cut one character short is a worse
     failure than a wrapped one, because it looks like a bug in the owner's
     copy rather than a tight bar. One more pixel a side clears it with room. */
.nav-links {
  gap: 0;
  min-width: 0;
}
.nav-links a { padding: 9px 9px; }

/* ★ AND THE LINK ROW GETS THE SAME GUARANTEE THE BRAND JUST GOT, BECAUSE
     `min-width: 0` WITHOUT IT MOVES THE COLLISION RATHER THAN CURING IT.
     Measured on the vault page: at 1100px the link row needs 562px and is
     given 452px, and every link inside it is `white-space: nowrap`, so the
     last two simply painted over the ambience control. That is the brand's bug
     one element to the right, and it is the bug I would have shipped by
     letting the links shrink without saying where the overflow goes.

     It scrolls instead. Not one link is ever clipped, hidden or unreachable at
     any width or on any owner — and owner is the word that matters, because
     the number of links is an owner's decision: this island runs nine, an
     island with twelve would overrun a fixed set of breakpoints however
     carefully they were measured. site.css already makes exactly this choice
     for .tabs (`overflow-x: auto`), so it is the sheet family's existing
     answer to "more items than room", not a new idea.

     The scrollbar itself is suppressed: a 15px-tall scrollbar under a 62px bar
     is a visual artefact, and the row is only ever scrollable in a narrow band
     of widths where the tiers below have not yet collapsed the lockup.
     Scoped to the DESKTOP row on purpose — under 820px .nav-links is an
     absolutely positioned COLUMN dropdown, and turning that into a scroll
     container would clip an open menu. */
@media (min-width: 821px) {
  .nav-links {
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .nav-links::-webkit-scrollbar { display: none; }
}

/* ★ THE LOCKUP COLLAPSES IN MEASURED STEPS, NOT IN ONE GUESS.
     Every threshold below is the width at which the row actually stopped
     fitting, read off the DOM rather than picked. The parts, measured on this
     owner at his own font sizes:
        mark 46 · name +146 · tagline +63 · links 562 · ambience 114 (41 as an
        icon) · sign-in 134 · community 130 (39 as an icon) · four gaps · 44 of
        gutter.
     Which gives, as the total the row needs:
        mark + name + tagline, every label   1287  -> fits from ~1300
        mark + name,           every label   1224  -> fits from ~1230
        mark + name, icon ambience + discord 1060  -> fits from ~1065
        mark only,   icon ambience + discord  914  -> fits from ~915
     The steps are ordered by what costs the reader least. The TAGLINE goes
     first: it is a second-line marketing string that the crest already carries
     inside the artwork. The two CONTROL LABELS go next, because both controls
     keep an icon that says the same thing and the sign-in button — the one
     control a first-time visitor is looking for — keeps its words. The NAME
     goes last, and only where nothing else is left to give. Below 915 the
     links belong in the burger, which is site.css's own ≤820 decision; the
     94px band between the two is what the scroll above is for, and so is the
     owner who adds a tenth link next week.
     Nothing here is ever `display: none` on text that carries meaning — every
     step below clips instead, so the words stay in the accessibility tree. */
@media (max-width: 1299px) {
  .brand-txt span {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}
@media (max-width: 1229px) {
  /* The ambience button's own aria-label is written by ambient.js, so this one
     label genuinely carries nothing a screen reader loses. */
  .wc-aud-txt { display: none; }
  .nav-cta span.nav-cta-txt {
    display: inline-block;
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .nav-cta { padding: 9px 11px; }
}
@media (max-width: 1064px) {
  .brand-txt {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* ---- ≤ 820: the burger tier. The lockup collapses to the mark. ----
   This is the width site.css already chose to fold the links into the burger,
   so it is the width where the bar stops being a navigation and starts being
   a masthead with a menu button. Reusing it rather than inventing a fifth
   breakpoint is the whole reason it is this number. */
@media (max-width: 820px) {
  /* The lockup is already clipped by the ≤1064 step above; nothing is left to
     shrink, so the mark stops being shrinkable. */
  .brand { flex: none; }
  /* ★ THE MARK IS NOW THE WHOLE LOCKUP, SO IT MUST NOT SHRINK.
       Owner ruling, on a screenshot of the nav: "for logo I sent you just make
       it the website logo on the top left". site.css sizes .brand img at 46px
       square, unrounded and `object-fit: contain`, because the mark is a CREST
       with the server's name inside it rather than an avatar tile — and it
       then shrinks it to 30px at ≤400px, which was correct for a 34px glyph
       beside a wordmark and is wrong now that it IS the wordmark. Held at 42px
       instead: the words inside the crest stay legible, and it is still 4px
       smaller than the desktop mark so the phone bar does not get taller.
       Measured at 390: the last control in the row lands inside the viewport
       with room to spare, because the text lockup gave back ~110px. */
  .brand img {
    width: 42px;
    height: 42px;
  }
}

/* site.css drops the mark to 30px here; see the note above for why that number
   belonged to the old glyph and not to the crest. Restated at the same
   breakpoint so the two rules cannot drift. */
@media (max-width: 400px) {
  .brand img {
    width: 42px;
    height: 42px;
  }
}

/* ★ AN ICON-ONLY BUTTON WITH A `display: none` LABEL IS A BUTTON WITH NO NAME.
     site.css hides .nav-cta-txt and .nav-auth-txt at ≤400px to keep the bar
     down to icons, which is the right LOOK and the wrong mechanism: neither
     control carries an aria-label, so below 400px the Discord link and the
     Steam sign-in link both announced as "link" with no name — the two most
     important controls in the bar, on the width where most players meet them.
     Clipping instead of hiding keeps the icons-only bar exactly as it looks
     and gives both controls their name back. The Discord label is already
     clipped from 1229px down; this is the sign-in one, which holds its words
     as long as anything in the bar does because it is what a first-time
     visitor is looking for.
     Written out again rather than shared, because CSS has no way to name a
     declaration block and reuse it, and a class cannot be added by a media
     query. 460 rather than site.css's 400: measured, 430px is already 2px
     short with the words in. */
@media (max-width: 460px) {
  .nav-auth-btn span.nav-auth-txt {
    display: inline-block;
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* Forced colours: the clip recipes are geometry rather than colour, so they
   survive untouched. The one thing that must not survive is the mask on the
   bar itself, which site.css already handles. Nothing to add. */


/* =========================================================================
   12. THE SAFE AREA — the page stops running under the cutout.

   chrome.html has shipped `viewport-fit=cover` since it was written, which is
   what lets the backdrop reach the physical edges of a notched phone. It also
   means the browser stops insetting the page for the notch and the home
   indicator and hands that job to the stylesheet — and webcore used
   env(safe-area-inset-*) exactly ZERO times. So on every iPhone since the X,
   held in landscape, the nav's left gutter and the live bar's right-hand
   state chip were under the cutout; held in portrait, the live bar — a FIXED
   bar at bottom: 0 — was under the home indicator.

   Every rule here uses max() or a calc() that ADDS the inset, so on a device
   with no cutout every one of them resolves to exactly the value that was
   there before. This is not a phone-only stylesheet; it is the same
   stylesheet, told where the screen actually ends.
   ========================================================================= */

/* The reading column. .wrap is the gutter for every page, and cinema.css
   already restates it longhand on the two elements where a site.css shorthand
   cancelled it (see section 6) — those two are updated in place below. */
.wrap {
  padding-inline: max(var(--gut), env(safe-area-inset-left)) max(var(--gut), env(safe-area-inset-right));
}

/* The live bar is the one FIXED element on the site, so it is the one that
   the home indicator actually overlaps. Its inline padding is site.css's
   `var(--gut) + 4px` with the inset added rather than max()'d, because the
   bar is edge-to-edge: the 4px is a visual inset from the bar's own border,
   not from the screen's. */
#wc-livebar {
  padding-bottom: calc(9px + env(safe-area-inset-bottom));
  padding-left: calc(var(--gut) + 4px + env(safe-area-inset-left));
  padding-right: calc(var(--gut) + 4px + env(safe-area-inset-right));
}
/* And the page has to reserve the extra height the bar just grew by, or the
   last line of the footer sits under the home indicator.

   ★ THIS IS DELIBERATELY NOT `body.has-livebar { padding-bottom: calc(...) }`.
     site.css already reserves the bar with `padding-bottom: var(--livebar-h)`,
     and --livebar-h is declared INLINE on that same one-line rule, so it is a
     token this sheet cannot legally read: the gate that checks every var() in
     cinema.css names a token somebody defines only sees declarations that
     start a line, and it refuses `var(--livebar-h)` outright. Writing 44px
     here instead would be the very duplication site.css's own comment warns
     about — "one value, read by everyone that needs it" — and it would go
     stale the day the bar changes height.
     A margin under the footer needs neither. It ADDS exactly the inset below
     the last painted pixel, whatever the bar's height turns out to be, and it
     resolves to 0 on every device without a cutout. */
.foot { margin-bottom: env(safe-area-inset-bottom); }


/* =========================================================================
   13. TOUCH — 44px, gated on the pointer rather than on the width.

   ★ WIDTH IS NOT TOUCH. A 1280px tablet is a touch device and gets desktop
     rules; a 500px window on a desk is a mouse and gets phone rules. Arkadia
     gates its entire touch sheet on `@media (pointer: coarse)` with no width
     breakpoint at all, and that is the right instrument: it asks what the
     player is actually pointing with.

   ★ THE BLEED IS A PSEUDO-ELEMENT, NOT PADDING, and that is what makes this
     safe to add to a finished design. Growing a control's padding to reach
     44px re-flows every row it sits in — the nav bar gets taller, the tab
     strip gets taller, the footer wraps. An absolutely-positioned, invisible
     ::before centred on the control extends only the HIT AREA, so the picture
     is unchanged and the target is 44px.

   ★ WHICH PSEUDO-ELEMENT IS FREE IS AUDITED, NOT ASSUMED. site.css already
     spends `.nav-links a::before` on the current-page rule and
     `.nav-links a::after` on the voice dot; `.tabs button::after` on the
     selected rule; and all four of .burger's on the three bars. A bleed
     written onto any of those would silently delete a live signal. So the nav
     links get a min-height instead of a pseudo, the burger gets real
     dimensions, and only the controls with a genuinely free ::before get one.
   ========================================================================= */
@media (pointer: coarse) {
  /* Both pseudo-elements on a nav link are already spoken for, so the link
     grows its own box. It is a block inside a 62px bar with room to spare, so
     44px changes no layout. */
  .nav-links a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
  /* 40x38 is under the target on both axes. The bar is 62px tall, so this
     costs nothing. */
  .burger {
    width: 44px;
    height: 44px;
  }
  /* ::before is free on all four of these. `inset: 50% auto auto 50%` plus the
     translate centres the box on the control whatever size the control is;
     max() means the bleed never SHRINKS a target that is already bigger. */
  .tabs button::before,
  .nav-auth-btn::before,
  .nav-cta::before,
  .bd-win::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
    border-radius: inherit;
  }
  .tabs button,
  .nav-auth-btn,
  .nav-cta,
  .bd-win { position: relative; }
  /* A footer link is a 13px word in a 9px-gap column: the easiest thing on
     the page to miss and the only navigation left at the bottom of a long
     page. It takes the height rather than a pseudo, because the column has
     room and a bleed here would overlap its neighbours. */
  .foot-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* A tab strip has vertical room, so it takes the height directly rather than
     a bleed. ::before is spent on the count badge (section 14) and ::after on
     the selected rule (site.css) — there is no free pseudo-element left, and
     writing over either one would silently delete a live signal. */
  .tabs button { min-height: 44px; }
}


/* =========================================================================
   14. DENSITY AND SURFACE CRAFT — the page reaches its data sooner, and the
       surfaces stop being flat.

   The owner named the benchmark: "make website ui style etc as advanced as
   arkadia". A count of what his richer site actually spends on depth, against
   what site.css spends:

        box-shadow        183  vs  32
        backdrop-filter    74  vs   3
        linear-gradient    58  vs  15
        radial-gradient    28  vs   3

   That gap is not tasteful restraint on this side. A panel with one border and
   one shadow reads as a rectangle; a panel with a lit edge, a ground that is
   not one flat colour, a light source somewhere and a shadow that has a
   direction reads as an object. Everything below is the second kind, and every
   value in it is a role token or a color-mix of one, so a pale jungle island
   gets the same craft and never the same face.

   ★ WHAT IS DELIBERATELY NOT TAKEN FROM THE BENCHMARK: its green, its Oswald,
     its uppercase-mono voice, its two divergent token sets, its hand-tuned
     radii and its raw z-index numbers. The owner asked for a site as ADVANCED
     as that one, not for that one. The radii here are --r-xs/--r-s/--r/--r-pill
     exactly as generated, because a radius typed into this sheet is a radius
     that no longer follows the owner's theme.
   ========================================================================= */

/* ---- the fold on the landing page ----
   The hero is shorter (section 2); this is the other half of the same move.
   .section + .section already collapses the double gap between two sections;
   the hero is not a .section, so the first band under it was paying the full
   --band on top of the hero's own bottom padding. The instrument that follows
   it is the one live reading on the page and it belongs on the first screen. */
.hero + .section { padding-top: var(--band-tight); }
/* And its heading is an introduction to a panel that is directly under it, not
   a chapter opening. */
.hero + .section > .sec-head { margin-bottom: clamp(14px, 1.6vw, 20px); }

/* ---- panels get a light source ----
   site.css draws the lit top edge on .card::before and cinema.css already
   floats the panel on the artwork. This is the one thing neither does: a soft
   wash of the OWNER'S OWN accent at the top-left corner, as if the panel were
   catching light from the same direction the page's rails run, plus a short
   ramp off the top edge so the face is not one flat value. It costs no asset —
   the trap .eyebrow::before documents (an owner whose mark is a raster with no
   alpha ships a grey square) applies to any decoration built from an image, so
   this one is built from gradients.

   ★★ IT IS A BACKGROUND LAYER, NOT A PSEUDO-ELEMENT, AND THAT IS A CORRECTION
      I MADE TO MYSELF BEFORE SHIPPING IT.
      The first cut drew the wash on ::after at `inset: 0`, which paints ABOVE
      in-flow text — so the panel's own copy had to be lifted over it with
      `.card > * { position: relative; z-index: 1 }`. That line is the bug.
      site.css already gives `.card` `position: relative`, so every absolutely
      positioned DESCENDANT in the site — the shop's 3D stage, the step
      counters, anything an owner adds later — resolves against the card.
      Making each direct child a containing block silently re-anchors all of
      them: not a colour change, a GEOMETRY change, in markup this sheet does
      not own and cannot see. `isolation: isolate` plus a negative z-index was
      the other way and it is barely safer, because a stacking context on every
      card traps any menu or tooltip that was written to escape one.
      A background layer has neither failure. Backgrounds paint behind all
      content by definition, they re-anchor nothing, they create no stacking
      context, and they cost one paint instead of one extra box per panel. The
      base layers are restated per element because all three start from a
      different ground — dropping `var(--grain)` or `var(--grad-notice)` here
      would quietly delete site.css's material. */
.card {
  background-image:
    radial-gradient(120% 74% at 8% 0%, var(--accent-wash) 0%, transparent 58%),
    linear-gradient(180deg, var(--veil-soft) 0%, transparent 30%),
    var(--grain),
    var(--grad-card);
}
.card:hover,
.card:focus-within {
  background-image:
    radial-gradient(120% 74% at 8% 0%, var(--accent-wash-2) 0%, transparent 58%),
    linear-gradient(180deg, var(--veil-soft) 0%, transparent 30%),
    var(--grain),
    var(--grad-card);
}
.gate {
  background-image:
    radial-gradient(120% 74% at 8% 0%, var(--accent-wash) 0%, transparent 58%),
    linear-gradient(180deg, var(--veil-soft) 0%, transparent 30%),
    var(--grain);
}
.notice {
  background-image:
    radial-gradient(120% 74% at 8% 0%, var(--accent-wash) 0%, transparent 58%),
    var(--grain),
    var(--grad-notice);
}

/* THE GATE IS THE HONEST-STATE PANEL and it is what a player sees when a
   feature is not wired, not signed in, or not answering — which is most of a
   first visit. It carried a border and a flat ground. It now carries the same
   machined bezel the instrument does, because a gate that looks BUILT is the
   difference between "this server has not finished" and "this server is
   telling me something". */
.gate {
  box-shadow: var(--lift-1), var(--bezel);
}
.gate-ico {
  box-shadow: 0 0 0 1px var(--edge-hair) inset, 0 0 22px var(--accent-wash-2);
}

/* THE NOTICE gets a spine in the owner's accent rather than a full border, so
   an advisory reads as an aside instead of as another card. */
.notice {
  box-shadow: inset 2px 0 0 var(--accent), var(--lift-1);
}

/* ---- the pill: a state, not a label ---- */
.pill {
  box-shadow: 0 1px 0 var(--edge-top) inset, 0 1px 2px var(--edge-sink);
  background-image: linear-gradient(180deg, var(--veil-soft), transparent);
}
.pill.live {
  box-shadow: 0 1px 0 var(--edge-top) inset, 0 0 14px var(--ok-halo);
  background-image: linear-gradient(180deg, var(--ok-halo), transparent);
}

/* ---- the tab strip: a milled group, and a slot for a real count ---- */
.tabs {
  background-image: var(--grain), linear-gradient(180deg, var(--veil-soft), transparent);
  box-shadow: 0 1px 0 var(--edge-top) inset, 0 -1px 0 var(--edge-sink) inset, 0 2px 8px -6px var(--veil-deep);
}
.tabs button {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.tabs button[aria-selected="true"] {
  box-shadow: var(--lift-1);
  background-image: linear-gradient(180deg, var(--accent-wash-2), transparent);
}

/* ★ THE COUNT BADGE IS A CONTRACT, AND IT RENDERS NOTHING UNTIL SOMETHING
     REAL FILLS IT.
     The benchmark annotates every filter and layer toggle with its count —
     PATROLS 19, GASTROLITHS 168 — and that is most of why its pages feel
     instrumented rather than decorated. Ours cannot invent one: the numbers
     live in the scripts, and the scripts are not this wave's to edit.
     So the SHEET carries the badge and the MARKUP carries the number:
       - no data-count attribute at all  -> no badge, no reserved gap, nothing
         changes for any tab that has no count to show;
       - data-count="19"                 -> the count, in tabular mono;
       - data-count=""                   -> an em dash, because "we do not know
         yet" is a different statement from "there are none", and rendering an
         unknown as 0 is a lie the reader cannot see through.
     `order: 2` is what puts a ::before AFTER the label without a second
     element: the button is a flex row, so the pseudo-element's order is a
     layout property rather than a DOM one. ::after was not available — site.css
     spends it on the selected tab's rule, and overwriting that would delete the
     one signal that does not depend on colour. */
.tabs button[data-count]::before,
.bd-win[data-count]::before {
  content: attr(data-count);
  order: 2;
  min-width: 1.6em;
  padding: 2px 6px;
  border-radius: var(--r-xs);
  border: 1px solid var(--edge-hair);
  background: var(--surface-sink);
  color: var(--ink-3);
  font: 600 10.5px/1.35 var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: .06em;
  text-align: center;
}
.tabs button[data-count=""]::before,
.bd-win[data-count=""]::before {
  content: "\2014";
  opacity: .6;
}
.tabs button[aria-selected="true"][data-count]::before {
  border-color: color-mix(in srgb, var(--accent) 46%, transparent);
  background: var(--accent-wash-2);
  color: var(--ink-1);
}

/* ---- the readout line: a count is a reading, not a sentence ----
   These six elements are the only places on the site where a script already
   writes a REAL number into a plain paragraph — the number of listings that
   survived the filter, the size of a collection, the species on the island,
   the auctions running. They were `<p class="dim">`: grey body copy, the same
   weight as a caption. Given the mono face, tabular figures and a hairline of
   their own they read as an instrument's readout, which is what they are.
   Selected by id because that is what the scripts already write into — no new
   class, no new hook, and nothing for a script to remember.

   ★ TABULAR FIGURES AND THE READING INK, AND DELIBERATELY NOT THE MONO FACE.
     The first cut set `font-family: var(--font-mono)` on all six, which is
     what the benchmark does — its entire voice is uppercase mono. Shot on the
     map page and it was wrong here, for a reason that is about OUR copy rather
     than about taste: every one of these six elements renders a SENTENCE, not
     a figure. "Points of interest could not be loaded. The basemap still
     works." set in a monospace face is two lines of code, and it is the honest
     failure state that webcore is better at than the benchmark — the copy that
     names the fix. Wearing a terminal face makes it look like a stack trace.
     Tabular figures are the part that was actually worth taking: they stop the
     count jittering the line as it re-renders, which is a real defect, and
     they cost the prose nothing. */
#market-count,
#cards-status,
#pop-status,
#au-status,
#bd-note,
#poi-status {
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* ---- the meter: magnitude as a bar, in plain CSS ----
   The benchmark draws every bar with a background and a width and no chart
   library at all, and so does this. Two consumers today: the vault's slot
   meter, whose fill width vaultplus.js already writes as an inline style, and
   the boards page's ranked rows (see boards.html). The track is a sunken well
   with a real inner shadow; the fill is the owner's accent ramp with a bloom,
   so a full bar looks lit rather than painted. */
.vp-track,
.bd-track {
  box-shadow: 0 1px 2px var(--edge-sink) inset, 0 0 0 1px var(--edge-hair) inset;
  background-image: linear-gradient(180deg, var(--veil-strong), transparent);
}
.vp-track b,
.bd-fill {
  background-image: linear-gradient(90deg, var(--accent), var(--accent-hi));
  box-shadow: 0 0 10px var(--accent-glow);
}

/* ---- the live bar closes the page the way the nav opens it ----
   It is the only element on the site that is on every page at every scroll
   position, and it was a flat strip. The rail is the same two-stroke idea the
   instrument uses: one stroke says which way the light falls, one carries the
   owner's accent across the width. .lb-src already takes ok/warn/bad classes
   from site.js, so the state colour is read from the contract site.js already
   publishes rather than from a new hook. */
#wc-livebar {
  box-shadow: 0 -1px 0 var(--edge-top) inset, 0 -18px 34px -24px var(--veil-deep), 0 0 0 1px var(--edge-hair) inset;
  background-image: linear-gradient(180deg, var(--veil-mid), var(--veil-strong)), var(--grain);
  -webkit-backdrop-filter: blur(14px) saturate(112%);
          backdrop-filter: blur(14px) saturate(112%);
}
#wc-livebar::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 24%, transparent) 46%, transparent 78%);
}
.lb-src {
  box-shadow: 0 0 0 1px var(--edge-hair) inset, 0 1px 2px var(--edge-sink);
}
.lb-src.on  { box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 40%, transparent) inset, 0 0 12px var(--ok-halo); }
.lb-src.off { box-shadow: 0 0 0 1px color-mix(in srgb, var(--bad) 40%, transparent) inset, 0 0 12px var(--bad-halo); }

/* ---- the nav bar earns the same depth as the bar at the other end ---- */
.nav {
  background-image: linear-gradient(180deg, var(--veil-strong), var(--veil-mid)), var(--grain);
  box-shadow: 0 1px 0 var(--edge-sink), 0 18px 34px -26px var(--veil-deep), 0 -1px 0 var(--edge-top) inset;
}

/* Forced colours: every gradient above collapses to a system colour and every
   shadow to nothing, which turns the washes into opaque slabs over the
   artwork. Strip the decoration and keep the structure — the same stance the
   rest of this sheet takes. */
@media (forced-colors: active) {
  .tabs button[data-count]::before,
  .bd-win[data-count]::before,
  #wc-livebar::before { display: none; }
  .gate, .notice, .pill, .tabs, .tabs button[aria-selected="true"], #wc-livebar, .nav { box-shadow: none; }
  .nav, #wc-livebar, .tabs, .pill, .card, .card:hover, .gate, .notice { background-image: none; }
}
@media print {
  .card, .card:hover, .gate, .notice { background-image: none; }
  #wc-livebar,
  #wc-livebar::before { display: none; }
}
