/* site-chrome.css — shared page-chrome visual layer, added 2026-08-23.
 *
 * Companion to site-chrome.js (same directory), which injects/wires the
 * markup this stylesheet styles. Prototyped on codex.html first per direct
 * user instruction, meant to be ported to every other live page afterward
 * without re-deriving any of this — one file to edit for a future aesthetic
 * change, per the whole point of this split (see CLAUDE.md's "Site map"
 * discussion / the 2026-08-23 background/header-banner planning session).
 *
 * Three pieces:
 *   1. Whole-page ambient background (.site-bg-img / .site-bg-wash /
 *      .site-glow) — three fixed, negative-z-index layers sitting behind
 *      every page's real content. Chosen art: bg7_space.webp (dark,
 *      evenly-spread nebula/star texture) over 15 other candidates —
 *      picked specifically because it's dark/desaturated enough not to
 *      fight text legibility or clash with per-page accent colors, and has
 *      real visible texture unlike a flat gradient (verified via an actual
 *      rendered A/B comparison, not guessed from stills).
 *   2. .page-header-banner — a per-page header art band (breadcrumb/
 *      eyebrow/title/subtitle sit inside it) with its own bottom scrim so
 *      text stays legible regardless of how busy the source art is. Source
 *      art is 1900×400, matching the site's own 1900px .wrap/.codex-wrap
 *      content-width convention, so it's meant to be used CONTAINED within
 *      that column, not full-viewport-bleed.
 *   3. The animated glow (.site-glow) generalizes Advent Expedition's
 *      existing header glow site-wide, correctly scoped this time (that
 *      page's own version bleeds into empty space below short content —
 *      see ADVENT_EXPEDITION_NOTES.md). mix-blend-mode:screen per the
 *      standing screen-blend-for-glow-on-dark-bg rule (plain alpha reads as
 *      too subtle to see against this site's near-black theme).
 *
 * Stacking-order note (load-bearing, don't "simplify" this away): a
 * position:fixed element with z-index:auto/0 still paints ABOVE ordinary
 * unpositioned normal-flow content (page markup like #page-codex) per the
 * CSS stacking spec — positioned-but-z-index:auto descendants paint in a
 * later step than in-flow non-positioned descendants, regardless of DOM
 * order. These three layers use NEGATIVE z-index specifically so they
 * paint in the step right after the root canvas's own background, before
 * any real page content — verified this is safe (not swallowed behind the
 * body's own opaque background-color) since body/html carry no transform/
 * filter/opacity of their own that would trap a negative-z-index child in
 * a nested stacking context.
 */

/* Self-hosted Lato/Bebas Neue, replacing the Google Fonts <link> every page
   used to load — added 2026-08-24, direct user follow-through after being
   asked about the font-swap flash: "I have put the fonts into the github
   repo now. Does this help?" Real answer: yes, two ways — (1) removes the
   external DNS lookup + connection setup to fonts.googleapis.com AND
   fonts.gstatic.com (two separate third-party hosts) that every page paid
   on every visit, and (2) these are compressed to real .woff2 (fontTools),
   not the raw .ttf/.otf files as originally supplied — ~68-74% smaller
   (e.g. Lato-Regular.ttf 657KB -> 212KB woff2). Only the 5 Lato
   weights/styles + Bebas Neue this site actually uses were converted (the
   supplied font/ folder has the full 18-file Lato family; most were never
   referenced anywhere — confirmed by grep against the site's own Google
   Fonts URL, `family=Lato:ital,wght@0,300;0,400;0,700;0,900;1,400`).
   font-display:swap (same behavior as Google Fonts' own default) still
   shows a fallback system font for a moment on first load — that part is
   inherent to using a custom web font at all, not something self-hosting
   removes; the "the wrong font shows first" flash was never really about
   Google Fonts specifically. Files live at font/woff2/ in the deployed
   site repo AND this local working copy (kept in sync so local file://
   preview shows the same real fonts, not a fallback — a deliberate
   exception to the usual images-only-in-the-deployed-repo convention,
   since the combined footprint here is small, ~1MB total for all 6 files).

   **font-display swap -> optional -> REVERTED back to swap, same day**:
   tried `optional` (theory: skip the visible re-render entirely, since a
   font ready within a very short window gets used immediately and
   otherwise the browser never swaps it in later). Direct report: "the
   font adjustment caused the font to go back to the non-google one" —
   `optional`'s real behavior in this environment was far more aggressive
   about falling back permanently than the spec's happy-path suggested
   (`file://` preview likely doesn't warm this exact kind of font cache
   the same way real HTTP caching would, and even on the live site
   `optional` can decide a font "isn't worth fetching" for a visit based
   on its own internal heuristics, not just a simple timer) — the real
   font stopped showing at all rather than showing after a brief flash,
   a strictly worse outcome. Reverted to `swap`: guarantees the real font
   always ends up applied, accepting the brief flash as the traded-off
   cost rather than gambling on `optional`/`fallback`'s own less
   predictable heuristics without much more real-device testing than this
   session had time for.

   **2026-09-01: `<link rel="preload">` added for Bebas Neue + SKR Global**
   (in every page's own <head>, right before this stylesheet's own <link>
   — a stylesheet can't inject a preload hint, so it has to be a literal
   tag per page, same hand-copy burden as @font-face on main.html/
   coming-soon.html). Real user report: on the LIVE site (not local file://,
   where disk reads are instant and this never reproduces) the page title
   visibly flashed as a fallback sans-serif for a moment before snapping to
   Bebas Neue's much narrower condensed caps — the `swap` fallback WINDOW
   itself, not a new bug. This is a different lever than the swap/optional/
   reverted experiment above: preload doesn't touch font-display's fallback
   behavior at all, it only starts the font fetch in parallel with CSS
   parsing instead of after it (the browser doesn't know a font URL exists
   until it's parsed this file, one avoidable round-trip), shrinking how
   often the fallback window is even hit. Carries none of `optional`'s
   "real font stops applying" risk since `swap` itself is untouched.
   `crossorigin` is mandatory even though same-origin — fonts are always
   fetched in anonymous-CORS mode regardless of origin, and a preload
   without a matching `crossorigin` attribute is a wasted fetch (the real
   @font-face request that follows doesn't recognize it as the same
   resource and fetches again from scratch). Only Bebas Neue + SKR Global
   400 are preloaded (the 2 faces actually used at/near first paint on
   every page — nav logo + page title + body text); Lato/CJK/Thai fallback
   and the 700/900 fill-in weights stay un-preloaded on purpose, per this
   site's payload-reduction rule — they either load lazily as designed
   (CJK/Thai, via unicode-range) or aren't needed until well after first
   paint. */
@font-face{font-family:'Lato';font-style:normal;font-weight:300;font-display:swap;src:url('font/woff2/Lato-Light.woff2') format('woff2');}
@font-face{font-family:'Lato';font-style:normal;font-weight:400;font-display:swap;src:url('font/woff2/Lato-Regular.woff2') format('woff2');}
@font-face{font-family:'Lato';font-style:italic;font-weight:400;font-display:swap;src:url('font/woff2/Lato-Italic.woff2') format('woff2');}
@font-face{font-family:'Lato';font-style:normal;font-weight:700;font-display:swap;src:url('font/woff2/Lato-Bold.woff2') format('woff2');}
@font-face{font-family:'Lato';font-style:normal;font-weight:900;font-display:swap;src:url('font/woff2/Lato-Black.woff2') format('woff2');}
@font-face{font-family:'Bebas Neue';font-style:normal;font-weight:400;font-display:swap;src:url('font/woff2/BebasNeue.woff2') format('woff2');}

/* 'SKR Global' = the game's actual UI body font (SKR_GlobalBasic, pulled from the
   datamine's Font export, Latin+Hangul coverage), replacing Lato as the site's
   primary font 2026-09-01 -- Lato was never what the game itself uses, and being
   a pure-Latin face it forced Korean hero names onto an inconsistent OS fallback
   (the "weird fallback" symptom). Kept as ONE font-family via unicode-range so
   every existing `font-family:'SKR Global','Lato',...` stack needs no per-script
   branching: browsers use SKR_GlobalBasic by default and only fetch the CJK/Thai
   faces below when a page actually renders a codepoint in their range (near-zero
   cost otherwise, per the site's payload-reduction rule). 'Lato' itself stays
   self-hosted as the trailing fallback, unchanged, for any stray missing glyph. */
@font-face{font-family:'SKR Global';font-style:normal;font-weight:400;font-display:swap;src:url('font/SKR_Global_01.woff2') format('woff2');}
@font-face{font-family:'SKR Global';font-style:normal;font-weight:400;font-display:swap;unicode-range:U+4E00-9FFF,U+3400-4DBF,U+F900-FAFF,U+3000-303F;src:url('font/NotoSansSC-SemiBold.woff2') format('woff2');}
@font-face{font-family:'SKR Global';font-style:normal;font-weight:400;font-display:swap;unicode-range:U+0E00-0E7F;src:url('font/NotoSansThai_Condensed-Medium.woff2') format('woff2');}
/* SKR_Global_01 is a single-weight font (confirmed via its own OS/2.usWeightClass=400
   and name table -- the composite font it's pulled from only ever references it through
   one "Font_Global_Medium" slot, no separate Bold cut exists anywhere in the datamine).
   This site's UI leans heavily on font-weight:700/800/900 for badges/buttons/tabs, so a
   400-only 'SKR Global' would render all of that as browser-synthesized faux-bold. Fill
   the gap by registering the OTHER weights under the SAME 'SKR Global' family name
   pointing at Lato's real cuts -- every existing weight:700+ declaration site-wide gets
   genuine crisp Lato Bold/Black glyphs with no changes needed anywhere else. */
@font-face{font-family:'SKR Global';font-style:normal;font-weight:300;font-display:swap;src:url('font/woff2/Lato-Light.woff2') format('woff2');}
@font-face{font-family:'SKR Global';font-style:italic;font-weight:400;font-display:swap;src:url('font/woff2/Lato-Italic.woff2') format('woff2');}
@font-face{font-family:'SKR Global';font-style:normal;font-weight:700;font-display:swap;src:url('font/woff2/Lato-Bold.woff2') format('woff2');}
@font-face{font-family:'SKR Global';font-style:normal;font-weight:900;font-display:swap;src:url('font/woff2/Lato-Black.woff2') format('woff2');}

/* Brightened 2026-09-13 (direct request, sitewide). First pass (opacity
   .35->.42, wash midpoint a3->8f) was confirmed via direct pixel sampling
   to move the visible margin's mean RGB by under 1% -- genuinely
   imperceptible, not just a subjective miss ("no I dont feel any
   changes" was correct). The image itself is mostly dark space/nebula,
   so opacity alone (blending toward an already-near-black layer) can't
   move the needle much regardless of how far it's pushed -- a real
   brightness LIFT needs a filter that multiplies existing pixel values,
   not just more of the same dark image blended in. This pass adds
   filter:brightness()/saturate() to .site-bg-img (confirmed via pixel
   sampling: visible-margin mean RGB roughly +40-45% relative, vs. under
   1% before) and eases the wash's midpoint further (8f->70). Top/bottom
   wash stops (cc, ~80% dark) still left untouched -- those sit right
   behind the header banner text and the page's own bottom edge, both of
   which still need the same legibility margin as before. */
.site-bg-img{position:fixed;inset:0;z-index:-3;background-size:cover;background-position:center;opacity:.55;filter:brightness(1.35) saturate(1.15);pointer-events:none;}
.site-bg-wash{position:fixed;inset:0;z-index:-2;pointer-events:none;background:linear-gradient(180deg,#08111ecc 0%,#08111e70 30%,#08111ecc 100%);}
/* Blob centers deliberately kept BELOW ~30% viewport height — a page's own
   .page-header-banner sits in roughly that top band, and its rounded
   top-left corner (overflow:hidden clips its own content, but NOT what's
   behind it outside that curve) will otherwise let a nearby glow blob peek
   through as a small, oddly-shaped bright patch that reads as a rendering
   glitch, not an ambient effect — found on codex.html 2026-08-23, the
   original top-left-centered blob did exactly this. Keeping every blob's
   center clear of the header band avoids the interaction entirely, on any
   page, regardless of that page's own exact header height. */
.site-glow{position:fixed;inset:-6%;z-index:-1;pointer-events:none;mix-blend-mode:screen;
  background:radial-gradient(ellipse 55% 45% at 78% 30%,#c8922a30 0%,transparent 68%),
             radial-gradient(ellipse 55% 46% at 15% 62%,#3b82f628 0%,transparent 68%),
             radial-gradient(ellipse 50% 42% at 70% 88%,#9d6fe822 0%,transparent 68%);
  animation:siteGlowDrift 22s ease-in-out infinite;
}
/* 0%/100% identical (seamless loop) with the midpoint most different — an
   opacity pulse reads as motion far more clearly than a pure few-percent
   spatial drift on blurred gradients, which is nearly imperceptible to a
   glancing viewer (direct user report, 2026-08-23: "i dont see glow drift"). */
@keyframes siteGlowDrift{0%,100%{transform:translate(0,0) scale(1);opacity:.7;}50%{transform:translate(2%,-2.5%) scale(1.06);opacity:1;}}
@media (prefers-reduced-motion:reduce){.site-glow{animation:none;}}

/* Per-page header banner. --banner-img is set via SiteChrome.setHeaderBanner()
   (site-chrome.js) — the class itself is also added by that same call, so a
   page's own header wrapper div starts as plain unstyled markup and only
   becomes a banner once JS resolves its real image path through that page's
   own img() helper (a raw relative path here would break local file://
   preview against the sibling site repo — same reasoning as every other
   image on these pages). Text sits at the BOTTOM of the box (art breathes
   at the top, same convention as most game key-art headers) via the bottom-
   heavy scrim + flex justify-content:flex-end below. */
/* text-shadow is inherited, so this one declaration gives every descendant
   text element (breadcrumb, eyebrow, title, count — whatever a given page's
   own header block contains) a dark contact shadow for contrast against
   busy art, without this shared file needing to know that page's own class
   names. A page can still layer its OWN colored text-shadow on a specific
   element (e.g. codex-title's gold glow) — that just replaces this value on
   that one element, it doesn't need to preserve it. Colors themselves (e.g.
   codex.html's --gold-dim eyebrow, too dark to read on bright art even with
   a shadow) still need a per-page override scoped to `.page-header-banner`
   — a shadow alone can't fix dark-on-dark, see codex.html's own override. */
/* Bottom padding bumped 22px->32px + min-height 260px->266px — just enough
   extra room below the text's own bottom edge for a real (but SHORT/quick)
   fade runway, without the box reading as noticeably taller overall. A
   first pass bumped these much further (64px/280px) to give the fade a
   long, gradual runway, but that made the whole banner read as too tall on
   its own — direct correction, 2026-08-24: "the fade off needs to come
   sooner. Now the entire header banner looks too long." Fade itself (see
   ::before/::after below) is now a short, snappy transition confined to
   this small padding zone, not a long gradual dissolve. */
/* margin-bottom trimmed 22px->8px (2026-08-24, direct report: "a lot of
   unncessary space between the header and the actual content below") — the
   box's own 32px bottom padding already does the fade-zone/breathing-room
   job (see the mask-image comment below); a full 22px margin ON TOP of
   that, THEN the next element's own top padding again, compounded into a
   real, visibly dead gap — worst on pages with a subtitle line
   (tierlist-pve.html etc.), where the banner box grows taller to fit that
   extra text but the fixed-size fade zone below it does not shrink to
   compensate, so the redundant margin read as even more excessive there. */
/* min-height 266px->325px, 2026-09-08 (direct request, shows more of the
   real 400px-tall source art at full width — see the background-position
   comment below for the crop/anchor side of this same banner). */
/* justify-content:flex-end -> flex-start, same day, follow-up. 325px is
   much closer to (sometimes less than) what a long-content page's header
   text needs than 266px was, so bottom-anchoring the WHOLE text block as
   one group made the gap ABOVE the title vary a lot by page: codex.html
   (no subtitle, ~230px of content) showed a 96px gap above the title,
   while tierlist-pve.html (subtitle+count, subtitle sometimes wraps 2-3
   lines) showed almost none — direct report, screenshots of both side by
   side. flex-start fixes breadcrumb/eyebrow/title at a CONSTANT distance
   from the box's own top (nothing before them varies, so their position
   never can either) on every page, regardless of what follows. The
   trailing line(s) (subtitle, or the count line on pages with no subtitle)
   still flow down toward the bottom instead of sitting stacked right under
   the title — each such page gives ITS OWN first trailing element
   `margin-top:auto` (a per-page CSS rule, see e.g. resource-codex.html's
   `.subtitle`/codex.html's `.codex-count`), which — in a flex column with
   flex-start — consumes all remaining free space as margin above that one
   element, pushing it (and anything after it) down while leaving every
   earlier sibling's position completely unaffected. Verified via
   getBoundingClientRect() that breadcrumb's top-gap-from-banner-top is
   now IDENTICAL across codex.html/tierlist-pve.html/resource-codex.html
   regardless of subtitle line count. If a NEW page is added with this
   header pattern, its own first trailing element needs the same
   margin-top:auto or its title will float again. */
/* border-radius overridden directly to 20px 20px 20px 0 (both top corners
   rounded, bottom-right kept rounded, bottom-left kept sharp), 2026-09-08
   direct request -- this element no longer reads var(--RL), which every
   OTHER card/panel on the same pages still uses for the site's normal
   asymmetric "2 rounded + 2 sharp" convention; changing --RL itself would
   have rounded every one of those too. Scoped to just this selector. */
/* padding-top bumped 26px->60px, same day -- a STATIC value (unlike the
   auto-margin approach tried and reverted right above), so breadcrumb sits
   at the exact same distance from the banner's top edge on EVERY page
   regardless of how much subtitle/count content follows it, while still
   landing lower/less cramped than the original 26px value read as. */
.page-header-banner{position:relative;border-radius:20px 20px 20px 0;overflow:hidden;margin-bottom:8px;padding:60px 34px 32px;display:flex;flex-direction:column;justify-content:flex-start;min-height:325px;text-shadow:0 2px 10px rgba(0,0,0,.9),0 1px 3px rgba(0,0,0,.9);}
/* Mobile header text scaling, added 2026-09-08 (overnight mobile-
   friendliness audit) — .title/.eyebrow/.breadcrumb/.subtitle/the various
   `*-count` classes are each hand-copied per page at one fixed desktop
   size (title 64px, letter-spacing 5px) with no responsive fallback at
   all, so any title containing a single word too wide for a phone screen
   at that size clips/overflows rather than wrapping — confirmed on
   multiple real pages (calculator.html "CALCULATOR", ruby-calculator.html
   same, shop-recommendations.html "RECOMMENDATIONS", advent-expedition.html
   "EXPEDITION" all cut off past the right edge at 375px-390px viewports).
   Direct correction after an early draft of this same fix treated it as
   just a diagnostic note rather than an actual fix: "Maybe you have to
   actually think about FONT SIZE REDUCTION in mobile format? instead of
   stubbornly keep the font size and then saying it doesnt shrink or wrap."
   `!important` is necessary here specifically because site-chrome.css's
   own <link> loads BEFORE each page's own <style> block in every real
   page's <head> — at equal selector specificity the page's own later,
   unconditional `.title{font-size:64px}` would otherwise always win over
   a plain (non-!important) media-query rule in this earlier-loaded file,
   regardless of which viewport width actually matches. `[class$="-count"]`
   (attribute suffix selector) catches every page's own differently-named
   count line (`.codex-count`/`.pcx-count`/`.ic-count`/`.tl-count`/
   `.history-count`/`.bundle-count`/`.filter-count`) in one rule rather
   than needing a per-page override. Only font-size/letter-spacing are
   touched — each page's own hand-tuned margin-bottom values (deliberately
   different per page, see the bottom-fade-zone reasoning earlier in this
   file) are left alone on purpose, this fix is scoped to the actual
   overflow/clipping bug, not a full mobile spacing pass. */
@media (max-width:600px){
  .breadcrumb{font-size:11px !important;}
  .eyebrow{font-size:10px !important;letter-spacing:2px !important;}
  .title{font-size:34px !important;letter-spacing:1px !important;}
  .subtitle{font-size:12px !important;}
  [class$="-count"]{font-size:13px !important;}
}
/* Mobile: every text input/textarea site-wide gets at least 16px, added
   2026-09-08 -- direct report: "Opening the Hero Picker (and Pet Picker)
   coz the page to zoom to fit their windows. When closed, the page
   remains in a zoomed stage." Root cause: iOS Safari (and Chrome for iOS,
   same WebKit rule) force-zooms the WHOLE PAGE the instant a focused text
   input's computed font-size is under 16px -- and the zoom doesn't auto-
   revert on its own once the input blurs or its modal closes, matching
   exactly what was reported. Every text input on this site was authored
   at 13-14px (a desktop-first convention, never revisited for mobile),
   and the Hero/Pet Picker modal's own search box makes this the FIRST
   thing that happens on open since it auto-focuses itself immediately --
   but the exact same zoom would trigger on any other search box too the
   moment a visitor taps into it, just less jarringly obvious as a "bug"
   since it followed their own tap. Fixed at the root (keep every input
   at/above the 16px trigger threshold) rather than the heavier-handed
   alternative (a `maximum-scale=1` viewport meta tag disabling ALL
   user-pinch-zoom site-wide, a real accessibility regression) -- this
   only removes the browser's own INVOLUNTARY zoom, deliberate user zoom
   still works everywhere. !important needed for the same reason as the
   block above: site-chrome.css's <link> loads before each page's own
   <style>, and several inputs (e.g. codex.html's .chghero-search) are
   more-specific class selectors that would otherwise still win. */
@media (max-width:760px){
  input[type="text"],input[type="search"],input:not([type]),textarea{font-size:16px !important;}
}
/* Scrim + art merged into ONE element's stacked `background-image` layers
   (CSS supports multiple comma-separated background-images on one element;
   the first-listed one paints on top, matching the old scrim-above-art
   z-index order) rather than two separate ::before/::after elements each
   fading on their own. This matters: masking two independently-opaque
   stacked layers with the SAME fade curve does NOT fade the combined
   result smoothly — the scrim (already near-opaque black by the bottom of
   its own gradient) THINS OUT at the same rate as the art underneath it,
   so for a stretch right as the fade begins the art becomes MORE visible
   relative to the now-weakening scrim than it was before the fade started,
   producing a brief "lighter" flash of the art's own bright/colorful
   details right before everything fades to nothing. Found for real, direct
   report 2026-08-24: "WHY IS there this strange lighted area before the
   fade off?" Merging them into one pre-composited layer and masking THAT
   fades the already-blended pixels together as a single unit, with no
   relative-visibility mismatch. Fades to FULLY transparent before reaching
   the box's own bottom edge (not just a dark-colored gradient stop) — a
   flat color, however close to the page's own #08111e, still reads as a
   visible hard-edged rectangle sitting on top of the real (textured,
   animated) ambient background behind it; true transparency lets that real
   background show through directly, so the banner blends rather than
   approximates. Gradient mask (not an image mask) per the standing
   file://-compatibility rule — see feedback_mask_image_broken_on_file_protocol. */
.page-header-banner::before{content:'';position:absolute;inset:0;z-index:0;
  background-image:linear-gradient(180deg,#08111e80 0%,#08111e59 30%,#08111ecc 70%,#08111ef7 100%),var(--banner-img);
  /* background-position vertical anchor: center -> top, 2026-09-08 (direct
     request: "fixed at the top and if there was cropping to be done, do it
     from the bottom instead"). cover still crops the art to fit the box at
     any width narrower than the source's own 4.75:1 ratio, but now keeps
     the top of the image intact and drops the crop off the bottom edge
     only, instead of splitting it evenly off both edges. Horizontal stays
     centered (unrelated axis, no report against it). */
  background-size:cover,cover;background-position:center 20px,center 20px;background-repeat:no-repeat,no-repeat;
  /* Fade start pulled up from 88% to 65% (2026-08-24, "the fade only
     happens very near the end... can it fade further up? to make it more
     gradual") — spans more of the box's existing height without growing
     the box itself (that was the OTHER direction's correction, same day:
     "the entire header banner looks too long"). Safe against legibility
     even though this now overlaps the lower edge of the text block (the
     orn-divider sits right around here) — the mask is still ~fully opaque
     at 65%, only meaningfully thinning out in the back half of the zone,
     well past where any text sits. Pushed back down to 85% (2026-09-13,
     direct report: "theres a slated transparency effect, can we level
     it?") — the 2026-09-13 sitewide ambient-background brightening
     (`.site-bg-img`'s filter:brightness()) turned this SAME reveal zone
     from "barely visible dark corner" into a genuinely colorful, sharply-
     shaped diagonal nebula patch bleeding into the banner art, since the
     mask itself is a plain uniform vertical fade but the real background
     image behind it has its own diagonal cloud shapes — a wider reveal
     zone simply exposed more of that shape. Shrinking the zone (less
     height for the real background to show through) fixes it without
     touching the brightness change itself, which was separately
     confirmed working as wanted ("it's actually great"). */
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 85%,transparent 100%);mask-image:linear-gradient(180deg,#000 0%,#000 85%,transparent 100%);}
/* Left-to-right brightening, added 2026-08-26 (direct request: banner art is
   deliberately composed with its emphasis on the right, while the
   breadcrumb/title text sits at bottom-LEFT, so the art's own bright side
   was getting no extra lift while the text-safe side did). Two rejected
   attempts before this one: (1) a `screen`-blended white gradient layered
   BETWEEN the scrim and the art got crushed back down by the scrim's own
   near-opaque bottom half ("the right is still so dim and dark"); (2)
   moving that same white layer to paint on TOP fixed the crushing but then
   washed the art toward white/gray, killing its actual hue ("why are u
   just applying a white gradient there? I want it in its original
   saturation and colour"). Fix: a SEPARATE ::after duplicates the banner
   art itself (not a flat color) and screens it against the fully-
   composited result below via `mix-blend-mode` (an ELEMENT-level blend --
   unlike ::before's `background-blend-mode`, which only blends layers
   WITHIN that one element against each other, this blends the whole
   ::after against everything already painted underneath it, so it isn't
   crushed by the scrim the way attempt (1) was). Screening an image with a
   copy of itself is a standard "self-glow" brightening technique -- it
   lifts luminance using the art's OWN colors (screen(x,x)=2x-x²), so hue/
   saturation stay intact instead of bleeding toward flat white. Two masks,
   ANDed via `mask-composite:intersect` (multiplies their alpha rather than
   the default union), keep this confined to the same bottom-fade zone as
   ::before AND ramp it in left(invisible)->right(full), same shape as the
   two earlier attempts. */
.page-header-banner::after{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  /* background-position must stay IN SYNC with ::before's own (center top,
     2026-09-08) — this layer screens an exact copy of the same art against
     ::before's for the self-glow effect below, which only works when both
     copies are pixel-aligned; letting them drift to different anchors
     produces a visible ghosted double-exposure (2 offset copies of the art
     blended together) rather than a clean brightened single image. Found
     via direct report + screenshot the same session ::before's anchor was
     changed — verify both stay identical any time either one changes. */
  background-image:var(--banner-img);background-size:cover;background-position:center 20px;background-repeat:no-repeat;
  mix-blend-mode:screen;
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 85%,transparent 100%),linear-gradient(90deg,transparent 0%,transparent 35%,#000 100%);
  -webkit-mask-composite:source-in;
  mask-image:linear-gradient(180deg,#000 0%,#000 85%,transparent 100%),linear-gradient(90deg,transparent 0%,transparent 35%,#000 100%);
  mask-composite:intersect;}
.page-header-banner>*{position:relative;z-index:2;}
/* REVERTED same day, 2026-09-08 -- giving .breadcrumb its own margin-top:
   auto (tried right above this comment for one round) made its position
   vary WITH content again (a flex container splits free space evenly
   across however many auto-margin edges exist, and a long-content page
   like tierlist-pve.html has near-zero free space left once its content
   nearly fills the 325px box, so its breadcrumb stayed near the top while
   a short page like pets.html — with lots of free space — got pushed much
   lower: 26px vs 61px, still inconsistent). Direct correction: "It's
   honestly driving my crazy that they are not at the same height... You
   need to anchor the top of the text starting from the breadcrumbs."
   Consistency wins over "not too high" -- see the padding-top bump on
   .page-header-banner itself (a STATIC value, not content-dependent) for
   how the lower position is achieved instead, without reintroducing any
   variance. Don't re-add an auto margin on breadcrumb without re-solving
   this same conflict. */

/* Hides an <img> that has no `src` attribute AT ALL yet (not even src="") —
   several pages (codex.html's Type-filter classification icons and similar
   JS-patched-in-later icons elsewhere) render their <img> tags in the
   static HTML with no src, then set el.src via JS once the page's data has
   loaded. In the gap between "browser paints the raw HTML" and "that JS
   finally runs" (worse the longer that JS is blocked behind other heavy
   <script src> data files earlier in the document — a separate, real
   contributor to this same window), a src-less <img> renders as a small
   broken-image glyph, which reads as a stray checkbox/empty box next to
   filter chip labels. Direct report, 2026-08-24: "the flash of that
   horrible font and bareness" (this rule addresses the broken-icon part of
   that; the font part is Google Fonts' own standard swap behavior, not
   fixable this way — see project_site_chrome_redesign_2026_08_23.md for
   the fuller diagnosis and what a real fix would take). Once JS sets a
   real src, the attribute exists and this rule stops applying — no visual
   change to the final, fully-loaded state. */
img:not([src]){visibility:hidden;}

/* Nav mega-menu accordion — added 2026-08-24, see site-chrome.js's
 * mountMegaMenu() for the full markup contract + rationale. Replaces the
 * old small per-item `.nav-dropdown` (vertical list, absolutely
 * positioned under just that one nav-item) site-wide.
 *
 * position:fixed (not absolute) — the nav bar itself is
 * position:sticky;top:0, so it can be pinned at the very top of the
 * viewport in EITHER scroll state (page-top OR scrolled-and-stuck);
 * `fixed` at `top:56px` (the site's own unchanging `.nav{height:56px}`)
 * stays correctly flush under the nav in both cases without needing to
 * track which state the nav is currently in.
 *
 * The open/close animation is the CSS grid-rows accordion trick
 * (`grid-template-rows: 0fr` -> `1fr`) rather than `max-height` — this
 * category's content height varies a lot (Tier Lists: 3 items, PVE
 * Guides: 7 items), and grid-rows sizes to the ACTIVE panel's real
 * content height automatically on both open and close, no JS
 * measurement needed. `.nav-megamenu-inner` is the grid's one row/item;
 * `min-height:0` on it is required for the trick to work (grid items
 * default to a content-based minimum size that would otherwise fight
 * the collapsed 0fr state). */
/* Glassy translucent (2026-08-24, 2 correction rounds — direct reports:
 * "The accordion we set out to do was supposed to have a glassy
 * translucent feel. This isnt it." then, after the first alpha pass
 * still read as effectively opaque, "no glass effect"). Round 1 used the
 * same alpha the sticky bars' own STUCK state uses (d9/e6, ~85-90%
 * opaque) — computed-style-verified `backdrop-filter:blur(18px)` WAS
 * genuinely applied, but at that alpha almost none of the real page
 * behind it (ambient stars, header banner art) shows through, so the
 * blur has nothing visible to soften — reads as flat color, not glass.
 * Round 2: dropped alpha hard (a6/b3, ~65-70% opaque) so the page behind
 * genuinely shows through, which is what makes the blur actually
 * perceptible as glass rather than a technically-present but invisible
 * effect. Kept blur strong (18px) so it still tames busy content
 * scrolling underneath rather than reading messy. */
.nav-megamenu{
  /* top tracks --nav-h (not a bare 50px) -- otherwise, while closed, this
     collapses to a ~1px sliver (just its own border-bottom, since
     grid-template-rows:0fr zeroes its content height) that stays floating
     at a stale fixed y:50 even after the nav itself slides out of view,
     showing up as a second, independent stray gold line. */
  position:fixed;top:var(--nav-h,66px);left:0;right:0;z-index:790;
  display:grid;grid-template-rows:0fr;
  transition:grid-template-rows .32s cubic-bezier(.22,.61,.36,1),top .25s ease;
  background:linear-gradient(180deg,#0b18268a,#08111e99);
  -webkit-backdrop-filter:blur(22px);backdrop-filter:blur(22px);
  border-bottom:1px solid #c8922a33;
  box-shadow:0 20px 44px #00000066;
  overflow:hidden;
}
.nav-megamenu.open{grid-template-rows:1fr;}
.nav-megamenu-inner{min-height:0;overflow:hidden;}
.nav-megamenu-panel{display:none;}
/* Padding/gaps cut down hard (2026-08-24 correction, direct report: "SO
 * MUCH UNNECESSARY SPACE") — panel padding 26/40/30 -> 12/40/14, item
 * padding 12/6 -> 5/6, every gap 10 -> 6, icon 52 -> 40. Reads as a tight
 * utility strip now instead of a spacious showcase panel. */
/* width:fit-content (2026-09-12, was a full-width flex row with
 * max-width:1900px;margin:0 auto that had no real centering effect since
 * a block-level flex container fills its available width by default) --
 * lets the panel shrink to just its own items' width, still wrapping
 * past max-width for a many-item panel (Tier Lists, Others). Its actual
 * horizontal POSITION is picked by centerPanelUnderTrigger() in
 * site-chrome.js (an inline margin-left override applied every open),
 * not this rule -- see that function's own comment for why. */
.nav-megamenu-panel.active{
  display:flex;flex-wrap:wrap;gap:6px;
  width:fit-content;max-width:min(1900px,calc(100% - 80px));padding:12px 40px 14px;
}
.mega-item{
  display:flex;flex-direction:column;align-items:center;gap:6px;
  width:80px;padding:5px 6px;border-radius:8px 0 8px 0;
  text-decoration:none;transition:background .15s,transform .15s;
}
.mega-item:hover{background:#c8922a14;transform:translateY(-2px);}
.mega-item-icon-wrap{position:relative;width:40px;height:40px;flex-shrink:0;}
.mega-item-icon{width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 2px 6px #00000066);}
/* .dd-live (the small green "shipped" badge on a mega-item's icon)
 * removed sitewide 2026-09-12, direct request -- was reading as visual
 * clutter across every dropdown rather than useful signal. Markup pulled
 * from NAV_TEMPLATE.html + every real page via sync_nav_footer.js; this
 * rule is now dead CSS by design, kept only as history -- don't re-add
 * without a fresh design pass on what the badge is even for. */
.mega-item-title{
  font-size:11px;font-weight:700;color:var(--text-mid);
  text-align:center;line-height:1.25;letter-spacing:.2px;
}
.mega-item:hover .mega-item-title{color:var(--gold-lt);}
@media (prefers-reduced-motion:reduce){.nav-megamenu{transition:none;}}

/* Mobile nav hamburger + vertical menu panel, added 2026-09-08 — the
 * horizontal nav (10 items, no wrap/collapse strategy at all) had zero
 * responsive fallback below ~1100px, just squeezed/broke (direct report +
 * screenshot: "nav bar also completely gets screwed up"). Markup for both
 * the hamburger button and the panel is injected entirely by
 * mountMobileNav() (site-chrome.js, called automatically from the end of
 * mountMegaMenu() — no page-level script changes needed), which CLONES the
 * real `.nav` children + `#nav-megamenu`'s panel content rather than
 * duplicating a second nav list here or in NAV_TEMPLATE.html — this file
 * only supplies the visual styling for whatever that function builds.
 *
 * `.nav-hamburger` sits at the far right of the slim 56px bar via
 * margin-left:auto — this works regardless of how many `.nav-link`/
 * `.nav-item` siblings sit between it and `.nav-logo` in DOM order,
 * because those get `display:none` below the breakpoint and don't
 * participate in the flex layout at all once hidden.
 *
 * `.nav-mobile-panel.open{display:flex}` is scoped INSIDE the same media
 * query as everything else here (not a bare, always-matching rule) so
 * resizing back past the breakpoint while the panel is left open hides it
 * immediately via the query no longer matching, regardless of the JS
 * "open" class still being present — the matching resize listener in
 * mountMobileNav() only resets the hamburger's own animated-X visual
 * state, the actual show/hide is CSS-driven, not JS-driven, so it can
 * never desync from the real viewport width. */
.nav-hamburger{display:none;flex-direction:column;justify-content:center;gap:5px;width:34px;height:34px;margin-left:auto;background:none;border:none;cursor:pointer;padding:0;flex-shrink:0;}
.nav-hamburger-bar{display:block;width:100%;height:2px;background:var(--gold-lt);border-radius:1px;transition:transform .2s ease,opacity .2s ease;}
.nav-hamburger.open .nav-hamburger-bar:nth-child(1){transform:translateY(7px) rotate(45deg);}
.nav-hamburger.open .nav-hamburger-bar:nth-child(2){opacity:0;}
.nav-hamburger.open .nav-hamburger-bar:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
/* Same glassy-translucent recipe as .nav-megamenu above (2026-09-08 fix,
 * direct report + screenshot of the PC sub-nav's own glass look: "I want
 * the hamburger menu to retain the same glossy glass properties as the
 * sub nav header like in PC") -- this panel had the EXACT same "round 1"
 * mistake .nav-megamenu's own comment already documents: f7 alpha (~97%
 * opaque) leaves almost nothing of the real page behind it visible, so
 * backdrop-filter:blur has nothing to soften and reads as flat color, not
 * glass. Reused verbatim rather than re-deriving new alpha/blur values --
 * this panel sits directly below the same nav bar the megamenu drops
 * from, so it should read as the same glass surface continuing down, not
 * a visually distinct material. */
/* Backdrop (2026-09-18 redesign, direct request: the drawer only covers
 * 80% of the screen width now -- see .nav-mobile-panel's own comment just
 * below -- so the remaining 20% needs SOMETHING to visually connect it to
 * the open menu rather than reading as "just more of the page, untouched".
 * This is a separate full-width/full-height element sitting BEHIND the
 * drawer (lower z-index) that blurs+dims whatever real page content is
 * still showing through that 20% strip. Its own base rule stays
 * display:none outside the @media block below, same convention as
 * .nav-mobile-panel.open -- so widening back past the breakpoint hides it
 * immediately via the query no longer matching, regardless of JS state. */
.nav-mobile-backdrop{display:none;position:fixed;top:var(--nav-h,66px);left:0;right:0;bottom:0;z-index:845;background:rgba(4,9,17,.45);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);}
/* Left-side drawer (2026-09-18 redesign, corrected same day -- 2 direct
 * follow-up fixes below). Fixes a real reported bug: the OLD full-width
 * panel let its own internal overflow-y:auto scroll rubber-band/chain into
 * the page's scroll once you scrolled past the last category, which fired
 * mountMobileNav()'s `window.addEventListener('scroll', closeMobilePanel)`
 * and silently closed the whole menu -- reading as "the menu disappears as
 * if the page scrolled up" (direct report). `overscroll-behavior:contain`
 * below is the actual fix for that (stops the chain at this element's own
 * scroll boundary); mountMobileNav() also now real-locks page scroll for
 * the `html`/`body` themselves while open, per mobile-responsive.md's
 * standing scroll-lock pattern (plain `overflow:hidden`, never a
 * position:fixed+scrollY save/restore), so the underlying page can't
 * scroll at all while the drawer is open -- belt and suspenders, either
 * alone would already fix the reported bug.
 * Width capped at max-width so the drawer doesn't balloon to ~900px on a
 * wide tablet sitting just under the 1180px hamburger breakpoint -- 80% is
 * the intent for the dominant phone-width case, the cap just keeps it a
 * real drawer (not a near-full-screen panel) at the wider end of that
 * range.
 * Side corrected right->left same day, direct request ("make it appear on
 * the left of the mobile browser") -- the hamburger TRIGGER stays at the
 * nav's far right (unrelated, margin-left:auto on .nav-hamburger), the
 * drawer opening from the opposite side is a normal, common pattern.
 * Background alpha corrected back down to the original full-width panel's
 * value (~54-60%, `#0b18268a`/`#08111e99`) same day too, direct report
 * ("completely lost its glassy vibe") -- the first pass bumped it to ~93-
 * 96% reasoning the new separate backdrop layer no longer needed this
 * element to double as the page-dimming overlay, but that's EXACTLY the
 * "round 1" mistake this same file's own .nav-megamenu comment already
 * warns about: past a certain alpha there's almost nothing of what's
 * behind left for `backdrop-filter:blur` to actually soften, so it reads
 * as flat color instead of glass -- confirmed the hard way by repeating it
 * here despite the standing warning. The glass look was never about
 * whether a separate dim layer exists behind it; it's about THIS
 * element's own alpha staying low enough for its own blur to have
 * something real to work with. Reverted to the exact value this drawer
 * already had before the redesign (itself a deliberate verbatim reuse of
 * `.nav-megamenu`'s recipe, see that rule's own comment above) rather than
 * re-tuning a 3rd number by guesswork.
 * Scrollbar hidden 3 ways (below) per direct request ("dont show scroll
 * bar like current design"); `padding-bottom` is the "empty space
 * allowance" scrolling past the last category lands on, instead of the
 * last row sitting flush against the drawer's own bottom edge. */
.nav-mobile-panel{display:none;position:fixed;top:var(--nav-h,66px);left:0;right:auto;width:80%;max-width:400px;bottom:0;z-index:850;background:linear-gradient(180deg,#0b18268a,#08111e99);-webkit-backdrop-filter:blur(22px);backdrop-filter:blur(22px);box-shadow:12px 0 32px #00000066;overflow-y:auto;overscroll-behavior:contain;-ms-overflow-style:none;scrollbar-width:none;flex-direction:column;padding-bottom:48px;}
.nav-mobile-panel::-webkit-scrollbar{display:none;}
.nav-mobile-link,.nav-mobile-group-head{display:flex;align-items:center;gap:12px;padding:15px 20px;color:var(--text-mid);text-decoration:none;border-bottom:1px solid var(--border);font-size:13px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;cursor:pointer;}
.nav-mobile-link.active,.nav-mobile-group-head.active{color:var(--gold);}
/* 20px -> 26px (2026-09-08, direct report: "increase the icon sizes") */
.nav-mobile-link img,.nav-mobile-group-head img{width:26px;height:26px;object-fit:contain;flex-shrink:0;}
.nav-mobile-caret{margin-left:auto;font-size:10px;color:var(--text-dim);transition:transform .2s ease;}
.nav-mobile-group.open .nav-mobile-caret{transform:rotate(180deg);}
/* Animated open/close (2026-09-09 direct request: "more...animated.
   Instead of a very abrupt dropdown") -- was a hard display:none/flex
   toggle, popping the sub-list in/out instantly. Same grid-template-rows
   0fr->1fr technique already used for the desktop .nav-megamenu (see its
   own comment) -- animates smoothly to WHATEVER the sub-list's real
   content height is without needing to know it ahead of time, unlike a
   max-height guess. Needs the .nav-mobile-sub-inner wrapper for the same
   reason .nav-megamenu-inner exists: a grid row transitions between 0fr
   and 1fr, and the actual child content must sit one level down so its
   own intrinsic height can still drive the 1fr track's real size while
   overflow:hidden on both levels keeps the 0fr state visually clipped. */
.nav-mobile-sub{display:grid;grid-template-rows:0fr;background:#0b16238a;overflow:hidden;
  transition:grid-template-rows .28s cubic-bezier(.22,.61,.36,1);}
.nav-mobile-sub-inner{min-height:0;overflow:hidden;display:flex;flex-direction:column;}
.nav-mobile-group.open .nav-mobile-sub{grid-template-rows:1fr;}
@media (prefers-reduced-motion:reduce){.nav-mobile-sub{transition:none;}}
/* --text-dim -> --text-mid (2026-09-08, direct report: "make the
   subcategory font brighter") -- --text-dim is reserved for deliberate
   de-emphasis (inactive tabs, disabled hints), not a default for smaller
   nested text; a real navigable sub-link belongs on --text-mid same as
   every other clickable nav row, see frontend-ui.md's own standing rule. */
.nav-mobile-sub-link{display:flex;align-items:center;gap:10px;padding:13px 20px 13px 46px;color:var(--text-mid);text-decoration:none;font-size:12px;font-weight:600;border-bottom:1px solid #ffffff0d;}
/* 18px -> 22px, same round as the main-row icon bump just above -- kept
   slightly smaller than .nav-mobile-link's 26px so the indented/nested
   hierarchy still reads visually, just less extreme than before. */
.nav-mobile-sub-link img{width:22px;height:22px;object-fit:contain;flex-shrink:0;}
/* Server time + daily reset countdown (2026-09-12, direct request) --
 * markup: NAV_TEMPLATE.html (single source, synced to every real page);
 * live values: SiteChrome.mountServerClock() below. Pinned to the nav's
 * far right via margin-left:auto -- same trick .nav-hamburger already
 * uses on mobile; the 2 never show at once (hamburger only exists past
 * the collapse breakpoint, this widget only exists BEFORE it, see the
 * media query below), so there's no fight over which one gets the
 * auto-margin. Declared BEFORE that media query block on purpose -- its
 * own `.nav-server-clock{display:none;}` override must come AFTER this
 * base rule in source order to win at equal specificity (the exact
 * source-order gotcha mobile-responsive.md already warns about;
 * confirmed the hard way here too -- the widget stayed visible on mobile
 * the first time this was written with the media query above it).
 * padding-left trimmed 20px -> 14px, 2026-09-16, same pass as the nav's
 * own padding trims below -- see that comment for why.
 *
 * margin-left:auto REMOVED 2026-09-16, moved onto the new Changelog
 * nav-icon-link (NAV_TEMPLATE.html) that now sits immediately before this
 * element in DOM order -- see .nav-link.nav-icon-link's own comment just
 * below. This element no longer needs its own auto margin: it now simply
 * follows that link in normal flow, and the two are pushed to the nav's
 * far right TOGETHER as one group, sitting flush against each other
 * (the whole point of the change -- "flushed to the right, before the
 * border of the server clock," direct request). Leaving margin-left:auto
 * on BOTH elements would have split the free space between them instead
 * of collapsing it, opening a gap the request specifically didn't want. */
.nav-server-clock{
  flex-shrink:0;
  display:flex;flex-direction:column;justify-content:center;gap:4px;
  padding-left:14px;border-left:1px solid var(--border2,#253d57);height:34px;
}
/* Changelog nav icon (2026-09-16, direct request) -- markup lives in
 * NAV_TEMPLATE.html, right before #nav-server-clock. Icon-only (no
 * .nav-link-label span in the template), so .nav-link's own
 * flex-direction:column layout just centers the single icon child with no
 * further sizing needed here. margin-left:auto (moved off .nav-server-clock,
 * see that rule's own comment) pushes THIS element and everything after it
 * (the clock) to the nav's far right as one flush group -- an attribute
 * selector here rather than hand-copying into every page's own hand-copied
 * .nav-link{} block (frontend-ui.md's standing "nav CSS is hand-copied per
 * page" note only covers the BASE .nav/.nav-link/.nav-item rules; a small
 * targeted override layered on top through the shared stylesheet every page
 * already links is simpler and can't drift out of sync the way 20+ hand
 * edits could). Excluded from mountEqualWidthNav()'s measurement (see that
 * function's own selector in site-chrome.js) so it stays a compact square
 * icon button instead of stretching to match the widest labeled tab. */
.nav-link.nav-icon-link{margin-left:auto;}
/* Standard visually-hidden-but-accessible utility -- used by the Changelog
 * link's real text label (kept for screen readers + mountMobileNav()'s own
 * textContent fallback, see that link's own comment) without showing on
 * the desktop icon-only button. Generic name/rule, reusable by any future
 * icon-only control that needs the same treatment. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
.nsc-row{display:flex;align-items:baseline;justify-content:space-between;gap:10px;}
/* --text-mid (not --text-dim, direct report -- "cant see text if
   background is too bright"): the nav is a translucent glass panel
   (backdrop-filter blur over whatever real page content/art scrolls
   underneath it), so its own effective brightness isn't constant the
   way an opaque bar's would be -- --text-dim (reserved for deliberate
   de-emphasis, per frontend-ui.md's own standing rule, not just "smaller
   text") washed out completely over a bright patch. The text-shadow is
   the real fix for the underlying variable-brightness problem though --
   a fixed dark shadow keeps both label and value readable regardless of
   what's actually behind the glass at any given moment, the same reason
   .page-header-banner text gets a legibility treatment over real art. */
.nsc-label{font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--text-mid);white-space:nowrap;text-shadow:0 1px 4px #000000cc;}
.nsc-value{font-size:13px;font-weight:800;letter-spacing:.3px;color:#7cc4f5;white-space:nowrap;font-variant-numeric:tabular-nums;text-shadow:0 1px 4px #000000cc;}
.nsc-value.nsc-reset{color:var(--gold-lt);}
/* Breakpoint corrected 1100px -> 1180px, 2026-09-16. History: the old
 * 1100px number had gone quietly stale (tuned before the nav-icon size
 * bump 22px->26px and the whole .nav-server-clock widget existed, never
 * re-verified after either landed) -- real overlap measured between
 * 1100-1241px. A JS-driven overflow-guard (measuring the row live and
 * force-switching to the hamburger early, above the CSS breakpoint) was
 * built and then REVERTED same-day on direct request ("i dont like this
 * forcing hamburger early situation") in favor of fixing the static
 * number properly instead -- see mountEqualWidthNav()'s own comment in
 * site-chrome.js for that mechanism's short history. First fix pass
 * just raised the breakpoint to 1300px (a padded-safe number above the
 * measured 1241px floor); direct follow-up ("isnt making it at 1300 too
 * soon??") asked to bring it back down closer to the original 1100
 * instead of just padding the threshold upward -- so nav-link padding
 * (11px->7px), the nav's own side padding (32px->24px), and the clock
 * widget's left padding (20px->14px) were ALL trimmed too (see those
 * rules' own comments), which pulled the real measured minimum down to
 * ~1139px. 1180px keeps a real ~40px safety margin above THAT floor --
 * re-measure this exact way (getBoundingClientRect() on the last visible
 * cell vs #nav-server-clock, at a wide unconstrained viewport) before
 * changing nav content again, rather than guessing a replacement number
 * OR reaching for a dynamic JS mechanism again. */
@media (max-width:1180px){
  .nav>a.nav-link,.nav>.nav-item{display:none;}
  .nav-hamburger{display:flex;}
  .nav-mobile-panel.open{display:flex;}
  .nav-mobile-backdrop.open{display:block;}
  .nav-server-clock{display:none;}
}

/* Main nav-bar icons (2026-08-24) — every top-level trigger/link
 * (Home/Hero Codex/.../Banners), not just the accordion sub-items. Same
 * data-rel-then-JS-resolved pattern as .mega-item-icon (see
 * mountMegaMenu()'s own comment) — no src at all until JS sets one, the
 * site-wide img:not([src]){visibility:hidden;} FOUC rule covers the gap.
 * Stacked ABOVE the label (2026-08-24 correction, direct follow-up: "the
 * main header nav icons should be above the words too in the same style"
 * as the accordion items) — .nav-link itself switched to
 * flex-direction:column for this, so no margin-right needed anymore; the
 * label text is wrapped in its own .nav-link-label span so a dropdown
 * trigger's "LABEL ▾" stays together on the row below the icon instead of
 * the caret becoming a 3rd stacked row. */
.nav-link-icon{width:26px;height:26px;object-fit:contain;flex-shrink:0;}

/* Nav auto-hide: slides the whole nav bar out on scroll-down, back in on
 * scroll-up (mountNavAutoHide() in site-chrome.js toggles .nav-hidden).
 * --nav-h tracks the nav's live visible height (66px normally, 0px while
 * hidden) -- mountNavAutoHide() writes it on <html> alongside the class.
 * Every secondary sticky bar that offsets itself to sit flush under the
 * nav (search bars, jump bars, sidebars, etc) reads --nav-h in its own
 * `top` instead of hardcoding a px value, so it slides up to fill the gap
 * rather than leaving a floating band of empty space above it.
 * Bumped 56->66px 2026-09-12 (main.html nav-revamp parity rollout: the
 * whole .nav block switched to a fixed glass/blur treatment matching
 * main.html exactly, sitewide -- see each real page's own new
 * body{padding-top:66px} compensating rule, added the same day, right
 * after its .nav{} block). */
:root{--nav-h:66px;}
.nav{transition:transform .25s ease;}
/* -100% alone lands the nav's own border-bottom pixel exactly at y:0 (the
 * new top edge after translating up by the box's own full height), so it
 * stays visibly stuck there as a stray line even though everything else
 * scrolled away -- the extra -4px carries the border off-screen too. */
.nav.nav-hidden{transform:translateY(calc(-100% - 4px));}

/* BACK TO TOP — standard site-wide FAB, injected by mountBackToTop() in
 * site-chrome.js. First shipped hand-rolled on resource-codex.html (no
 * way back up from a 14-category/137-tile page without a long manual
 * scroll), promoted here 2026-08-25 so every page gets it via one call.
 * Fixed bottom-right, hidden until scrolled past 0.7 viewport heights
 * (0.8 originally, tuned down 2026-09-16 on direct request)
 * (see mountBackToTop()'s scroll listener). Independent of --nav-h /
 * nav-hide state -- it's bottom-fixed, never sits under the nav.
 * font-family/font-weight set explicitly here (fixed 2026-09-01) --
 * a <button> doesn't inherit font-family by default, so without this
 * the injected '↑' glyph rendered in the browser's own default UI font
 * (thin, tall) on any page lacking a page-wide button{font-family:
 * inherit} reset (codex.html/pets.html), while every page WITH that
 * reset got 'SKR Global's bolder glyph -- same component, 2 different
 * looks. Never rely on a host page's own reset for this shared FAB. */
.back-to-top,.scroll-to-bottom{position:fixed;right:28px;bottom:28px;z-index:850;width:44px;height:44px;border-radius:50%;
  background:var(--card2);border:1px solid var(--border2);color:var(--gold-lt);font-size:18px;font-weight:700;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;
  display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(0,0,0,.5);
  transition:opacity .2s ease,transform .2s ease,border-color .15s,background .15s;}
/* .back-to-top: hidden-by-default, needs JS to add .show past the scroll
 * threshold (unchanged original behavior). .scroll-to-bottom: the OPPOSITE
 * default -- visible from the very first paint, no fade-in, no threshold
 * wait -- direct correction 2026-09-16 ("should already be seen when
 * entering a page without having to scroll a tiny bit before it appears").
 * JS only ever ADDS .hide to it once scrolled past threshold; it never
 * needs a .show class, so there is no JS-timing window where it could
 * render invisible on load. */
.back-to-top{opacity:0;pointer-events:none;transform:translateY(10px);}
.back-to-top.show{opacity:1;pointer-events:auto;transform:translateY(0);}
.scroll-to-bottom{opacity:1;pointer-events:auto;transform:translateY(0);}
.scroll-to-bottom.hide{opacity:0;pointer-events:none;transform:translateY(10px);}
.back-to-top:hover,.scroll-to-bottom:hover{border-color:var(--gold);background:var(--gold-glow);}
.back-to-top img,.scroll-to-bottom img{width:20px;height:20px;object-fit:contain;filter:drop-shadow(0 1px 3px #00000066);}
/* .scroll-to-bottom shares .back-to-top's EXACT slot (right:28px;bottom:28px)
 * on purpose, added 2026-09-16 -- the two are mutually exclusive by design
 * (mountBackToTop()'s updateVisibility() toggles them as exact inverses of
 * each other), so occupying the same spot reads as one button whose
 * icon/behavior flips near the top vs. past the reveal threshold, not 2
 * competing FABs. Never give this its own bottom offset. */

/* BACK BUTTON -- added 2026-09-17, companion mount function
 * SiteChrome.mountBackButton() (site-chrome.js). Deliberately its OWN
 * top-left corner, not stacked with the bottom-right .back-to-top/
 * .scroll-to-bottom/.feedback-bubble cluster -- top-left is the direction
 * people actually look for a "back" affordance (direct user correction).
 * `top` tracks --nav-h the same way .nav-mobile-panel does above, so it
 * stays flush under the nav whether the nav is at its normal 66px or
 * hidden (0px) via mountNavAutoHide() -- never a hardcoded px offset.
 * Always visible, no scroll-based show/hide (same reasoning as
 * .feedback-bubble): this button exists specifically for a
 * browser-fullscreen situation with no OS/browser back button reachable,
 * so it has to be usable from the very first paint. */
.back-fab{position:fixed;left:28px;top:calc(var(--nav-h,66px) + 16px);z-index:850;width:44px;height:44px;border-radius:50%;
  background:var(--card2);border:1px solid var(--border2);color:var(--gold-lt);font-size:20px;font-weight:700;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;
  display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(0,0,0,.5);
  transition:border-color .15s,background .15s,transform .15s,top .2s ease;}
.back-fab:hover{border-color:var(--gold);background:var(--gold-glow);transform:translateX(-2px);}
.back-fab img{width:20px;height:20px;object-fit:contain;filter:drop-shadow(0 1px 3px #00000066);}
/* Hidden below the standard 760px breakpoint -- direct request: on mobile
 * the real browser back gesture (edge-swipe) and OS/browser chrome back
 * button are already within thumb's reach, so this FAB is redundant there
 * (unlike desktop-fullscreen, the actual motivating case). */
@media (max-width:760px){ .back-fab{display:none;} }

/* FORWARD BUTTON -- added 2026-09-17, companion mount function
 * SiteChrome.mountForwardButton() (site-chrome.js). Top-RIGHT mirror of
 * .back-fab, same `top` formula so the two sit on the same row across the
 * page ("in line with the stack in terms of page axes", direct request) --
 * NOT the same corner as the bottom-right .back-to-top/.scroll-to-bottom/
 * .feedback-bubble cluster, that's a separate, unrelated stack. */
.forward-fab{position:fixed;right:28px;top:calc(var(--nav-h,66px) + 16px);z-index:850;width:44px;height:44px;border-radius:50%;
  background:var(--card2);border:1px solid var(--border2);color:var(--gold-lt);font-size:20px;font-weight:700;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;
  display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(0,0,0,.5);
  transition:border-color .15s,background .15s,transform .15s,top .2s ease;}
.forward-fab:hover{border-color:var(--gold);background:var(--gold-glow);transform:translateX(2px);}
.forward-fab img{width:20px;height:20px;object-fit:contain;filter:drop-shadow(0 1px 3px #00000066);}
/* Hidden below 760px, same reasoning as .back-fab above. */
@media (max-width:760px){ .forward-fab{display:none;} }

/* FEEDBACK BUBBLE -- added 2026-09-15, companion mount function
 * SiteChrome.mountFeedbackBubble() (site-chrome.js). Stacked directly
 * above .back-to-top in the same right:28px column (84px = 28px
 * bottom-offset + 44px back-to-top height + 12px gap) so the two read as
 * one FAB cluster. Unlike .back-to-top this is always visible -- no
 * scroll-based .show toggle -- feedback should be reachable the instant a
 * page loads, not gated behind a scroll threshold. */
.feedback-bubble{position:fixed;right:28px;bottom:84px;z-index:850;width:44px;height:44px;border-radius:50%;
  background:var(--card2);border:1px solid var(--border2);font-size:19px;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;
  display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(0,0,0,.5);
  transition:border-color .15s,background .15s,transform .15s;}
.feedback-bubble:hover{border-color:var(--gold);background:var(--gold-glow);transform:translateY(-2px);}
.feedback-bubble img{width:20px;height:20px;object-fit:contain;filter:drop-shadow(0 1px 3px #00000066);}
@media (max-width:760px){ .feedback-bubble{right:16px;bottom:76px;} .back-to-top,.scroll-to-bottom{right:16px;} }

/* Backdrop-flex-center pattern (never fixed+translate), overflow:hidden
 * scroll-lock (never position:fixed+scrollY save), touch-action:none on
 * the backdrop -- per .claude/rules/mobile-responsive.md. Visibility is
 * the native [hidden] attribute, never a bare-class `display` (frontend-
 * ui.md's own display-vs-hidden gotcha) -- so no base `display` is set
 * here at all, only the :not([hidden]) override. */
.fb-backdrop{position:fixed;inset:0;background:#000c;z-index:2000;padding:24px;touch-action:none;}
.fb-backdrop:not([hidden]){display:flex;align-items:center;justify-content:center;}
.fb-modal{position:relative;width:min(480px,100%);max-height:88vh;overflow-y:auto;
  background:linear-gradient(160deg,color-mix(in srgb, var(--gold) 10%, var(--card)) 0%, var(--card) 55%);
  border:1px solid var(--border2);border-radius:var(--R);box-shadow:0 20px 50px #000000aa;padding:26px 26px 22px;}
.fb-modal-close{position:absolute;top:14px;right:14px;width:32px;height:32px;border-radius:50%;
  background:rgba(0,0,0,.4);border:1px solid var(--border2);color:var(--text-mid);font-size:15px;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;}
.fb-modal-close:hover{border-color:var(--gold);color:var(--gold-lt);}
.fb-modal-head{margin-bottom:18px;padding-right:30px;}
.fb-modal-title{font-family:'Bebas Neue',sans-serif;font-size:26px;letter-spacing:1.5px;color:var(--text);margin-bottom:4px;}
.fb-modal-sub{font-size:12.5px;color:var(--text-mid);line-height:1.5;}
.fb-form{flex-direction:column;gap:16px;}
.fb-form:not([hidden]){display:flex;}
.fb-field{display:flex;flex-direction:column;gap:7px;}
.fb-label{font-size:12px;font-weight:700;color:var(--text-mid);letter-spacing:.3px;}
.fb-opt{font-size:10.5px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-left:2px;}
.fb-req{color:var(--gold-lt);}
.fb-input,.fb-textarea{width:100%;box-sizing:border-box;background:var(--bg2);border:1px solid var(--border2);
  border-radius:5px;color:var(--text);font-size:13px;padding:9px 10px;font-family:'SKR Global','Lato',sans-serif;
  transition:border-color .15s;resize:vertical;}
.fb-input:focus,.fb-textarea:focus{outline:none;border-color:var(--gold);}
.fb-input.fb-err,.fb-textarea.fb-err{border-color:#e05070;}
.fb-scale{display:flex;align-items:center;gap:8px;}
.fb-scale-end{font-size:10.5px;color:var(--text-dim);flex-shrink:0;}
.fb-scale-pills{display:flex;gap:4px;flex:1;justify-content:space-between;}
.fb-pill{flex:1;height:30px;min-width:0;border-radius:6px;border:1px solid var(--border2);background:var(--bg2);
  color:var(--text-mid);font-size:11.5px;font-weight:700;font-family:'SKR Global','Lato',sans-serif;cursor:pointer;
  display:flex;align-items:center;justify-content:center;transition:all .15s;}
.fb-pill:hover{border-color:#c8922a55;}
.fb-pill.on{background:var(--gold);border-color:var(--gold);color:#1a1206;}
.fb-field-err .fb-pill{border-color:#e0507055;}
.fb-error{font-size:12px;color:#e05070;background:#e0507014;border:1px solid #e0507044;border-radius:6px;padding:9px 11px;}
.fb-submit{width:100%;padding:12px;border-radius:var(--RS);border:1px solid var(--gold);background:var(--gold);
  color:#1a1206;font-size:13px;font-weight:800;letter-spacing:1px;text-transform:uppercase;cursor:pointer;
  font-family:'SKR Global','Lato',sans-serif;transition:all .15s;}
.fb-submit:hover:not(:disabled){background:var(--gold-lt);border-color:var(--gold-lt);}
.fb-submit:disabled{opacity:.6;cursor:not-allowed;}
.fb-success{flex-direction:column;align-items:center;gap:10px;padding:30px 10px 10px;text-align:center;}
.fb-success:not([hidden]){display:flex;}
.fb-success-icon{width:52px;height:52px;border-radius:50%;background:#2fbf7122;border:1px solid #2fbf7166;
  color:#2fbf71;font-size:24px;display:flex;align-items:center;justify-content:center;}
.fb-success-text{font-size:14px;color:var(--text-mid);}

/* fb-csel -- one-off trimmed custom-select scoped under its own class
 * names (never .csel-*) so this cross-page shared component can't clash
 * with whatever csel port a given host page already ships (frontend-ui.md
 * "every filter <select> gets this wrapper, never a bare native select").
 * Visual language matches the site's standing csel component; z-index sits
 * above .fb-backdrop's own 2000 since the menu is portalled to
 * document.body (outside the modal), not nested inside it. */
.fb-csel-wrap{position:relative;display:flex;}
.fb-csel-wrap select{position:absolute;inset:0;opacity:0;pointer-events:none;}
.fb-csel-trigger{flex:1;min-width:0;display:flex;align-items:center;box-sizing:border-box;background:var(--bg2);
  border:1px solid var(--border2);border-radius:5px;color:var(--text);font-size:13px;padding:9px 26px 9px 10px;
  font-family:'SKR Global','Lato',sans-serif;cursor:pointer;position:relative;transition:border-color .15s;}
.fb-csel-trigger:hover{border-color:#c8922a55;}
.fb-csel-wrap.open .fb-csel-trigger{border-color:var(--gold);}
.fb-csel-input{width:100%;background:transparent;border:none;outline:none;color:inherit;font:inherit;padding:0;
  margin:0;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.fb-csel-arrow{position:absolute;right:9px;top:50%;transform:translateY(-50%);font-size:9px;color:var(--text-dim);
  transition:transform .15s;}
.fb-csel-wrap.open .fb-csel-arrow{transform:translateY(-50%) rotate(180deg);}
.fb-csel-menu{display:none;position:fixed;min-width:100px;width:max-content;max-width:320px;background:var(--card);
  border:1px solid var(--border);border-radius:var(--RS);box-shadow:0 16px 32px #00000077;z-index:2100;
  max-height:240px;overflow-y:auto;}
.fb-csel-menu.show{display:block;}
.fb-csel-option{padding:9px 12px;font-size:12.5px;font-weight:600;color:var(--text-mid);cursor:pointer;
  white-space:nowrap;transition:background .12s,color .12s;}
.fb-csel-option:hover{background:#c8922a16;color:var(--gold-lt);}
.fb-csel-option.on{color:var(--gold-lt);background:#c8922a10;}

@media (max-width:760px){
  .fb-modal{padding:22px 18px 18px;}
  .fb-scale-pills{gap:3px;}
  .fb-pill{font-size:10px;height:27px;}
}

/* PAGE FOOTER — moved here 2026-09-01 (previously hand-copied per page with
 * 3 different drifted markup/text/CSS variants found across the live site).
 * Markup source of truth is FOOTER_TEMPLATE.html, written into every page
 * by sync_nav_footer.js -- never hand-edit a page's own <div class="footer">
 * directly. Sharing the CSS here too (rather than re-copying it into every
 * page's own <style> block) removes a second, independent duplication path
 * for the exact same drift. main.html/coming-soon.html hand-copy this rule
 * (no site-chrome.css link), same convention as .back-to-top above. */
.footer{border-top:1px solid #1e314833;padding:20px 40px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;max-width:1900px;margin:0 auto;}
.footer span{font-size:12px;color:var(--text-dim);}
