@font-face {
    font-family: 'Fistalle';
    src: url('assets/fonts/Fistalle.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    /* paint the title immediately in a fallback, then swap to Fistalle when it loads —
       no invisible-text (FOIT) delay while the font downloads */
    font-display: swap;
}

@font-face {
    font-family: 'IoskeleyMono';
    src: url('assets/fonts/IoskeleyMono-Light.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

html {
    overflow-x: hidden;
    -webkit-text-size-adjust: 100%;
    /* THE master scale. Everything sized in rem/em is a multiple of this, so the whole
       page (title, buttons, columns, margins, calendar, text) scales as ONE unit and all
       proportions are kept. Caps at 1rem so wide screens look exactly as before, then
       shrinks with the viewport ALL THE WAY DOWN — NO floor — so every element keeps
       scaling uniformly as you narrow; the page only changes character at the mobile
       breakpoint. Knob: 1.1vw sets how fast it scales; 16px is the full-size ceiling. */
    font-size: min(1.1vw, 16px);
    /* ── Max content width — the fix for "it stops scaling and just stretches" ──
       The master scale above CAPS at 16px, which it reaches at a viewport of 16 / 0.011 ≈ 1454px.
       Past that, every rem is frozen but the containers are still width:100%, so the page stops
       SCALING and starts STRETCHING: the columns get wider and the ENTER/SAVE row (pinned to the right
       gutter) walks off toward the screen edge while the type stays put. Capping the content at 90rem
       stops that — the layout freezes at the exact size it had when scaling ended, and the extra width
       becomes margin instead.
       Why 90rem specifically: below the cap, root = 1.1vw, so 90rem = 90 × 0.011 × W ≈ 0.99 × W — i.e.
       it's effectively full-bleed and NOTHING about narrow/medium screens changes. At the cap it works
       out to ~1440px, and there it stays. Widen this to let the page run wider on big monitors. */
    --page-max: 90rem;
    /* Distance from a SCREEN edge to the capped content box's edge — i.e. the margin the cap creates.
       0 while the page is full-bleed, growing once --page-max bites. Anything that needs to break OUT
       of the content box and touch the screen (the beveled rules) offsets by this. Deliberately free of
       percentages, so it means the same thing wherever it's used: a % here would resolve against
       whichever element it landed in (the title box, say) instead of the page. */
    --page-gutter: max(0px, calc((100vw - var(--page-max)) / 2));
    /* shared side indent for the title, its buttons AND the body columns, in rem so it
       scales with the master font-size; steps down on mobile below. */
    --title-indent: 4rem;
    /* settings panel scale — steps down at the same breakpoints as the title/buttons
       (the panel transform: scale()s by this). 1 = full size on desktop. */
    --ui-scale: 1;
    /* Root background-color is propagated by iOS into the safe-area strips (notch /
       status bar / overscroll). iOS won't tile the granite IMAGE into that strip, only
       this flat color — so set it to the granite texture's own average tone (#f5eef1, the
       measured mean of granite.jpg) so the strip reads as the same pale stone as the
       page instead of the old flat yellow. Also the first-paint color before the texture
       loads. */
    background-color: #f5eef1;
    background-image: url("assets/webpages/granite.jpg");
    /* Tile size in rem so the granite scales on the SAME root unit as every element — as
       the page resizes the texture grows/shrinks WITH the title etc. instead of them
       gliding across a fixed-size texture. 16rem = the old 16rem at root 16. */
    background-size: 16rem;
    /* anchor the repeating texture from the top-left with the per-load random
       pixel offset (set on <html> by the inline script) so it stays put when the
       viewport width changes — `center` re-phased the tiling on every resize */
    background-position: var(--bg-offset-x, 0rem) var(--bg-offset-y, 0rem);
    background-attachment: local;
}

body {
    background: transparent;
    font-family: 'Fistalle', sans-serif;
    /* reset the default UA body margin: the full-width title mark is clipped to
       the body box (overflow-x:hidden), so an 0.5rem margin would stop the mark
       short of the screen's left edge (this was why CAMP looked different) */
    margin: 0;
    overflow-x: hidden;
}

/* iOS safe area (notch / status bar): the content pages set viewport-fit=cover so the
   <html> granite background extends edge-to-edge UNDER the notch (matching the game) —
   otherwise iOS fills that strip with the page's flat background-color (the yellow
   --page-color). With the page now under the notch, push the topmost content (the title
   bar, the highest element on every page) down by the safe-area inset so it isn't hidden
   behind the notch/status bar. env() resolves to 0 on desktop and non-cover browsers, so
   this is a no-op everywhere except cover-mode iOS. */
.title-outer {
    padding-top: env(safe-area-inset-top, 0px);
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
}

.title-outer {
    /* Title font-size == mark height. Shared on this common ancestor of the <h1>
       and .title-actions so the buttons size + position off the mark and track it
       at every breakpoint. Overridden in the same @media blocks the title uses. */
    /* Pure rem now, so the title scales with the master root font-size in lockstep with
       everything else (no separate vw rate). At full root it's 3rem = 3rem; it shrinks as
       the root shrinks. The mobile block below still overrides it for phones. */
    --title-fs: 3rem;
    /* ── Shared look for ALL FOUR beveled rules under the title (the two full-width ::before/::after
       and the two title-box <i class="title-rule"> lines). Each rule sets only its own WIDTH (left/right)
       and VERTICAL offset; the bevel itself is defined ONCE here so all four stay in sync — change a
       colour or thickness here and every rule, on every page, updates. */
    --rule-thickness: 0.1rem;                        /* border width = bevel depth */
    --rule-height:    calc(var(--title-fs) * 0.08);  /* bar height */
    --rule-fill:      rgba(0, 0, 0, 0.1);            /* faint dark fill (page shows through) */
    --rule-shadow:    rgba(0, 0, 0, 0.3);           /* recessed edge → top + left */
    --rule-highlight: rgb(255, 255, 255, 0.6);      /* lit edge → bottom + right */
    /* How far the THREE FULL-WIDTH rules (::before at 0.8, .title-rule-mid at 0.9, ::after at 1.0 ×
       --title-fs) drop below their authored offsets — the ones that run under the desk sub-title. One
       knob so all three move as a set and keep their spacing; raise it to push them further down the
       page, set it to 0 to put them back where they were. Does NOT touch the four short rules inside
       the title box (those sit at --title-rule-top and stay pinned to the title).

       The rules stay put; it's the SUB-TITLE that's positioned to meet them — its bottom border sits on
       this top rule's top border (see the derivation on .title-sub's transform). So this number sets
       where the line group lives, and the sub follows it: move it and redo that transform. */
    --rule-drop: calc(var(--title-fs) * 0.28);
    /* ── Full-bleed escape hatch for the three horizontal rules ──
       .title-outer is now capped at --page-max and centred, so its absolutely-positioned rules would
       stop at the CONTENT edge on a wide monitor. This reaches them back out to the viewport: a
       NEGATIVE offset of exactly the margin the cap created. It's 0 while the page is full-bleed, so
       narrow screens are unaffected. Used as `left: calc(var(--rule-bleed) + <inset>)`, same for right. */
    --rule-bleed: calc(-1 * var(--page-gutter));
    min-height: auto;
    height: auto;
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
    /* guaranteed minimum gap between the title and the action buttons: the buttons'
       margin-left:auto eats free space on wide screens, but this column-gap is reserved
       first, so when the title grows / the page narrows the buttons can't close right up
       against it. Scales with the title. */
    column-gap: calc(var(--title-fs) * 0.6);
    width: 100%;
    /* Stop stretching once the master scale caps out (see --page-max on html): past ~1454px the page
       would otherwise keep widening with the type frozen, walking the ENTER/SAVE row off to the screen
       edge. Capped + centred, so the buttons stay on the same right margin as the body columns below,
       and the extra screen width becomes side margin. The full-width rules (::before/::after and
       .title-rule-mid) are positioned inside this box, so they end at the content edge too. */
    max-width: var(--page-max);
    margin-inline: auto;
    position: relative;
    z-index: auto;
    padding: 2em 0 0 var(--title-indent);
    /* keep the 4em padding inside the 100% width so the title-outer never grows
       wider than its container — otherwise pages without a global border-box
       reset (CAMP, PumpRoom) overflow page-center and get re-centered, shifting
       the title left of where the bordered-box pages render it */
    box-sizing: border-box;
}

/* ── Page body: THE content box. Every page wraps everything below the title bar
   (.title-outer) in a single <div class="page-body"> — the location buttons +
   scoreboard on index, the log + calendars on DraftingRoom, the image grids on
   Catalyst / AnnexBloom, the scan row on CAMP, the editor on PumpRoom. This one rule
   is the only place the page's side margins are set, so changing it here moves the
   content edge on EVERY page at once, index included.

   The box matches the title bar exactly: same --title-indent side padding, same
   --page-max cap, same centring — so the body's left edge lines up under the title
   and its right edge under the title-action buttons, with equal margins at any width.
   Content inside must NOT re-add a side indent of its own; it just fills this box.
   The top margin clears the title bar's bottom ridge line. */
.page-body {
    width: 100%;
    max-width: var(--page-max);
    margin: 8rem auto 0;
    padding: 0 var(--title-indent);
    box-sizing: border-box;
}

/* ── Two-column body (log column + calendar/notes column) — the standard location-page
   layout. Just the row: its side margins come from .page-body above, like every other
   page's content. Everything here is in rem, so the column gaps, the calendar width and
   (via inherited em text) all the log + calendar text scale with the master root
   font-size — one proportion. The calendar caps at 44rem then shrinks. */
.log-calendar-row {
    display: flex;
    align-items: flex-start;
    gap: 1.5rem;
    width: 100%;
}
.log-calendar-row > .log-section { flex: 1; min-width: 0; }
.log-calendar-row > #right-column {
    /* rem cap + floor scale with the master font-size; the 48vw middle lets it keep
       shrinking on narrow screens so the log never gets squeezed to nothing. */
    width: clamp(17.5rem, 48vw, 44rem);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}
/* (No per-column font-size overrides: the log + calendar text is em-based and inherits the
   master root font-size, so it scales in proportion with everything else automatically.) */
/* "MENU" back-to-index button — same beveled, page-colored look as the in-page
   arrow buttons (e.g. CAMP's #camp-pdf-nav button). Lives inside .title-outer as a
   flex item: margin-left:auto pushes it to the right end of the title's row and
   align-self:center lines it up vertically with the title. */
.menu-btn {
    align-self: center;
    margin-left: auto;
    margin-right: 1.5em;
    cursor: pointer;
    font-family: 'Fistalle', sans-serif;
    font-size: 1rem;
    letter-spacing: 0.125rem;
    line-height: 1;
    padding: 0.375rem 1rem;
    color: #fff;
    text-decoration: none;
    background: var(--page-color, #05489b);
    border-top:    0.25rem outset rgba(255, 255, 255, 0.55);
    border-left:   0.25rem outset rgba(255, 255, 255, 0.3);
    border-bottom: 0.25rem outset rgba(0, 0, 0, 0.65);
    border-right:  0.25rem outset rgba(0, 0, 0, 0.35);
}
.menu-btn:active { border-style: inset; }

h1 {
    color: rgb(255, 255, 255);
    margin: 0;
    font-family: Fistalle, sans-serif;
    font-weight: bold;
    --title-ls: .45em;
    font-size: var(--title-fs);
    letter-spacing: var(--title-ls);
    display: inline-block;
    width: auto;
    line-height: 1.2;
    white-space: nowrap;   /* never let the title stack onto a second line — it shrinks instead */
}
/* Granite for the iOS notch / status-bar strip ONLY. The ROOT (html) background-image is
   NOT re-tiled by iOS into the safe-area strip — it only fills that strip with the root
   background-COLOR — so the texture never reached it (the flat color showed: first yellow,
   now black). We CAN'T just paint a full-viewport granite layer: the title's letters are
   filled with the SAME granite at the SAME fixed anchor (background-clip:text +
   attachment:fixed), so a full fixed-granite background matches the letters exactly and the
   title vanishes. Instead paint granite ONLY in the top safe-area strip, which sits ABOVE
   the title (the title bar is padded down by the same inset), so it never touches the text.
   height:0 on desktop / non-notch browsers → invisible no-op. Same size + per-load offset as
   the page granite so it lines up seamlessly with the scrolling background at the top. */
body::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: env(safe-area-inset-top, 0px);
    z-index: 0;
    pointer-events: none;
    background-color: #f5eef1;   /* granite's average tone — fallback / shows if the inset is 0 */
    background-image: url("assets/webpages/granite.jpg");
    background-size: 16rem;
    background-position: var(--bg-offset-x, 0rem) var(--bg-offset-y, 0rem);
    background-repeat: repeat;
}

body > * {
    position: relative;
    z-index: 1;
}

/* Remove stacking context so mix-blend-mode can reach the page background */
.page-center {
    z-index: auto;
}

mark {
    background: var(--page-color);
    color: #ffffff;
    display: inline-block;
    padding: 0rem 0 0 4rem;
}

/* Center wrapper for scoreboard and content (shared) */
.page-center {
    width: 100%;
    max-width: 100vw;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
}

.scoreboard-section {
    width: 26.875rem;
    height: 31.3125rem;
    margin: 2.5rem auto 0 auto;
    padding: 1.25rem;
    background:
        linear-gradient(rgba(255, 255, 255, 0.267), rgba(255, 255, 255, 0.253)),
        url('assets/webpages/greenGranite.jpg') center / cover no-repeat;
    position: relative;
    border-top: 0.125rem solid #dfdfdf;
    border-left: 0.125rem solid #dfdfdf;
    border-bottom: 0.125rem solid #404040;
    border-right: 0.125rem solid #404040;
    flex-shrink: 0;
    overflow: hidden;
    align-self: center;
    box-sizing: border-box;
}

/* Top border — mitered corners */
.scoreboard-section::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1.25rem;
    background: url('assets/webpages/celticBack.jpg') repeat-x;
    background-size: auto 2.5rem;
    clip-path: polygon(1.25rem 0, calc(100% - 1.25rem) 0, 100% 100%, 0 100%);
    z-index: 3;
    pointer-events: none;
    opacity: 1;
    mix-blend-mode: overlay;
    transform: scaleY(-1);
}

/* Bottom border — mitered corners */
.scoreboard-section::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 1.25rem;
    background: url('assets/webpages/celticBack.jpg') repeat-x;
    background-size: auto 2.5rem;
    clip-path: polygon(1.25rem 0, calc(100% - 1.25rem) 0, 100% 100%, 0 100%);
    z-index: 3;
    pointer-events: none;
    opacity: 1;
    mix-blend-mode: overlay;
}

.scoreboard-inner {
    width: 100%;
    height: 100%;
    padding: 0;
    position: relative;
    box-sizing: border-box;
}

.scoreboard-bevel {
    padding: 1.5rem 2rem 2rem 2rem;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    border-top: 0.125rem solid #404040;
    border-left: 0.125rem solid #404040;
    border-bottom: 0.125rem solid #dfdfdf;
    border-right: 0.125rem solid #dfdfdf;
}

/* Right border */
.scoreboard-inner::before {
    content: '';
    position: absolute;
    top: -1.25rem;
    right: 0;
    width: 31.0625rem;
    height: 1.25rem;
    background: url('assets/webpages/celticBack.jpg') repeat-x;
    background-size: auto 2.5rem;
    clip-path: polygon(1.25rem 0, calc(100% - 1.25rem) 0, 100% 100%, 0 100%);
    transform-origin: top right;
    transform: rotate(-90deg);
    z-index: 2;
    pointer-events: none;
    opacity: 1;
    mix-blend-mode: overlay;
}

/* Left border */
.scoreboard-inner::after {
    content: '';
    position: absolute;
    top: -1.25rem;
    left: 0;
    width: 31.0625rem;
    height: 1.25rem;
    background: url('assets/webpages/celticBack.jpg') repeat-x;
    background-size: auto 2.5rem;
    clip-path: polygon(1.25rem 0, calc(100% - 1.25rem) 0, 100% 100%, 0 100%);
    transform-origin: top left;
    transform: rotate(90deg);
    z-index: 2;
    pointer-events: none;
    opacity: 1;
    mix-blend-mode: overlay;
}


.scoreboard-section h2 {
    font-size: 1.5em;
    margin-bottom: 0.125rem;
    color: #ff0000;
    font-family: 'Fistalle', sans-serif;
    letter-spacing: 0.25rem;
    mix-blend-mode:soft-light;
    border-top: 0.125rem solid #dfdfdf;
    border-left: 0.125rem solid #dfdfdf;
    border-bottom: 0.125rem solid #404040;
    border-right: 0.125rem solid #404040;
    text-shadow:
        0.05rem 0.03125rem 0rem #ffffff,
        -0.05rem 0 0rem #000000,
        0.05rem 0 0rem #ffffff,
        0.05rem -0.05625rem 0rem #000000;
}

/* ── Logging-only scoreboard (location pages) ───────────────────────────────
   The leaderboard now lives on index.html. Location pages still load
   scoreboard.js so the in-game SAVE keeps logging kills, but the board itself
   is neutralized here — only the name prompt surfaces (for a brand-new player
   on save). These are scoped overrides; the full styling above is untouched and
   still used by the index. Add class "sb-logging-only" to #scoreboard-mount. */
.sb-logging-only .scoreboard-section,
.sb-logging-only .scoreboard-inner,
.sb-logging-only .scoreboard-bevel {
    display: block;
    width: auto; height: auto; min-height: 0;
    margin: 0; padding: 0;
    border: none; background: none; box-shadow: none; overflow: visible;
}
.sb-logging-only .scoreboard-section::before,
.sb-logging-only .scoreboard-section::after,
.sb-logging-only .scoreboard-inner::before,
.sb-logging-only .scoreboard-inner::after,
.sb-logging-only .celtic-sides { display: none; content: none; }
/* hide everything in the board except the on-demand name prompt */
.sb-logging-only .scoreboard-bevel > *:not(#namePrompt) { display: none; }

.scoreboard-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 1rem;
}
.scoreboard-table th,
.scoreboard-table td {
    padding: 0.375rem 0rem;
    text-align: left;
    color: #797700;
    border-bottom: 0.0625rem solid rgba(255,255,255,0.15);
    font-family: 'Fistalle', sans-serif;
    letter-spacing: 0.03125rem;
    mix-blend-mode:difference;
    text-shadow:
        0.05rem 0.03125rem 0rem #ffffff,
        -0.05rem 0 0rem #000000,
        0.05rem 0 0rem #000000,
        0.05rem -0.05625rem 0rem #000000;
}
.scoreboard-table th {
    font-weight: 600;
    background: rgba(255, 255, 255, 0);
    letter-spacing: 0.03125rem;
}
.scoreboard-table td {
    color: #fffeb5;
    mix-blend-mode:hard-light;


}

#logKillsBtn {
    padding: 0.625rem 1.5rem;
    font-size: 0.9375rem;
    background: rgba(180, 40, 40, 0.85);
    color: #ffffff;
    border: 0.0625rem solid rgba(255,100,100,0.4);
    border-radius: 0.375rem;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s;
    margin-right: 0.5rem;
}
#logKillsBtn:hover {
    background: rgba(200, 60, 60, 0.95);
    border-color: rgba(255,150,150,0.6);
}
#logKillsBtn:disabled {
    opacity: 0.5;
    cursor: default;
}
.kill-info {
    display: inline-block;
    margin-left: 0.75rem;
    color: #ff4400;
    font-size: 0.875rem;
}

/* ═════════════════ MOBILE (phones), max-width 600px ═════════════════
   Single shared mobile block. Placed after all the base rules it overrides so
   equal-specificity rules win on source order. Page-specific mobile tweaks
   (e.g. index.html's location tiles / quality picker) live in that page's own
   <style>, not here. The separate max-width:12.5rem block below handles tiny widths. */
/* max-width covers portrait phones; max-height covers the SAME phones rotated to
   landscape (the long side is now >37.5rem wide, but the short side is <37.5rem tall),
   so the title, MENU button and other mobile sizing stay identical when rotated
   instead of jumping to desktop size. */
#saveKillsBtn {
    padding: 0.625rem 1.5rem;
    font-size: 0.9375rem;
    background: rgba(180, 40, 40, 0.85);
    color: #ffffff;
    border: 0.0625rem solid rgba(255,100,100,0.4);
    border-radius: 0.375rem;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s;
    margin-top: 0.75rem;
    mix-blend-mode: overlay;
    display: block;
    width: 100%;
}
#saveKillsBtn:hover {
    background: rgba(200, 60, 60, 0.95);
    border-color: rgba(255,150,150,0.6);
}
#saveKillsBtn:disabled {
    opacity: 0.5;
    cursor: default;
}

/* ── Location thumbnail buttons (index.html) ── */
.imgWrap img {
    display: block;
}
/* Bevel widths / insets / press offsets are in rem, so they scale with the root in step
   with the rem-sized location button (.box) and stay proportionate at every screen size.
   (0.5rem/0.125rem/0.375rem @ root 16 = 0.5rem/0.125rem/0.375rem.) */
.imgWrap {
    position: relative;
    display: block;
    mix-blend-mode: difference;
    transform: translateY(-0.125rem);
    cursor: pointer;
}
.imgWrap::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 1;
    border-top:    0.5rem inset rgba(255, 255, 255, 0.55);
    border-left:   0.5rem inset rgba(255, 255, 255, 0.3);
    border-bottom: 0.5rem inset rgba(0, 0, 0, 0.65);
    border-right:  0.5rem inset rgba(0, 0, 0, 0.35);
}
.imgWrap::after {
    content: "";
    position: absolute;
    inset: 0.5rem;
    pointer-events: none;
    z-index: 1;
    border-top:    0.125rem inset rgba(0, 0, 0, 0.5);
    border-left:   0.125rem inset rgba(0, 0, 0, 0.5);
    border-bottom: 0.125rem inset rgba(255, 255, 255, 0.35);
    border-right:  0.125rem inset rgba(255, 255, 255, 0.35);
}
.imgWrap:hover {
    filter: sepia(1);
}
.imgWrap:active {
    transform: translateY(0.1875rem);
}
.imgWrap:active::before {
    border-top:    0.375rem inset rgba(0, 0, 0, 0.7);
    border-left:   0.375rem inset rgba(0, 0, 0, 0.35);
    border-bottom: 0.375rem inset rgba(255, 255, 255, 0.35);
    border-right:  0.375rem inset rgba(255, 255, 255, 0.5);
}
.imgWrap:active::after {
    inset: 0.375rem;
    border-top:    0.125rem inset rgba(255, 255, 255, 0.35);
    border-left:   0.125rem inset rgba(255, 255, 255, 0.35);
    border-bottom: 0.125rem inset rgba(0, 0, 0, 0.5);
    border-right:  0.125rem inset rgba(0, 0, 0, 0.5);
}

/* ── Gallery: images in a flow ───────────────────────────────────────────────
   A reusable block — CAMP is just the first page to carry one, so nothing here is
   named for it. Drop <div class="gallery"> inside any page's .page-body and fill it
   with .gallery-item figures; the side margins come from .page-body like all other
   content, so the gallery must not add an indent of its own.

   THE FLOW IS CSS COLUMNS, not a grid. Photographs arrive at whatever height they
   happen to be, and a grid would either crop them to a common ratio or leave ragged
   holes under the short ones. Multi-column instead lets each image keep its own shape
   and packs the next one directly beneath it, so a column of tall and short pictures
   closes up on its own. It also reflows for free: column-width (not column-count) means
   the browser fits as many columns as the page is wide, so the same markup is one
   column on a phone and four on a desktop with no breakpoints to maintain.

   The trade, stated because it will look like a bug otherwise: columns flow TOP TO
   BOTTOM and then across, so reading order runs down column one before column two. For
   a gallery that is fine. If a future use needs strict left-to-right order, this is the
   rule to replace, not to patch.

   Everything is in rem so the whole block scales with the master root font-size, like
   the rest of the body content. */
.gallery {
    /* Target column width — the browser divides the available space by this and rounds,
       so it is a "how big should a picture be" knob rather than a column count. */
    --gallery-col: 18rem;
    --gallery-gap: 1rem;
    --gallery-bevel: 0.2rem;

    column-width: var(--gallery-col);
    column-gap: var(--gallery-gap);
    width: 100%;
    /* Clears whatever content sits above it. No side margin — .page-body owns those, and
       adding any here would push the gallery out of line with the rest of the page. The
       last row of items keeps its own bottom margin, which is the block's trailing space. */
    margin: 2rem 0 0;
}

.gallery-item {
    /* Without this an image can be sliced in half across a column boundary — the single
       most important line in the block. */
    break-inside: avoid;
    -webkit-column-break-inside: avoid;   /* older WebKit spelling */
    page-break-inside: avoid;             /* and the legacy one, for print + old engines */
    display: block;
    /* Vertical rhythm only: the horizontal gaps are column-gap's job. figure carries a
       default margin in every browser, so this resets it rather than adding to it. */
    margin: 0 0 var(--gallery-gap);
}

.gallery-item img {
    display: block;      /* kills the inline-baseline gap under the image */
    width: 100%;
    height: auto;        /* the whole point — every picture keeps its own aspect ratio */
    /* Raised bevel, the same idiom (and the same two greys) the blend blocks and the
       title rules use, so a gallery reads as part of the page rather than a web widget.
       border-box because these pages have no global reset: without it the border is
       added OUTSIDE the 100% width and the last column overflows its track. */
    box-sizing: border-box;
    border-top:    var(--gallery-bevel) solid #ffffff;
    border-left:   var(--gallery-bevel) solid #ffffff;
    border-bottom: var(--gallery-bevel) solid #404040;
    border-right:  var(--gallery-bevel) solid #404040;
}

/* The picture is clickable. `zoom-in` is the browser's own magnifier-with-a-plus, and it
   is what CAMP's scan images already use (#camp-pdf-stage img), so click-to-enlarge looks
   the same everywhere on the site. */
.gallery-item img {
    cursor: zoom-in;
}

/* ── Gallery lightbox ────────────────────────────────────────────────────────
   Built and owned by gallery.js — there is no markup for it in any page. One is
   created on first use and reused by every gallery on the page. It is the ONLY
   fullscreen viewer on the site: CAMP's scans open in this one too, which is why the
   zoom and pan below are written here rather than on any page.

   Scroll to zoom, drag to pan, arrows to cycle, Esc or the backdrop to leave. */
.gallery-lightbox {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(0, 0, 0, 0.88);
    justify-content: center;
    align-items: center;
    /* Clicking the backdrop closes, so the backdrop advertises it. */
    cursor: zoom-out;
    /* A zoomed picture runs past the viewport on every side; it is reached by dragging,
       never by scrolling the overlay. */
    overflow: hidden;
}
.gallery-lightbox.active { display: flex; }

/* The transformed box. It is exactly the size of the picture and origin 0 0, which is what
   lets gallery.js zoom toward the cursor with a plain subtraction. Sized by the img inside
   it, so it must not stretch: flex centring on the parent would otherwise pull it taller. */
.gallery-lightbox-content {
    position: relative;
    flex: 0 0 auto;
    transform-origin: 0 0;
    /* The picture is NOT a close target — clicking it should do nothing, so it must not
       inherit the backdrop's zoom-out and promise otherwise. */
    cursor: default;
}
/* Only a zoomed-in picture has anywhere to be dragged to, so only then does it offer the
   hand. gallery.js sets both classes. */
.gallery-lightbox.zoomed  .gallery-lightbox-content { cursor: grab; }
.gallery-lightbox.dragging .gallery-lightbox-content { cursor: grabbing; }

.gallery-lightbox img {
    max-width: 92vw;
    max-height: 92vh;
    display: block;
    user-select: none;
    -webkit-user-drag: none;
}

/* Arrows + close. Same beveled, page-coloured button as CAMP's scan arrows and .menu-btn:
   outset borders that flip to inset when pressed, which is the whole visual language of
   the site's controls. */
.gallery-lightbox button {
    position: absolute;
    cursor: pointer;
    font-size: 1.125rem;
    line-height: 1;
    padding: 0.25rem 1.125rem;
    color: #fff;
    background: var(--page-color, #05489b);
    border-top:    0.25rem outset rgba(255, 255, 255, 0.55);
    border-left:   0.25rem outset rgba(255, 255, 255, 0.3);
    border-bottom: 0.25rem outset rgba(0, 0, 0, 0.65);
    border-right:  0.25rem outset rgba(0, 0, 0, 0.35);
}
.gallery-lightbox button:active { border-style: inset; }

/* Vertically centred against the viewport, not the picture, so they hold still while you
   cycle through images of different heights. */
.gallery-lightbox .gallery-prev,
.gallery-lightbox .gallery-next { top: 50%; transform: translateY(-50%); }
.gallery-lightbox .gallery-prev  { left: 1.5rem; }
.gallery-lightbox .gallery-next  { right: 1.5rem; }
.gallery-lightbox .gallery-close { top: 1.5rem; right: 1.5rem; }

/* Hidden when there is only one picture: arrows that cannot go anywhere are worse than
   no arrows. gallery.js sets this. */
.gallery-lightbox.single .gallery-prev,
.gallery-lightbox.single .gallery-next { display: none; }

/* Optional caption — a .gallery-item is a <figure>, so it can carry a <figcaption>.
   Inherits the page's font; em-based so it tracks whatever size it is dropped into. */
.gallery-item figcaption {
    margin-top: 0.4em;
    font-size: 0.85em;
    line-height: 1.3;
}

/* ── Blend mode test blocks ── (rem throughout so the whole strip scales with the root
   like the rest of the body content; px values are the @ root-16 equivalents)

   DEBUG-ONLY: this is a reference swatch, not page content, so it's hidden unless DEBUG is
   on (the switch in index's SETTINGS panel → localStorage 'debugMode'). Each page stamps
   class="debug" on <html> in its <head> from that key — early, so the row never flashes in
   before being hidden — and index's switch toggles the class live. */
#blend-test-row {
    display: none;
    flex-wrap: wrap;
    gap: 0.75rem;
    width: 100%;
    max-width: 43.75rem;
    margin: 1.5rem auto 0;
    z-index: auto;
}
.debug #blend-test-row {
    display: flex;
}

.blend-test-block {
    position: relative;
    /* four to a row: 4 × (25% − ¾ of the 0.75rem gap) + 3 gaps = 100% exactly. That only
       holds if the borders + padding are counted INSIDE the width — without border-box the
       block is 1.5rem wider than it asks for and only three fit (index + CAMP wrapped 3×5
       while Catalyst, which has its own `* { box-sizing: border-box }` reset, showed 4×4). */
    box-sizing: border-box;
    width: calc(25% - 0.5625rem);
    aspect-ratio: 1 / 1;
    border-top: 0.25rem solid #ffffff;
    border-left: 0.25rem solid #ffffff;
    border-bottom: 0.25rem solid #404040;
    border-right: 0.25rem solid #404040;
    overflow: hidden;
    padding: 0.5rem;
}

.blend-test-block .blend-bg {
    position: absolute;
    inset: 0;
    background: var(--page-color, #888);
}

.blend-test-block .blend-bevel {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    border-top: 0.125rem solid #404040;
    border-left: 0.125rem solid #404040;
    border-bottom: 0.125rem solid #dfdfdf;
    border-right: 0.125rem solid #dfdfdf;
    display: flex;
    align-items: center;
    justify-content: center;
}

.blend-test-block .blend-label {
    color: #fff;
    font-size: 0.75em;
    letter-spacing: 0.04em;
    text-shadow: 0 1px 3px rgba(0,0,0,0.8);
    pointer-events: none;
    text-align: center;
}

/* ── Title styling for index.html ── */
h2 {
    font-family: fistalle, sans-serif;
    font-weight: 400;
    font-stretch: condensed;
    word-spacing: 0.5em;
    text-shadow:
                0.03125rem 0.03125rem 0 #fcfae2,
                -0.0625rem -0.0625rem 0 #000000,
                0.0625rem -0.0625rem 0 #000000,
                -0.03125rem 0.03125rem 0 #fffcd4;
    margin: 0;
    line-height: 1;
    margin-left: 4em;

}

.title {
    margin: 0;
    text-align: left;
    --title-bevel: #000000;
}

/* the colored box around the title. A plain container (not <mark>) so it doesn't
   inherit the shared `mark` padding. inline-block shrink-wraps the beveled text, so the
   negative-margin letter-spacing cancel below sizes correctly and the border hugs it. */
/* transparent frame so the blue mark behind it shows through; beveled outset
   (lit top-left, shadowed bottom-right) + cast shadow so it rises off the page */
.title .title-box {
    display: inline-block;
    background: transparent;
    /* collapse the inline "strut" so this wrapper's height comes from its inner
       box, not its inherited line-height — otherwise the border can't shrink */
    line-height: 0;
    /* no real border on this box itself; the raised beveled frame is drawn by the
       ::before overlay below (so it adds no layout/reflow). */
    border: 0;
    /* containing block for the ::before frame overlay */
    position: relative;
}

/* ── Raised beveled frame around the title ──
   A single RAISED (outset) bevel: lit on the top-left (grey #b8b8b8, softer than pure
   white) and shadowed on the bottom-right (#404040), so the title reads as a panel
   lifting off the page toward the viewer. Drawn as one overlay frame on .title-box's
   ::before with NEGATIVE insets so it sits just outside the letters without reflowing
   the title — the mark and SETTINGS buttons stay put. Widths/insets are in em so it
   scales with the title. */
.title .title-box::before {
    content: "";
    position: absolute;
    pointer-events: none;
    box-sizing: border-box;
    inset: -0.14em -0.2em;
    /* ── Inner gem facet ──
       A polished, faceted edge: a bright SPECULAR highlight on the top-left (a hard
       near-white glint, as if a light source sits up-left) fading to a deep, near-black
       shadow on the bottom-right. Per-side alpha steps the light around the bezel (top
       brightest → left → right → bottom darkest) so the corners read as cut facets
       catching the light rather than a flat frame. */
    border-top:    0.045em solid rgba(255, 255, 255, 0.62);
    border-left:   0.045em solid rgba(255, 255, 255, 0.48);
    border-bottom: 0.045em solid rgba(15, 15, 20, 0.55);
    border-right:  0.045em solid rgba(15, 15, 20, 0.45);
    /* Stepped inner facets — THREE crisp ridges marching in from the border toward the text.
       Each is an inset box-shadow with ZERO blur (hard edge, so the steps stay sharp/faceted,
       NOT rounded): a light band offset up-left + a dark band offset down-right, at growing
       offsets so they read as distinct cut steps. Listed tightest-first (earlier shadows paint
       on top), and it's all box-shadow so no size/reflow is added. */
    box-shadow: inset  0.016em  0.016em 0 0 rgba(255, 255, 255, 0.60),
                inset -0.016em -0.016em 0 0 rgba(10, 10, 15, 0.55),
                inset  0.042em  0.042em 0 0 rgba(255, 255, 255, 0.34),
                inset -0.042em -0.042em 0 0 rgba(10, 10, 15, 0.38),
                inset  0.070em  0.070em 0 0 rgba(255, 255, 255, 0.18),
                inset -0.070em -0.070em 0 0 rgba(10, 10, 15, 0.22),
                /* two more steps reaching deeper into the space inside the frame toward the
                   text, fading out so the innermost ridges sit softly against the letters */
                inset  0.098em  0.098em 0 0 rgba(255, 255, 255, 0.12),
                inset -0.098em -0.098em 0 0 rgba(10, 10, 15, 0.15),
                inset  0.126em  0.126em 0 0 rgba(255, 255, 255, 0.07),
                inset -0.126em -0.126em 0 0 rgba(10, 10, 15, 0.10);
    /* ICE/GEM MIX — blend the whole beveled facet (its lit + shadowed ridges) into the
       granite/treeline behind it, so the edges refract the background instead of sitting on
       top as flat lines. `overlay` lightens the highlights and deepens the shadows against
       the backdrop for a cut-glass look. Knob — swap the value to re-mix:
         overlay     → glassy, punchy facets (default)
         soft-light  → gentler, frostier ice
         hard-light  → sharper, harder gem edges
         screen      → highlights only, very icy/bright
         luminosity  → keeps backdrop colour, takes the bevel's light */
    mix-blend-mode: hard-light;
}

/* ── Outer gem facet ──
   A second, thinner beveled ring set just OUTSIDE the inner facet, same light direction,
   with a small gap between the two so the pair reads as a stepped, cut-gem bezel (two
   parallel bright ridges up-left, two shadow ridges down-right). A soft white outer glow
   makes the whole jewel look like it's catching light and lifting off the page. */
.title .title-box::after {
    content: "";
    position: absolute;
    pointer-events: none;
    box-sizing: border-box;
    inset: -0.24em -0.32em;
    border-top:    0.03em solid rgba(255, 255, 255, 0.48);
    border-left:   0.03em solid rgba(255, 255, 255, 0.38);
    border-bottom: 0.03em solid rgba(0, 0, 0, 0.42);
    border-right:  0.03em solid rgba(0, 0, 0, 0.34);
    /* Soft outer bloom + cast shadow (kept blurred — that's the lift-off-page drop shadow, not
       a bevel edge), THEN two more crisp inset ridges (zero blur) marching IN from this outer
       edge — they land in the gap between this ring and the inner facet, so the pair of existing
       rings now has extra stepped ridges between them. Sharp steps, no rounding, no size change. */
    box-shadow: 0 0 0.18em rgba(255, 255, 255, 0.28),
                0.04em 0.06em 0.18em rgba(0, 0, 0, 0.35),
                inset  0.018em  0.018em 0 0 rgba(255, 255, 255, 0.50),
                inset -0.018em -0.018em 0 0 rgba(10, 10, 15, 0.46),
                inset  0.045em  0.045em 0 0 rgba(255, 255, 255, 0.26),
                inset -0.045em -0.045em 0 0 rgba(10, 10, 15, 0.30);
    /* same ICE/GEM MIX as the inner facet so the two rings refract the background as one
       piece of glass (see the knob list on .title-box::before). */
    mix-blend-mode: hard-light;
}

/* inner ring carries the same bevel direction to deepen the raised look */
.title .bevel-out {
    display: inline-block;
    padding: -10.15em -1.9em;
    /* same strut collapse as .title-box so the border tracks the inner text height */
    line-height: 0;
    /* host the blue mark here (not on .title-bevel) so the mark stays outside
       the title text's mix-blend-mode group and keeps blending against the page */
    position: relative;
}


/* recessed ring: shadowed top-left, lit bottom-right */


/* innermost ring holds the text with no padding so the border hugs it on all sides. */
.title .title-bevel {
    display: inline-block;
    padding: 0;
    /* line-height sets the box that clip-path (below) measures from. Keep it >=1
       so the box top clears the cap tops — a sub-1 line box sits BELOW the caps,
       so clip-path trims the glyph TOPS no matter what the top inset is. The
       glyphs don't move (baseline-aligned + centred), so the mark and plinth,
       which key off this element's 50% centre, stay put. */
    line-height: 1;
    position: relative;
    /* balance the trailing letter-spacing (added after the last glyph) with an
       equal leading indent so the beveled borders sit centered on the text */
    text-indent: var(--title-ls);
    /* subtle outline so the texture-filled glyphs keep a crisp edge;
       brightness() lifts the granite fill so the letters read lighter */
    /* Fill the glyphs with the SAME granite texture as the page background, clipped
       to the letter shapes. background-attachment: fixed anchors the texture to the
       viewport so it reads like the stone showing straight through the letters —
       and it never touches the blue mark (the mark is a separate sibling). */
    color: transparent;
    -webkit-text-fill-color: transparent;
    background-image: url("assets/webpages/granite.jpg");
    background-size: 16rem;
    background-attachment: fixed;
    -webkit-background-clip: text;
    background-clip: text;
    /* slightly transparent granite fill so the gloss + treeline behind read faintly through
       the letters (glassy), instead of a fully opaque stone fill */
    opacity: 0.9;
    /* crop the rendered glyphs: inset(top right bottom left). The bottom value is
       larger than the top so the letters are trimmed more along the bottom edge —
       raise it to cut more off the bottom, lower it to cut less. */
    clip-path: inset(0 0 0 0);
}

/* simple highlight mark: a snug band behind the title text (z-index:-1), the
   height of the text and just a touch wider, like a highlighter swipe. */
.title .bevel-out::before {
    content: "";
    position: absolute;
    z-index: -1;
    /* Confine the band to the inside of the title's beveled frame so the treeline is
       CROPPED at the borders (no trees poking out the top). The frame's outer ridge
       sits at inset -0.14em (vertical) / -0.2em (horizontal) on .title-box, ~0.03em
       outside .bevel-out's margin — so the frame interior runs from ~0.11em above
       .bevel-out's top to ~0.11em below its bottom, and ~0.17em past each side. */
    top: 100%;
    /* Expanded to reach OUT UNDER the beveled facet rings (::before at ~0.14/0.20em out,
       ::after at ~0.24/0.32em out) so the treeline texture — not the page granite — sits
       beneath the now semi-transparent borders. Height/offset cover ~0.24em above and below
       the text box; left/right reach ~0.32em past each side, matching the outer facet edge. */
    height: 1.48em;                 /* 1em text box + ~0.24em top + ~0.24em bottom */
    transform: translateY(-1.24em); /* raise so the band spans from above the top facet to below the bottom facet */
    left: -0.32em;
    right: -0.32em;
    /* treeline image behind the text, tinted to the page color WITHOUT darkening it:
       a solid page-color layer sits ON TOP of the image and `color` blends them, so
       the result takes the page color's hue+saturation but keeps the image's original
       brightness (luminance). That recolors the trees instead of shading them dark
       like `multiply` did. For a softer/partial tint, fade the gradient toward
       transparent or swap to `overlay` / `soft-light`. */
    background-image: linear-gradient(var(--page-color), var(--page-color)),
                      url("assets/textures/Weymouth.jpg");
    background-blend-mode: color;
    /* slightly see-through so the page granite behind the box glimmers through the fill —
       glassier, less like a solid painted band */
    opacity: 0.82;
    /* ICE/GEM MIX — blend the treeline TEXTURE into the page granite behind it so the fill
       reads like something frozen/suspended inside the gem rather than a flat printed band.
       Knob — swap the value to re-mix (same family as the facets):
         hard-light  → texture punches through, icy contrast (default)
         overlay     → texture melts into the background more
         soft-light  → faint, frosted-glass texture
         luminosity  → keeps the page granite's colour, takes the treeline's light */
    filter: contrast(1.5) brightness(1) saturate(1.05);
    /* tiles 3em wide; FIXED 1.5em tall (taller than the ~1.22em band) and anchored to
       the bottom, so the band crops the tree TOPS at the frame instead of squashing
       the whole treeline shorter. Raise this height to crop more off the top; lower it
       toward the band height to show the full trees. */
    background-size: 3.4em 2.4em;
    background-position: center bottom;
    background-repeat: repeat-x;
}

/* ── Specular gloss across the gem face ──
   A diagonal band of light glancing off the polished surface: a strong glint in the
   top-left corner and a fainter secondary reflection in the bottom-right, with the
   middle left clear so the title stays readable. `screen` blend only ever LIFTS the
   surface toward white (never darkens), so it reads as a reflection on the gem rather
   than a grey wash. Sits above the mark/treeline but BELOW the text (z-index), so it
   shines on the stone around the letters without hazing them over. */
.title .bevel-out::after {
    content: "";
    position: absolute;
    pointer-events: none;
    z-index: 1;
    inset: -0.11em -0.17em;   /* fill the frame interior, matching the treeline band */
    background: linear-gradient(125deg,
        rgba(255, 255, 255, 0.16) 0%,
        rgba(255, 255, 255, 0.05) 16%,
        rgba(255, 255, 255, 0.00) 38%,
        rgba(255, 255, 255, 0.00) 63%,
        rgba(255, 255, 255, 0.03) 86%,
        rgba(255, 255, 255, 0.09) 100%);
    mix-blend-mode: screen;
}

/* keep the granite letters crisply above the gloss and treeline */
.title .title-bevel { z-index: 2; }

/* ── A single letter of the title, made a link ──
   Used for the I of LOCATIONS on index (→ the ISLAND axon). The letter must look EXACTLY like its
   neighbours, and the way it gets there is by being as close to "not an element" as possible.

   The glyphs of the title are painted TRANSPARENT; what you see is .title-bevel's granite
   background, clipped to the text (background-clip: text). That fill is painted by the PARENT, so
   it only reaches this letter while the letter stays in the parent's paint — give the anchor its
   own stacking context (position + z-index, opacity, filter, transform …) and the parent's clipped
   background stops at its box, leaving a transparent glyph and an I-shaped HOLE in the title.
   So: no positioning, no z-index, no filter here. The title's facet/gloss layers on top are all
   pointer-events:none, so the click reaches the letter anyway. */
.title .title-link {
    color: inherit;
    -webkit-text-fill-color: transparent;
    text-decoration: none;
    cursor: pointer;
}
/* Hover/focus: the letter fills in with colour — the one tell that it's a button.
   It must be -webkit-text-fill-color, not `color`: the inherited fill is transparent (that's what
   lets the parent's granite show through the glyph), and text-fill-color WINS over color, so
   setting only `color` here would change nothing. The filled glyph paints over the granite,
   because text is drawn above the background it's clipped from. Knob: --title-link-hover. */
.title .title-link:hover,
.title .title-link:focus-visible {
    -webkit-text-fill-color: var(--title-link-hover, #ffd479);
    color: var(--title-link-hover, #ffd479);
    outline: none;
}

/* ── Extra beveled border ring inside the title box — RECESSED (inset) ──
   One more bevel frame in the GAP between the inner facet (.title-box::before at ~0.14/0.20em
   out) and the outer facet (.title-box::after at ~0.24/0.32em out). This ring is INSET (recessed)
   as part of the undulating inset/outset/inset/outset run of rings: light direction is FLIPPED
   from the raised facets — shadowed on the top-left, lit on the bottom-right — so it reads as a
   groove cut below the surface. Own pseudo box + box-sizing:border-box + negative inset +
   mix-blend, so it stays sizeless and refracts the background. Knobs: inset, border widths, alphas. */
.title .title-bevel::before {
    content: "";
    position: absolute;
    pointer-events: none;
    z-index: 3;                 /* above the granite text */
    inset: -0.19em -0.26em;     /* land in the gap between the inner + outer facet rings */
    box-sizing: border-box;
    border-top:    0.03em solid rgba(10, 10, 15, 0.50);   /* inset: dark up-left */
    border-left:   0.03em solid rgba(10, 10, 15, 0.40);
    border-bottom: 0.03em solid rgba(255, 255, 255, 0.55);/* inset: lit down-right */
    border-right:  0.03em solid rgba(255, 255, 255, 0.42);
    mix-blend-mode: overlay;
}

/* ── Another beveled border ring — the innermost one, RECESSED (inset) ──
   The tightest ring, hugging the letters just inside the inner facet (.title-box::before). INSET
   (recessed) — the first step of the undulating inset/outset run: light direction FLIPPED from
   the raised facets (shadowed top-left, lit bottom-right), so it reads as a groove around the
   text. Own pseudo box + box-sizing:border-box + negative inset + mix-blend, so it stays sizeless
   and refracts the background. Knobs: the inset (how tight to the text), border widths, alphas. */
.title .title-bevel::after {
    content: "";
    position: absolute;
    pointer-events: none;
    z-index: 3;                 /* above the granite text */
    inset: -0.08em -0.12em;     /* hug the letters, just inside the inner facet ring */
    box-sizing: border-box;
    border-top:    0.028em solid rgba(10, 10, 15, 0.46);   /* inset: dark up-left */
    border-left:   0.028em solid rgba(10, 10, 15, 0.36);
    border-bottom: 0.028em solid rgba(255, 255, 255, 0.50);/* inset: lit down-right */
    border-right:  0.028em solid rgba(255, 255, 255, 0.38);
    mix-blend-mode: overlay;
}


/* ── Full-width beveled rule across the screen, below the title ──
   A thin RAISED bevel bar (lit on top + left, shadowed on bottom + right) that
   spans the FULL viewport width and resizes with it at any screen size. It hangs
   off .title-outer (not .bevel-out): .title-outer is position:relative, width:100%
   with the body margin reset to 0, so its left/right edges sit on the screen's
   left/right edges. left:0 + right:0 stretch the bar edge to edge responsively, and
   because it's a border-box element the left/right beveled end-caps land flush
   INSIDE those edges (visible, not pushed off-screen). It's anchored to the title
   bar's bottom (top:100%) and dropped by a fraction of the title size so the gap
   below the title tracks the title at every breakpoint. All four borders are semi-
   transparent + mix-blend so the page background image reads through them. (On pages
   where .title-outer isn't full-bleed — e.g. CAMP / PumpRoom if page-center is
   re-centered — the bar follows .title-outer's width.) Knobs: --rule-thickness, the
   0.28 gap, the 0.18 height. */
.title-outer::after {
    content: "";
    position: absolute;
    /* Anchor to the TITLE's bottom, NOT the container's bottom. .title-outer is a flex
       row whose height grows with its tallest child — and the ENTER/SAVE buttons are
       2×--title-fs, taller than the title, so `top:100%` (container bottom) would drop
       these lines whenever those buttons appear. The title bottom is a fixed distance
       from the container top: padding-top (2em) + h1 line box (line-height 1.2 ×
       --title-fs). Adding the 1.0×--title-fs gap keeps this the lowest of the rules.
       All title-derived, so it tracks every breakpoint but is immune to the buttons. */
    top: calc(2em + var(--title-fs) * 1.2 + var(--title-fs) * 1 + var(--rule-drop));
    /* Reach back out past the content cap to the screen edges (see --rule-bleed), then inset 1em so the
       beveled end-caps land visibly inside them. Edge to edge at ANY width. */
    left:  calc(var(--rule-bleed) + 1em);
    right: calc(var(--rule-bleed) + 1em);
    /* shared bevel look — all four rules use these --rule-* vars (defined on .title-outer) */
    height: var(--rule-height);
    box-sizing: border-box;
    background: var(--rule-fill);
    border-top:    var(--rule-thickness) solid var(--rule-shadow);
    border-left:   var(--rule-thickness) solid var(--rule-shadow);
    border-bottom: var(--rule-thickness) solid var(--rule-highlight);
    border-right:  var(--rule-thickness) solid var(--rule-highlight);
    pointer-events: none;
    z-index: 5;    /* over the desk sub-title (4), so the line runs unbroken across it */





}


/* Second copy of the beveled rule, sitting CLOSER to the title (smaller gap) so it
   rides just above the ::after line. Same bevel/fill/thickness — only the vertical
   offset differs. */
.title-outer::before {
    content: "";
    position: absolute;
    /* Same title-anchored offset as ::after (see there) — immune to the button-row
       height — just a smaller 0.8×--title-fs gap so it rides above the ::after line. */
    top: calc(2em + var(--title-fs) * 1.2 + var(--title-fs) * 0.8 + var(--rule-drop));
    /* full-bleed past the content cap — see --rule-bleed */
    left:  calc(var(--rule-bleed) + 0.3em);
    right: calc(var(--rule-bleed) + 0.3em);
    /* shared bevel look — see --rule-* on .title-outer */
    height: var(--rule-height);
    box-sizing: border-box;
    background: var(--rule-fill);
    border-top:    var(--rule-thickness) solid var(--rule-shadow);
    border-left:   var(--rule-thickness) solid var(--rule-shadow);
    border-bottom: var(--rule-thickness) solid var(--rule-highlight);
    border-right:  var(--rule-thickness) solid var(--rule-highlight);
    z-index: 5;    /* over the desk sub-title (4), so the line runs unbroken across it */
    pointer-events: none;
}


/* ── Third full-width rule, BETWEEN ::before and ::after ──
   .title-outer only has two pseudo-element slots (::before/::after), both used by the
   two full-width rules above, so this third full-width line is a REAL <i class="title-rule-mid">
   dropped straight into .title-outer (which is full-bleed + position:relative, so left/right
   insets reach the screen edges just like the pseudo rules). Same title-anchored top formula
   as those, with the 0.9×--title-fs gap — exactly halfway between ::before (0.8) and ::after
   (1.0) — so it stays centered and, like them, is immune to the button-row height. Inset from
   both edges (a left/right margin) like the other two, so it doesn't run fully edge to edge. */
.title-outer > .title-rule-mid {
    position: absolute;
    top: calc(2em + var(--title-fs) * 1.2 + var(--title-fs) * 0.9 + var(--rule-drop));
    /* full-bleed past the content cap — see --rule-bleed */
    left:  calc(var(--rule-bleed) + 0.65em);
    right: calc(var(--rule-bleed) + 0.65em);
    height: var(--rule-height);
    box-sizing: border-box;
    background: var(--rule-fill);
    border-top:    var(--rule-thickness) solid var(--rule-shadow);
    border-left:   var(--rule-thickness) solid var(--rule-shadow);
    border-bottom: var(--rule-thickness) solid var(--rule-highlight);
    border-right:  var(--rule-thickness) solid var(--rule-highlight);
    pointer-events: none;
    z-index: 5;    /* over the desk sub-title (4), so the line runs unbroken across it */
}


/* ── Title-box-width rules ──
   REAL <i class="title-rule"> elements inside .title-box (see each page's title markup) — not pseudo-
   elements. Because they all share the SAME host box (.title-box, position:relative), every rule gets
   the identical left/right → identical width automatically, with no per-element fudge. Each <i> only
   sets its own height via --title-rule-top (inline style). They're position:absolute (out of flow) so
   they never grow the box or get wrapped by the raised frame. Add another rule by dropping one more
   <i class="title-rule" style="--title-rule-top: …em"></i> into the title box — no pseudo-slot limit.
   Change the width/thickness/bevel here once and every rule (on every page) updates together. */
.title .title-box > .title-rule {
    position: absolute;
    /* Near end → the SCREEN's left edge, at any width. The title box sits --title-indent in from the
       content box, and the content box is --page-gutter in from the screen once the cap bites — so
       reaching the screen means backing out past both. (--title-indent alone only worked while the
       title bar was full-bleed; --page-gutter is 0 then, so narrow screens are unchanged.) */
    left:  calc(-1 * (var(--title-indent) + var(--page-gutter)));
    right: -.5em;                              /* far end → past the title box (tracks the title width) */
    top:   calc(100% + var(--title-rule-top, 0.4em));   /* vertical: each <i> sets --title-rule-top */
    /* shared bevel look — see --rule-* on .title-outer */
    height: var(--rule-height);
    box-sizing: border-box;
    background: var(--rule-fill);
    border-top:    var(--rule-thickness) solid var(--rule-shadow);
    border-left:   var(--rule-thickness) solid var(--rule-shadow);
    border-bottom: var(--rule-thickness) solid var(--rule-highlight);
    border-right:  var(--rule-thickness) solid var(--rule-highlight);
    pointer-events: none;
    /* ON TOP of everything else in the title box — including the desk sub-title (z-index 4) that rides
       across them. Without this the rules paint at z-index:auto, so the sub-title covers them and cuts
       four gaps in the lines; with it the lines run unbroken OVER the sub, the same way they cut across
       the bottom of the title above. Highest in the box's z ladder: glyphs 2, bevel rings 3, sub 4. */
    z-index: 5;
}


/* ── Desk sub-title ──
   Each computer's own name (e.g. "Drafting Room") set small and sitting under the LOCATION title,
   aligned to the box's LEFT edge and riding ACROSS the four beveled rules — which are drawn OVER it
   (see the z-index note on .title-rule), the same way they cut across the bottom of the title above.
   Sized at 0.75× the main title (--title-fs). Lives inside .title-box (position:relative) so left:0
   tracks the title box at every breakpoint. */
.title .title-box > .title-sub {
    position: absolute;
    top: 100%;
    /* Nudged in from the title box's left edge (0 = flush with it). In the sub's own (0.75×) em. */
    left: 0.9em;
    /* How much further DOWN the plate reaches, and the letters with it. The top edge is anchored (top:100%
       + the translateY below), so this is added as padding-TOP: the box grows downward only, and the text
       rides down by the same amount instead of staying pinned near the top of a taller plate. 0 restores
       the original tight plate. In the sub's own (0.75×) em — so it scales with the title at every
       breakpoint. THE one knob for this; don't also offset .title-sub-text. */
    --title-sub-drop: 0.16em;
    /* Vertical position, in the sub's own (0.75×) em. TUNED so that AT --title-sub-drop:0 the sub's BOTTOM
       border lands exactly on the TOP border of the first of the three full-width rules below it.
       Derivation, all in --title-fs (the sub's em is 0.75 of it), measured down from the title box's bottom:
           top rule's top edge = 0.8 (its authored offset) + --rule-drop (0.28)  = 1.080
           sub plate height    = padding 0.14em × 2 + line-height 1              = 0.960
           so the sub's top    = 1.080 − 0.960                                   = 0.120  F
           as the sub's own em = 0.120 / 0.75                                    = 0.160em
       Which means this is COUPLED to three things: --rule-drop, the sub's padding, and its font-size
       ratio. Change any of them and redo the arithmetic above, or the line drifts off the border.
       --title-sub-drop deliberately breaks that landing — it extends the plate PAST the first rule, which
       is what it's for. Only the TOP edge is held by the arithmetic above; the bottom is free to travel. */
    transform: translateY(0.16em);
    font-size: calc(var(--title-fs) * 0.75);   /* 0.75× the title text size */
    letter-spacing: 0.06em;
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
    z-index: 4;                       /* above the granite glyphs (2) + bevel rings (3) */
    /* room inside for the beveled frame (drawn on ::before/::after) to hug the text, plus the drop above
       on the TOP side only — that's what pushes both the letters and the plate's bottom edge downward. */
    padding: calc(0.14em + var(--title-sub-drop)) 0.34em 0.14em;
}

/* ── Sub-title letters — the SAME granite text as the title ──
   The letters aren't coloured: they're cut-outs with the page's own granite showing through, exactly
   as .title .title-bevel does it. background-attachment:fixed anchors the stone to the VIEWPORT (not
   to this box), so the sub's letters are cut from the same continuous slab as the title's and the page
   behind — one piece of rock seen through the type, not a swatch pasted into each glyph.

   WHY THIS LIVES ON AN INNER <span> AND NOT ON .title-sub ITSELF:
   the granite arrives as a background clipped to the text, and an element's own background paints
   BELOW its negative-z-index children — so .title-sub's ::before plate (z-index:-1) drew straight over
   the letters and they vanished, leaving just the textured box. Content, however, paints ABOVE those
   children. So the letters have to be CONTENT of .title-sub, not its background. That is precisely why
   the title nests .bevel-out > .bevel-in > .title-bevel: fill on the ancestor, glyphs on a descendant. */
.title .title-box > .title-sub > .title-sub-text {
    color: transparent;
    -webkit-text-fill-color: transparent;
    background-image: url("assets/webpages/granite.jpg");
    background-size: 16rem;           /* same tile scale as the title's letters */
    background-attachment: fixed;
    -webkit-background-clip: text;
    background-clip: text;
    /* No keyline: the title's letters don't carry one either, and a text-shadow under a transparent
       fill would show straight THROUGH the glyphs as a grey smear rather than outlining them. */
    /* NOTE: nothing positions the letters here. They're moved by .title-sub's --title-sub-drop, which
       carries the plate down with them — a relative offset on this span would slide the text out of its
       plate instead, which is the thing we don't want. Tune the drop there, not here. */
}

/* ── Sub-title box — the title's own fill, in a lighter frame ──
   Same raised, faceted bevel as the big title box (see .title-box::before/::after) but lightened:
   brighter highlights, softer shadows. The FILL is now the title's exact background — the treeline
   photo tinted to --page-color (see .title .bevel-out::before, which this mirrors line for line) —
   so the sub reads as the same material as the title instead of a pale cream plate. ::before is the
   inner facet + fill; ::after is the thinner outer ring + soft drop shadow. Insets/widths/tiling are
   in the sub's own (0.75×) em, so the frame AND the trees scale down with the smaller text. */
.title .title-box > .title-sub::before {
    content: "";
    position: absolute;
    pointer-events: none;
    box-sizing: border-box;
    inset: 0;
    z-index: -1;                      /* behind the sub's text, inside its own box */
    /* Three stacked layers, top to bottom:
         1. SPECULAR GLOSS — the same diagonal glint as the title's gem face (.title .bevel-out::after):
            a strong highlight in the top-left corner, a fainter secondary reflection bottom-right, and
            the middle left clear so the letters stay readable. `screen` only ever LIFTS toward white,
            never darkens, so it reads as light glancing off a polished surface rather than a grey haze.
            Alphas run a touch hotter than the title's because the brightness() filter below knocks this
            layer down along with the fill. The title gets its gloss from a whole separate pseudo-element;
            the sub has both of its pseudos spoken for (this plate + the outer ring), so it rides here as
            a background layer instead — same look, no extra element.
         2. TREELINE TINT — a solid page-colour layer that `color`-blends onto the photo below, taking
            the page's hue+saturation but keeping the image's brightness (recolours the trees rather
            than shading them dark). Identical recipe to the title's band.
         3. The treeline photo itself. */
    background-image:
        linear-gradient(125deg,
            rgba(255, 255, 255, 0.22)  0%,
            rgba(255, 255, 255, 0.07) 16%,
            rgba(255, 255, 255, 0.00) 38%,
            rgba(255, 255, 255, 0.00) 63%,
            rgba(255, 255, 255, 0.05) 86%,
            rgba(255, 255, 255, 0.14) 100%),
        linear-gradient(var(--page-color), var(--page-color)),
        url("assets/textures/Weymouth.jpg");
    background-blend-mode: screen, color, normal;
    /* per-layer: gloss spans the whole plate; the trees tile and crop their tops at the frame */
    background-size:     100% 100%,   3.4em 2.4em,  3.4em 2.4em;
    background-position: center,      center bottom, center bottom;
    background-repeat:   no-repeat,   repeat-x,      repeat-x;
    /* DARKER than the title's band (which sits at brightness 1) so the sub reads as the same material
       in shadow — the cream text also gains contrast against it. This is the knob: lower to darken
       further (0.5 is quite deep), back to 1 to match the title exactly. */
    filter: contrast(1.5) brightness(0.7) saturate(1.05);
    opacity: 0.82;                    /* glassy — the page granite glimmers through, as in the title */
    /* ── The ONE border, cut INSET (recessed) ──
       Light direction is FLIPPED from a raised frame: SHADOWED on the top-left, LIT on the bottom-right
       — the same trick the title's recessed rings use (.title-bevel::before/::after). That single flip
       is what makes the plate read as a groove pressed INTO the page instead of a slab sitting on it.
       This is now the only frame on the sub: the outer ring pseudo, its stepped ridges and its
       lift-off-the-page drop shadow are all gone. */
    border-top:    0.03em solid rgba(10, 10, 15, 0.55);    /* inset: dark up-left */
    border-left:   0.03em solid rgba(10, 10, 15, 0.45);
    border-bottom: 0.03em solid rgba(255, 255, 255, 0.60); /* inset: lit down-right */
    border-right:  0.03em solid rgba(255, 255, 255, 0.48);
    /* soft inner shade falling from the top-left lip — the cast shadow of a recess, which is what sells
       the depth. Raise the alpha/blur to sink it deeper; drop it for a flat, purely-linear inset. */
    box-shadow: inset 0.06em 0.06em 0.14em rgba(0, 0, 0, 0.45);
    /* NOTE: no mix-blend-mode here any more. The old pale plate used hard-light to sit into the page;
       with a real textured fill that blend would double up on the treeline and blow out its contrast.
       Add `mix-blend-mode: hard-light` back if you want the icier, gem-like mix the title comments
       describe as a knob. */
}
/* (No ::after on the sub-title. It used to carry a second, outer bevel ring plus the glow and cast
   shadow that lifted the plate off the page — both removed: the sub now has exactly ONE border, and a
   recess shouldn't cast a shadow outward anyway.) */


/* ── Title action buttons (SETTINGS / BUTTON) ──
   A row of buttons that sit on the blue title mark. Placed as the last flex item
   inside .title-outer: margin-left:auto shoves the row to the far right, align-self:
   center vertically centers it on the title mark, and margin-right is the gap from
   the screen's right edge. position/z-index keeps them above the mark (z-index:-1). */
.title-actions {
    display: flex;
    gap: 2em;
    /* Lock the button bottoms to the title's bottom BORDER, derived from the title itself:
         • align-self: flex-end snaps the row to the title's h1 box bottom, so if the
           title moves/grows the buttons follow automatically.
         • the frame's bottom border sits --title-frame-overhang below the title text box
           (the .title-box::before bottom inset, 0.14em), and h1's line-height adds
           --title-line-leading of bottom leading below that box (1.2 line-height →
           (1.2-1)/2 ≈ 0.10em). The border ends up the DIFFERENCE below the h1 box bottom,
           so drop the row by exactly that. Both terms are the title's own numbers and are
           scaled by --title-fs, so this tracks the title at every size and breakpoint. */
    align-self: flex-end;
    --title-frame-overhang: 0.14;   /* .title-box::before bottom inset, in --title-fs units */
    --title-line-leading:   0.10;   /* h1 line-height 1.2 → (1.2 - 1) / 2 bottom leading */
    /* Drop the row onto the title's bottom border. Use a NEGATIVE margin-bottom (not a
       transform): with align-self:flex-end this pushes the row's border box that far below
       the line end — same visual offset as translateY — but a transform would become the
       containing block for the position:fixed MENU and trap it inside this row instead of
       the viewport corner. */
    margin-bottom: calc(-1 * var(--title-fs) * (var(--title-frame-overhang) - var(--title-line-leading)));
    margin-left: auto;
    margin-right: var(--title-indent);   /* same inset as the title's left indent — tracks breakpoints */
    position: relative;
    z-index: 1;
    
}

/* Each button is half the mark height. Everything is derived from --title-fs (the
   mark height) so the buttons scale and stay positioned on the mark at every size:
   height is half the mark; font-size/padding/letter-spacing/border-width scale too.
   Beveled, page-colored look matching the MENU button across pages. */
.title-btn {
    /* All button sizing derives from --btn-fs, which defaults to the mark height
       (--title-fs). Per-button overrides (e.g. the 2× ENTER/SAVE below) just bump
       --btn-fs, so the whole bevel + text scale together and the layout box grows
       with them (no transform overlap). */
    --btn-fs: var(--title-fs);
    height: calc(var(--btn-fs) * 0.7);  /* taller than the old 0.5 */
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 calc(var(--btn-fs) * 0.18);   /* original width: content + this side padding */
    cursor: pointer;
    font-family: 'Fistalle', sans-serif;
    font-size: calc(var(--btn-fs) * 0.27);
    letter-spacing: calc(var(--btn-fs) * 0.03);
    line-height: 1;
    color: #ffffff;
    text-decoration: none;   /* used on <a> MENU links too, not just <button>s */
    /* transparent fill so only the beveled border + text show; the blue mark
       shows through behind the button */
    background: rgba(0, 0, 0, 0.3);
    /* reversed (inset) bevel, with no bottom border (override the UA default too) */
    border-top:    calc(var(--btn-fs) * 0.04) inset rgba(0, 0, 0, 0.65);
    border-left:   calc(var(--btn-fs) * 0.04) inset rgba(0, 0, 0, 0.35);
    border-right:  calc(var(--btn-fs) * 0.04) inset rgba(255, 255, 255, 0.3);
    border-bottom: none;
}
/* ENTER and SAVE: twice as big as the other title-bar buttons. Everything is
   derived from --btn-fs, so doubling it scales height/padding/text/bevel as one. */
#gameEnterBtn, #gameSaveBtn {
    --btn-fs: calc(var(--title-fs) * 2);
}
/* pressed: flip to a raised bevel for click feedback */
.title-btn:active {
    border-top:    calc(var(--btn-fs) * 0.04) outset rgba(255, 255, 255, 0.55);
    border-left:   calc(var(--btn-fs) * 0.04) outset rgba(255, 255, 255, 0.3);
    border-right:  calc(var(--btn-fs) * 0.04) outset rgba(0, 0, 0, 0.35);
}
/* ENTER/SAVE label text — engraved look copied from the old floating-column buttons'
   inner span: mid-grey fill, overlay blend against the button so it tints to the
   green/red behind it, and a 4-way emboss text-shadow (black below/right, white
   above/left). It's a child element so mix-blend-mode has the button to blend against. */
.title-btn-label {
    display: inline-block;
    color: #555555;
    mix-blend-mode: overlay;
    text-shadow:
        0rem 0.03125rem 0rem #000000,
        -0.05rem 0   0rem #ffffff,
        0.05rem  0   0rem #000000,
        0rem -0.05625rem 0rem #ffffff;
}

/* The game's ENTER/SAVE title-bar buttons ship hidden (only the GameEngine reveals
   them in fullscreen). .title-btn sets display:inline-flex, which would beat the UA
   [hidden] rule, so restate it. */
.title-btn[hidden] { display: none; }
/* disabled = ENTER still loading, or SAVE with no kills to log */
.title-btn:disabled {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
}
/* MENU is the only <a> title-btn — pull it out of the title-actions row and pin it to the
   top-left corner of the screen. Short and wide (not the square shape), but keeps the same
   beveled .title-btn look (border/background/colour all inherited). */
a.title-btn {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1000;
    aspect-ratio: auto;                         /* drop the square ratio */
    height: calc(var(--title-fs) * 0.38);       /* smaller / shorter */
    padding: 0 calc(var(--title-fs) * 0.6);     /* wide: generous side padding */
    font-size: calc(var(--title-fs) * 0.22);
}

/* ── ENTER is ready: the game finished loading + GPU-warming, so webpageBonfire.js
   cleared the button's `disabled` attribute (see _enableEnter). While it's enabled and
   visible, make it REALLY glow green — green fill, glowing text, layered green halo, and
   a pulsing animation so it reads as "click me". Only #gameEnterBtn, not SAVE. */
#gameEnterBtn:not(:disabled) {
    color: #f0fff5;
    /* Green-tinted granite: paint the SAME granite texture as the page, then multiply the
       page-color over it (background-blend-mode) so the button reads as the granite
       showing through, tinted green. background-attachment: fixed anchors the tiling to
       the viewport so it lines up with the page granite behind it. (mix-blend-mode can't
       reach the page background here — .title-actions z-index:1 isolates it — so we blend
       the green into the granite inside the button itself.) */
    background-color: transparent;
    /* match the old floating-column buttons' green tint (rgba(67,120,94,0.55) → #43785e),
       the "good green" we're copying — deeper and more muted than the old #79b896 */
    --enter-green: #43785e;
    background-image:
        linear-gradient(var(--enter-green), var(--enter-green)),
        url("assets/webpages/granite.jpg");
    background-size: auto, 16rem;
    background-position: 0 0, var(--bg-offset-x, 0rem) var(--bg-offset-y, 0rem);
    background-attachment: fixed, fixed;
    background-blend-mode: multiply;   /* green gradient × granite = green-tinted granite */
    /* ready: INVERSE the bevel to a RAISED (outset) frame — light highlight on top+left,
       dark shadow on bottom+right — so the button appears to come up toward the viewer.
       (override the base .title-btn's border-bottom:none too.) */
    border-top:    calc(var(--btn-fs) * 0.04) outset rgba(170, 255, 200, 0.65);
    border-left:   calc(var(--btn-fs) * 0.04) outset rgba(170, 255, 200, 0.5);
    border-right:  calc(var(--btn-fs) * 0.04) outset rgba(0, 80, 28, 0.5);
    border-bottom: calc(var(--btn-fs) * 0.04) outset rgba(0, 80, 28, 0.7);
    text-shadow:
        0 0 0.375rem  rgba(91, 119, 101, 0.95),
    /* static inset glow (no animation) — pale green, hugs the button edges */
}
/* clicking the ready ENTER button: make it look pressed like the settings-panel .qbtn —
   the raised bevel reverses to a sunken (inset) one and the button drops down. Higher
   specificity than .title-btn:active, so it overrides that button's generic feedback. */
#gameEnterBtn:not(:disabled):active {
    transform: translateY(calc(var(--btn-fs) * 0.04));   /* drop down on press */
    border-top:    calc(var(--btn-fs) * 0.04) inset rgba(0, 80, 28, 0.7);
    border-left:   calc(var(--btn-fs) * 0.04) inset rgba(0, 80, 28, 0.5);
    border-right:  calc(var(--btn-fs) * 0.04) inset rgba(170, 255, 200, 0.5);
    border-bottom: calc(var(--btn-fs) * 0.04) inset rgba(170, 255, 200, 0.65);
}

/* ── SAVE is clickable: the player has kills to log, so _refreshSaveButton cleared
   the `disabled` attribute. Mirror the ENTER-ready treatment but in light red —
   INVERSE the base inset bevel to a RAISED (outset) frame and tint the granite red
   so it reads as "you can click me". */
#gameSaveBtn:not(:disabled) {
    color: #fff2f2;
    background-color: transparent;
    /* the button's own light red (a soft, pale red — not crimson) */
    --save-red: #d98c8c;
    background-image:
        linear-gradient(var(--save-red), var(--save-red)),
        url("assets/webpages/granite.jpg");
    background-size: auto, 16rem;
    background-position: 0 0, var(--bg-offset-x, 0rem) var(--bg-offset-y, 0rem);
    background-attachment: fixed, fixed;
    background-blend-mode: multiply;   /* red gradient × granite = red-tinted granite */
    /* inverted bevel: light highlight on top+left, dark shadow on bottom+right */
    border-top:    calc(var(--btn-fs) * 0.04) outset rgba(255, 200, 200, 0.65);
    border-left:   calc(var(--btn-fs) * 0.04) outset rgba(255, 200, 200, 0.5);
    border-right:  calc(var(--btn-fs) * 0.04) outset rgba(90, 20, 20, 0.5);
    border-bottom: calc(var(--btn-fs) * 0.04) outset rgba(90, 20, 20, 0.7);
    text-shadow: 0 0 6px rgba(120, 80, 80, 0.95);
}
/* Hovering a clickable ENTER/SAVE: warm it with a sepia wash (borrowed from the
   old floating-column buttons' mouseenter filter). Only on the enabled buttons —
   :disabled has pointer-events:none so it can't be hovered anyway. */
/* ONLY while the iframe is actually interactive (fullscreen). The game adds .game-interactive to this
   page's <html> when it enables pointer events, and drops it on exit — see _setIframeInteractive.
   Without that gate the hover STICKS: you click ENTER inside the iframe, the game exits to pointer-lock,
   and no further mouse event ever reaches the page — so :hover never clears and the button sat there
   sepia-brown on the in-world monitor instead of going back to green. (A synthetic mouseleave can't fix
   it; :hover is driven by the browser's own hit-testing, not by dispatched events.) */
html.game-interactive #gameEnterBtn:not(:disabled):hover,
html.game-interactive #gameSaveBtn:not(:disabled):hover {
    filter: sepia(1);
}
/* clicking the ready SAVE button: reverse the raised bevel to a sunken (inset) one
   and drop it down — same press feedback as the ready ENTER button. */
#gameSaveBtn:not(:disabled):active {
    transform: translateY(calc(var(--btn-fs) * 0.04));
    border-top:    calc(var(--btn-fs) * 0.04) inset rgba(90, 20, 20, 0.7);
    border-left:   calc(var(--btn-fs) * 0.04) inset rgba(90, 20, 20, 0.5);
    border-right:  calc(var(--btn-fs) * 0.04) inset rgba(255, 200, 200, 0.5);
    border-bottom: calc(var(--btn-fs) * 0.04) inset rgba(255, 200, 200, 0.65);
}

/* ════════════════════════════════════════════════════════════════════════════
   SETTINGS PANEL  —  graphics-quality picker + debug toggle
   Shared across pages (markup lives on each page; behaviour is wired by the small
   inline scripts that read window.Quality / localStorage). Opened by the title-bar
   SETTINGS button toggling `.open`. The whole panel scales as one unit via
   transform: scale(var(--ui-scale)) at the title's breakpoints — so nothing inside
   needs its own responsive sizing.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Panel shell ──────────────────────────────────────────────────────────────
   Hidden until `.open`; pinned flush to the right edge, below the title mark.
   display:none/flex = instant (no animation). */
.q-panel {
    /* absolute (not fixed) so the panel stays anchored to this spot in the page and
       scrolls away with the content, instead of following the viewport down. */
    position: absolute;
    top: 10.75rem;                      /* below the title mark AND its full-width horizontal rules */
    /* Sit inside the body's right margin (not flush to the screen): the content box is --page-gutter in
       from the screen edge, and the SETTINGS button is --title-indent inside that — so match both, and the
       panel's right edge lines up under the SETTINGS button + the body columns, tracking the same breakpoints. */
    right: calc(var(--page-gutter) + var(--title-indent));
    z-index: 1000;
    max-height: calc(100vh - 10.75rem);
    box-sizing: border-box;
    overflow-y: auto;
    display: none;
    flex-direction: column;
    align-items: center;
    padding: 0.125rem 0.1875rem;               /* small margin around the outer box (the .q-outer box owns the inset) */
    /* scale the whole panel down at the same breakpoints as the title/buttons,
       anchored to its top-right corner so it stays flush-right below the mark. */
    transform: scale(var(--ui-scale, 1));
    transform-origin: top right;
}
.q-panel.open { display: flex; }

/* ── Beveled dark box (shared) ─────────────────────────────────────────────────
   The translucent-black, raised-bevel box from the loading registry's log
   (build/jsm/loadingRegister.js — the loadingLog element). Light top-left edges,
   dark bottom-right edges = a soft raised bevel over the page behind it. The
   SETTINGS table + debug box use it here; the registry re-declares the SAME rule
   into its own injected <style> (engine.html doesn't load this file), so keep
   the two definitions in sync. */
.bevel-box {
    background: rgba(0, 0, 0, 0.82);
    border-top:    0.125rem solid #c0c0c0;
    border-left:   0.125rem solid #dcdcdc;
    border-bottom: 0.125rem solid #404040;
    border-right:  0.125rem solid #404040;
    box-sizing: border-box;
}

/* ── Outer settings container ─────────────────────────────────────────────────
   One bevel box wrapping ALL the settings panels (table + debug boxes). Same
   raised-bevel border as .bevel-box, but a MORE TRANSPARENT background so the
   inner boxes (0.82) read as nested inside it. Owns the layout/gap of the inner
   boxes + the inset from its own border (declared AFTER .bevel-box so the
   background override wins). */
.q-outer {
    background: rgba(0, 0, 0, 0.42);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4375rem;
}

/* ── Section headers (GRAPHICS QUALITY / DEBUG) ───────────────────────────────
   Sit inside their dark box — the table's top row, or the debug box. */
.q-label { font-family: monospace; letter-spacing: 0.125rem; color: #e8e8e8; font-size: 0.8125rem; }
.q-table .q-title, .q-debug .q-title {
    font-family: 'IoskeleyMono', monospace; letter-spacing: 0.125rem; color: #e8e8e8; font-size: 0.8125rem;
    font-weight: 700; text-align: left; padding: 0.4375rem 0.5625rem;
}

/* ── Quality-tier reference table ─────────────────────────────────────────────
   Dark, semi-transparent box with light divider lines between rows. The selected
   tier highlights only its data-cell text (no background box). */
.q-table {
    border-collapse: collapse;
    /* translucent-black box + bevel comes from .bevel-box (added in the HTML) */
    color: #d8d8d8;
    font-family: 'IoskeleyMono', monospace;
    font-size: 0.625rem;
    border-radius: 0;
}
.q-table th, .q-table td {
    /* horizontal padding kept tight — 7 columns x 2 sides, so it drives the panel width */
    padding: 0.3125rem 0.375rem;
    text-align: left;
    white-space: normal;
    vertical-align: middle;
    border-bottom: 0.0625rem solid rgba(255, 255, 255, 0.15);   /* light divider lines */
}
.q-table tbody tr:last-child td { border-bottom: none; padding-bottom: 0.75rem; }  /* margin below last row */
.q-table thead th, .q-table tbody th { color: #ffffff; font-weight: 700; }
.q-table tbody td { color: #cfcfcf; }
/* selected row: yellow text + a highlight bar spanning the data cells. Excluding
   .q-btncell makes the bar stop at the button column instead of running under it. */
.q-table tbody tr.active td:not(.q-btncell) { color: #ffea00; font-weight: 700; background: rgba(255, 234, 0, 0.14); }
/* Footnote under the table. A sibling of the table, NOT a cell in it — as a colspan <td> its text
   forced auto table layout to widen the columns it spanned.
   `width: 0; min-width: 100%` is what stops it widening the PANEL. .q-outer is a column flex
   container, so its width is the widest child's MAX-CONTENT width — and a paragraph's max-content
   width is the whole thing on one line (~46rem here). align-self:stretch does not help: it sizes the
   item AFTER the container width is known, it does not remove the item from that calculation.
   An explicit width of 0 contributes nothing, and the percentage min-width (resolved against the
   container only once its width is settled) then paints it back out to full width.
   THE TABLE MUST BE THE ONLY THING THAT SETS THIS PANEL'S WIDTH. Anything else added to .q-outer
   with free-flowing text needs the same treatment. */
.q-note {
    width: 0;
    min-width: 100%;
    box-sizing: border-box;
    color: #9a9a9a;
    font-family: 'IoskeleyMono', monospace;
    font-size: 0.5625rem;
    line-height: 1.5;
    padding: 0 0.375rem;
}

/* the cell that holds the per-row quality button (last column) */
.q-table td.q-btncell { padding: 0.25rem 0.375rem 0.25rem 0.5rem; vertical-align: middle; border-bottom: none; }

/* ── Quality button (reusable) ────────────────────────────────────────────────
   One beveled, INSET-bordered button style for the Low/Medium/High picker. Fills
   its table column so all three are equal-width and the labels never overflow.
   Sized in rem so the text scales together with the box when the panel scales. */
.qbtn {
    width: 100%;
    box-sizing: border-box;
    /* Tracking and size drive the widest column in the panel: 'HYPER LOW' is 9 glyphs, so every
       0.0625rem of letter-spacing adds ~0.5rem to the table. Trimmed for width — raise them back
       together with the padding below if the buttons should read larger again. */
    padding: 0.625rem 0.375rem;
    font-family: 'Fistalle', sans-serif;
    font-size: 0.9375rem;
    letter-spacing: 0.125rem;
    text-align: center;
    text-transform: uppercase;
    white-space: nowrap;
    color: #ffffff;
    text-shadow: 0px 0.5px 0px #000000, -0.8px 0 0px #000000, 0.8px 0 0px #000000, 0px -0.9px 0px #000000;
    background: rgba(165, 95, 95, 0.5);
    border-radius: 0;
    border-top:    0.25rem inset rgba(255, 255, 255, 0.55);
    border-left:   0.25rem inset rgba(255, 255, 255, 0.30);
    border-bottom: 0.25rem inset rgba(0, 0, 0, 0.65);
    border-right:  0.25rem inset rgba(0, 0, 0, 0.35);
    transform: translateY(-0.125rem);
    cursor: pointer;
}
.qbtn:hover { filter: sepia(1); }   /* instant — no transition */
.qbtn.active {
    /* selected tier sinks in: reversed bevel + dropped down, like a pressed button
       (no background highlight — the table row's text is highlighted instead). The
       sepia wash stays on (same as :hover) so the picked tier reads as selected. */
    filter: sepia(1);
    transform: translateY(0.125rem);
    border-top:    0.25rem inset rgba(0, 0, 0, 0.70);
    border-left:   0.25rem inset rgba(0, 0, 0, 0.35);
    border-bottom: 0.25rem inset rgba(255, 255, 255, 0.35);
    border-right:  0.25rem inset rgba(255, 255, 255, 0.50);
}

/* ── Debug toggle ─────────────────────────────────────────────────────────────
   Its own dark box (matching the table). Header + on/off switch share one row,
   with the same divider line beneath them. */
/* Matches the table's width exactly: .q-outer is sized by the table (see .q-note), and these two
   properties make the debug box take that width rather than its own. `width: 0; min-width: 100%`
   rather than plain align-self:stretch, so its own content (the DEBUG label + the switch) can never
   set the panel width either — stretch alone would still let it push the container wider. */
.q-debug {
    width: 0;
    min-width: 100%;
    box-sizing: border-box;
}   /* box look comes from .bevel-box in the HTML */
.q-debug-row {
    display: flex; align-items: center; justify-content: space-between; gap: 1.125rem;
    padding: 0.4375rem 0.75rem; border-bottom: 0.0625rem solid rgba(255, 255, 255, 0.15);
}
.q-debug .q-title { padding: 0; text-align: left; }
.q-switch { position: relative; display: inline-block; width: 2.75rem; height: 1.375rem; flex-shrink: 0; }
.q-switch input { opacity: 0; width: 0; height: 0; }
.q-slider {
    position: absolute; inset: 0; cursor: pointer;
    background: rgba(120, 120, 120, 0.5);
    border-top: 0.125rem inset rgba(0, 0, 0, 0.6); border-left: 0.125rem inset rgba(0, 0, 0, 0.4);
    border-bottom: 0.125rem inset rgba(255, 255, 255, 0.4); border-right: 0.125rem inset rgba(255, 255, 255, 0.3);
}
.q-slider::before {
    content: ""; position: absolute; height: 1rem; width: 1rem; left: 0.125rem; top: 0.0625rem;
    background: #d8d8d8;
    border-top: 0.125rem outset rgba(255, 255, 255, 0.6); border-left: 0.125rem outset rgba(255, 255, 255, 0.4);
    border-bottom: 0.125rem outset rgba(0, 0, 0, 0.6); border-right: 0.125rem outset rgba(0, 0, 0, 0.4);
}
.q-switch input:checked + .q-slider { background: rgba(255, 234, 0, 0.45); }
.q-switch input:checked + .q-slider::before { transform: translateX(1.375rem); }

/* ── AXON VIEW ───────────────────────────────────────────────────────────────
   The live model view — a REUSABLE BLOCK, like .gallery above. A page drops one
   div and build/src/axonView.js builds everything inside it:

       <div class="axon" data-axon-locations="Camp"></div>

   Every page that carries one therefore looks identical; the only thing that
   changes page to page is --page-color, which the buttons fill with. Sizes are
   in rem so the whole block scales with the master root font-size, like the rest
   of the body content.

   THE PAGE MUST ALSO carry <base href="../">, the three importmap entries and
   build/src/quality.js — the view runs the real game engine, whose asset paths
   are written for a document at the site root. See pages/Island.html.  */

/* The mount. Its own trailing rhythm matches .gallery's, so a page can stack a
   view, a gallery and anything else without spacing rules of its own — except at
   the very top of .page-body, where the title bar above already provides the gap.

   THE SPACE IS RESERVED BEFORE THERE IS ANYTHING IN IT.
   The div a page writes is EMPTY — axonView.js builds the frame and its two chrome
   rows into it once the module has loaded and run. So the page's first layout had
   the axon block at zero height, and everything below it sat there until the script
   landed and then got shoved down by forty-odd rem. This holds the block open at its
   final height from the first paint, so nothing moves when the view appears.

   The two row heights are named here and used TWICE — to hold the rows themselves
   open below, and to add up the reservation — so the space kept and the space taken
   are the same numbers rather than two guesses that drift apart. Each includes the
   row's own margin; the min-heights subtract it again.
   Variants that build less chrome hand the difference back — see buildChrome. */
.axon {
    display: block;
    margin: 2rem 0 0;
    --axon-topbar-h: 2.4rem;    /* GHOST / FULLSCREEN row, + its 0.5rem margin-bottom */
    --axon-bar-h: 2.45rem;      /* tree + hint + switches row, + its 0.75rem margin-top */
    min-height: calc(var(--axon-height, 40rem) + var(--axon-topbar-h) + var(--axon-bar-h));
}
.page-body > .axon:first-child { margin-top: 0; }

/* The frame. Fills .page-body, so its edges land on the page's shared side
   margins — same as every other block on the site. */
.axon-view {
    position: relative;
    width: 100%;
    /* tall-ish landscape box; --axon-height lets one page run taller or shorter
       without a rule of its own. */
    height: var(--axon-height, 40rem);
    background: rgba(0, 0, 0, 0.1);
    border-top:    0.375rem inset rgba(255, 255, 255, 0.55);
    border-left:   0.375rem inset rgba(255, 255, 255, 0.3);
    border-bottom: 0.375rem inset rgba(0, 0, 0, 0.65);
    border-right:  0.375rem inset rgba(0, 0, 0, 0.35);
    box-sizing: border-box;
    overflow: hidden;
    /* SketchUp bindings: nothing happens on a left-drag, so no grab cursor to
       promise otherwise. The cursor only changes once a middle-drag is underway. */
    cursor: default;
}
.axon-view.orbiting { cursor: grabbing; }
.axon-view.panning  { cursor: move; }

/* The engine appends its canvas into #container and its modules reach for it by
   id, so the id has to be exactly that. It is NOT game.css's #container, though:
   that one is position:fixed and fills the window, which would tear a page's
   layout apart. Here it is just the inside of the frame. */
.axon-view #container { position: relative; width: 100%; height: 100%; }
.axon-view #container canvas { display: block; width: 100%; height: 100%; }

/* The FEED canvas — the desk-screen mode, where the picture comes from the game
   running outside the iframe rather than from an engine in the page (see
   build/src/screenFeed.js). Fills the frame exactly like the engine's own canvas
   does; the backing resolution is set in JS, so this only governs the box. */
.axon-feed { display: block; width: 100%; height: 100%; }

/* Readout over the canvas — a fault, or a feed with nothing to show.
   NOT a loading screen: the view has none. It comes up empty and fills in as
   the world streams, rather than hiding behind a panel and appearing all at
   once. There is deliberately no dimming wash either; whatever HAS loaded
   should be seen plainly while the rest arrives. */
.axon-status {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    text-align: center;
    letter-spacing: 0.125rem;
    color: #ffffff;
    /* Legible over any picture without covering it — the shadow does the work a
       backdrop used to. */
    text-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.9), 0 0 0.125rem rgba(0, 0, 0, 1);
    pointer-events: none;
    z-index: 2;
}
.axon-status[hidden] { display: none; }
.axon-progress {
    font-family: 'IoskeleyMono', monospace;
    font-size: 0.8rem;
    letter-spacing: 0.0625rem;
    opacity: 0.8;
}

/* The bar under the frame is two columns: the world TREE (with the flight hint
   under it) on the left, the HAZE/SUN switches hard right. margin-left:auto on
   the switches is what pins them there however wide the tree gets.
   align-items:flex-start so the switches sit level with the top of the tree
   rather than floating in the middle of it. */
.axon-bar {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    margin-top: 0.75rem;
    /* Held to the height .axon reserved for it, less the margin above. A page with no
       switches would otherwise come out shorter than the space kept for it. */
    min-height: calc(var(--axon-bar-h) - 0.75rem);
}
.axon-left { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
.axon-switches { display: flex; gap: 0.5rem; margin-left: auto; flex-wrap: wrap; flex: none; }
.axon-hint {
    font-size: 0.8rem;
    letter-spacing: 0.125rem;
    opacity: 0.75;
}

/* ── World tree ──
   LOCATION rows, each with its AREAS indented beneath. The indent guide is a
   border on the child list rather than characters, so it stays put whatever the
   buttons wrap to. Built only when there is more than one area to choose
   between — see buildChrome. */
.axon-tree { display: flex; flex-direction: column; gap: 0.3rem; align-items: flex-start; }
.axon-loc { display: flex; flex-direction: column; gap: 0.2rem; align-items: flex-start; }
.axon-loc-areas {
    display: flex; flex-wrap: wrap; gap: 0.3rem;
    margin-left: 0.55rem;
    padding-left: 0.55rem;
    border-left: 0.125rem solid rgba(255, 255, 255, 0.28);
}

/* ONE base rule for every button in the block — FULLSCREEN, EXIT, HAZE/SUN and
   the whole tree. Sized off the LOCATION row, so the switches are peers of the
   top level of the tree rather than a size of their own. The hierarchy comes
   from the indent and the smaller area rows below, not from the switches being
   bigger than everything else. */
.axon-btn {
    cursor: pointer;
    font-family: 'Fistalle', sans-serif;
    font-size: 0.82rem;
    line-height: 1;
    letter-spacing: 0.11rem;
    padding: 0.18rem 0.7rem;
    color: #fff;
    background: var(--page-color);
    border-top:    0.25rem outset rgba(255, 255, 255, 0.55);
    border-left:   0.25rem outset rgba(255, 255, 255, 0.3);
    border-bottom: 0.25rem outset rgba(0, 0, 0, 0.65);
    border-right:  0.25rem outset rgba(0, 0, 0, 0.35);
}
/* Hover: the same sepia wash the page's ENTER/SAVE buttons use — keep in step.
   No transition, so it lands the instant the pointer arrives, as those do. */
.axon-btn:hover { filter: sepia(1); }

/* ── NOT ACTIVATED — the look of a DISABLED ENTER/SAVE ──
   Lifted from .title-btn + .title-btn:disabled above: no page-colour fill, just
   a dark translucent panel behind a REVERSED (inset) bevel, at half opacity. So
   an area that is hidden, or a switch that is off, reads as unavailable in
   exactly the way a not-yet-clickable ENTER does — rather than merely dimmer.
   Deliberately NOT copied: `cursor: default` and `pointer-events: none`. Those
   buttons are disabled; these are simply off, and clicking one is how you turn
   it back on.
   SCOPED TO THE SWITCH ROW, not written as `.axon-btn:not(.active):not(...)`.
   Every class inside :not() counts toward specificity, so that chain weighed
   (0,4,0) and beat the ACTIVATED rule below it at (0,3,0) — which meant a lit
   tree button rendered dim, and every location and area on the island read as
   OFF while plainly loaded. Naming the container instead keeps this at (0,3,0),
   so source order decides, which is what the pair below is written to rely on.
   It also excludes FULLSCREEN and EXIT for free: they are actions, not switches,
   so "off" is not a state they have, and they live outside this row. */
.axon-switches .axon-btn:not(.active),
.axon-ghost:not(.active),
.axon-tree .axon-btn.off {
    opacity: 0.5;
    background: rgba(0, 0, 0, 0.3);
    border-top:    0.25rem inset rgba(0, 0, 0, 0.65);
    border-left:   0.25rem inset rgba(0, 0, 0, 0.35);
    border-bottom: 0.25rem inset rgba(255, 255, 255, 0.3);
    border-right:  0.25rem inset rgba(255, 255, 255, 0.3);
}
/* ── ACTIVATED — the raised page-colour button, as above ──
   The tree is the inverse of the switches: an area is ON unless it carries
   `.off`, whereas a switch is OFF unless it carries `.active`. Restating the
   base look here for a tree button without `.off` is what lets one rule above
   cover both senses of "not activated". */
.axon-tree .axon-btn:not(.off) {
    opacity: 1;
    background: var(--page-color);
    border-top:    0.25rem outset rgba(255, 255, 255, 0.55);
    border-left:   0.25rem outset rgba(255, 255, 255, 0.3);
    border-bottom: 0.25rem outset rgba(0, 0, 0, 0.65);
    border-right:  0.25rem outset rgba(0, 0, 0, 0.35);
}
/* PRESSED — held down, whatever the button's state. The bevel reverses and the
   face nudges down, matching #gameSaveBtn:active on the location pages.
   The second selector is not redundant: the ACTIVATED rule above carries two
   classes, so a bare `.axon-btn:active` loses to it on specificity and a lit
   tree button would show no press at all. This matches that weight and comes
   later, so it wins. */
.axon-btn:active,
.axon-tree .axon-btn:active { border-style: inset; transform: translateY(0.0625rem); }
/* AREA rows sit a step down from their location — smaller and tighter-tracked. */
.axon-tree .axon-btn { font-size: 0.7rem; padding: 0.15rem 0.5rem; letter-spacing: 0.06rem; }
/* The LOCATION row is the parent, so it keeps the base size — restated because
   the rule above would otherwise shrink it along with its areas. `.off` here
   means every area under it is hidden (see setLocation); the dimming is what
   separates it from a merely pressed button. */
.axon-tree .axon-btn.axon-loc-btn {
    font-size: 0.82rem; letter-spacing: 0.11rem; padding: 0.18rem 0.7rem;
}

/* EXIT — the way out of TOUCH FLIGHT. A phone has no Esc and nothing to unlock,
   so the mode has to carry its own exit. Pinned inside the frame so it is
   reachable without scrolling the page. Shown only while flying by touch. */
.axon-exit {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 3;
    font-size: 0.9rem;
    letter-spacing: 0.125rem;
    padding: 0.25rem 1rem;
}
.axon-exit[hidden] { display: none; }

/* FULLSCREEN toggle — in a bar ABOVE the frame, right-aligned. While fullscreen
   the frame covers the whole viewport, so the button floats to the top-right
   corner over the fill (see .floating) to stay reachable — the only way back out
   on iOS, where fullscreen is the CSS fill below and there is no Esc. Hidden
   while flying so it never lands under the touch-flight EXIT button. */
.axon-topbar {
    display: flex;
    align-items: center;
    /* GHOST (left group) against FULLSCREEN (right). The left group is always built,
       even empty, so a page without it still gets FULLSCREEN hard right rather than
       pulled across by space-between. */
    justify-content: space-between;
    margin-bottom: 0.5rem;
    min-height: calc(var(--axon-topbar-h) - 0.5rem);   /* see .axon-bar */
}
.axon-topbar-left { display: flex; gap: 0.5rem; }
.axon-ghost { font-size: 0.9rem; letter-spacing: 0.125rem; padding: 0.25rem 1rem; }
.axon-fullscreen { font-size: 0.9rem; letter-spacing: 0.125rem; padding: 0.25rem 1rem; }
.axon-fullscreen.active { border-style: inset; }
/* Floating over the fill while fullscreen. Safe-area insets keep it clear of the
   notch / camera housing in locked landscape, where a bare 0.5rem could put it
   under an untappable region. */
.axon-fullscreen.floating {
    position: fixed;
    top: max(0.5rem, env(safe-area-inset-top, 0px));
    right: max(0.5rem, env(safe-area-inset-right, 0px));
    z-index: 10000;
}
html.flying .axon-fullscreen { display: none; }

/* No cursor while flying — the mouse IS the look, and the pointer is locked away
   anyway. touch-action goes with it: while flying, a drag is the look/joystick,
   never a page scroll or a pinch-zoom. Off the mode the view scrolls with the
   page like any other block. */
html.flying .axon-view { cursor: none; touch-action: none; }

/* Fullscreen fill. One class, added for BOTH paths: native fullscreen (desktop /
   Android / iPad via requestFullscreen, which also hides the browser chrome) AND
   the iOS-iPhone fallback (no element-fullscreen API there, so the view is pinned
   to the viewport in CSS instead). 100dvh excludes the dynamic iOS toolbars so
   the frame is not clipped. */
.axon-view.fs-active {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    height: 100dvh;
    z-index: 9999;
    margin: 0;
    border: none;
    border-radius: 0;
}

/* ── Game UI scaffolding ─────────────────────────────────────────────────────
   Elements the engine's own modules create or look up by id. axonView.js builds
   any that are missing (ensureGameUI), and GameEngine styles them through
   game.css — which a webpage can't load, since game.css pins #container fixed and
   fullscreen. So their look lives here instead. Inert on pages with no axon view:
   the ids simply don't exist there.

   Keep these in step with game.css. */
#interactionCursor, #immunityGlow, #damageFlash { display: none; }
#songTitleDisplay, #songProgressBar { position: fixed; z-index: 1000; display: none; }
#info { display: none; }

/* The in-game crosshair. playerControls builds `#crosshair` itself (a div with
   two thin bars) on init, so it exists on any page running the engine; what it
   doesn't have is its look. These are game.css's rules verbatim. The axon view
   keeps it hidden (the flight camera doesn't shoot), but the rules stay here so
   it is styled the moment anything shows it. */
#crosshair {
    position: fixed;
    width: 20px;
    height: 20px;
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 10000;
    mix-blend-mode: difference;
}
#crosshair::before,
#crosshair::after {
    content: '';
    position: absolute;
    background-color: rgb(255, 255, 255);
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}
#crosshair::before { width: 12px; height: 1px; }
#crosshair::after  { width: 1px;  height: 12px; }

/* The thumb joystick, for TOUCH FLIGHT. The game's own element, which the axon
   view drives itself rather than through mobileControls — see the TOUCH FLIGHT
   block in axonView.js for why. game.css's rules verbatim; display:none is its
   resting state — it appears under the thumb, where the thumb lands, and goes
   away when it lifts. */
#dynamicJoystick {
    position: fixed;
    width: 120px;
    height: 120px;
    background: rgba(255, 255, 255, 0.15);
    border: 3px solid rgba(255, 255, 255, 0.4);
    border-radius: 50%;
    touch-action: none;
    box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
    display: none;
    z-index: 2000;
    pointer-events: none;
}
#dynamicJoystickKnob {
    position: absolute;
    width: 40px;
    height: 40px;
    background: rgba(255, 255, 255, 0.9);
    border: 2px solid rgba(255, 255, 255, 1);
    border-radius: 50%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    touch-action: none;
    box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
    transition: box-shadow 0.15s ease;
}
#dynamicJoystickKnob.sprinting {
    box-shadow: 0 0 30px 10px rgba(255, 220, 40, 0.8), 0 0 80px 0px rgba(255, 200, 0, 0.5);
}
