/* ---- 00-tokens.css */
/* THE GAME PAGE'S TOKENS (docs/ui-overhaul/PLAN.md 2.1-2.3). One sheet, read by every other
   ui/*.css and by the editor's Battle preview, which links this file on its own.

   Colour, faces, the window size and the HUD's tokens live here and nowhere else. The tools' sheet
   (packages/kit/public/gs.css, ops and the editor) keeps its own --gs-* set; the game reads none.

   Rule: every colour is a token. Literals in the old sheets move here surface by surface as each
   one is ported, not up front. */

:root {
  /* THE SAFE AREA (docs/ui-overhaul/SAFE-AREA.md): the device's notch and home-bar insets, read here and in
     gui/mode.ts only. --frame-w/-h are the glass less them, written by mode.ts in css px; these
     fallbacks cover the first paint. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --frame-w: calc(100dvw - var(--safe-l) - var(--safe-r));
  --frame-h: calc(100dvh - var(--safe-t) - var(--safe-b));
}
/* iPhone Safari in landscape reports a 20 px home-bar inset while its toolbar is open and none once
   it has shrunk to the address pill, with the page's bottom on the same glass edge both times: the
   HUD's bottom row and the dial floated over the world in one state and not the other (iOS 26,
   2026-10-15). In the browser it is taken as none; an installed app keeps it. */
@supports (-webkit-touch-callout: none) {
  @media (display-mode: browser) and (orientation: landscape) { :root { --safe-b: 0px; } }
}

:root {
  /* MIDNIGHT ASTROLABE (owner, 2026-10-07: theme lab docs/ui-overhaul/themes t03): deep navy
     surfaces, star-white ink, ONE accent metal (pale brass), brass hairlines, starlight washes.
     The ladder first; every token below is a meaning that reads it. rgb triples beside the
     colours that are used at an alpha: rgba(var(--x-rgb), a). */
  --navy-0: #080d1a;                 /* the well: inputs, tile hollows, bar troughs */
  --navy-1: #0d1321;                 /* the slab: window ground, card ground */
  --navy-2: #141c2e;
  --navy-3: #1a2439;
  --navy-4: #223049;                 /* the lightest surface: a head band, a lit plate */
  --navy-0-rgb: 8, 13, 26;
  --navy-1-rgb: 13, 19, 33;
  --navy-2-rgb: 20, 28, 46;
  --navy-3-rgb: 26, 36, 57;
  --navy-4-rgb: 34, 48, 73;
  --wash-rgb: 6, 11, 24;             /* every translucent wash: the veil, the HUD rows, a count's plate */
  --sky: #c9d6f2;                    /* starlight: the selected row, a band, a hover, a lit edge */
  --sky-rgb: 201, 214, 242;
  --brass-pale: #f4e6c4;             /* THE accent metal, light to dark */
  --brass-hi: #e6cf9c;
  --brass: #d4b57a;
  --brass-dim: #7c6844;
  --brass-pale-rgb: 244, 230, 196;
  --brass-hi-rgb: 230, 207, 156;
  --brass-rgb: 212, 181, 122;
  /* The primary plate's navy: bluer than the slab, so a button reads as a thing on the surface. */
  --plate-navy: #18324c;
  --plate-navy-hi: #1e3c5a;
  --plate-navy-lo: #0f2135;

  /* surfaces and ink. Dark is the one tone (PLAN D8): the light parchment tone is gone, and the
     few surfaces that stay paper put their own values back (20-components.css PAPER SURFACES). */
  --paper: var(--navy-1);
  --paper-2: var(--navy-2);
  --ink: #e3e8f1;
  --ink-strong: #f4f6fb;
  --ink-strong-rgb: 244, 246, 251;
  --ink-mute: #9ba7bd;
  --ink-faint: #5f6b82;
  --white: var(--navy-4);
  --shadow-ink: 1, 3, 10;           /* rgba(var(--shadow-ink), a): every drop shadow */
  --ink-wash: var(--sky-rgb);       /* rgba(var(--ink-wash), a): hover and empty-slot washes */
  --error: #e0776a;

  /* THE ONE WINDOW SIZE. Every dimension in the window system - and every width
     and offset in apps/game/public/ui/25-card.css, which places windows and reads nothing else
     from here - is an em against this number, so this is the single knob that
     scales the whole window system. Lower it and all 34 windows get smaller
     together, in proportion, with no second drawing anywhere.
     Two things deliberately do NOT follow it: the always-on HUD (apps/game/public/ui/40-hud.css
     owns its own sizes and zooms by --hud-z), and the world layer's nameplates and
     damage numbers, which are px because they are drawn over the canvas at camera pixels.
     14px is the desktop M figure; apps/game/client/gui/mode.ts writes the live one on <html>
     (mode x S/M/L), and nothing else does. */
  --win-size: 14px;

  /* The window type ladder, five rungs in px of --win-size (11 / 12 / 14 / 16 / 18 at desktop).
     Text is sized off --win-size, not the parent em, so a label reads the same in a small tile as
     in a big one: badge is counts and tags, small is captions, notes and secondary lines, body is
     the reading size, heading names a section or a card, title is the window's own name. */
  /* Floored at 10 px: a short phone's window size put a count at 8. */
  --fs-badge: max(10px, calc(var(--win-size) * 11 / 14));
  --fs-small: max(10px, calc(var(--win-size) * 12 / 14));
  --fs-heading: calc(var(--win-size) * 16 / 14);
  --fs-title: calc(var(--win-size) * 18 / 14);

  /* type: sans for everything; the display face is for dramatic moments only */
  --face-ui: "Tamer Sans", "Nunito", "Segoe UI", system-ui, sans-serif;
  --face-weight: 600;
  --face-display: "Cinzel", "Tamer Sans", serif;
  --face-display-weight: 600;

  /* engraving overlays: a brass hairline inside a plate */
  --engrave-light: rgba(var(--brass-rgb), .24);
  --engrave-lift: .03em;

  /* button: a flat navy face in a 1px brass line (the square .btn, default, the action). The
     hexagon (.btn--special) lays a sheen over the face and a small brass sparkle at each end (--btn-orn,
     ui/btn-spark). The variants only swap face, rim and ink. */
  --btn-face: var(--plate-navy);
  --btn-rim: #c4a76f;
  --btn-text: #fbf4e2;
  --btn-text-shadow: rgba(var(--shadow-ink), .9);
  --btn-sheen: linear-gradient(180deg, rgba(var(--sky-rgb), .1), transparent 48%, rgba(var(--shadow-ink), .3));
  --art-btn-spark: url(/data/art/ui/btn-spark.4e969877.webp);
  --btn-orn:
    var(--art-btn-spark) left .65em center / .6em .6em no-repeat,
    var(--art-btn-spark) right .65em center / .6em .6em no-repeat;
  --btn2-face: var(--navy-3);        /* .btn--cream, the second action */
  --btn2-rim: var(--brass-dim);
  --btn2-text: var(--ink);
  --btn-bad-face: #5e2630;           /* .btn--rose, the one that destroys */
  --btn-bad-rim: #b07a6e;
  --btn-bad-text: #fbe7e2;
  --btn-ok-face: #24443a;            /* .btn--sage, the one that agrees */
  --btn-ok-rim: #8fae86;
  --btn-ok-text: #eef3e6;
  /* disabled: the same plate gone dim in the night, never grey */
  --btn-off-filter: saturate(.7) brightness(.62);
  --btn-off-opacity: .9;
  /* the wide primary (.btn--block): a navy plate in one thin brass line, the astrolabe ornament
     (cut from the old painted plate) at each end, a fifth of it past the edge */
  --btn-plate-orn: url(/data/art/ui/btn-orn.a3080b9d.webp);
  --btn-plate-line: rgba(var(--brass-rgb), .85);
  /* a ready button's orbiting light (.btn--charged): a star-white head, a pale-brass tail fading
     into starlight, a faint second spark; brass and star on navy, never amber */
  --btn-charge-hot: rgba(var(--brass-pale-rgb), 1);
  --btn-charge: rgba(var(--brass-hi-rgb), .85);
  --btn-charge-dim: rgba(var(--sky-rgb), .22);
  --btn-charge-soft: rgba(var(--sky-rgb), .45);

  /* button geometry, all relative to font-size */
  --btn-point: .65em;                 /* depth of the pointed end */
  --btn-rim-w: max(1px, .06em);      /* hairline rim */
  --btn-ring: .2em;                  /* inset of the inner line from the edge */
  --btn-line-fade: 1.7em;            /* the inner line is gone by here, where the ornament begins */
  --btn-pad-x: 1.94em;               /* the hexagon (.btn--special): room for the points */
  --btn-sq-pad-x: 1.1em;             /* the square (.btn) */
  --btn-pad-y: .6em;

  /* window chrome: the midnight panel, a double brass rim, a navy head with a flat title plate,
     a brass corner scale growing from the top-left and bottom-right corners, starlight selection */
  --win-face: var(--navy-1);           /* a solid navy: cards, lists, a strip that is not a window */
  --win-rim: rgba(var(--shadow-ink), .92);
  --rim: var(--navy-2);                /* a dark band round a framed thing that is not a window (the chat, the shell pill) */
  --rim-in: rgba(var(--brass-rgb), .3);  /* the brass hairline inside that band */
  --line: rgba(var(--brass-rgb), .18);   /* the inner content line in the body */
  --win-shadow: 0 30px 70px -8px rgba(var(--shadow-ink), .82), 0 10px 22px rgba(var(--shadow-ink), .5);
  /* The window's own paint (.win). Top to bottom: a faint star field in two offset tiles, a navy
     wash, the midnight panel at its own 1024 px, navy under it all so the body is opaque. The body
     and foot paint nothing (--win-body), so this is their ground. --win-arc lies over it on .win
     only (the chat's face shares --win-bg and stays plain): the login sky's astrolabe limb engraved
     into the panel, a double brass ring with half-degree ticks, numerals and zodiac glyphs. Its circle
     sits near the bottom-right (owner's sketch, 2026-10-09): centre .13 of the window's width in from
     the right and .108 up from the foot, outer ring .49 of the width, so only its upper-left quarter
     shows, rising from the foot at ~39% across and leaving through the right side, the compass at
     the middle of that span facing the window's middle. The art is just that segment with the
     window's bottom-right corner as its own, so it sits at `right bottom` and is 63.04% of the window
     wide at any size. It fades toward both edges; its quietness is baked into the art (line alpha
     .34 at most). */
  --art-panel: url(/data/art/ui/astro-panel.76e32d74.webp);
  --art-win-arc: url(/data/art/ui/win-astrolabe.be4a39b1.webp);
  --win-sky:
    radial-gradient(circle at 23px 31px, rgba(var(--sky-rgb), .5) 0 .7px, transparent 1.3px) 0 0 / 211px 173px,
    radial-gradient(circle at 141px 12px, rgba(var(--sky-rgb), .35) 0 .6px, transparent 1.2px) 0 0 / 211px 173px,
    radial-gradient(circle at 96px 88px, rgba(var(--brass-pale-rgb), .4) 0 .8px, transparent 1.4px) 0 0 / 211px 173px,
    radial-gradient(circle at 187px 131px, rgba(var(--sky-rgb), .3) 0 .5px, transparent 1.1px) 0 0 / 211px 173px,
    radial-gradient(circle at 52px 146px, rgba(var(--sky-rgb), .28) 0 .5px, transparent 1.1px) 0 0 / 211px 173px,
    radial-gradient(circle at 71px 59px, rgba(var(--sky-rgb), .38) 0 .6px, transparent 1.2px) 0 0 / 307px 241px,
    radial-gradient(circle at 160px 70px, rgba(var(--sky-rgb), .22) 0 .5px, transparent 1px) 0 0 / 307px 241px,
    linear-gradient(180deg, rgba(var(--navy-3-rgb), .62), rgba(var(--navy-2-rgb), .7) 40%, rgba(var(--navy-1-rgb), .84));
  --win-bg: var(--win-sky), var(--art-panel) 0 0 / 1024px 1024px, var(--navy-1);
  /* The always-on chat's face (the box and the phone's peek, 40-hud.css): the window's. A finger's
     device swaps the panel for its own flat colour (the media rule at the foot): the panel is one
     colour to within 1.5 levels under that wash, and it is a 1024 px picture (4 MB decoded) the
     always-on layer would hold for nothing. */
  --chat-bg: var(--win-bg);
  --art-panel-flat: #070d21;
  --win-arc: var(--art-win-arc) right bottom / 63.04% auto no-repeat;
  --win-body: transparent;
  /* The rim, outside the box: a dark line and one thin brass line (owner, 2026-10-09: thinner),
     a lit top edge, the deep cool drop, an inner vignette. */
  --win-ring:
    0 0 0 1px rgba(var(--shadow-ink), .92), 0 0 0 2px rgba(var(--brass-rgb), .44),
    inset 0 1px 0 rgba(var(--brass-pale-rgb), .2), var(--win-shadow), inset 0 0 80px rgba(var(--shadow-ink), .55);
  /* The engraved inner plane: a brass hairline 5px inside the rim, drawn with the corners. */
  --win-plane: rgba(var(--brass-rgb), .1);
  /* The corners (owner, 2026-10-09): a CORNER PIECE, never a medallion. An astrolabe's edge scale:
     a double brass hairline runs out along each edge with graduated ticks between, and on the
     corner point a small faceted lozenge under a slim crescent, its tip a short pointer down the
     diagonal. The art's own (0,0) is the window's corner point; top-left and bottom-right only (the
     second the same art turned half a turn). --corner-size is the art's side, in em of --win-size. */
  --art-corner-tl: url(/data/art/ui/corner-crest.212d8f8c.webp);
  --art-corner-br: url(/data/art/ui/corner-crest-br.c0aa305c.webp);
  --corner-size: 8.32em;
  --win-head-bg:
    linear-gradient(180deg, rgba(var(--sky-rgb), .05), transparent 55%),
    linear-gradient(90deg, rgba(var(--brass-rgb), .08), transparent 55%),
    linear-gradient(rgba(var(--navy-4-rgb), .86), rgba(var(--navy-3-rgb), .7));
  --win-head-lit: rgba(var(--brass-pale-rgb), .2);    /* the head's top edge */
  --win-title-shadow: 0 1px 0 rgba(var(--shadow-ink), .9), 0 0 .6em rgba(var(--sky-rgb), .2);
  /* The title plate: a flat navy that runs in from the window's left edge under the title. */
  --title-plate: #0d1929;
  --title-run: 3.6;                    /* how far the plate runs past the last letter, x --win-size */
  /* Dividers: a section head's plain brass hairline, fading out to the right (owner, 2026-10-09:
     no ornament). A section head paints `background: var(--divider-bg)` and keeps its own box. */
  --divider-bg:
    linear-gradient(90deg, rgba(var(--brass-rgb), .32), rgba(var(--brass-rgb), .14) 80%, transparent) left bottom / 100% 1px no-repeat;
  /* The slot frame under a hotbar slot, and the ring for a round portrait (the HUD's to use:
     40-hud.css). */
  --art-slot: url(/data/art/ui/astro-slot.50e12046.webp);
  --art-ring: url(/data/art/ui/astro-ring.bbeffbf9.webp);
  --parch: var(--navy-3);
  --parch-edge: var(--navy-2);
  /* The SELECTED fill: brass, with navy ink on it (a tag, a ticked box, a toggle that is on). */
  --tan: #c9aa70;
  --tan-rgb: var(--brass-rgb);         /* rgba(var(--tan-rgb), a): every soft brass wash and ring */
  --tan-rim: #8a7449;
  /* A name you can press: the brass lifted until it reads as ink rather than as a border. */
  --link-ink: #ebcf92;
  --tan-text: var(--navy-1);
  --hairline: rgba(var(--brass-rgb), .14);
  --field-border: rgba(var(--brass-rgb), .28);
  --field-face: var(--navy-0);
  /* A chosen row, a band, a hover: starlight over navy, never a brass wash (brass at .2 over navy
     cancels to khaki). The brass stays on rims and edges. */
  --row-on: linear-gradient(90deg, rgba(var(--sky-rgb), .17), rgba(var(--sky-rgb), .07) 60%, rgba(var(--sky-rgb), .03));
  --row-on-flat: rgba(var(--sky-rgb), .11);    /* the same, for a row drawn as separate cells */
  --row-hover: linear-gradient(90deg, rgba(var(--sky-rgb), .08), transparent 80%);
  --menu-hover: linear-gradient(90deg, rgba(var(--brass-rgb), .16), transparent);
  --band-bg: linear-gradient(90deg, rgba(var(--sky-rgb), .08), rgba(var(--sky-rgb), .03) 70%, transparent), rgba(var(--navy-3-rgb), .62);
  /* The selected tab: brass light rising from its foot, a starlight core, the brass underline. */
  --tab-on-bg: radial-gradient(ellipse 60% 90% at 50% 100%, rgba(var(--brass-rgb), .2), transparent 75%), radial-gradient(ellipse 40% 60% at 50% 100%, rgba(var(--sky-rgb), .09), transparent 75%), rgba(var(--navy-4-rgb), .7);
  --tab-on-ink: var(--brass-pale);
  --tab-on-ring: inset 0 0 0 1px rgba(var(--brass-rgb), .34);
  --tab-line: linear-gradient(90deg, transparent, var(--brass) 22%, var(--brass-pale) 50%, var(--brass) 78%, transparent);
  /* A card (tooltip, menu, toast, dropdown list): the navy plate in a brass rim, a dark gap and a
     faint second hairline; a lit top edge, a deep drop. */
  --card-bg: linear-gradient(var(--navy-3), var(--navy-0));
  --card-ring: 0 0 0 1px rgba(var(--brass-rgb), .55), 0 0 0 3px rgba(var(--shadow-ink), .8), 0 0 0 4px rgba(var(--brass-rgb), .16), inset 0 1px 0 rgba(var(--brass-pale-rgb), .2), 0 14px 34px rgba(var(--shadow-ink), .75);
  --card-number: #f0cf86;              /* a figure that matters inside a card's prose */
  /* The veil under a modal: clear through the middle, navy at the corners. */
  --veil: radial-gradient(circle at 50% 50%, rgba(var(--wash-rgb), 0) 33%, rgba(var(--wash-rgb), .64) 100%), rgba(var(--wash-rgb), .2);
  --count-bg: rgba(var(--wash-rgb), .85);   /* the plate under a stack count */
  --dot-ring: rgba(var(--brass-pale-rgb), .75);
  --bubble-own: rgba(40, 54, 84, .95);  /* your own chat line: a lit navy, not brass-brown */

  /* grey pills and strips, the dropdown's grey variant, the unlearned skill */
  --grey-pill: var(--navy-3);
  --grey-pill-2: var(--navy-4);
  --scroll-thumb: var(--brass-dim);
  --scroll-thumb-hover: #a08655;

  /* stepper and round buttons: a navy disc, a brass ring, a brass glyph */
  --cream: var(--navy-3);
  --cream-rim: rgba(var(--brass-rgb), .3);
  --cream-ink: var(--brass);
  --ibtn-on-ring: rgba(var(--navy-1-rgb), .45);
  /* A pill that says go or stop (the trade's agreement): state colours, kept bright on purpose. */
  --pill-stop-bg: #d62828;
  --pill-stop-ink: #fff;
  --pill-go-bg: #22c55e;
  --pill-go-ink: #0b3b1d;

  /* numbers */
  --green: #86c46a;
  --orange: #eba062;
  /* The stat blue, and it is the family's own: --bar-mp is the mana bar and this is one step
     brighter so it carries at 1em on the card's cream paper, where the bar's depth is meant to be
     read as a filled trough. A third hue beside green and orange rather than a fourth invention. */
  --blue: #79a3dc;
  --bar-hp: #9d3b34;
  --bar-mp: #3b5a86;
  --bar-exp: #c9a227;                /* gold: experience is the one bar that is a reward */
  --bar-sage: #3d6f30;
  --bar-gold: #d6a326;               /* the energy bar: brighter than --bar-exp, because it glows */
  --bar-track: #060a14;              /* the empty part of every bar is night ink */
  --bar-text: #fff8f0;               /* the engraving light at full alpha */

  /* over the world: the one place this system paints on grass, water and snow
     instead of on paper. The face is the engraving light, the outline is the
     darkest ink in the family rather than pure black, so a nameplate still
     reads as this game and not as a browser default. --world-rim is a hairline
     at small sizes and grows with the type, so a big cast line keeps its edge. */
  --world-text: #fdf6ec;
  --world-dark: 27, 15, 8;             /* one dark, used at four alphas: rim, halo, glow, plate */
  --world-outline: rgb(var(--world-dark));
  --world-rim: max(1px, .06em);
  --world-veil-loss: rgba(143, 42, 30, .6);   /* the dim behind a defeat, and only a defeat */
  --offline-wash: rgba(40, 40, 40, .55);      /* the freeze: the world and HUD greyed while the socket is down */
  /* The shell (ui/90-shell.css): the night behind the vista until its picture lands, the white chip a
     provider's own mark sits on (Google's G must stand on white), and the ink written straight on
     the vista (the build line). */
  --shell-sky: var(--navy-1);
  --provider-chip: #fff;
  --vista-ink: #fff;
  /* The fight's four figures. Every one of them is a panel colour lifted: the
     error red, the tan gold, the sage green and the paper grey are all too dark
     to read at speed over grass, so each gets its own token here rather than a
     filter or an opacity at the point of use. */
  --world-hit: #e0705a;                /* an ordinary blow */
  --world-crit: #f2c66a;               /* a critical, and the burst it fires */
  --world-crit-rgb: 242, 198, 106;     /* rgba(var(--world-crit-rgb), a): the burst and the orb */
  --world-heal: #8ecb63;               /* a heal, and an ally's health */
  --world-gold: #e0c68f;               /* the quiet gold of the facts beside a name: the level, the guild line */
  /* THE WORLD'S OWN QUALITY INKS. The paper palette's bands (--rarity-*) are inks
     for a parchment card and go dark on purpose; over the canvas, behind a dark
     halo, the deepest of them - the purple at 5.2 on paper - is close to
     invisible. Same bands, same classes, the palette this surface asks for; the
     rule the .chip--bare block below already states. */
  --world-q-green: #86d45f;
  --world-q-blue: #63b8ff;
  --world-q-gold: #ffa14a;
  --world-q-yellow: #ffe066;
  --world-q-purple: #c98bff;
  /* An item's quality as the ground under its picture (owner, 2026-10-09): the band's ink sunk into
     the navy, one flat soft dark colour over the whole well (no shaded edge), so the grid reads its
     qualities without any one tile shouting.
     The window tiles (.tile--{band}) and the hotbar (.hud .slot.q-{band}) both stand on these. */
  --q-bg-green: color-mix(in srgb, var(--world-q-green) 24%, var(--navy-1));
  --q-bg-blue: color-mix(in srgb, var(--world-q-blue) 24%, var(--navy-1));
  --q-bg-yellow: color-mix(in srgb, var(--world-q-yellow) 20%, var(--navy-1));
  --q-bg-gold: color-mix(in srgb, var(--world-q-gold) 22%, var(--navy-1));
  --q-bg-purple: color-mix(in srgb, var(--world-q-purple) 24%, var(--navy-1));
  /* Whoever DID the thing being announced: a player, a guild, a team, or the boss
     that was felled. A lime of its own rather than the quality green above, so an
     actor and a green-band item are not the same word twice. */
  --world-name: #5cf04a;

  /* THE PLATES AND THE FIGHT (ui/45-plates.css, 50-battle.css). Px on purpose: the
     plate layer is never zoomed. --world-shadow is the halo every word on the world
     wears; --plate-rim the four-sided rim a name is cut out with. The world's own nameplates
     are painted by glplates.ts, which keeps its copies of these inks beside its drawing. */
  --world-shadow: 0 0 2px rgba(0, 0, 0, .95), 0 0 5px rgba(0, 0, 0, .75), 0 0 10px rgba(0, 0, 0, .55), 0 1px 2px rgba(0, 0, 0, .9);
  --plate-rim: #120a06;
  --plate-name-shadow: 1px 0 0 var(--plate-rim), -1px 0 0 var(--plate-rim), 0 1px 0 var(--plate-rim), 0 -1px 0 var(--plate-rim), 0 2px 4px rgba(0, 0, 0, .9);
  --plate-lift: 0 1px 2px rgba(0, 0, 0, .75);
  /* The one size of every line on a nameplate (glplates.ts TEXT; the fight plates, the quest
     tracker): 12px, 10px under the phone camera (html.world-far, glplates.ts FAR). Screen px:
     a zoomed layer divides its zoom back out. */
  --plate-fs: 12px;
  /* The floating numbers (numbers.ts, ui/50-battle.css .fn): lifted inks, because a number sits
     on sand and sea as often as on a dark arena, cut out by one near-black outline. A critical
     is the brass turned to hot gold, landing in white with a red and cyan fringe either side. */
  --fn-outline: #0b0604;
  --fn-hit: #ff7a52;
  --fn-crit: #ffd25e;
  --fn-crit-glow: rgba(255, 186, 72, .55);
  --fn-flash: #ffffff;
  --fn-fringe-a: rgba(255, 64, 96, .6);
  --fn-fringe-b: rgba(72, 214, 255, .6);
  --fn-heal: #8fe86a;
  --fn-zero: var(--sky);
  --plate-ink: #f6efe1;                /* a friend's name, and the shop board's face */
  --plate-foe: #ff9d86;                /* a foe's name, and a refusal under a body */
  --plate-stall-ink: #2a1a0e;
  --plate-stall-hover: #fffaf0;
  --bar-wash: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26));
  /* The gauges' paint (owner 2026-10-10: a worn painted strip, not a flat fill, and not bright): a
     gloss on the top half, a shade along the bottom edge, and the grunge grain (data/art/ui/bar-grain,
     tiled along, one tile high). Laid on an overlay ABOVE the fill, never on it - the fill is a
     scaleX, which would stretch the grain with the value - and blended `overlay`, which leaves the
     near-black empty track untouched: only the coloured part wears it. */
  --bar-sheen:
    linear-gradient(180deg, rgba(255, 255, 255, .22) 0, rgba(255, 255, 255, .08) 42%, transparent 52%, transparent 62%, rgba(0, 0, 0, .4) 100%),
    url(/data/art/ui/bar-grain.8ffc8f17.webp) 0 0 / auto 100% repeat-x;
  /* Over every gauge's own colour, so the fills sit a step down from the swatch. */
  --bar-dim: linear-gradient(rgba(0, 0, 0, .26), rgba(0, 0, 0, .26));
  --bar-gold-glow-rgb: 214, 163, 38;  /* the energy bar's halo */
  --bar-gold-core-rgb: 255, 236, 170;
  /* The fight's floats: a miss's grey, a defeat's banner, mana's blue, a status count's ground. */
  --stone-200: #a7a8aa;
  --ember-300: #fa9b83;
  --world-mana: #8fb6f0;
  --mark-count-bg: rgba(6, 9, 26, .82);
  /* The critical burst (warm, additive) and the soul orb (cold): rgb triples, used at alphas. */
  --fx-white-rgb: 255, 255, 255;
  --burst-cream: #ffefc4; --burst-spike: #ffeec8;
  --burst-amber-rgb: 255, 196, 92; --burst-orange-rgb: 255, 138, 48; --burst-ember-rgb: 255, 120, 40; --burst-tip-rgb: 255, 208, 120;
  --orb-lilac: #e6ccff;
  --orb-violet-rgb: 168, 92, 255; --orb-deep-rgb: 120, 48, 220; --orb-night-rgb: 96, 32, 190; --orb-ring-rgb: 198, 150, 255;

  /* item rarity: the tile's ground (--q-bg-*) and the card's name */
  --tile-face: #111a2b;
  /* The tile: a navy hollow lit from above its middle, a dark inset line, a soft inner vignette (a
     quality tile stands on its --q-bg-* instead, flat). The portrait's well is the same night. */
  --tile-bg: radial-gradient(circle at 50% 38%, var(--navy-3), var(--navy-0) 78%);
  --tile-empty-bg: radial-gradient(circle at 50% 40%, rgba(var(--navy-2-rgb), .9), var(--navy-0));
  --tile-rim-base: rgba(var(--shadow-ink), .9);
  --tile-vignette: inset 0 0 .9em rgba(var(--shadow-ink), .55);
  --tile-hover: 0 0 0 1px var(--brass-hi), 0 0 .7em rgba(var(--brass-hi-rgb), .3);
  --portrait-bg: radial-gradient(circle, rgba(var(--navy-2-rgb), .95) 60%, rgba(var(--navy-0-rgb), .9));
  --portrait-ring: rgba(var(--brass-hi-rgb), .45);
  --rarity-purple: #b393d6;
  --rarity-blue: #86a8d0;
  --rarity-gold: #f0964a;
  --rarity-yellow: #e3c456;
  --rarity-green: #93c283;
  --rarity-gold-text: #ffa15e;
  --rarity-yellow-text: #f2d35c;
  --rarity-blue-text: #86a8d0;
  --rarity-green-text: #93c283;
  /* Purple has a second tone for the same reason gold does: the rim and the NAME are read on
     different grounds, so each has its own token. */
  --rarity-purple-text: #c7a6ea;

  /* one size each, read by the item and by the grid that holds it.
     --tile is thirds of an em rather than a round figure because the target is
     40px at the 12px window size, and 3.33em would land at 39.96 and round a
     ten-column bag half a pixel short. It stays in em so it still follows
     --win-size like everything else. */
  --tile: calc(10em / 3);
  --node-w: 6.4em;


  /* ---------------------------------------------------------------- the HUD (40-hud.css)
     The always-on layer's own palette: text straight on the world under a soft wash. On :root
     rather than on .hud so a detached chat window and the world plates read the same values. */
  --hud-ink: rgba(var(--wash-rgb), .78);
  --hud-ink-2: rgba(var(--wash-rgb), .48);
  --hud-wash: linear-gradient(90deg, var(--hud-ink) 0%, var(--hud-ink-2) 55%, transparent 100%);
  --hud-wash-rev: linear-gradient(270deg, var(--hud-ink) 0%, var(--hud-ink-2) 55%, transparent 100%);
  --hud-text: #ffffff;
  --hud-dim: rgba(255, 255, 255, .62);
  --hud-gold: var(--brass-hi);
  --hud-blue: #86bcff;
  --hud-teal: #7de6da;
  /* The Pet Dealer's bench (ui/w-dealer.css): the energy line and its halo. */
  --fuse-ink: #3f9aa6;
  --fuse-glow: var(--hud-teal);
  --hud-hp: #e0453a;
  --hud-mp: #3f7dff;
  --hud-good: #6fcf6a;     /* the green that means live or healthy: a good ping, a war clock running */
  --hud-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 4px rgba(0, 0, 0, .6);
  /* The rail's face: the UI sans, for readouts and names alike. */
  --hud-face: var(--face-ui);
  --hud-edge: 10px;
  --hud-slot: 34px;
  /* Where every gauge in the left rail starts, measured from the rail's own left edge: you, your
     pet and each teammate spend whatever gap is left after their own padding and portrait. */
  --hud-gauge-x: 84px;
  /* The bars (gui/hud/bar.ts): a slot's navy well under its painted frame, the round toggles'
     backing, the lift under both, the key cap's figure, and the cover a cooldown or the turn
     clock wipes away. */
  --hud-slot-bg: radial-gradient(circle at 50% 38%, rgba(var(--navy-3-rgb), .92), rgba(var(--navy-0-rgb), .92) 80%);
  --hud-knob-bg: rgba(var(--navy-0-rgb), .55);
  --hud-lift: 0 1px 3px rgba(0, 0, 0, .5);
  --hud-key: #d9dfeb;
  --hud-cover: rgba(0, 0, 0, .62);
  /* The HUD's own washes as triplets, rgba(var(--x), a), so 40-hud.css names no colour: the rail's
     dark (as --hud-ink), the round wells under faces and the pet ring, the account fan's night,
     the open tile's gold (as --hud-gold), the good dot's glow, and the live event tile's glow and
     motes. */
  --hud-ink-rgb: var(--wash-rgb);
  --hud-well-rgb: var(--navy-0-rgb);
  --hud-night-rgb: 6, 9, 26;
  --hud-gold-rgb: var(--brass-hi-rgb);
  --hud-good-glow-rgb: 125, 220, 133;
  --hud-event-hi-rgb: 255, 214, 110;
  --hud-event-rgb: 255, 190, 70;
  --hud-spark: #ffe7a0;
  --hud-spark-glow-rgb: 255, 196, 64;
  /* The chat (gui/hud/chat.ts): each channel's ink - the speaker's name, the lit tab's glow, the
     field's placeholder - lifted for text straight on the world (owner, 2026-10-09: all white, city
     (nearby) blue, party green, guild purple; 2026-10-10: world a darkish yellow, system white). The tab
     glyphs (Lucide, chat.ts) are stroked in the same ink. */
  --chan-all: #ffffff;
  --chan-nearby: #8cc4ff;
  --chan-world: #e0b83c;
  --chan-trade: #f0915a;   /* 2026-10-14: Trade, a warm orange apart from World yellow */
  --chan-team: #8fdc82;
  --chan-guild: #c9a8ff;
  --chan-private: #ff9fd6;
  --chan-system: #ffffff;
  --chat-thumb: rgba(var(--brass-rgb), .28);  /* the log's hairline scroll thumb, on no track */
  --chat-thumb-hi: rgba(var(--brass-rgb), .5);
  --chat-bubble: rgba(6, 11, 24, .62);  /* a chat bubble's dark, tinted by its channel's ink (40-hud.css) */
  --chat-face-bg: rgba(255, 255, 255, .08);  /* a chat face's well while it shows the initial */
  --chat-face-ring: rgba(var(--brass-rgb), .45);

  /* ---------------------------------------------------- what the old stone sheet still lends
     The values of the gs.css tokens the game's own sheets and scripts read, without the gs-
     prefix (PLAN 2.5 step 2). gs.css keeps its --gs-* originals for ops and the editor. */
  --gold-bright: var(--brass-hi);
  --text: #e8e9ea;
  --text-mute: #babbbd;
  --stamp-outline: #000000;
  --border-hard: #01030a;
  --border-width: 1px;
  --void: #060b18;
  --page-ground: var(--navy-1);  /* behind the renderer canvas while a map streams */
  --scrim: #060b18cc;
  --stone-50: #e8e9ea;
  --ember-400: #e0553d;
  --ember-500: #b8392a;
  --moss-400: #6ec344;
  --moss-500: #4e9130;

  /* ------------------------------------------------------------- the item card (25-card.css)
     The star rating's brass ramp and its light, and the socket marks. */
  --star-hi: #ecc472;
  --star-mid: #b8862f;
  --star-lo: #6b4c16;
  --star-glint: #fff6c2;
  --star-glow: #ecc472;
  --star-drop: #0b0b0c;
  --socket-full: var(--moss-400);

  /* ------------------------------------------------------------- type scale (PLAN 2.2)
     Six rungs, 10/12/14/16/18/24 px at M, driven by --ui-scale (S .9, L 1.1, and in mobile mode
     the height fit), never under 10 px on screen. For the windows, cards, menus and toasts, which
     are not zoomed. A layer that wears a zoom of its own (the HUD, --hud-z) must not read them, or
     it scales twice: it gets its own rungs when it ports (P09). No font-size literal in a ported
     sheet; out of scale on purpose: the world plates, combat floats, medal numbers and the loading
     curtain. */
  --fs-xs: max(10px, calc(10px * var(--ui-scale)));
  --fs-sm: max(10px, calc(12px * var(--ui-scale)));
  --fs-md: max(10px, calc(14px * var(--ui-scale)));
  --fs-lg: calc(16px * var(--ui-scale));
  --fs-xl: calc(18px * var(--ui-scale));
  --fs-2xl: calc(24px * var(--ui-scale));
  /* The smallest thing a finger presses, whatever the size. */
  --touch: max(40px, calc(40px * var(--ui-scale)));

  /* The scale and the mode (apps/game/client/gui/mode.ts writes the live values on <html>; these
     are what a page without it - the editor - reads). */
  --ui-scale: 1;
  --hud-z: 1.15;
  --win-k: 1;

  /* ------------------------------------------------------------- the layer table (PLAN 2.3)
     The same numbers as apps/game/client/gui/z.ts Z (windows and modals take theirs from the
     stack there, written per element; these are for everything at one fixed layer). */
  --z-plates: 0;
  --z-hud: 10;
  --z-shell: 30;
  --z-win: 100;
  --z-pinned: 190;
  --z-cast: 200;
  --z-procl: 201;
  --z-toast: 202;
  --z-modal: 300;
  --z-tip: 400;
  --z-menu: 410;
  --z-ghost: 420;
  --z-fx: 500;
  --z-loading: 600;
  --z-fs-ask: 650;
  --z-orient-curtain: 660;
  --z-offline: 700;
  --z-devstats: 900;
  --z-sim: 901;
  --z-gpulab: 902;

  /* windows */
  /* The window sheets' own (ui/w-*.css). A thing carried over a row or a place it can land on:
     starlight under it, the brass lifted on its edge (never a brass wash: khaki on navy). */
  --drop-on-bg: rgba(var(--sky-rgb), .1);
  --drop-on-rim: var(--brass-hi);
  /* The paid track (the Tamer Pass's Starward card): brass light falling on a lit navy plate, in a
     brass rim, never a solid brass-brown. */
  --prem-bg: radial-gradient(ellipse 90% 70% at 50% 0, rgba(var(--brass-rgb), .28), transparent 70%), linear-gradient(160deg, var(--navy-4), var(--navy-2));
  --prem-rim: rgba(var(--brass-rgb), .6);
  /* The light that runs round a premium rung and rises off a claimable one: the brass ladder. */
  --mote: radial-gradient(circle, var(--brass-pale) 0 20%, var(--brass-hi) 45%, var(--brass) 62%, transparent 74%);
  --mote-glow: rgba(var(--brass-hi-rgb), .8);
  /* The Tamer Pass meter (w-ev-pass.css): pale brass rising into star-white, a starlight halo. Not
     the EXP gold: that colour means experience and stays on the real EXP bars. */
  --pass-fill: linear-gradient(90deg, var(--brass-dim), var(--brass-hi) 75%, var(--brass-pale));
  --pass-glow: 0 0 .45em rgba(var(--sky-rgb), .2);
  /* A tag that says when (the grand prize's level): starlight, so it is not a second brass. */
  --tag-cool-bg: rgba(var(--sky-rgb), .1);
  --tag-cool-rim: rgba(var(--sky-rgb), .35);

  /* hud */
  /* MIDNIGHT ASTROLABE ON THE ALWAYS-ON LAYER (40-hud.css) and the shell's own surfaces
     (90-shell.css, 60-curtain.css). Still no boxes on the HUD: the brass goes on rings round the
     faces and the minimap, a thin name plate under a level and name, hairlines and the slot frames;
     text keeps its navy wash. */
  --hud-ring-out: 15%;                 /* the astro ring past a small portrait's edge, each side */
  /* A gauge's rim: a starlight hairline in, a brass hairline out. Experience is the one brass bar. */
  --hud-gauge-rim: inset 0 0 0 1px rgba(var(--sky-rgb), .08), 0 0 0 1px rgba(var(--brass-rgb), .34);
  --hud-xp-fill: linear-gradient(90deg, var(--brass-dim), var(--brass-hi) 75%, var(--brass-pale));
  /* The bar's slots: a navy hollow lit above its middle, a dark inset line, a faint brass rim; the
     astro slot frame over the picture, full on a filled slot and a faint engraving on an empty one. */
  --hud-slot-rim: inset 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 1px rgba(var(--brass-rgb), .22), 0 2px 6px rgba(var(--shadow-ink), .6);
  --hud-slot-hover: inset 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 1px var(--brass-hi), 0 0 10px rgba(var(--brass-hi-rgb), .3);
  --hud-frame-op: .6;
  --hud-frame-empty-op: .24;
  --hud-frame-tint: sepia(.25) brightness(.92);
  /* A round well that is a button (the account switch, the +): a brass hairline. */
  --hud-well-rim: 0 0 0 1px rgba(var(--brass-rgb), .34), 0 2px 6px rgba(var(--shadow-ink), .7);
  /* A framed thing outside the windows that is not a card (the shell's top bar): the window rim
     with a vignette its own height. */
  --bar-ring:
    0 0 0 1px rgba(var(--shadow-ink), .92), 0 0 0 2px rgba(var(--brass-rgb), .44), 0 0 0 5px rgba(var(--navy-1-rgb), .9), 0 0 0 6px rgba(var(--brass-rgb), .16),
    inset 0 1px 0 rgba(var(--brass-pale-rgb), .2), var(--win-shadow), inset 0 0 1.4em rgba(var(--shadow-ink), .4);
  /* The picked plinth on the home screen: lit brass. */
  --picked-ring: 0 0 0 1px rgba(var(--shadow-ink), .92), 0 0 0 .18em var(--brass-hi), 0 0 1.2em rgba(var(--brass-hi-rgb), .4), 0 .5em 1.2em rgba(var(--shadow-ink), .35);

  /* THE EVENTS HUB (ui/w-events.css): each event its own accent over the theme's navy and brass.
     -rgb is the accent at an alpha (rings, glows, the motif); -deep the dark of its own back plate.
     A machine with no row of its own reads the brass. */
  --ev-brass-rgb: var(--brass-hi-rgb);       --ev-brass-deep: #2a2414;
  --ev-wardrobe-rgb: 186, 160, 255;          --ev-wardrobe-deep: #1d1640;   /* starlight violet */
  --ev-fortune-rgb: 255, 196, 92;            --ev-fortune-deep: #3a1410;    /* temple gold on vermilion */
  --ev-fortune-2-rgb: 214, 72, 52;
  --ev-well-rgb: 108, 226, 196;              --ev-well-deep: #10302c;       /* jade water under sakura */
  --ev-well-2-rgb: 255, 160, 190;
  --ev-codex-rgb: 140, 214, 255;             --ev-codex-deep: #141a3a;      /* arcane ink on midnight vellum */
  --ev-codex-2-rgb: 196, 150, 255;
  --ev-topup-rgb: 255, 214, 140;             --ev-topup-deep: #241a3a;      /* lamplight gold on a night-violet bedroom */
  --ev-topup-2-rgb: 170, 150, 255;
  --ev-pass-rgb: 128, 196, 255;              --ev-pass-deep: #0f2244;       /* azure, the season's sky */
  --ev-signin-rgb: 255, 172, 128;            --ev-signin-deep: #3a1c22;     /* dawn coral */
  --ev-journey-rgb: 248, 206, 120;           --ev-journey-deep: #1e2a1c;    /* sunset gold over the meadow */
  --ev-dim: saturate(.45) brightness(.72);   /* a card not picked */
  --ev-veil: rgba(var(--wash-rgb), .62);     /* the world under the page */
}

/* The phone camera stands back: the plate size comes down with its bodies (glplates.ts FAR). */
html.world-far { --plate-fs: 10px; }

/* A finger's device (tier.ts isTouch): the always-on chat on the panel's flat colour (--chat-bg). */
@media (hover: none) and (pointer: coarse) {
  :root { --chat-bg: var(--win-sky), var(--art-panel-flat); }
}

/* ---- 10-base.css */
/* THE PAGE ITSELF (PLAN 2.4): what the game page asks of the whole document, before any component.

   Selection: a drag is this game's main gesture, so nothing paints a text selection; fields keep
   theirs. Nor the blue box a phone browser flashes over whatever is tapped (owner, 2026-10-14). The colour scheme keeps the remaining form controls dark on a machine set to a light
   theme. The ground behind the renderer canvas is what a player sees while
   a map streams. The page never scrolls: everything on it is placed.

   Elsewhere: the cursor rules (20-components.css CURSORS; cursors.ts writes the --cur-* pictures),
   the scrollbars (20-components.css .scroll) and focus (40-hud.css: no ring on anything clicked). */
* { user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
input, textarea, [contenteditable] { user-select: text; -webkit-user-select: text; }
/* No double tap zooms the page (Safari on an iPhone ignores the viewport's user-scalable=no; its pinch
   is refused in gui/mode.ts). Pans and a component's own touch-action still apply. */
html { color-scheme: dark; touch-action: manipulation; }
/* EVERY scroller on the page wears the game's bar unless it says otherwise: thin, the brass thumb,
   no track (owner, 2026-10-10: a pane that rolled its own overflow without .scroll showed the
   platform's grey bar). Zero specificity, so a component's own choice - scrollbar-width: none, the
   chat log's 3px bar - still wins. */
:where(*) { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
html, body { background: var(--page-ground); }
body { margin: 0; overflow: hidden; }
canvas { display: block; }

/* THE FRAME (docs/ui-overhaul/SAFE-AREA.md): a UI root is the glass less the device's safe area
   (notch, home bar), clipped, and everything in it is placed from the frame's edges. Absolute, z
   auto: no stacking context, so the window stack's z still interleaves with toasts and modals
   (gui/z.ts). A frame root never takes a z-index (but .hud, one layer on purpose), transform,
   filter, opacity, contain or will-change: that would break the stack or the opt-out below.
   Explicit width/height, never inset: on iOS an absolute box's inset is the initial containing
   block, which need not be the visual viewport. */
.frame { position: absolute; left: var(--safe-l); top: var(--safe-t); width: var(--frame-w); height: var(--frame-h);
  overflow: clip; pointer-events: none; }
/* The opt-out: something that must draw to the screen's edge (a backdrop, a band's end, a curtain)
   is a .bleed DIRECT child of a frame or of <body>. Fixed, so its containing block is the viewport
   and no frame clips it. Only .bleed rules (and the probe) read --safe-*. */
.bleed { position: fixed; }
/* gui/mode.ts watches this box: insets that change with no resize (a cutout changing sides). */
.safe-probe { position: fixed; top: var(--safe-t); right: var(--safe-r); bottom: var(--safe-b); left: var(--safe-l);
  visibility: hidden; pointer-events: none; }
/* gui/mode.ts dock(): the typed field's row, docked as one bar above the on-screen keys. */
.kb-dock { position: relative; z-index: 1; background: var(--chat-bg); border-radius: 6px;
  box-shadow: 0 -2px 10px rgb(0 0 0 / .45); }
/* Its HUD piece rises over the others (the skill dial drew over the bar). */
.frame > :has(.kb-dock) { z-index: 100; }

/* ---- 20-components.css */
/* =====================================================================
   THE COMPONENTS: the window design system's source of truth (was
   apps/game/public/ui/20-components.css). Every window in the game is built
   from it. A component is one class plus modifiers; every dimension is in
   em so a component scales with its font-size and never needs a second
   drawing. The editor's Battle preview links this file on its own.
   ===================================================================== */

/* The tokens (colour, faces, the window size and its type ladder) live in
   apps/game/public/ui/00-tokens.css, the one token sheet of the game page. */

* { box-sizing: border-box; }

/* The one flag every window and every foldable block in the game is opened and
   closed by is el.hidden, and the browser's own [hidden] rule is display: block,
   which loses to any component that declares a display of its own: .win is flex,
   .rows and .tiles and .table are grid, .chip and .field are inline-flex. Without
   this line every panel is on screen from boot and open() does nothing visible.
   It carries !important on purpose: a component's display must never win over
   "this is not on screen". */
[hidden] { display: none !important; }

/* Pixel art is drawn at exact halves of a 2x master, so it is sampled, never
   smoothed. Anything painted (the plates, the class rings, the coins) must NOT
   take this: nearest-neighbour at an arbitrary scale is what makes art look
   cheap. This utility is for the few bitmaps that are magnified on purpose,
   such as the world map's walk-grid fallback. */
.px { image-rendering: pixelated; }
/* The type stack: every
   component reads --face-ui / --face-weight, so the game body sets them once and no component
   needs a font of its own. No background here: the world canvas is the game's background. */
body { color: var(--ink); font: var(--face-weight) 15px/1.45 var(--face-ui); }

/* A form control does not inherit type. The browser hands a <button>, <input>,
   <select> and <textarea> its own face - 13.333px Arial on this platform - and no
   component here declares one, so every button, field and pill in the game was
   drawn in a font that belongs to the browser and not to the game. The system
   says it once, here, and `font-size: 1em` on .btn finally means the em of the
   thing the button sits in rather than the em of Arial. */
button, input, select, textarea { font: inherit; letter-spacing: inherit; }

/* ---------------------------------------------------------------------
   Button. Two shapes (owner, 2026-10-09):
   .btn            THE SQUARE, every button by default: square corners, one 1px
                   line, a flat face. Cancel / OK pairs, rows of choices, tool
                   strips, list verbs, menus.
   .btn--special   THE HEXAGON, rare on purpose. Only the one call to action a
                   surface exists for, when it commits something the player
                   feels: it spends Lumi or money (Buy Tamer Ticket), takes a
                   reward (Claim, Claim all), seals a deal (trade's Confirm) or
                   starts the game (Enter world). At most one in sight: where
                   two qualify (the pass foot's Buy premium pass and Claim all)
                   the one the player comes back for wins. Never on Cancel,
                   Close, Back, a secondary (--cream) or a destroying (--rose /
                   --danger) verb, never in a tab row or a toolbar.
                   btn({ special: true }) in gui/c/controls.ts.
   Layers (both shapes):
   .btn      the line: `background` is --btn-rim, so the edge is the rim showing
             round the face (a light such as .btn--charged rides it).
   ::before  the face, inset 1px (the hexagon: --btn-rim-w).
   ::after   the hexagon's inner line only.
   Width is free; every dimension is in em.
   --------------------------------------------------------------------- */
.btn {
  position: relative;
  isolation: isolate;        /* the pseudo layers sit under the label, over the line */
  display: inline-grid;
  place-items: center;
  padding: var(--btn-pad-y) var(--btn-sq-pad-x);
  border: 0;
  border-radius: 0;
  font-size: 1em; line-height: 1.2;
  letter-spacing: .03em;
  color: var(--btn-text);
  text-shadow: 0 .06em 0 var(--btn-text-shadow);
  background: var(--btn-rim);
  cursor: var(--cur-hand, pointer);
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: filter .12s, transform .06s;
}
.btn::before, .btn::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
}
.btn::before { inset: 1px; background: var(--btn-face); }
.btn::after { content: none; }

.btn:hover { filter: brightness(1.18); }
.btn:active { filter: brightness(.9); transform: translateY(.06em); }
.btn:focus-visible { outline: .12em solid var(--ink); outline-offset: .2em; }
.btn:disabled, .btn.is-disabled {
  cursor: var(--cur-arrow, default);
  filter: var(--btn-off-filter);
  opacity: var(--btn-off-opacity);
  transform: none;
}

/* THE HEXAGON (.btn--special): the rim clipped to a pointed hexagon, the face the same hexagon
   under a sheen with a brass sparkle at each end (--btn-orn), and a one-pixel hollow hexagon inset by
   --btn-ring that turns at the chamfer angle and fades out where the ornament begins. */
.btn--special {
  --hex: polygon(0 50%, var(--btn-point) 0, calc(100% - var(--btn-point)) 0, 100% 50%, calc(100% - var(--btn-point)) 100%, var(--btn-point) 100%);
  padding-inline: var(--btn-pad-x);
  clip-path: var(--hex);
  filter: drop-shadow(0 .1em .1em rgba(var(--shadow-ink), .22));
}
.btn--special::before {
  inset: var(--btn-rim-w);
  clip-path: var(--hex);
  background: var(--btn-orn), var(--btn-sheen), linear-gradient(var(--btn-face), var(--btn-face));
}
.btn--special::after {
  content: "";
  inset: var(--btn-ring);
  clip-path: polygon(evenodd,
    0 50%, var(--btn-point) 0, calc(100% - var(--btn-point)) 0, 100% 50%, calc(100% - var(--btn-point)) 100%, var(--btn-point) 100%, 0 50%,
    1px 50%, calc(var(--btn-point) + 1px) 1px, calc(100% - var(--btn-point) - 1px) 1px, calc(100% - 1px) 50%, calc(100% - var(--btn-point) - 1px) calc(100% - 1px), calc(var(--btn-point) + 1px) calc(100% - 1px), 1px 50%);
  background: var(--engrave-light);
  -webkit-mask: linear-gradient(to right, transparent calc(var(--btn-line-fade) - 1.2em), #000 var(--btn-line-fade), #000 calc(100% - var(--btn-line-fade)), transparent calc(100% - var(--btn-line-fade) + 1.2em));
  mask: linear-gradient(to right, transparent calc(var(--btn-line-fade) - 1.2em), #000 var(--btn-line-fade), #000 calc(100% - var(--btn-line-fade)), transparent calc(100% - var(--btn-line-fade) + 1.2em));
}
.btn--special:hover { filter: drop-shadow(0 .1em .1em rgba(var(--shadow-ink), .22)) brightness(1.07); }
.btn--special:active { filter: brightness(.94); }
.btn--special:disabled, .btn--special.is-disabled { filter: var(--btn-off-filter); }

/* sizes: font-size only, relative to the panel the button sits in - .8 / 1 / 1.33 / 1.87 of
   whatever --win-size is, so a button never carries a pixel figure of its own */
.btn--sm { font-size: var(--fs-small); }
.btn--md { font-size: 1em; }
.btn--lg { font-size: 1.33em; }
.btn--block { display: grid; width: 100%; }
/* THE WIDE PRIMARY: a navy plate in one thin brass line (owner, 2026-10-09), the astrolabe ornament
   at each end, pushed a fifth out past the edge and cut off there (the plate clips). ::before is the
   left one, ::after the same picture mirrored. Width is the container's: a wider menu, a wider
   plate. A coloured or special block is the plain square or the hexagon, full width. */
.btn--block:not(.btn--cream, .btn--rose, .btn--sage, .btn--special) {
  overflow: hidden;
  background: var(--plate-navy);
  box-shadow: inset 0 0 0 1px var(--btn-plate-line);
}
.btn--block:not(.btn--cream, .btn--rose, .btn--sage, .btn--special)::before,
.btn--block:not(.btn--cream, .btn--rose, .btn--sage, .btn--special)::after {
  content: ""; inset: 0 auto; height: 100%; aspect-ratio: 59 / 60;
  background: var(--btn-plate-orn) center / 100% 100% no-repeat;
}
.btn--block:not(.btn--cream, .btn--rose, .btn--sage, .btn--special)::before { left: 0; translate: -20% 0; }
.btn--block:not(.btn--cream, .btn--rose, .btn--sage, .btn--special)::after { right: 0; translate: 20% 0; transform: scaleX(-1); }
/* The same plate for a verb that destroys or drops something (menu.ts `danger`: Throw away,
   Release, Delete, Kick): the brass line and ornaments stay, the face is the destroy red. */
.btn--block.btn--danger { background: var(--btn-bad-face); --btn-text: var(--btn-bad-text); }

/* colour variants: only the face tokens change. --cream is the secondary (a muted line on a
   darker face), --rose / --danger the one that destroys (a red face), --sage the one that agrees. */
.btn--cream {
  --btn-face: var(--btn2-face);
  --btn-rim: var(--btn2-rim);
  --btn-text: var(--btn2-text);
}
.btn--rose, .btn--danger {
  --btn-face: var(--btn-bad-face);
  --btn-rim: var(--btn-bad-rim);
  --btn-text: var(--btn-bad-text);
}
.btn--sage {
  --btn-face: var(--btn-ok-face);
  --btn-rim: var(--btn-ok-rim);
  --btn-text: var(--btn-ok-text);
}

/* ---------------------------------------------------------------------
   Chrome glyphs: monochrome, from hud-ui/icons.svg, fill currentColor.
   --------------------------------------------------------------------- */
.ic { width: 1em; height: 1em; fill: currentColor; flex: none; display: inline-block; vertical-align: -.15em; }

/* A star that is a PICTURE and not a sprite. data/art/ui/star.svg and its
   three ranked cousins (star-empty, star-luxury, star-radiant) are whole files
   rather than symbols in icons.svg, so they arrive as an <img> and cannot take
   .ic's currentColor fill. It sizes off the LINE it sits in, which is the whole
   of why a rating tier differs by font-size alone and never by a second
   drawing. A rating that is a count rather than a row of stars uses the .ic
   sprite (#i-star) in a .chip instead: this is for the drawn ones. */
.star { width: 1em; height: 1em; flex: none; vertical-align: -.12em; }
/* Currency pictures are paintings, never glyphs: coin-gold, coin-lumi (gui/h.ts icon()).
   THE ORDER IS FIXED: the number first, then the coin. A price is a quantity and
   the coin is the unit it is quoted in, and a price column that opened every row
   with the same picture could not be scanned down. `chip()` in gui/c/layout.ts writes
   the pair. The exception is a coin used as a LABEL rather than a unit - the disc
   purse, a band's leading mark, the lead inside a Gold input - where the
   picture names the thing and no figure follows it. */
img.coin { width: 1.25em; height: 1.25em; object-fit: contain; flex: none; vertical-align: -.3em; }

/* ---------------------------------------------------------------------
   Item picture: one icon, one picture of its own (data/art/icons/<name>.png).
   The client writes the picture's URL INLINE (shared/icons.ts iconStyle),
   because the URL is content-hashed and a stylesheet may not assume a
   filename, and the picture fills the box (background-size 100%). THE CELL IS
   THE EM: the host says how big the picture should be with --ico-box, that
   becomes this element's font-size, and the box is 1em. So one icon fits a
   3.33em bag tile and a .7em party row with no second drawing and no per-host
   magic number, and it keeps working when a size modifier changes the host's
   font-size.

   It was `zoom: calc(var(--ico-box, 32px) / 32px)` once, over a shared sheet
   positioned in px. Dividing a length by a length is CSS Values 4 arithmetic
   only Blink implements: Gecko drops the whole declaration, so every icon in
   the game stood at its raw 32px in Firefox. The em carries the ratio instead.
   --------------------------------------------------------------------- */
.ico {
  display: block; flex: none;
  font-size: var(--ico-box, 32px);
  width: 1em; height: 1em;
  background-repeat: no-repeat;
  image-rendering: pixelated;         /* the picture is 2x and is drawn at an exact half */
}
/* the hosts, each saying how much room the picture gets. Inside a tile it is
   the same .3em inset the <img> takes, centred the way the lock glyph is. */
.tile > .ico { position: absolute; inset: 0; margin: auto; --ico-box: calc(var(--tile) - .6em); }
.tile.is-dim > .ico { filter: grayscale(1); opacity: .45; }
.portrait > .ico { position: absolute; inset: 0; margin: auto; border-radius: 50%; --ico-box: 4.6em; }
.item-row > .ico { --ico-box: 2.4em; }

/* The talent node's picture: the icon cell at 32px on the 12px window it was drawn for, in em so
   it grows with --win-size like every other picture in a window. 3.333em of the .8em .skill--sm
   node is that 32px. The box is said again because .skill__pic below sizes an <img> at 4.3em,
   and on an .ico that em is --ico-box itself: the talent icons stood four times too big. */
.skill__pic.ico { --ico-box: 3.333em; width: 1em; height: 1em; }

/* ---------------------------------------------------------------------
   Engraving. Every mask ornament (hud-ui/ornaments.css) is a black shape used
   as a mask: two pseudo layers share the mask, the light one (::before)
   sits --engrave-lift below the dark one (::after), so the tokens paint
   every engraving and retuning them retunes all of it.
   --------------------------------------------------------------------- */

/* ---------------------------------------------------------------------
   Window (Midnight Astrolabe). .win paints the whole ground (--win-bg: orbit
   arcs, star field, navy wash, the midnight panel); the head, body and foot
   lay their own thin paint over it, the body none at all (--win-body).
   The rim is outside the box (--win-ring: a thin brass rim, the deep drop).
   .win::after is the top layer: the engraved inner plane, a brass hairline
   5px inside the rim, and on the window layer (.wins) the brass corner
   scale, its crest on the top-left and bottom-right corner points.
   .win::before is the soft shadow strip the head casts down onto the body.
   A window's title sits on its cartouche (.win__title::before); the head's
   foot has no dividing line (owner, 2026-10-08).
   Every one of them is paint: no box here moves for the theme. */
.win {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 14em;
  padding: 3px;                        /* the outer band, under the panel */
  background: var(--win-arc), var(--win-bg);
  color: var(--ink);
  border-radius: 0;
  box-shadow: var(--win-ring);
  line-height: 1.3;
  font-size: var(--win-size);          /* the one window size; everything inside is in em */
}
.win::after {                          /* the engraved inner plane, 5px inside the rim */
  content: ""; position: absolute; inset: 5px;
  box-shadow: inset 0 0 0 1px var(--win-plane);
  pointer-events: none; z-index: 4;
}
/* A window on the window layer wears the brass corner scale in its top-left and bottom-right corners
   (owner, 2026-10-09: two corners). The layer's box is the window's own (inset 0), so each quarter
   is laid at 0,0 of its corner: the crest on the corner point, the rest of it cut away by the
   frame. One layer holds the corners and the plane as four 1px lines. Not on a card built from the
   window parts (the roster's plinths). */
.wins > .win::after, .shell__signin::after, .shell__section::after {
  --plane: 5px;
  inset: 0; box-shadow: none;
  background:
    var(--art-corner-tl) left top / var(--corner-size) var(--corner-size) no-repeat,
    var(--art-corner-br) right bottom / var(--corner-size) var(--corner-size) no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) top var(--plane) / calc(100% - 2 * var(--plane)) 1px no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) bottom var(--plane) / calc(100% - 2 * var(--plane)) 1px no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) top var(--plane) / 1px calc(100% - 2 * var(--plane)) no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) right var(--plane) top var(--plane) / 1px calc(100% - 2 * var(--plane)) no-repeat;
}
/* Room for the bottom-right crest: the foot's last control stops short of it; the scale runs on under it. */
.wins > .win > .win__foot, .shell__section > .win__foot { padding-right: 2.9em; }
.win::before {                         /* the head's shadow cast down on the body */
  content: ""; position: absolute; left: 3px; right: 3px; top: calc(3px + 2.5em);
  height: .45em; z-index: 3; pointer-events: none;
  background: linear-gradient(rgba(var(--shadow-ink), .26), rgba(var(--shadow-ink), .06) 55%, rgba(var(--shadow-ink), 0));
}
.win__head {
  position: relative;
  display: flex;
  align-items: center;
  gap: .45em;
  height: 2.5em;                       /* owner, 2026-10-10: was 3em, the title sat lost in it */
  padding: 0 1.2em;                    /* the same 1.2em rhythm as the inner line */
  background: var(--win-head-bg);
  background-origin: border-box;
  overflow: hidden;
  flex: none;
}
.win__head::before {                   /* highlight hairline along the head's top edge */
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: var(--win-head-lit);
}
.win__head > * { position: relative; z-index: 1; }
.win__head > .ic { width: 1.5em; height: 1.5em; }
/* the title sits over the head band; a dark halo behind the letters keeps it readable */
/* A window's name is in the UI's sans (owner, 2026-10-09), bold. */
.win__title { font-family: var(--face-ui); font-weight: 700; font-size: var(--fs-title); letter-spacing: .03em; white-space: nowrap; color: var(--ink-strong); text-shadow: var(--win-title-shadow); }
/* The title plate (owner, 2026-10-09): no frame, only the plate's navy, the head's full height,
   from the window's edge to --title-run past the last letter, fading out there; no ornament
   (owner, 2026-10-09). Lengths in --win-size, because the title's own em is the bigger title
   rung. Behind the letters: the title is a stacking context (.win__head > *), so -1 is under its
   text and over the head. */
.wins > .win > .win__head > .win__title::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: calc(-1.2 * var(--win-size) - 3px); right: calc(-1 * var(--title-run) * var(--win-size));
  top: 50%; height: calc(2.5 * var(--win-size)); translate: 0 -50%;
  background: linear-gradient(to right, var(--title-plate) calc(100% - 2 * var(--win-size)), transparent);
}
/* With a head glyph before it, the plate starts under the glyph's gap too. */
.wins > .win > .win__head > .ic + .win__title::before { left: calc(-3.15 * var(--win-size) - 3px); }
.win__close {
  display: grid; place-items: center; margin-left: auto;
  width: 2.4em; height: 2.4em; border: 0;
  background: transparent; color: var(--ink); cursor: var(--cur-hand, pointer); padding: 0;
  opacity: .9;
}
/* The close star is pale starlight (ui-close): hover lights it with a sky glow, a press sinks it. */
.win__close .ic { width: 1.75em; height: 1.75em; transition: filter .15s, transform .1s; }
.win__close:hover { opacity: 1; }
.win__close:hover .ic, .win__close:focus-visible .ic { filter: brightness(1.15) drop-shadow(0 0 .25em rgba(var(--sky-rgb), .75)); }
.win__close:active .ic { transform: scale(.9); filter: brightness(.95) drop-shadow(0 0 .15em rgba(var(--sky-rgb), .5)); }

/* Put away, beside shut. It is the same button shape and the same ink - both are "this window
   stops being in front of me" - and only the close star takes the auto margin, so a head with
   both reads as one pair at the right end rather than as two controls at two indents. */
.win__body { position: relative; flex: 1; padding: .9em 1.2em 1.1em; background: var(--win-body); }
.win__body:not(:last-child) { padding-bottom: .5em; }
.win__body > * { position: relative; z-index: 1; }
.win__foot { display: flex; align-items: center; gap: .6em; padding: 0 1.2em 1.1em; flex: none; background: var(--win-body); }
/* A HEADLESS WINDOW: the box with no title bar and no close, for a window
   that is a whole view and has nowhere to be closed to (the sign-in screen).
   The head's cast shadow - .win::before, a bar drawn 2.5em down - is switched
   off, because there is no head above it to cast one. That frees ::before on a headless window for whatever the view
   wants to paint there. */
.win--headless::before { content: none; }

/* The cover a modal sits on. A question the player must answer takes the whole
   screen away from the world until it is answered. It is a VIGNETTE, not a
   wash: clear through the middle third, dark at the corners, so the window
   still stands out of the world without the world being painted over. The ink
   is neutral and near-black - the old warm --shadow-ink read as red over a
   green map, and a modal is not an emotion. .scrim--loss is the one that is.
   It centres the one window it holds. Its layer is the top of the stack:
   windows 20, dialogs 80, floating cards 90, this 100, the menus above it 120.
   A stranded hover card must never float over a modal. */
.scrim {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: var(--veil);
}

/* ---------------------------------------------------------------------
   Tabs: a navy well in a brass hairline; the selected tab is lit from its
   foot (brass light, a starlight core) with brass ink and the brass
   underline (::after). Hairlines between unselected tabs.
   --------------------------------------------------------------------- */
.tabs {
  --tabs-radius: 0;                    /* square: no rounding anywhere in a tab strip (owner, 2026-10-09) */
  display: inline-flex; flex: none;
  padding: 0;
  background: var(--field-face);
  border: 1px solid var(--field-border);
  border-radius: var(--tabs-radius);
}
.tabs__tab {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: .4em;
  min-width: 5.6em; height: 2.3em; padding: 0 1.4em;
  border: 0; border-radius: var(--tabs-radius); background-color: transparent;
  font: inherit; color: var(--ink); cursor: var(--cur-hand, pointer); white-space: nowrap;
}
.tabs__tab .ic { width: 1.35em; height: 1.35em; position: relative; z-index: 1; }
.tabs__tab + .tabs__tab:not(.is-on)::before { content: ""; position: absolute; left: 0; top: 28%; height: 44%; width: 1px; background: var(--hairline); }
.tabs__tab.is-on + .tabs__tab::before { display: none; }
.tabs__tab:hover:not(.is-on) { background-color: rgba(var(--ink-wash), .05); }
.tabs__tab.is-on {
  background: var(--tab-on-bg);
  color: var(--tab-on-ink);
  text-shadow: 0 .06em .2em rgba(var(--shadow-ink), .8);
  box-shadow: var(--tab-on-ring);
  padding: 0 1.7em;
  overflow: hidden;
}
/* The underline, and on it a small orbit: a brass ring round a star-white point. */
.tabs__tab.is-on::after {
  content: ""; position: absolute; left: 14%; right: 14%; bottom: 0; height: .5em; pointer-events: none;
  background:
    radial-gradient(circle at 50% 100%, var(--ink-strong) 0 .08em, var(--navy-1) .11em .2em, var(--brass-hi) .22em .28em, transparent .32em),
    var(--tab-line) left bottom / 100% 2px no-repeat;
  filter: drop-shadow(0 0 .25em rgba(var(--brass-hi-rgb), .55));
}
/* A page tab lit as a drag's drop target. The bag and the warehouse are three
   pages deep and only one page of cells is on screen, so a stack let go on a
   tab is how it crosses - and the tab has to say so while the item is still in
   the air. Read as .drop.is-over reads, the dashed brass edge over the same starlight
   wash, so one drawing means "let go here" wherever it turns up. The dash is
   an outline pulled inside the tab, because the strip owns the border and a
   tab has none of its own to colour. */
.tabs__tab.is-drop {
  background-color: var(--drop-on-bg);
  outline: 1px dashed var(--drop-on-rim);
  outline-offset: -.15em;
}
.tabs--text .tabs__tab { min-width: 7em; padding: 0 2.2em; }
.tabs--block { display: flex; }
.tabs--block .tabs__tab { flex: 1; }
/* A window's own tab strip stands centred over the pane it switches. A full-width block strip is
   already centred by filling the row. */
.win__body > .tabs:not(.tabs--block) { display: flex; width: fit-content; margin-inline: auto; }
.tabs--sm { font-size: var(--fs-small); }
/* A tab that is off is still worth reading: the quality ladder greyed while
   auto-sell is switched off says what it is set to, and a barred door in the
   instance list says the door exists. Faded, and the pointer does not promise
   anything. Both spellings, because one of these is a real <button> and the
   other is a div that says so with aria. */
.tabs__tab:disabled, .tabs__tab[aria-disabled="true"] { opacity: .45; cursor: var(--cur-arrow, default); }
.tabs__tab:disabled:hover, .tabs__tab[aria-disabled="true"]:hover { background-color: transparent; }

/* ---------------------------------------------------------------------
   Select: a custom dropdown, never a native <select>. The trigger is a
   real <button> (glyph, centred label, chevron-down); the popover list
   (.select__list, role listbox) opens under it, styled like the kv rows,
   with a check on the current option. .is-open on the root shows the
   list; the small script at the end of the page does click, arrow keys,
   Enter and Escape. --grey is the info variant.
   --------------------------------------------------------------------- */
.select { position: relative; display: inline-block; flex: none; min-width: 12em; color: var(--ink); }
.select__trigger {
  display: flex; align-items: center; gap: .3em; width: 100%;
  height: 2.2em; padding: 0 .5em;
  background: var(--field-face); color: inherit;
  border: 1px solid var(--hairline); border-radius: .35em;
  font: inherit; cursor: var(--cur-hand, pointer); text-align: center;
}
.select__trigger > .ic { width: 1.25em; height: 1.25em; }
.select__label { flex: 1; min-width: 0; padding: 0 .3em; text-align: left; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.select__trigger > .select__chev { width: .9em; height: .9em; }
.select__trigger:focus-visible, .select.is-open .select__trigger { border-color: var(--brass); outline: 0; }
.select__list {
  position: absolute; left: 0; right: 0; top: calc(100% + .25em); z-index: 20;
  display: none; margin: 0; padding: .2em .35em; list-style: none;
  background: var(--card-bg); color: var(--ink);
  border-radius: .4em; box-shadow: var(--card-ring);
}
.select.is-open .select__list { display: block; }
.select__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 1em;
  padding: .5em .35em; border-bottom: 1px solid var(--hairline); cursor: var(--cur-hand, pointer); white-space: nowrap;
}
.select__opt:last-child { border-bottom: 0; }
.select__opt .ic { width: 1em; height: 1em; visibility: hidden; }
.select__opt.is-on .ic { visibility: visible; }
.select__opt:hover, .select__opt.is-active { background: var(--menu-hover); border-radius: .3em; }
.select--block { display: block; }
.select--sm { font-size: var(--fs-small); }

/* ---------------------------------------------------------------------
   Band: a light grey information strip. Sparkle, label, green number,
   an optional centre item, an action at the right.
   --------------------------------------------------------------------- */
.band {
  display: flex; align-items: center; gap: .45em;
  min-height: 2.3em; padding: .3em .6em .3em .8em;
  background: var(--band-bg); color: var(--ink-mute); border-radius: .3em;
}
.band > .ic { width: 1.15em; height: 1.15em; color: var(--ink); }
.band__label { color: var(--ink); }
.band__num { color: var(--green); }
.band__num--orange { color: var(--orange); }
.band__mid { display: inline-flex; align-items: center; gap: .4em; margin: 0 auto; color: var(--ink); }
.band__mid img { width: 1.6em; height: 1.6em; }
.band__action { display: inline-flex; align-items: center; gap: .5em; margin-left: auto; }

/* ---------------------------------------------------------------------
   Key/value list: label muted at the left, value dark at the right,
   hairline between rows.
   --------------------------------------------------------------------- */
.kv { margin: 0; padding: 0; }
.kv > div { display: flex; justify-content: space-between; align-items: baseline; gap: 1em; padding: .5em .25em; border-bottom: 1px solid var(--hairline); }
.kv > div:last-child { border-bottom: 0; }
.kv dt { margin: 0; color: var(--ink-mute); }
.kv__val { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; text-align: right; } /* a value that wraps stays on the right edge */
.kv__val--hot { color: var(--orange); }
.kv__val--good { color: var(--green); }
/* THE GEAR STAT BLOCK reads in blue: the figures on an equipment card are what the piece grants,
   and giving them a hue of their own separates them from the label beside them and from the plain
   ink every other .kv in the game prints. items.ts statBlock marks the list.

   The orange is restated at this specificity ON PURPOSE: this selector outranks the bare
   `.kv__val--hot` above, so without the second line the blue would swallow the one signal the
   block carries about WHERE a number came from - a line drawn from an option band, which is what
   a recast redraws. Blue is the base colour here, not the only one.

   The green socket addition needs no such line. It is a nested span of a different class, so the
   blue never matches it, and its own rule beats the colour it would otherwise inherit. */
.kv[data-gear-stats] .kv__val { color: var(--blue); }
.kv[data-gear-stats] .kv__val--hot { color: var(--orange); }
/* A card's stat list (items.ts statBlock, the effect rows): the label at the left, the figure at the
   right in tabular digits so a column of numbers lines up, and no hairline per row - the card's
   sections carry the only lines. A socket addition stands beside its total ("191 +24"). */
.kv--stat > div { gap: .8em; padding: .08em 0; border-bottom: 0; }
.kv--stat dd { white-space: nowrap; }
.kv__bonus { margin-left: .3em; }
/* A kv value that DOES something: the "go and stand here" line in the quest panel. Underlined
   rather than coloured, because colour in this family already means pass or refuse and a third
   meaning on the same channel would be read as one of those two. Disabled when the place is on
   another map - it still names the map, it just cannot walk you there. */
/* A kv value that names an NPC: the name (the walk), then a map pin with the errand in its ring. */
.kv__npc { display: inline-flex; align-items: center; gap: .4em; }
.kv__npc { --pin-size: 1.75em; }
.kv__npc.is-dim .pin { opacity: .45; }

/* ---------------------------------------------------------------------
   Pin: gui/h.ts pin(), the forged map pin with a hole in its ring. The picture is
   data/art/ui/pin.png; the hole is measured on it in data/art/ui/pin.json
   and the builder writes the hole box from that as inline layout, so the
   sheet sizes the pin and nothing else. 1.25em tall by default, a text-line
   ornament that scales with the words beside it; a row that puts a picture in
   the ring asks for more, because the hole is half the pin's width and a glyph
   inside it needs 8 px or so to read.
   --------------------------------------------------------------------- */
.pin { position: relative; display: inline-block; flex: none; height: var(--pin-size, 1.25em); vertical-align: -.25em; line-height: 0; }
.pin__img { display: block; width: 100%; height: 100%; object-fit: contain; }
.pin__hole { position: absolute; display: grid; place-items: center; }
.pin__hole > .ic, .pin__hole > img { width: 100%; height: 100%; display: block; fill: currentColor; color: var(--ink); object-fit: contain; }

/* ---------------------------------------------------------------------
   Stat row: a grey pill. Glyph, short key, value columns, and the
   stepper: minus, plus, and the double-chevron disc that spends every
   remaining point on this stat. .stats is the stack; .stats__head its
   column caption row (its last span is as wide as the stepper).
   --------------------------------------------------------------------- */
.stat, .stats__head { display: grid; grid-template-columns: 1.4em 3.4em repeat(var(--stat-cols, 3), 1fr) auto; align-items: center; gap: .5em; }
.stat {
  min-height: 2.9em; padding: .3em .35em .3em .8em;
  background: var(--band-bg); border-radius: .7em; color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}
.stat > .ic { width: 1.25em; height: 1.25em; color: var(--ink); }
.stat__key { color: var(--ink); letter-spacing: .04em; }
.stat__val { text-align: center; }
.stat__val--dark { color: var(--ink); }
/* What a stepper has added but not yet committed, in the gain colour and beside the
   figure rather than in place of it: the base number stays where the eye left it and
   the green "+4" is the part that is still yours to take back. A single recoloured
   total said "this is different" without saying which way or by how much. */
.stat__val .stat__add { margin-left: .3em; color: var(--green); font-style: normal; }
.stat__stepper { display: inline-flex; gap: .45em; }
/* THE NUMBER INPUT (gui/c/input.ts numField): [−] [the typed number] [+], one row. */
.numf { align-items: center; }
.numf > .field { width: 5.5em; }
.numf > .field input { text-align: center; }
.numf--block { display: flex; width: 100%; }
.numf--block > .field { flex: 1; width: auto; }
.stats { display: grid; gap: .5em; }
.stats__head { padding: 0 .35em 0 .8em; color: var(--ink-mute); font-size: var(--fs-small); line-height: 1.1; text-align: center; }
.stats__head span { grid-column: span 1; }
.stats__head span:first-child { grid-column: 1 / 3; }

/* ---------------------------------------------------------------------
   Round icon button: a nearly flat cream disc, one faint ring, the glyph
   in pale tan; the stepper's minus, plus and double chevron, and the
   refresh action. --white is the plain disc with the dark glyph, --sq is
   the rounded square used at a skill node's side.
   --------------------------------------------------------------------- */
.ibtn {
  position: relative; flex: none;
  display: inline-grid; place-items: center;
  width: 2.1em; height: 2.1em; padding: 0;
  border: 1px solid var(--cream-rim); border-radius: 50%;
  background: var(--cream); color: var(--cream-ink);
  cursor: var(--cur-hand, pointer); font: inherit;
}
.ibtn::after { content: ""; position: absolute; inset: .16em; border: 1px solid var(--cream-rim); border-radius: inherit; opacity: .3; pointer-events: none; }
.ibtn .ic { width: 1em; height: 1em; }
.ibtn:hover { color: var(--ink-mute); filter: brightness(1.02); }
.ibtn:active { transform: translateY(.06em); }
.ibtn:disabled, .ibtn.is-disabled { opacity: .45; cursor: var(--cur-arrow, default); }
.ibtn--white { background: var(--field-face); border-color: var(--field-border); color: var(--ink); }
.ibtn--white::after { content: none; }
.ibtn--sq { border-radius: .3em; width: 1.8em; height: 1.8em; }
.ibtn--sm { font-size: .8em; }
/* The "?" (c/layout.ts helpTip): a surface's words live on its card, a title over short paragraphs. */
.card--help { display: grid; gap: .4em; }
.card--help > .card__title { font-size: var(--fs-lg); }
.card--help > .card__body { margin: 0; }

/* ---------------------------------------------------------------------
   Tile: an item slot. One neutral face for every item; the picture sits
   inside, a stack count at the bottom-right. Rarity is optional and
   only a thin coloured rim (--purple, --blue, --gold, --green), nothing
   louder. --empty is the bare slot with a faint inner line, --locked
   adds the lock glyph, .is-dim greys the picture (an unequipped preview).
   The server speaks six quality bands and this system draws four rims: a white
   item wears no rim at all (it is the common case and a rim on everything is a
   rim on nothing) and yellow rides with gold. That is the ruling for the whole
   client, made once here; a fifth colour would have to earn its place against
   the four, not be added because a table has six rows.
   --------------------------------------------------------------------- */
.tile {
  --tile-rim: var(--tile-rim-base);
  --tile-ring: transparent;            /* the OUTER ring is the state channel, see below */
  position: relative;
  width: var(--tile); height: var(--tile); flex: none;
  background: var(--tile-bg);
  box-shadow: inset 0 0 0 max(1px, .1em) var(--tile-rim), 0 0 0 max(1px, .14em) var(--tile-ring), 0 0 0 1px var(--hairline), 0 .07em .14em rgba(var(--shadow-ink), .3), var(--tile-vignette);
  color: var(--ink);
  overflow: hidden;
}
/* The quality is the tile's whole ground, not its rim (00-tokens.css --q-bg-*): flat, no vignette. */
.tile:is(.tile--purple, .tile--blue, .tile--gold, .tile--yellow, .tile--green) { --tile-vignette: 0 0 transparent; }
.tile--purple { --tile-bg: var(--q-bg-purple); }
.tile--blue { --tile-bg: var(--q-bg-blue); }
.tile--gold { --tile-bg: var(--q-bg-gold); }
.tile--yellow { --tile-bg: var(--q-bg-yellow); }
.tile--green { --tile-bg: var(--q-bg-green); }
.tile > img { position: absolute; inset: .3em; width: calc(100% - .6em); height: calc(100% - .6em); object-fit: contain; }
.tile.is-dim > img { filter: grayscale(1); opacity: .45; }
.tile__count { position: absolute; z-index: 2; right: .2em; bottom: .2em; font-size: max(var(--fs-badge), .78em); line-height: 1.3; padding: 0 .35em; border-radius: .3em; background: var(--count-bg); color: var(--ink); }
/* The +N enhance (top left) and the worn E (bottom left) ride OVER the picture: the .ico is
   positioned, so an unpositioned label would be drawn under it. */
.tile > :is(.tile__en, .tile__eq) { position: absolute; z-index: 2; left: .2em; font-size: max(var(--fs-badge), .78em); font-weight: 700; line-height: 1.1;
  color: var(--brass-hi); text-shadow: 0 .06em 0 var(--btn-text-shadow), 0 0 .25em rgba(var(--shadow-ink), .9); pointer-events: none; }
.tile > .tile__en { top: .15em; }
.tile > .tile__eq { bottom: .15em; color: var(--ink-strong); }
.tile--empty, .tile--locked {
  background: var(--tile-empty-bg);
  box-shadow: inset 0 0 0 1px var(--tile-rim-base), 0 0 0 1px var(--hairline);
}
/* A tile is a square well and its rim: no painted frame, no engraving (owner, 2026-10-09). */
.tile:hover:not(.tile--empty, .tile--locked, .tile--bare) { box-shadow: inset 0 0 0 max(1px, .1em) var(--tile-rim), 0 0 0 max(1px, .14em) var(--tile-ring), var(--tile-hover), var(--tile-vignette); }
.tile--locked > .ic { position: absolute; inset: 0; margin: auto; width: 1.5em; height: 1.5em; color: rgba(var(--ink-wash), .22); }
/* A tile that is locked but NOT empty: a pet standing in a place the account is not entitled
   to, an item on a shut pack. The art stays - the thing is still owned, and hiding it would
   read as having lost it - and the padlock becomes a corner badge over it rather than the
   whole face. Pair with .is-dim for the grey. */
.tile--barred { --tile-rim: rgba(var(--ink-wash), .25); cursor: var(--cur-attack-far, not-allowed); }
.tile--barred > .ic { position: absolute; z-index: 2; inset: auto; right: .1em; top: .1em; width: .95em; height: .95em;
  color: var(--paper); background: rgba(var(--ink-wash), .72); border-radius: .3em; padding: .08em; }
/* Grey the picture, never the count or the badge: the reading has to stay legible. */
.tile--barred > :not(.ic):not(.tile__count) { filter: grayscale(1); opacity: .5; }
/* An item its owner LOCKED (note 221; gui/c/slots.ts `guard`): a brass padlock badge in the top
   right, the art untouched - the thing is fully there to look at, only refused to every use, sale
   and throw. On a shut pack the guard keeps the corner and the pack's own padlock moves left. */
.tile > :is(.tile__guard, .tile__mark, .tile__pick) {
  position: absolute; z-index: 2; top: .1em; width: .95em; height: .95em; padding: .08em; box-sizing: border-box;
  display: grid; place-items: center; border-radius: .3em;
}
.tile > :is(.tile__guard, .tile__mark, .tile__pick) > .ic { width: 100%; height: 100%; }
.tile > .tile__guard { right: .1em; background: rgba(var(--shadow-ink), .78); color: var(--brass-hi); box-shadow: 0 0 0 1px rgba(var(--brass-rgb), .5); }
.tile--barred.tile--guard > .ic:not(.tile__ok) { right: auto; left: .1em; }
/* The Lock mode is on and this item is not locked yet: an EMPTY brass box where the padlock will
   appear, the same size and corner, so a press reads as "fill this box". Marked for "Lock now": the
   padlock in the box on a pale wash - never the dark badge a real lock wears. */
.tile > .tile__mark { right: .1em; border: 1px solid var(--brass-hi); background: rgba(var(--shadow-ink), .35); }
.tile > .tile__mark:not(.is-on) > .ic { visibility: hidden; }
.tile > .tile__mark.is-on { background: var(--brass-pale); color: var(--navy-1); }
/* Picked in a selection (the bag's Throw mode): a small check in the top-left corner, no ring. */
.tile > .tile__pick { left: .1em; background: var(--brass-hi); color: var(--navy-1); }
/* The item card's line under a locked item: the padlock before the words. */
/* The owner's padlock, top right of an item card's head; its line is the hover (tip.ts). */
.card__lock { margin-left: auto; align-self: flex-start; display: grid; place-items: center; padding: .1em; cursor: var(--cur-arrow, default); }
.card__lock > .ic { width: 1.1em; height: 1.1em; color: var(--brass-hi); }
.tile--sm { font-size: .75em; }
.tile--lg { font-size: 1.4em; }
/* A skill or talent icon: the picture alone, no face, no rim, no ring. The art is the whole tile. */
.tile--bare { background: none; box-shadow: none; border-radius: 0; }
.tile--bare > .ico { --ico-box: var(--tile); }
.tiles { display: grid; grid-template-columns: repeat(var(--cols, 5), max-content); gap: .4em; }

/* An item with no icon of its own still has to be recognisable, and a shelf
   of costumes or an archetype list is mostly that. The fallback is the first
   letters of its name, centred on the bare face. It is a type size, not a
   layout, so a panel cannot supply it inline. */
.tile__text { position: absolute; inset: .3em; display: grid; place-items: center; font-size: max(var(--fs-badge), .8em); line-height: 1.15; text-align: center; color: var(--ink-mute); font-variant-numeric: tabular-nums; overflow: hidden; }

/* Cooldown. The sweep is a dark wedge that unwinds clockwise as the skill comes
   back, --cd being how much of the circle is still owed, in degrees; the digits
   sit on top of it. Both live on the tile so a hotbar cell needs no wrapper. */
.tile--cool::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; border-radius: inherit; background: conic-gradient(transparent var(--cd, 0deg), rgba(var(--ink-wash), .55) 0); }

/* The ring OUTSIDE a tile is its state; the rim inside it is the item's rarity,
   and the two never fight for the same pixel. .is-on is armed or picked (a
   socket the smith is working, the card you chose); --opener is the move the
   fight opens with and --opener-soft the one that has drifted out of first
   place; --good and --bad are a helpful and a harmful effect, which is a
   direction rather than a quality and must not read as a green or purple item. */
.tile.is-on { --tile-ring: var(--tan); }
.tile--opener { --tile-ring: var(--tan); }
.tile--opener-soft { --tile-ring: rgba(var(--tan-rgb), .45); }
.tile--good { --tile-ring: var(--green); }
.tile--bad { --tile-ring: var(--error); }

/* What the pointer says a tile is: a locked one cannot be taken, a drag source
   one can be picked up. Neither is a layout property, so neither can be inline. */
.tile--locked { cursor: var(--cur-attack-far, not-allowed); }
[data-drag] { cursor: var(--cur-grab, grab); }
[data-drag]:active { cursor: var(--cur-grabbing, grabbing); }

/* ---------------------------------------------------------------------
   Skill node: the picture, the level pill, the name. Nothing else: no
   frame, no connectors, no stepper (skills level with skill books).
   A learned node's pill is tan; .is-off (not learned yet) greys the
   picture and keeps the grey pill. .skills is the grid the skill panel
   lists them in, --cols wide.
   --------------------------------------------------------------------- */
.skill { position: relative; display: grid; align-content: start; justify-items: center; gap: .3em; width: var(--node-w); color: var(--ink); text-align: center; }
.skill__pic { display: block; width: 4.3em; height: 4.3em; object-fit: cover; }
.skill__name { font-size: max(var(--fs-small), .85em); line-height: 1.15; color: var(--ink-mute); }
.skill--sm .skill__name { font-size: var(--fs-badge); }
.skill:not(.is-off) .pill { background: var(--tan); color: var(--tan-text); }
.skill--sm { font-size: .8em; }
.skill.is-off .skill__name { color: var(--ink-faint); }
.skill.is-off .skill__pic { filter: grayscale(1); opacity: .45; }
/* A node a point can go into right now: mark its add button in green. */
.skill.is-ready [data-add] { box-shadow: 0 0 0 .14em var(--green), 0 0 .5em color-mix(in srgb, var(--green) 65%, transparent); }
.skills { display: grid; grid-template-columns: repeat(var(--cols, 4), var(--node-w)); gap: .8em .4em; align-content: start; }
/* A small node takes its whole lane: --node-w is em of the node, so in the grid a small one is a
   fifth narrower than its column and its name wraps for no reason. */
.skills > .skill--sm { width: auto; }

/* ---------------------------------------------------------------------
   Chips, tags, pills.
   .chip        plain grey counter, a number and nothing else ("8/300")
   .chip--disc  the chip with a disc hanging off its left end: the disc
                (.chip__disc, or a round .ibtn) is larger than the chip
                and overlaps it, the glyph inside the disc, the number
                in the pill. A purse is this one, and it is the only place
                a coin stands in front of its figure.
   .tag         small tan tag ("NEW")
   .pill        grey level pill ("5/5")
   --------------------------------------------------------------------- */
.chip {
  position: relative;
  display: inline-flex; flex: none; align-items: center; gap: .4em;
  min-width: 5em; height: 1.9em; padding: 0 .8em;
  background: var(--grey-pill); color: var(--ink-mute); border-radius: .3em;
  font-variant-numeric: tabular-nums;
}
.chip > .ic { width: 1.15em; height: 1.15em; color: var(--ink); }
.chip > img { width: 1.5em; height: 1.5em; object-fit: contain; }
.chip__num { flex: 1; text-align: center; }
.chip--bare { background: transparent; min-width: 0; padding: 0 .3em; }
.chip--disc { margin-left: 1.25em; padding-left: 1.5em; min-width: 6.4em; overflow: visible; }
.chip--disc > .ibtn, .chip__disc { position: absolute; left: -1.25em; top: 50%; transform: translateY(-50%); width: 2.5em; height: 2.5em; }
.chip__disc {
  display: inline-grid; place-items: center; flex: none;
  border: 1px solid var(--cream-rim); border-radius: 50%;
  background: var(--cream); color: var(--ink);
  box-shadow: 0 .07em .14em rgba(var(--shadow-ink), .14);
}
.chip__disc::after { content: ""; position: absolute; inset: .16em; border: 1px solid var(--cream-rim); border-radius: inherit; opacity: .3; pointer-events: none; }
.chip__disc > .ic { width: 1.3em; height: 1.3em; }
.chip__disc > img { width: 1.9em; height: 1.9em; object-fit: contain; }
/* An item tile standing in for the coin (a barter counter's purse): the same 1.9em as the coin. */
.chip__disc > [data-token] { display: grid; place-items: center; }
.chip__disc > [data-token][hidden] { display: none; }
.chip__disc > [data-token] > .ico { --ico-box: 1.9em; }
.chip--disc > .ibtn .ic { width: 1.2em; height: 1.2em; }
.tag { display: inline-block; flex: none; font-size: var(--fs-badge); line-height: 1.55; padding: 0 .45em; border-radius: .3em; background: var(--tan); color: var(--tan-text); text-shadow: 0 .06em 0 rgba(var(--shadow-ink), .25); letter-spacing: .02em; }
/* The System Gift stamp, in green (user, 2026-09-23): a gift is good news about the copy, not a
   warning, and green is also what sets it apart from the tan "Equipped" beside it. The fill is
   the same green thinned, so the text keeps its contrast in both themes. */
.tag--green { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); text-shadow: none; }
.pill { display: inline-block; flex: none; min-width: 3.2em; text-align: center; font-size: var(--fs-badge); line-height: 1.5; padding: 0 .6em; border-radius: .35em; background: var(--grey-pill-2); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.pill--tan { background: var(--tan); color: var(--tan-text); }
.pill--ink { background: var(--ink); color: var(--paper); }
button.pill { appearance: none; border: 0; font-family: inherit; cursor: var(--cur-hand, pointer); }
button.pill:hover:not(:disabled) { filter: brightness(1.1); }
button.pill:disabled { opacity: .5; cursor: var(--cur-arrow, default); }
/* A state a player is waiting on, said loudly: the two answers to "has this side
   agreed yet". Brighter and larger than an ordinary pill on purpose - the trade
   window's whole question is whether the other person has pressed the button, and
   it is read across the window rather than looked for. The two fills take
   different inks because the ratio decides it, not the pairing: white clears 5.0
   on this red and only 2.3 on a green bright enough to read as bright, where a
   deep green ink clears 5.6. */
.pill--go, .pill--stop { font-size: 1em; font-weight: 600; padding: .1em .7em; letter-spacing: .01em; }
.pill--stop { background: var(--pill-stop-bg); color: var(--pill-stop-ink); }
.pill--go { background: var(--pill-go-bg); color: var(--pill-go-ink); }

/* A READING on a chip: the HUD status line's latency is good, slow or bad, and
   which of the three it is changes while the player is looking at it. That is a
   state, not a variant, so it is .is-* and not --good / --warn / --bad: a
   modifier is what a thing IS and this is how it is DOING. Only the figure takes
   the colour - the grey face stays the same face whatever it says, because three
   coloured pills in the corner of the screen would be three alarms. */
.chip.is-good { color: var(--green); }
.chip.is-slow { color: var(--orange); }
.chip.is-bad { color: var(--error); }

/* ---------------------------------------------------------------------
   Field: text input in the same white box as the select.
   The leading glyph is markup, not a rule: whatever goes in front of the
   input (the search glyph, the painted coin on the trade window's Gold
   box) is written into the label and dressed by `.field > .ic`. What
   --search is FOR is the box's own behaviour, below: an <input
   type=search> is the one input the browser decorates from the outside,
   and none of that decoration is ours.
   --------------------------------------------------------------------- */
.field {
  display: inline-flex; flex: none; align-items: center; gap: .4em;
  height: 2.4em; min-width: 12em; padding: 0 .6em;
  background: var(--field-face); color: var(--ink);
  border: 1px solid var(--field-border); border-radius: .35em;
}
.field > .ic { width: 1.2em; height: 1.2em; color: var(--ink-mute); }
.field input { flex: 1; min-width: 0; border: 0; background: transparent; outline: 0; font: inherit; color: inherit; padding: 0; }
.field input::placeholder { color: var(--ink-mute); opacity: .7; }
.field:focus-within { border-color: var(--brass); box-shadow: 0 0 .45em rgba(var(--brass-rgb), .22); }
.field.is-disabled, .field:has(input:disabled) { opacity: .5; }
.field.is-error { border-color: var(--error); }
.field--block { display: flex; }
.field--sm { font-size: var(--fs-small); }

/* THE BROWSER'S OWN FURNITURE, ONE PART REDRAWN AND ONE TAKEN OFF.

   --search, once the player has typed, grows the OS cancel glyph. Clearing a
   search you have typed is worth a control, and the browser already knows when
   to show one and how to wire it - it appears with the first character, goes
   with the last, and clears the field and fires `input` on click, all without a
   line of script. So the glyph stays and only its DRAWING is ours: appearance
   off, then the design's own cross painted back as a mask in the field's own
   ink, at the field's own em. Nothing here is a second element in the markup.

   A NUMBER FIELD grows the spinner: two stacked arrows, drawn by the OS,
   painted over the box's right padding. There are sixteen number fields in
   the game and the worst of them is the count dialog, where a 4.5em field
   carries the OS arrows AND the design's own .stat__stepper right beside
   it - two ways to add one, a hair apart, one of them not ours. The
   stepper, the slider or an All button is always beside the figure, so the
   arrows are never the only way up. Off, in one rule, for all sixteen.
   Figures are tabular here for the same reason they are on a bar: a count
   the player is stepping must not jitter under its own digits. */
.field--search input[type=search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.field--search input[type=search]::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: 1.1em; height: 1.1em; margin: 0 0 0 .4em; cursor: var(--cur-hand, pointer);
  background: url(/data/art/ui/ui-close.5c1656c2.webp) center / .8em .8em no-repeat;
  opacity: .6;
}
.field--search input[type=search]:hover::-webkit-search-cancel-button,
.field--search:hover input[type=search]::-webkit-search-cancel-button { opacity: 1; }
.field input[type=number] { appearance: textfield; -moz-appearance: textfield; font-variant-numeric: tabular-nums; }
.field input[type=number]::-webkit-outer-spin-button,
.field input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; }

/* ---------------------------------------------------------------------
   Scroll: a thin, light, round thumb.
   --------------------------------------------------------------------- */
.scroll { overflow: auto; scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
.scroll::-webkit-scrollbar { width: .45em; height: .45em; }
.scroll::-webkit-scrollbar-track { background: transparent; }
.scroll::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 1em; }
.scroll::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); }
/* A scroller whose rows go see-through at its top and bottom edges rather than being cut there
   (owner, 2026-10-10). The padding keeps the first and last rows clear of the fade at rest.
   --fade sets the depth; a parent may set it. */
.fade-edges {
  padding-block: var(--fade, 1.25em);
  -webkit-mask-image: linear-gradient(transparent, #000 var(--fade, 1.25em), #000 calc(100% - var(--fade, 1.25em)), transparent);
  mask-image: linear-gradient(transparent, #000 var(--fade, 1.25em), #000 calc(100% - var(--fade, 1.25em)), transparent);
}

/* ---------------------------------------------------------------------
   Bar: HP, MP, EXP, durability, progress. A black track, a fill of one
   solid dark colour set by --v (percent) with no gradient and no sheen
   over it, and the figure centred on top in whitish engraving light with
   no text shadow. Every fill is dark enough to carry that text over it.
   --sm is the thin bar with no text (party frames, pet rows).
   --------------------------------------------------------------------- */
.bar { --bar: var(--bar-sage); position: relative; isolation: isolate; height: 1.35em; border-radius: 99em; background: var(--bar-track); overflow: hidden; color: var(--bar-text); font-size: var(--fs-small); box-shadow: inset 0 0 0 1px rgba(var(--ink-wash), .22); }
/* The sheen and grain over fill and track alike; the figures stand above it. */
.bar::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--bar-sheen); mix-blend-mode: overlay; pointer-events: none; }
/* The fill is squared off, and it is not flat: one horizontal wash over the
   colour, dark at the left and light at the right, so a full bar reads as a
   thing filling from the near end. The wash is alpha over var(--bar), so a
   new colour needs no second gradient written for it. */
.bar__fill { position: absolute; inset: 0 auto 0 0; width: var(--v, 50%); background: var(--bar-dim), var(--bar); }
.bar__text { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; font-variant-numeric: tabular-nums; white-space: nowrap;
  text-shadow: 0 1px 1px rgba(var(--shadow-ink), .9), 0 0 2px rgba(var(--shadow-ink), .8); }
.bar--hp { --bar: var(--bar-hp); }
.bar--mp { --bar: var(--bar-mp); }
.bar--exp { --bar: var(--bar-exp); }
.bar--sage { --bar: var(--bar-sage); }
.bar--sm { height: .5em; font-size: 1em; }
/* --gold is the one bar that is LIT. Every other bar here is a trough with a
   flat fill; this one is a thing being charged.
   The halo is on the BAR, not on the fill, and that is not a preference: .bar
   is overflow:hidden so a glow drawn by the fill is clipped at the trough's
   own edge and never leaves it. So the outer light belongs to the track and
   the fill carries the inner one, which is what makes the charged part read as
   the lit part.
   `is-full` is written by the panel rather than derived from --v here, because
   99 percent must not be allowed to look finished.
   NO TEXT over this fill. The block above says every bar's fill is dark enough
   to carry whitish engraving light; gold is the one that is not, so the figure
   sits outside the bar. */
.bar--gold { --bar: var(--bar-gold); box-shadow: inset 0 0 0 1px rgba(var(--ink-wash), .22), 0 0 .45em rgba(var(--bar-gold-glow-rgb), .45); }
.bar--gold > .bar__fill { box-shadow: inset 0 0 .35em rgba(var(--bar-gold-core-rgb), .5); }
.bar--gold.is-full { box-shadow: inset 0 0 0 1px rgba(var(--ink-wash), .22), 0 0 .95em rgba(var(--bar-gold-glow-rgb), .95); animation: bar-gold-pulse 1.8s ease-in-out infinite; }
.bar--gold.is-full > .bar__fill { box-shadow: inset 0 0 .5em rgba(var(--bar-gold-core-rgb), .8); }
@keyframes bar-gold-pulse {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.15); }
}
@media (prefers-reduced-motion: reduce) { .bar--gold.is-full { animation: none; } }
.bar--lg { height: 1.7em; font-size: 1em; }
/* A vital that jumps is a vital nobody can read. Every fill slides to its new
   width over a fifth of a second, which is short enough that a hit still feels
   like a hit and long enough to see which way the number went. */
.bar__fill { transition: width .2s linear; }
/* --timed is the other kind of bar: not a value the server keeps sending, but a
   span of time running out in front of the player - a swing, a cast, one cycle
   of gathering. The server hands over how long it lasts and the bar fills once,
   so the picture and the blow cannot drift apart. The transition is off here on
   purpose: it would fight the animation's reset to zero. */
.bar--timed > .bar__fill { width: 0; animation: bar-fill var(--dur, 1s) linear forwards; transition: none; }
@keyframes bar-fill { from { width: 0; } to { width: 100%; } }

/* ---------------------------------------------------------------------
   Table: plain rows for rosters and boards. No cell borders and no cell
   boxes: the cells sit edge to edge so their shared bottom hairline reads
   as one line across the whole row, and the columns are separated by
   padding only. --cols is the grid template; a row is display:contents so
   .is-on washes the full width in one soft tone. The caption row is small
   capitals. --r1 .. --r6 right-align that column, for numbers.
   --------------------------------------------------------------------- */
.table { display: grid; grid-template-columns: var(--cols, 1fr 1fr); font-variant-numeric: tabular-nums; }
.table__row { display: contents; }
.table__row > * { display: flex; align-items: center; gap: .3em; min-width: 0; padding: .5em .7em; border-bottom: 1px solid var(--hairline); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.table__row:last-child > * { border-bottom: 0; }
.table__head > * { font-size: var(--fs-small); font-variant: small-caps; letter-spacing: .04em; color: var(--ink-mute); padding-top: .1em; padding-bottom: .25em; }
.table__row.is-on > * { background: var(--row-on-flat); }
.table__row.is-on > :first-child { border-radius: .35em 0 0 .35em; }
.table__row.is-on > :last-child { border-radius: 0 .35em .35em 0; }

/* Well: the multi-line band. A grey box for prose, a section of a body, a guide. */
.well { padding: .7em .8em; background: var(--band-bg); border-radius: .5em; color: var(--ink); line-height: 1.4; }
/* A section head: a plain brass hairline under it, fading to the right. */
.well__title { font-size: var(--fs-small); letter-spacing: .06em; text-transform: uppercase; color: var(--brass-hi); margin-bottom: .4em; background: var(--divider-bg); }
.cap { font-size: var(--fs-small); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-mute); }
.note-line { font-size: var(--fs-small); color: var(--ink-mute); line-height: 1.3; }
.note-line--error { color: var(--error); }
.note-line--good { color: var(--green); }
.note-line--mute { color: var(--ink-mute); }

/* Check and range: the settings controls. The box is a small white tile
   that turns tan with the check glyph; the range is the grey track with
   a cream round thumb, the same disc as the stepper. The part of the track
   already travelled is tan, so a slider reads at a glance from across the
   screen (how much gold to stake, how loud the music is): --v is the same
   percentage the bar's fill uses, written by whoever moves the thumb, and a
   range that nobody drives just shows the grey track. */
.check { position: relative; display: inline-flex; align-items: center; gap: .5em; cursor: var(--cur-hand, pointer); color: var(--ink); }
.check input { position: absolute; opacity: 0; width: 0; height: 0; }
.check__box { display: grid; place-items: center; width: 1.3em; height: 1.3em; border: 1px solid var(--field-border); border-radius: .3em; background: var(--field-face); color: var(--tan-text); flex: none; }
.check__box .ic { width: .85em; height: .85em; visibility: hidden; }
.check input:checked + .check__box { background: var(--tan); border-color: var(--tan-rim); }
.check input:checked + .check__box .ic { visibility: visible; }
.check input:focus-visible + .check__box { outline: .12em solid var(--ink); outline-offset: .15em; }
.check:has(input:disabled) { opacity: .5; cursor: var(--cur-arrow, default); }
.range { -webkit-appearance: none; appearance: none; width: 12em; height: .5em; margin: .5em 0; border-radius: .25em; background: linear-gradient(to right, var(--tan) 0 var(--v, 0%), var(--grey-pill-2) var(--v, 0%) 100%); outline: 0; cursor: var(--cur-hand, pointer); }
.range::-webkit-slider-thumb { -webkit-appearance: none; width: 1.3em; height: 1.3em; border-radius: 50%; background: var(--cream); border: 1px solid var(--cream-rim); box-shadow: 0 .07em .14em rgba(var(--shadow-ink), .18); }
.range::-moz-range-thumb { width: 1.3em; height: 1.3em; border-radius: 50%; background: var(--cream); border: 1px solid var(--cream-rim); }
.field--area { height: auto; align-items: flex-start; padding: .5em .6em; }
.field--area textarea { flex: 1; min-width: 0; min-height: 4em; border: 0; background: transparent; outline: 0; font: inherit; color: inherit; padding: 0; resize: vertical; }
.field__label { color: var(--ink-mute); white-space: nowrap; }

/* Paperdoll for the Character and Player screens: the front-view stand
   layers of data/art/avatar-front/front.0.png, each clipped to its own frame
   from the sidecar (never one background over the whole box). The atlas
   is 2x, so the 240x480 canvas is scaled by --s. Page-only helper. */
/* A caller sets --doll-w, the width it wants the figure to occupy IN EM, and
   the box is that wide and twice that tall - the 1:2 of the 240x480 canvas.
   The canvas is atlas pixels, so left alone the figure is the one thing in a
   window that ignores --win-size: shrink the system and it stays the size it
   was while the panel closes in around it. --doll-w is what stops that.

   The box used to be `--s: calc(var(--doll-w) / 240px)` with `.doll` a fixed
   240x480 scaled by it. Dividing a length by a length is CSS Values 4
   arithmetic only Blink implements: Gecko drops every declaration carrying it,
   so in Firefox the transform and the box's own width were both thrown away
   and the doll stood at the raw 240x480, unclipped, stretching the panel round
   it. Nothing here divides two lengths any more. `.doll` simply fills the box,
   and character.ts paintFront fits the figure to whatever it measures - the
   same arithmetic it always did, one step earlier. */
.doll-box { width: var(--doll-w, 8em); height: calc(var(--doll-w, 8em) * 2); overflow: hidden; }
/* The stage is only a positioning context now: the class emblem that used to
   hang behind the figure is gone, so the doll stands on the window's own
   paper. .doll-box still clips the canvas it sizes. */
.doll-stage { position: relative; }
.doll { position: relative; width: 100%; height: 100%; }
/* One black line round the figure, and only round the figure: #doll-ink (the
   filter in the sprite at the top of the body) dilates the alpha of the whole
   stack and floods it black under the art, so hair against face and sleeve
   against hand stay clean. It is the DOM half of the world's silhouette pass
   (apps/game/client/outline.ts); the Outline switch in Settings drops both, and the
   game marks that with body.no-ink. */
.doll { filter: url(#doll-ink); }
body.no-ink .doll { filter: none; }
/* the figure is sampled, not smoothed: the atlas is a 2x master and the doll is
   drawn at an exact half of it, so nearest neighbour is the sharper picture.
   No atlas background default: the game never draws a layer: character.ts bakes each look into one small picture and
   paints it on a single <i>, and an atlas default here would be a whole shared
   sheet painted over a box larger than one frame (repo CLAUDE.md). */
.doll i { position: absolute; display: block; image-rendering: pixelated; }

/* ---------------------------------------------------------------------
   Card: a small popover, the item tooltip. Same face as a window, a
   head with the tile and name in the rarity colour, a body, a foot.
   --------------------------------------------------------------------- */
.card {
  width: 19em; padding: .7em .8em;   /* 19, not 17: the body reads at full size now */
  background: var(--card-bg); color: var(--ink);
  box-shadow: var(--card-ring);
}
.card__head { display: flex; align-items: center; gap: .6em; flex-wrap: wrap; } /* wrap: a worn System Gift carries two tags */
/* The name takes the room beside the picture and wraps its own words there (owner, 2026-10-10): as a
   bare flex item a long name would not shrink, so the whole block wrapped under the picture. */
.card__name { display: grid; gap: .15em; flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
/* The item's own name line. .card__title is the class the client writes the
   name into (gui/tip.ts, items.ts itemCard, skill-card.ts) and
   the four --tone modifiers below colour it by rarity; a card whose name is
   written as plain markup says the same thing with a bare <b>
   inside .card__name. One rule for both, so the two spellings cannot drift.
   Size only, never colour: the tone modifiers own the colour, and a colour
   here would out-specify them on the descendant spelling. */
.card__title, .card__name b { font-size: var(--fs-heading); }
.card__name small { color: var(--ink-mute); font-size: var(--fs-small); }
/* The tier inks are doubled on purpose (0,2,0): a panel's own one-class name rule (.cmpw__named > b, .fuse__role...) must never out-ink a tier (owner, 2026-10-10: every tiered name wears its tier). */
.card__title--purple.card__title--purple { color: var(--rarity-purple-text); }

/* ---------------------------------------------------------------------
   THE GEAR CARD IS DARK WOOD, not parchment (user, 2026-09-18). Every other
   item hovers on the family's cream page; a piece of equipment hovers on the
   dark the world's own plates are cut from, so a stat block reads as a plate
   bolted to the frame rather than as a paragraph on a page.

   It repaints by REDEFINING THE TOKENS on the card root, never by restating a
   colour per element. --win-face is what .card already paints itself with, and
   --ink, --ink-mute, --hairline and --blue are what everything inside it reads,
   so the head, the caption, the description paragraphs, the stat rows, the
   durability note and the hairlines between them all turn over together and no
   part of the card can be left behind on the next thing added to it.

   THE FACE IS DARK (user, 2026-09-18, after trying the tab's own --tan and
   choosing this instead). It is the Backpack tab's brown taken down until a
   stat table can be read on it, which is further than a shade: --tan is a
   BUTTON face, and the game only ever makes it readable by putting one short
   light word on it behind a shadow. A card is ten rows of figures, so the face
   has to earn its own contrast - and at this depth it does, for every colour on
   it except one.

   THE NAME KEEPS THE PAPER INKS (user's standing call). Gold lands at 5.2:1
   here, green 6.0, blue 4.9. Purple is the one that pays for it, near 3.3:1 -
   legible, under the 4.5 the paper tones were mixed for. --world-q-purple
   (#c98bff) is the one-line swap if it is ever wanted.
   --------------------------------------------------------------------- */
.card--gear {
  /* A PROPORTION, not an amount: the two critical damages and the two damage reductions. Gold is
     the world's own critical colour (--world-crit), which is the same fact these lines carry. */
  --stat-pct: var(--world-crit);
  /* The card's HEAD is a header band, not a lighter patch of the face: the tile, the name and the
     line under it stand on their own pale ground, edge to edge, and the stats hang below it on the
     brown. It was a gradient into a lighter brown for one pass; a band says "this is the head"
     where a fade only says "this is the top".
     IT IS THE WINDOW HEAD'S OWN GROUND (user, 2026-09-19), the same parchment gradient .win__head
     is drawn on, so a tooltip's head and a panel's head are one surface said twice rather than two
     creams a stop apart. It was --cream, the family's stepper face, which was a colour the chrome
     already wore but a lighter one than any header in the game.
     The gradient's dark stop lands under the body strip. The head reaches .6em further down than
     its contents need, and ::after paints the brown back over that, so what a player sees is
     --parch flat with --parch-edge showing only in the two notches at the corners - which is
     exactly where a window head shows it too. */
  --head-face: var(--win-head-bg), var(--navy-2);
}
/* The label of a percentage line. On the CARD it is gold; anywhere else this block is drawn on
   paper the token is unset and the label keeps the ordinary muted ink, so nothing outside the gear
   card changes colour because a stat happens to be a proportion. */
.kv__pct { color: var(--stat-pct, inherit); }
.card__title--blue.card__title--blue { color: var(--rarity-blue-text); }
.card__title--gold.card__title--gold { color: var(--rarity-gold-text); }
.card__title--yellow.card__title--yellow { color: var(--rarity-yellow-text); }
.card__title--green.card__title--green { color: var(--rarity-green-text); }

/* The header band. It BLEEDS to the card's edges - the card carries .7em/.8em of padding and the
   head gives exactly that back as negative margin, then takes it again as its own - so the band
   reaches the corners instead of floating inset with the paper showing round it. Square, as the
   card is. Its ground is --parch, dark like every other card's head, and
   its inks are the card's own (user, 2026-09-26: the gear card reads like every other card). */
.card--gear .card__head {
  position: relative;
  margin: -.7em -.8em .25em;
  padding: .55em .8em;
  background: var(--head-face);
  color: var(--ink);
}
.card__body { font-size: 1em; color: var(--ink-mute); line-height: 1.4; }   /* the window's own size (owner, 2026-10-09: small was too small) */
.card--name > .card__body:only-child { color: var(--ink); }   /* a tooltip is its line alone */
.card__number { color: var(--card-number); font-weight: 700; }
.card__body .kv { font-size: 1em; }
/* --float is the same card lifted off the page and put wherever the pointer is (placed and
   layered in 25-card.css: fixed, above the windows, never eating a click). */
/* --name is the card with nothing in it but a name: what a HUD glyph answers with.
   One short line has no use for the reading column an item card holds, so it
   shrinks to its own label instead of standing 17em wide around three words. */
.card--name { width: max-content; max-width: min(22em, calc(var(--frame-w) - 1em)); } /* a long line wraps in a reading column */
/* the one floating card the player is meant to hit: the little menu a row opens.
   It is the same box in the same layer, it just answers the pointer. */
.card--float.card--pick { pointer-events: auto; }

/* ---------------------------------------------------------------------
   Portrait: the round medallion. The HUD status frames, the party rail,
   a pet row, a roster line. One neutral round face, a parchment rim and
   the picture filling it. --sigil hangs the class magic circle AROUND
   the medallion, and the avatar OVERLAYS it: the emblem is inked right
   through - the class weapon fills its middle - so the well has to cover
   that middle and leave only the outer band showing. The class is set by
   --sigil (warrior, mage, priest, ranger).
   --------------------------------------------------------------------- */
.portrait {
  --sigil: url(/data/art/ui/sigil-warrior.bdf39f22.webp);
  /* How far the ring hangs outside the well. One figure for all four, because
     the masters are cut tight to their own ink and land on the same 512
     canvas; the old art needed a different inset per class to cancel the dead
     margin each file shipped with.
     -36% is bounded on both sides. Below it the well swallows the emblem: the
     band sits at 0.7-0.9 of the art's half-width, so at the old -31% only a
     4px sliver cleared the rim. Above it the ring reaches into the row - this
     portrait is mostly used as a list-row picture (the friend list, the party
     invite), the ring is not in the row's reserved width, and at -42% it was
     printing over the name. The HUD's own sigil is a separate rule at -31%
     and does NOT want this number: its well is inset by the block's padding,
     so it already has the clearance this one has to buy. One number for every
     class, here and in the HUD: all four files are drawn edge to edge. */
  --sigil-in: -36%;
  /* block: most wells are a <span>, and an inline box ignores its width, so the face (absolute,
     4.6em) hung out of a zero-wide well over the name beside it (a pet row's head over its name). */
  display: block; position: relative; width: 4.6em; height: 4.6em; flex: none;
  border-radius: 50%;
  background: var(--portrait-bg);
  box-shadow: inset 0 0 0 max(1px, .11em) var(--portrait-ring), 0 0 0 1px var(--win-rim), 0 .1em .25em rgba(var(--shadow-ink), .5);
}
.portrait[hidden] { display: none; }
.portrait > img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; object-position: center 22%; }
.portrait .pill { position: absolute; left: 50%; bottom: -.35em; transform: translateX(-50%); font-size: max(var(--fs-badge), .7em); white-space: nowrap; z-index: 2; }
/* ::before, not ::after, and that is the whole of it. A pseudo-element with
   z-index auto paints in tree order among its positioned siblings, and ::before
   is the first child while .head and > img are the rest - so this lands UNDER
   the face and over the medallion's own background. The old art was a hollow
   ring and could sit on top; this one has the class weapon through its middle,
   and on top it drew a sword across the player's face. */
.portrait--sigil::before {
  content: ""; position: absolute; inset: var(--sigil-in);
  background: var(--sigil) center / contain no-repeat;
  /* the same halo the HUD gives it, so the priest's holy white still reads */
  filter: drop-shadow(0 0 .06em rgba(var(--shadow-ink), .45));
  pointer-events: none;
}
/* The well has to be OPAQUE for the overlay to work. A head crop is a cut-out
   with gaps around the shoulders, and the medallion's own fill sits below the
   sigil, so without this the emblem's middle showed through beside the head.
   Same fill .portrait carries, so a portrait with no sigil is unchanged. */
.portrait--sigil > .head {
  background: var(--portrait-bg), var(--tile-face);
}
.portrait--mage { --sigil: url(/data/art/ui/sigil-mage.e69b7ab3.webp); }
.portrait--priest { --sigil: url(/data/art/ui/sigil-priest.99fb5271.webp); }
.portrait--ranger { --sigil: url(/data/art/ui/sigil-ranger.04e5640f.webp); }

/* The face itself, when the face is a PLAYER: the head crop of the front
   paperdoll, which is what the game puts in the well (character.ts headHtml,
   HEAD = 8, 0, 104, 104 on the 1x front canvas, so 16, 0, 208, 208 at 2x).
   An NPC has a painted bust and uses <img>; a player never does. The layers
   are percentages of the crop, so one head fits a well of any size, and the
   blow-up is the HUD's own: the round well would lose the square crop's
   corners, so the crop is enlarged about the face and the shoulders fall off
   the bottom edge. */
.head { position: absolute; inset: 0; overflow: hidden; border-radius: 50%; }
/* no pixelated here, unlike the full doll: the head is blown up about the face
   by a figure that is not a whole ratio, and nearest neighbour at 1.38 makes a
   staircase of every eyelash. The doll is halved exactly and is sampled. */
.head i { position: absolute; display: block; }
.portrait > .head { transform: translateY(-11%) scale(1.38); transform-origin: 50% 36%; }
/* A sigil portrait standing as a LIST ROW's picture. The ring is deliberately
   hung OUTSIDE the medallion, and a row measures its picture column from the
   medallion - so the overhang lands on the name beside it, and on the rows above
   and below, neither of which reserved a pixel for it. The friend list is where
   this showed: a class emblem printed straight across "Pettest / Lv100 Warrior".
   In a row the emblem comes inside the well instead. It is smaller and the class
   still reads, which in a row is the whole of what it was ever saying - the row
   writes the class out in words underneath as well. */
.item-row .portrait--sigil { --sigil-in: -6%; }
/* And it greys with the rest of the row. `.is-dim` only ever reached the <img>,
   and a roster line has none - the server sends a name, a level and a class, no
   Appearance at all - so an offline friend wore a full-colour class badge beside
   their own greyed-out name. The halo goes with the colour, which is right: it
   was there to keep the priest's white readable, and a grey emblem is not being
   read for its class any more. */
.portrait.is-dim::before { filter: grayscale(1); opacity: .45; }
/* A presence dot, for the word it stands in front of. Two states and no third -
   a name is either reachable or it is not - so it is `.is-on` and not a family
   of variants, and the WORD beside it says which either way: the dot is the
   glance, never the only telling. */
.dot {
  display: inline-block; width: .5em; height: .5em; margin-right: .4em;
  border-radius: 50%; background: var(--ink-mute); vertical-align: baseline;
}
.dot.is-on { background: var(--green); }
.portrait--sm { font-size: .7em; }
.portrait--lg { font-size: 1.5em; }
.portrait.is-dim > img { filter: grayscale(1); opacity: .45; }

/* ---------------------------------------------------------------------
   Item row: the one list line the whole game is made of. A picture at
   the left, a name with a smaller second line under it, and an end
   column for the price, the count or the action. Rows sit in .rows and
   share one hairline; .is-on is the selected row's soft tan wash;
   --empty is the placeholder line ("not learned", "drop a pet here").
   Rarity on the name reuses the card title colours.
   --------------------------------------------------------------------- */
.rows { display: grid; align-content: start; min-width: 0; }
.item-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .6em; align-items: center; min-width: 0; padding: .45em .5em; border-bottom: 1px solid var(--hairline); color: var(--ink); text-align: left; }
.rows > .item-row:last-child { border-bottom: 0; }
.item-row__name { display: grid; gap: .1em; min-width: 0; }
.item-row__name b { font-weight: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-row__sub { font-size: var(--fs-small); color: var(--ink-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* THE NAME IN ITS OWN BAND, on a shelf. Opt-in per list (items.ts itemRow
   `tone`), and the counters are what opted in - a vendor's stock, a barter
   line, a buyback row, every Marketplace shelf - because those are lists a
   player is CHOOSING from and the band is what they sort by first. The inks are
   the card titles' own, so the name a shelf shows is the name its hover card
   raises, in the same colour. A bag or a mail attachment stays plain: there the
   tile's rim has already said it. */
.item-row--green .item-row__name b, .qs__item--green .qs__item-name { color: var(--rarity-green-text); }
.item-row--blue .item-row__name b, .qs__item--blue .qs__item-name { color: var(--rarity-blue-text); }
.item-row--gold .item-row__name b, .qs__item--gold .qs__item-name { color: var(--rarity-gold-text); }
.item-row--yellow .item-row__name b, .qs__item--yellow .qs__item-name { color: var(--rarity-yellow-text); }
.item-row--purple .item-row__name b, .qs__item--purple .qs__item-name { color: var(--rarity-purple-text); }
.item-row__end { display: flex; align-items: center; justify-content: flex-end; gap: .5em; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A barter row's second line: what the counter takes, as PICTURES. Four stones
   spelled out ("5 x Ancestral Stone [Blue] + ...") is wider than the row and
   ends the item's own name in an ellipsis, so the sub line carries the icon
   cells and their counts instead and the full names are read once, in the
   confirmation. The row keeps its own ellipsis for a price with more pieces in
   it than the column can hold. */
.item-row__sub.swap-cost { display: flex; align-items: center; gap: .5em; }
.swap-cost__one { display: flex; align-items: center; gap: .15em; flex: none; }
.swap-cost__one > .ico { --ico-box: 1.5em; }
.swap-cost__one > b { font-weight: inherit; font-variant-numeric: tabular-nums; color: var(--ink); }
/* A row spelled as a real <button> - the market's shelf list, the row menu a
   player or a bag cell opens. The browser paints its own button on those: a grey
   face and a 13px system label, which is neither the row's face nor the game's
   type. Chrome only, and no background of its own: :hover and .is-on are two
   classes and still out-rank this, so the row keeps its own states. */
button.item-row { appearance: none; -webkit-appearance: none; margin: 0; font: inherit; color: var(--ink); background: none; cursor: var(--cur-hand, pointer); border-top: 0; border-left: 0; border-right: 0; }
.item-row:hover { background: var(--row-hover); }
.item-row.is-on { background: var(--row-on); border-radius: .35em; }
.item-row--empty { color: var(--ink-faint); }
.item-row--empty:hover { background: none; }
/* A row that is still listed but cannot be used right now: a coach ride the
   player is barred from, a recipe short of a material. Dimmed as a whole, the
   same reading .is-dim gives a tile and a portrait, so one state word means one
   thing everywhere. */
.item-row.is-dim { opacity: .5; }
/* The same word on a button: a verb listed but unable to act now (the tap menu's dead verb). */
.btn.is-dim { opacity: .5; }

/* THE TAP MENU (gui/menu.ts tapMenu, owner 2026-10-08): the context card's touch face, opened by
   one tap on an item, a shelf row or a skill. The thing's card, its verbs beside it as buttons; on a
   landscape phone the height is the constraint, so the card scrolls and the buttons stay. */
.ctx.ctx--tap {
  display: flex; align-items: flex-start; gap: .5em;
  max-width: calc(var(--frame-w) - 8px); max-height: calc(var(--frame-h) - 8px);
}
.ctx--tap > .ctx__card { min-width: 0; max-height: calc(var(--frame-h) - 1.5em); overflow-y: auto; }
/* The thing's card is the menu's own ground: no second frame inside the menu's. */
.ctx__card > .card { box-shadow: none; background: none; }
/* The worn piece beside it: set apart by a hairline, a touch quieter (as .tip--cmp). */
.ctx--tap > .ctx__card--cmp { border-left: 1px solid var(--line, rgba(255,255,255,.12)); padding-left: .5em; opacity: .96; }
.ctx--tap > .ctx__acts { display: grid; gap: .35em; min-width: 10em; }
.ctx .item-row.is-danger b { color: var(--btn-bad-text); }

/* ---------------------------------------------------------------------
   Drop target: where a drag lands. A dashed tan outline and a line of
   guidance, nothing else. .drop is the area (mail attachments, a stall
   shelf, the blacksmith's equipment well); .tile--drop is the single
   dashed slot in a grid of tiles. .is-over is the lit state.
   --------------------------------------------------------------------- */
.drop { display: grid; place-items: center; align-content: center; gap: .35em; padding: 1em .8em; min-height: 4em; border: 1px dashed var(--tan-rim); border-radius: .5em; background: rgba(var(--ink-wash), .03); color: var(--ink-mute); font-size: var(--fs-small); text-align: center; }
.drop.is-over { border-color: var(--drop-on-rim); background: var(--drop-on-bg); }
.tile--drop { background: rgba(var(--ink-wash), .03); box-shadow: none; border: 1px dashed var(--tan-rim); }
.tile--drop::before { content: none; }
/* The lit slot. A tile is a drop target twice over: the dashed empty one and an
   ordinary filled one that will take a swap, so both light, and both light the
   same way the .drop area does. */
.tile--drop.is-over, .tile.is-over { border-color: var(--drop-on-rim); background: var(--drop-on-bg); --tile-ring: var(--drop-on-rim); }
/* The one cell a carried stack would land in (the bag): bright enough to find at a glance. */
.tiles .tile.is-over { background: rgba(var(--sky-rgb), .24); box-shadow: inset 0 0 0 2px var(--brass-pale), 0 0 .6em rgba(var(--brass-hi-rgb), .65); }
.drop, .tile--drop { cursor: var(--cur-grab, copy); }
/* A whole GRID of tiles as one drop area: the trade offer, where a stack and a pet land in the
   same twenty places and no single cell is the target. An outline rather than a border, because
   an outline is drawn outside the box and so cannot move forty tiles by a pixel. */
.tiles.is-over { outline: 1px dashed var(--drop-on-rim); outline-offset: .25em; }

/* ---------------------------------------------------------------------
   Bubble: one line of chat. The speaker's portrait, a grey bubble with
   the name above the words and a tail pointing back at the portrait,
   and the clock outside it. --own is the parchment bubble for your own
   line, its tail on the other side.
   --------------------------------------------------------------------- */
.msg { display: grid; grid-template-columns: auto auto minmax(0, 1fr); gap: .5em; align-items: flex-start; }
.msg__time { justify-self: start; font-size: var(--fs-small); color: var(--ink-mute); padding-top: .5em; font-variant-numeric: tabular-nums; }
.bubble { position: relative; justify-self: start; max-width: 26em; padding: .4em .7em; background: var(--grey-pill); border-radius: .5em; color: var(--ink); line-height: 1.35; }
.bubble::before { content: ""; position: absolute; left: -.28em; top: .75em; width: .6em; height: .6em; background: inherit; transform: rotate(45deg); }
.bubble--own { background: var(--bubble-own); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .22); }
.bubble--own::before { left: auto; right: -.28em; }
/* A TRANSCRIPT: a conversation read inside a panel column rather than across the
   foot of the screen, and read the way every messaging app has taught people to
   read one - the other person down the left behind their face, your own words
   down the right with no face at all, because you know who you are.

   Flex rather than the grid .msg uses: the sides differ only by reading order,
   and row-reverse says that in one declaration where a second column table would
   have to restate every track. The bubble's own 26em ceiling was measured for
   the HUD log, which is as wide as the screen; in a panel column it is wider
   than the column, so the row's last track - the clock - was squeezed out of a
   long line entirely. Here it is a share of the row and wraps inside it. */
.talk { display: grid; align-content: start; gap: .5em; }
.talk .msg { display: flex; align-items: flex-end; gap: .45em; }
.talk .bubble { max-width: 78%; overflow-wrap: anywhere; }
.talk .msg__time { padding-top: 0; padding-bottom: .15em; flex: none; }
.talk .msg--own { flex-direction: row-reverse; }
/* Your own line needs no portrait: it is the only one on its side of the column,
   and a face repeated down the right edge is the picture a reader already has.
   The element stays in the row rather than being left out of the markup, so both
   sides are built by the same builder. */
.talk .msg--own .portrait { display: none; }
/* msg() always emits the picture column, even when there is no picture, because
   its own grid needs the track. A flex row does not, and an empty box here is a
   gap's worth of indent under every line that has no face. */
.talk .msg > span:first-child:empty { display: none; }

/* A thing somebody dropped into what they said: an item, a pet, a place. It is
   coloured by the rarity title colours, which is the whole of its look, so all
   this owes is the promise that it can be clicked. */
.chat-link { cursor: var(--cur-hand, pointer); }
.chat-link:hover { text-decoration: underline; }

/* Equipment slot silhouettes: one frame of data/art/ui/equipment-empty.png
   drawn faintly inside an empty tile, so a bare slot says what belongs in
   it. The sheet is a 5 by 2 grid; the modifier picks the cell. */
.tile__slot { position: absolute; inset: .28em; background-image: url(/data/art/ui/equipment-empty.f9c85ae2.webp); background-repeat: no-repeat; background-size: 500% 200%; opacity: .3; }
.tile__slot--weapon { background-position: 0% 0%; }
.tile__slot--necklace { background-position: 25% 0%; }
.tile__slot--ring { background-position: 50% 0%; }
.tile__slot--coat { background-position: 75% 0%; }
.tile__slot--trousers { background-position: 100% 0%; }
.tile__slot--hat { background-position: 0% 100%; }
.tile__slot--shoes { background-position: 25% 100%; }
.tile__slot--bracelet { background-position: 50% 100%; }
.tile__slot--fashion { background-position: 75% 100%; }
.tile__slot--aircraft { background-position: 100% 100%; }

/* The icon button's set state: a toggle that is on (a pinned skill, a
   filter that is applied) fills tan the way the checkbox does. */
.ibtn.is-on { background: var(--tan); border-color: var(--tan-rim); color: var(--tan-text); }
.ibtn.is-on::after { border-color: var(--ibtn-on-ring); opacity: .5; }

/* A window set: the tab variants of one panel stacked in the same place, one
   page shown at a time. The strip that pages them carries data-pages; every
   other strip on the page just moves its own mark. A page may hold more than
   one window when the panel has a second state worth showing (a fusion
   result, a roster mid-registration). */

/* ---------------------------------------------------------------------
   HUD strip: the menu row at the bottom-right of the screen, the painted
   data/art/ui/hud-*.png plates as they are, one .hud__btn each.
   --------------------------------------------------------------------- */
.hud { display: flex; gap: .3em; align-items: flex-end; }

/* Something in there wants you. One mark, always the same one, in the top right
   corner of whatever carries it: a menu plate with unread mail, a tab with a
   finished job behind it. Orange because nothing else in this system is, so it
   is the only thing on a quiet HUD that moves the eye. */
.dot {
  position: absolute; right: .2em; top: .2em;
  width: .55em; height: .55em; border-radius: 50%;
  background: var(--orange); box-shadow: 0 0 0 max(1px, .06em) var(--dot-ring);
  pointer-events: none;
}

/* ---------------------------------------------------------------------
   On the world. The one text in the game that is not on paper: nameplates,
   damage figures, a guild name under a player, a stall sign, the broadcast
   line. It has to hold over grass, over snow, over water and over a white
   robe walking underneath, and no panel may be put behind it - a plate
   behind every name would wall the world off.
   So it is the reverse of the panel voice: the engraving light for the face
   and a hard dark rim on all four sides, one hairline thick and growing with
   the type, plus a soft halo to seat it. The rim is the darkest ink of this
   family, not black, so the world text still belongs to the same drawing.
   --glow is for a PICTURE over the world (a glyph, a plate art) where a text
   shadow has nothing to bite on; --plate is the last resort for a long line
   the halo alone cannot carry, and it drops the rim because the plate is the
   contrast then.
   --------------------------------------------------------------------- */
.on-world {
  color: var(--world-text);
  text-shadow:
    var(--world-rim) var(--world-rim) 0 var(--world-outline),
    calc(var(--world-rim) * -1) var(--world-rim) 0 var(--world-outline),
    var(--world-rim) calc(var(--world-rim) * -1) 0 var(--world-outline),
    calc(var(--world-rim) * -1) calc(var(--world-rim) * -1) 0 var(--world-outline),
    0 0 .4em rgba(var(--world-dark), .8);
}
/* a chip that rides inside a world line - a stall sign's price, a tracker count -
   is a piece of paper chrome and keeps its own crisp edges: the rim is for the
   loose words. A nameplate's level is NOT one of them any more: see below. */
.on-world .pill, .on-world .tag, .on-world .chip:not(.chip--bare) { text-shadow: none; }

/* A BARE CHIP IS NOT PAPER. Every other chip brings its own grey face and can
   sit anywhere; --bare threw the face away, so over the canvas it is loose words
   in panel ink, and over pale stonework it all but disappears.
   (The client's status line - clock, latency, FPS, build - used to be exactly
   that, printed straight on the world under the minimap. It is not any more: it
   rides its own slim ink strip above the map, in 40-hud.css. A reading in a panel
   still comes through here.)
   The fix is the one this section already exists to be: the row carries
   .on-world, and a bare chip inside it stops exempting itself from the rim and
   takes the engraving light like any other loose word. It is the only chip that
   does, because it is the only chip with nothing behind it.
   The three READINGS keep their meaning and change their colour: the panel
   green, gold and red are the same figures the fight found too dark to read at
   speed over grass, so a reading over the world takes the lifted world token
   instead. Same state, same class, the palette the surface asks for. */
.on-world .chip--bare { color: var(--world-text); }
.on-world .chip--bare.is-good { color: var(--world-heal); }
.on-world .chip--bare.is-slow { color: var(--world-crit); }
.on-world .chip--bare.is-bad { color: var(--world-hit); }
/* An announcement over the whole world. Its own three-colour reading, because a
   broadcast is a sentence about strangers and the eye has to find the two things
   in it that matter before reading any of it: WHO, and WHAT. So the actor is
   always the world's name lime, the thing is always its own quality band, and
   every joining word between them is plain white - `.on-world`'s warm off-white
   is the right ink for a nameplate sitting in the scene and slightly muddy for a
   line the reader is meant to skim.

   The band classes are the SAME ones a parchment card uses. Only the ink changes,
   for the reason the token block says: a card's purple is drawn to be read on
   paper, and over the canvas it disappears. */
.cast { color: #fff; }
.cast .cast__name { color: var(--world-name); }
.cast .card__title--green { color: var(--world-q-green); }
.cast .card__title--blue { color: var(--world-q-blue); }
.cast .card__title--gold { color: var(--world-q-gold); }
.cast .card__title--yellow { color: var(--world-q-yellow); }
.cast .card__title--purple { color: var(--world-q-purple); }
/* A top-of-the-ladder result (ten stars, a Perfect pet, a wedding, a war won):
   the same coloured line as any other broadcast, set larger so it stands out
   from ordinary news without a band across the world (owner, 2026-09-16). */
.cast__line--big { font-size: 1.4em; }

/* ---------------------------------------------------------------------
   Proclamation. The band the world says something in: you arrived
   somewhere, you reached a level, somebody on this server did a thing
   worth everyone's attention, the operator is talking. One band across
   the upper third, edge to edge, in gold on an ink wash that has no edge
   of its own - it fades out top and bottom into the world rather than
   being a box laid on it - with the engraved flourish either side of the
   words, dense at the outer edge and gone before the text.

   It is a sentence that crosses the screen and leaves on its own, and it
   never takes a click.

   Tone lives on the WORDS and never on the band: --good for something that
   went right, --bad for something that did not. The band itself is always
   the same ink.

   Enter with a short fade and a 6px rise, hold, leave with a fade:
   .is-on is on for the hold and off for the leave, so nothing here needs
   a keyframe and the caller owns how long the hold is. Two
   seconds is the hold unless the caller has a number of its own (a
   server announcement carries how long it stands).

   WHERE it sits is .tl-procl at the end of this sheet, over the upper third,
   beside the announcement ladder's .tl-cast.
   --------------------------------------------------------------------- */
.procl {
  display: flex; align-items: center; justify-content: center; gap: .4em;
  width: 100%; padding: .55em 0 .65em;
  /* No edge, top or bottom: the wash is strongest behind the words and
     dissolves into the world before it reaches anything with a corner. */
  background: linear-gradient(rgba(var(--world-dark), 0), rgba(var(--world-dark), .74) 42%, rgba(var(--world-dark), .74) 58%, rgba(var(--world-dark), 0));
  text-align: center;
  opacity: 0; transform: translateY(6px);
  transition: opacity .22s linear, transform .22s ease-out;
}
.procl.is-on { opacity: 1; transform: none; }
/* The flourish, one asset both sides (--orn-procl is mirrored by the
   scaleX below). A mask and not a picture, so the engraving takes the
   band's own gold and changes with it. It shrinks to nothing on a narrow
   screen rather than pushing the words out of the middle. */
.procl::before, .procl::after {
  content: ""; flex: 0 1 8.1em; min-width: 0; height: 3.2em;
  background: var(--world-gold); opacity: .85;
  -webkit-mask: url(/data/art/ui/orn-procl.5b8af75a.svg) left center / auto 100% no-repeat;
  mask: url(/data/art/ui/orn-procl.5b8af75a.svg) left center / auto 100% no-repeat;
}
.procl::after { transform: scaleX(-1); }
/* The one line, in the display face, large and tracked out: this is the
   only place in the game where a sentence is set as an inscription. The
   rim is the world layer's, because the band's wash is thin at the words'
   own height and a name over snow still has to hold. */
.procl__text {
  flex: 0 1 auto; max-width: 60%;
  font: var(--face-display-weight) 2.1em/1.2 var(--face-display);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--world-gold);
  text-shadow: 0 .04em 0 rgba(255, 245, 230, .18), 0 .08em .3em rgba(var(--world-dark), .8);
}
.procl__text--good { color: var(--world-heal); }
.procl__text--bad { color: var(--world-hit); }
/* A player's own notice: the same words, the same ink, nothing behind them.
   The wash and the flourish are the operator's announcement alone (owner,
   2026-09-16); without them the text's own rim is what holds it over the
   world, so the shadow is the world layer's full rim rather than the band's. */
.procl--plain { background: none; }
.procl--plain::before, .procl--plain::after { display: none; }
/* A notice that has pictures to show - the items a quest just paid. The band
   turns into a column so the row sits UNDER the words instead of competing
   with them for the middle; the tiles are the same small ones the quest sheet
   pays out with, so the thing that lands in the bag looks the same in both
   places. Only the plain notice ever stacks. */
.procl--stack { flex-direction: column; gap: .3em; }
.procl__items {
  display: flex; flex-wrap: wrap; gap: .35em;
  align-items: center; justify-content: center; max-width: 90%;
}
.procl--plain .procl__text {
  max-width: 90%;
  text-shadow:
    var(--world-rim) var(--world-rim) 0 var(--world-outline),
    calc(var(--world-rim) * -1) var(--world-rim) 0 var(--world-outline),
    var(--world-rim) calc(var(--world-rim) * -1) 0 var(--world-outline),
    calc(var(--world-rim) * -1) calc(var(--world-rim) * -1) 0 var(--world-outline),
    0 0 .4em rgba(var(--world-dark), .8);
}

/* ---------------------------------------------------------------------
   Quest sheet. What a quest IS, drawn in two places off one component:
   inside the NPC conversation, in place of its choice rows, and in the
   quest log's detail column. One component and not two, because the same
   quest read in a window and read in a conversation that differ in any
   particular are two quests to the player.
   The name is the one dramatic line here so it takes the display face;
   the category and the level band are the muted line beside it; the
   objectives are ordinary rows carrying the step kind's glyph and n/m,
   which turns gold the moment that step is full; the payout is one line
   of exp, money and the reward tiles. The quest's own prose is NOT here -
   it is behind the ( i ) - because this sheet says what to DO.
   --------------------------------------------------------------------- */
.qs { display: grid; gap: .5em; align-content: start; min-width: 0; }
.qs__head { display: flex; align-items: baseline; gap: .6em; }
.qs__name { font: var(--face-display-weight) var(--fs-heading)/1.15 var(--face-display); letter-spacing: .02em; color: var(--ink-strong); }
.qs__meta { display: flex; align-items: center; gap: .4em; margin-left: auto; color: var(--ink-mute); font-size: var(--fs-small); text-align: right; }
/* The story: muted, a few lines tall, scrolls past that. A long quest does not push the list
   and the pay out of the window. */
.qs__desc { max-height: 5.6em; padding-right: .4em; color: var(--ink-mute); font-size: var(--fs-small); line-height: 1.4; white-space: pre-line; }
/* The pay, one line: each figure with its coin, each item as a small tile with its name. The
   coins are one family at one size - gold, Lumi and the experience mark. */
.qs > .cap { font-weight: 700; color: var(--ink); }
.qs__pay { display: flex; align-items: center; flex-wrap: wrap; gap: .5em 1.2em; }
.qs__sums { display: inline-flex; align-items: center; gap: 1.2em; margin-left: auto; }
.qs__sum { display: inline-flex; align-items: center; gap: .3em; font-weight: 600; font-variant-numeric: tabular-nums; }
.qs__item { display: inline-flex; align-items: center; gap: .45em; }
.qs__item-name { color: var(--ink); }
.qs .item-row > .ic { width: 1.15em; height: 1.15em; color: var(--ink-mute); }
.qs .item-row__end.is-done { color: var(--rarity-gold-text); }

/* The quest links (.qlink: walk, map) are ui/w-quest.css. */

/* The objective line, coloured to be read in one pass: the thing to do in ink, the count in the
   figures' own tone, and the whole row in gold the moment it is full - the count alone turning
   gold was a two-character signal at the far right of a row the eye scans from the left. */
.qs .item-row__end { color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.qs .item-row:has(.item-row__end.is-done) .item-row__name b { color: var(--rarity-gold-text); }
.qs .item-row:has(.item-row__end.is-done) > .ic { color: var(--rarity-gold-text); }
/* The pay reads as a reward: the figures in gold, the item name in its own rarity tone (the tile
   beside it already carries the band, and a name in plain ink under a purple rim said nothing). */
.qs__sum { color: var(--rarity-gold-text); }

/* ---------------------------------------------------------------------
   Loading screen: the curtain (Midnight Astrolabe, owner 2026-10-09). Not
   a window - a window is a thing you open and close, and this is the whole
   screen going away and coming back. The night key art (or a map's own
   picture, --art) under a navy vignette; the painted astrolabe turning
   slowly behind the logo; the title in the display face and the note
   under it; at the foot one brass hairline meter with what is loading at
   its left and the percentage at its right. No frame, no engraving, no
   close: there is nothing here to act on. The turn curtain
   (orientation.ts, .ldg--turn) wears the same sky with no meter.
   --------------------------------------------------------------------- */
.ldg {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  overflow: hidden;
  color: var(--ink);
  background: var(--navy-0) var(--art, url("/data/art/landing/shell-night.7dfe29ad.webp")) center 42% / cover no-repeat;
}
html.m-mobile .ldg { background-image: var(--art, url("/data/art/landing/shell-night-1x.533db8a8.webp")); }
/* The night closes in: deepest at the foot where the meter is read, and at the edges. */
.ldg::before {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(0deg, rgba(var(--navy-0-rgb), .96), rgba(var(--navy-0-rgb), .55) 28%, rgba(var(--navy-0-rgb), .35) 60%, rgba(var(--navy-0-rgb), .7)),
    radial-gradient(ellipse 70% 60% at 50% 44%, transparent 30%, rgba(var(--navy-0-rgb), .65));
}
/* The astrolabe: the home screen's magic circle, face on, turning once in four minutes. */
.ldg__orrery {
  position: absolute; left: 50%; top: 44%;
  width: min(86vh, 52em); aspect-ratio: 1;
  translate: -50% -50%;
  background: url("/data/art/landing/astro-circle.86f1f5e9.webp") center / contain no-repeat;
  opacity: .34;
  filter: drop-shadow(0 0 .6em rgba(var(--brass-rgb), .35));
  animation: ldg-turn 240s linear infinite;
  pointer-events: none;
}
@keyframes ldg-turn { to { rotate: 360deg; } }
.ldg__band {
  position: relative;
  display: grid; justify-items: center; gap: .5em;
  margin-top: -6vh;
  padding: 1em 2em;
  text-align: center;
}
/* The official logo, the centre of the circle: a hidden title under it leaves no gap. */
.ldg__logo { width: min(20em, 46vw); height: auto; filter: drop-shadow(0 .4em 1.2em rgba(var(--shadow-ink), .7)); }
html.m-mobile .ldg__logo { width: min(15em, 36vw); }
.ldg__title[hidden], .ldg__note[hidden] { display: none; }
/* The one dramatic line on the screen, so it takes the display face. */
.ldg__title {
  font: var(--face-display-weight) clamp(1.5em, 4.4vh, 2.3em)/1.1 var(--face-display);
  letter-spacing: .08em; color: var(--brass-pale);
  text-shadow: 0 .06em 0 rgba(var(--shadow-ink), .9), 0 0 .8em rgba(var(--brass-hi-rgb), .3);
}
.ldg__note {
  font: var(--face-weight) var(--fs-md)/1.3 var(--face-ui);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-mute);
}
/* THE METER: what is loading and how far, at the foot of the screen. A hairline track with a
   brass fill whose head is a point of starlight; as wide as a reading line and no wider. */
.ldg__meter {
  position: absolute; left: 50%; bottom: max(7vh, 1.6em);
  translate: -50% 0;
  width: min(34em, 72vw);
  display: grid; gap: .5em;
}
.ldg__meter[hidden] { display: none; }
.ldg__meter-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1em;
  font: var(--face-weight) var(--fs-sm)/1.2 var(--face-ui);
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute);
}
.ldg__step { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ldg__pct {
  flex: none; font-variant-numeric: tabular-nums; letter-spacing: .06em;
  font-size: var(--fs-md); color: var(--brass-hi);
}
.ldg__track {
  position: relative; height: 2px;
  background: linear-gradient(90deg, transparent, rgba(var(--brass-rgb), .28) 8%, rgba(var(--brass-rgb), .28) 92%, transparent);
}
.ldg__fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--v, 0%);
  background: linear-gradient(90deg, rgba(var(--brass-rgb), 0), var(--brass) 18%, var(--brass-pale));
  box-shadow: 0 0 .5em rgba(var(--brass-hi-rgb), .55);
  transition: width .25s ease-out;
}
.ldg__fill::after {
  content: ""; position: absolute; right: -.35em; top: 50%;
  width: .7em; height: .7em; translate: 0 -50%; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--sky-rgb), 1), rgba(var(--sky-rgb), .4) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .ldg__orrery { animation: none; }
  .ldg__fill { transition: none; }
}

/* typeface: both faces are fixed on :root (Tamer Sans for the UI, Cinzel for display);
   the only control left is the preview toggle for the display face. */

/* ---------------------------------------------------------------- CURSORS
   The game's twelve cursors. The pictures are data/art/ui/cursor-*.png and
   data/art/ui/cursors.json holds the hotspot, the frame count and the native
   keyword for each; apps/game/client/cursors.ts reads that table and writes one
   --cur-<state> custom property per state onto <html> at boot.

   THE RULE: no sheet the game loads ever spells a bare cursor keyword. A component
   that presses writes `cursor: var(--cur-hand, pointer)`, one that rests writes
   `var(--cur-arrow, default)`, and so on - the drawn picture IS the meaning of the
   word, and the keyword after the comma is only what a page without the tokens
   (the editor, a sheet the script has not reached) falls back to. A bare keyword
   is the OS picture and nothing here can remap it, because `cursor` inherits but
   an element's own declaration always wins over what it inherits - which is why
   `cursor: default` is the worst offender: it reads as "back to normal" and shows
   the OS arrow. scripts/check-cursors.ts fails the build on any bare keyword.

   So this block is small: the page default, the elements that have no class and
   so no rule of their own, and the errand cursors that no component declares.
   Scoped to body.play so the editor keeps its native set. */
body.play { cursor: var(--cur-arrow, default); }
/* Bare elements a click presses: the UA sheet gives these `pointer` or nothing. */
body.play :is(a[href], button, summary, label, [role="button"]) { cursor: var(--cur-hand, pointer); }
/* A dropdown's open list wears the hand as one piece, padding between the rows included. */
body.play :is(.select__list) { cursor: var(--cur-hand, pointer); }
/* Typing, and the two gestures that move a thing rather than press it. */
body.play :is(textarea, [contenteditable="true"],
  input:not([type="checkbox"], [type="radio"], [type="range"], [type="submit"])) { cursor: var(--cur-text, text); }
body.play :is(.win__head, .gs-slot) { cursor: var(--cur-grab, grab); }
body.play :is(.win__head, .gs-slot, [data-drag]):active { cursor: var(--cur-grabbing, grabbing); }
/* The corner grip keeps the browser's own resize arrow: it is the one gesture the
   system already says well, and a drawn one would have to be drawn eight times. */
/* The states with the errand drawn into them: a link to an NPC walks you over and talks.
   (The gathering Start button and the fight banner drops keep theirs in ui/w-gather.css and
   ui/50-battle.css.) */
body.play .chat-link { cursor: var(--cur-talk, pointer); }
/* A name that walks you to someone presses like anything else - the hand - and the dimmed one
   on another map says nothing, because a click there does nothing. */
body.play .kv__npc { cursor: var(--cur-hand, pointer); }
body.play .kv__npc.is-dim { cursor: var(--cur-arrow, default); }
body.play .kv__npc .chat-link { cursor: inherit; }
/* The curtain: the wait is the game's, not a click that did nothing. */
body.play :is(.ldg, [aria-busy="true"]) { cursor: var(--cur-busy, progress); }
/* Nothing to press, nothing to promise. */
body.play :is(button, .btn, .ibtn, .tabs__tab):disabled,
body.play :is([aria-disabled="true"], .is-disabled, .tile--locked, .gs-slot--empty) { cursor: var(--cur-arrow, default); }

/* ------------------------------------------------------------------ PAPER SURFACES
   The windows are dark (the one tone since 2026-10-05; the light tone is gone). Notices and
   broadcasts stay PAPER (the loading screen went night 2026-10-09, the guild's pages 2026-10-10): painted over the world
   or framed in wood, not in a window (user, 2026-09-23 / 2026-10-05). Every token the dark base
   sets is put back to its paper value on these roots; being custom properties, the values flow on
   to everything inside.
   Zero specificity on purpose: a component that sets its own colour or token (.on-world paints
   --world-text) must still win; `color` re-reads the ink the body would have handed down. */
:where(.procl, .toast, .cast, .on-world) {
  color: var(--ink);
  --paper: #f9f7f4;
  --paper-2: #f1ede6;
  --ink: #4a2b22;
  --ink-strong: #3d2018;
  --ink-mute: #8a6a5e;
  --ink-faint: #a9998f;
  --white: #ffffff;
  --shadow-ink: 60, 25, 12;
  --ink-wash: 70, 30, 15;
  --error: #c0574a;
  --engrave-light: rgba(255, 248, 240, .62);
  --btn-face: #b98a6e;
  --btn-rim: #9c6f57;
  --btn-text: #fbe6d0;
  --btn-text-shadow: rgba(60, 25, 12, .30);
  --win-face: #f6f3ee;
  --win-rim: rgba(74, 43, 34, .22);
  --rim: #fbf8f2;
  --rim-in: #c2b7a1;
  --line: rgba(120, 92, 62, .30);
  --win-shadow: 0 .5em 1.4em rgba(60, 25, 12, .28);
  --parch: #e6ddcc;
  --parch-edge: #d6c9b3;
  --tan: #c99a76;
  --tan-rgb: 201, 154, 118;
  --tan-rim: #a97b5a;
  --link-ink: #8c5a33;
  --tan-text: #fbe6d0;
  --hairline: rgba(74, 43, 34, .14);
  --field-border: rgba(74, 43, 34, .28);
  --field-face: #ffffff;
  --grey-pill: #ebe8e3;
  --grey-pill-2: #e1ddd6;
  --scroll-thumb: #d6cdc0;
  --scroll-thumb-hover: #c6bbab;
  --cream: #ece4d4;
  --cream-rim: rgba(140, 105, 75, .18);
  --cream-ink: #b8a27f;
  --green: #5f9d48;
  --orange: #d68a4c;
  --blue: #3f6ea8;
  --bar-track: #1d1613;
  --tile-face: #e9e4dc;
  --rarity-purple: #9c7bb9;
  --rarity-blue: #6d8db2;
  --rarity-gold: #e0823a;
  --rarity-yellow: #d9b43a;
  --rarity-green: #7ba76b;
  --rarity-gold-text: #a8480c;
  --rarity-yellow-text: #8a5d0c;
  --rarity-blue-text: #1f5fae;
  --rarity-green-text: #2a6e2f;
  --rarity-purple-text: #6a3ba8;
  --tile-bg: radial-gradient(120% 90% at 25% 15%, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0) 60%), #e9e4dc;
  --tile-empty-bg: #ebe8e3;
  --tile-rim-base: rgba(255, 255, 255, .7);
  --tile-vignette: 0 0 0 transparent;
  --portrait-bg: radial-gradient(120% 90% at 30% 20%, rgba(255, 255, 255, .45), rgba(255, 255, 255, 0) 60%), #e9e4dc;
  --portrait-ring: #e6ddcc;
  --row-on: rgba(201, 154, 118, .16);
  --row-on-flat: rgba(201, 154, 118, .16);
  --row-hover: rgba(70, 30, 15, .04);
  --menu-hover: rgba(70, 30, 15, .06);
  --band-bg: #ebe8e3;
  --tab-on-bg: #c99a76;
  --tab-on-ink: #fbe6d0;
  --tab-on-ring: inset 0 0 0 1px #a97b5a;
  --ibtn-on-ring: rgba(255, 255, 255, .45);
  --dot-ring: rgba(255, 248, 240, .75);
  --drop-on-bg: rgba(201, 154, 118, .14);  /* a drop target on paper keeps the tan wash */
  --drop-on-rim: #c99a76;
}
/* A stack count keeps its pale plate inside a paper surface, whose ink stays dark. Zero
   specificity and after .tile__count, so it wins on source order. */
:where(.procl, .toast, .cast, .on-world) .tile__count { background: rgba(255, 255, 255, .82); }

/* ---- 20-icons.css */
/* PAINTED CHROME ICONS (gui/h.ts glyph/icon). A painted glyph is `img.ic.g` standing where the
   sprite's `svg.ic` stood, so 20-components.css's `.ic` size rules size it; these are only what a picture
   needs that a currentColor drawing did not. Turning is the `rotate` property, never `transform`,
   so a component's own transform still composes with it. */
img.ic { object-fit: contain; }
.ic--down { rotate: 90deg; }
.ic--left { rotate: 180deg; }
.ic--up { rotate: -90deg; }
/* The empty-locked tile faded its padlock through `color`; a picture fades by opacity. */
.tile--locked > img.ic { opacity: .3; }
/* The tile's tick (TileProps.ok) sits in the corner over the art. */
.tile > .tile__ok { position: absolute; z-index: 2; right: .12em; bottom: .12em; width: 1em; height: 1em; }

/* ---- 25-card.css */
/* CARDS OVER EVERYTHING: the tooltip host, the context menu and the toasts (gui/tip.ts, menu.ts,
   toast.ts). The look is 20-components.css's .card; this places them and gives them their layer. */

/* The host for a card that follows the pointer (a hover card, the market's Lumi help): above every
   window and every question (a prize in the gacha result is hovered over its scrim) at --z-tip
   (PLAN 2.3), and never a hit target of its own. */
.card--float { position: fixed; z-index: var(--z-tip); pointer-events: none; }
.card--float.tip--menu { z-index: calc(var(--z-menu) + 1); }
/* The pull (gacha-fx.ts) stands at --z-fx, over --z-tip: while it covers the page, a hovered prize's
   card goes over it, or it opens unseen behind the overlay (owner, 2026-10-10). */
html.gfx-cover .card--float { z-index: calc(var(--z-fx) + 1); }

/* The tooltip host paints nothing: it is placed by `translate` from the top-left corner. */
.tip { left: 0; top: 0; }

/* The context menu: a card of rows, placed by `translate`. */
.ctx {
  /* At the UI size like the windows and the hover card (Settings > Size S/M/L, and the phone's
     height factor): it had the page's own 14px, so on Small the menu and its card stood larger
     than the bag they opened from. */
  font-size: var(--win-size, 14px);
  position: fixed; left: 0; top: 0; z-index: var(--z-menu);
  width: max-content; min-width: 10em; max-width: min(22em, calc(var(--frame-w) - 8px));
  padding: .35em .4em;
}
.ctx .item-row { width: 100%; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: var(--cur-hand, pointer); }
.ctx .item-row:disabled { opacity: .45; cursor: var(--cur-arrow, default); }
.ctx .item-row:hover:not(:disabled) { background: var(--menu-hover); }
.ctx .item-row.is-on b { color: var(--link-ink); }
.ctx__ico { width: 1.5em; height: 1.5em; object-fit: contain; flex: none; }
.ctx__head { padding-bottom: .4em; margin-bottom: .3em; border-bottom: 1px solid var(--hairline); }
.ctx__sep { margin: .25em 0; border: 0; border-top: 1px solid var(--hairline); }
/* A row's "?" (its hover card says what the row does) and a toggle row's switch, at the right end. */
.ctx .item-row { display: flex; }
.ctx .item-row__name { flex: 1; }
.ctx__help { flex: none; display: grid; place-items: center; width: 1.6em; height: 1.6em; margin-left: .6em; border-radius: 50%; cursor: var(--cur-hand, pointer); opacity: .7; }
.ctx__help:hover { opacity: 1; background: rgba(var(--ink-wash), .08); }
.ctx__help > img.g { width: 1.15em; height: 1.15em; object-fit: contain; }
.ctx__switch { flex: none; position: relative; width: 2.2em; height: 1.2em; margin-left: .6em; border-radius: 1em;
  background: rgba(var(--shadow-ink), .55); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .35); transition: background .15s; }
.ctx__switch::after { content: ""; position: absolute; top: .15em; left: .15em; width: .9em; height: .9em; border-radius: 50%;
  background: var(--ink-mute); transition: translate .15s, background .15s; }
.ctx__switch.is-on { background: rgba(var(--brass-hi-rgb), .35); }
.ctx__switch.is-on::after { translate: 1em 0; background: var(--brass-pale); }

/* Toasts: one short line on a small card, a column under the top of the safe frame (they stand in
   the windows' layer, gui/win.ts layer()). */
.wins > .toasts {
  position: absolute; left: 50%; top: 4.5em; z-index: var(--z-toast);
  display: grid; justify-items: center; gap: .35em;
  transform: translateX(-50%); pointer-events: none;
  font-size: var(--win-size, 14px);
}
.toast-line { width: auto; max-width: min(28em, calc(var(--frame-w) - 32px)); text-align: center; animation: toast-life 2.7s ease-out forwards; }
.toast-line.is-err { color: var(--error); }
@keyframes toast-life {
  0% { opacity: 0; transform: translateY(-4px); }
  8%, 85% { opacity: 1; transform: none; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .toast-line { animation: none; }
}

/* ------------------------------------------------------------------ THE TOOLTIP HOSTS (gui/tip.ts)
   Two hosts, the card and the one it is compared against, both painting nothing: a card reads at
   the window size, so a tooltip and the window it came out of are one size on every scale. */
.tip { font-size: var(--win-size, 14px); }
.tip--cmp > .card { opacity: .96; } /* the worn piece is the reference, the hovered one the subject */

/* A HUD mark's card (gui/c/status.ts markCard): name, what it does, time left. Five lines at most
   in a narrow column: the name one line, the text three, the time one, so a long description
   never runs across a phone's short screen. */
.card--mark { width: max-content; max-width: 15em; display: grid; gap: .2em; }
.card--mark > .card__title { font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card--mark__text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
.card--mark__left { color: var(--card-number); font-weight: 700; }

/* ------------------------------------------------------------------ THE STAR RATING (items.ts starRow)
   One drawn star per rung, three tiers: plain under seven, luxury from seven (embossed, thicker
   flanking rules), radiant from ten (a breathing aura, a slow turn, nine sparks). The tiers differ
   by SIZE as well as colour, which is what a screenshot and a colour-blind reader keep. */
.card__stars {
  position: relative;
  min-height: 1.85em;
  display: grid;
  grid-template-columns: minmax(.85em, 1fr) auto minmax(.85em, 1fr);
  align-items: center;
  gap: .55em;
  color: var(--star-hi);
}
.card__stars::before, .card__stars::after { content: ""; height: 1px; background: var(--star-lo); }
.card__stars i { font-style: normal; font-size: .93em; line-height: 1; letter-spacing: 2px; text-shadow: 1px 1px 0 var(--stamp-outline); }
.card__star { width: 1em; height: 1em; vertical-align: -0.12em; margin-right: 2px; }
.card__stars.is-luxury { min-height: 2.15em; }
.card__stars.is-luxury::before, .card__stars.is-luxury::after { height: 2px; background: var(--star-mid); }
.card__stars.is-luxury i { font-size: 1.14em; letter-spacing: 3px; color: var(--star-hi); }
.card__stars.is-luxury .card__star {
  margin-right: 0;
  transform: translateY(-1px);
  filter:
    drop-shadow(0 -1px 0 color-mix(in srgb, var(--star-glint) 95%, transparent))
    drop-shadow(0 1px 0 var(--star-lo))
    drop-shadow(0 2px 1px color-mix(in srgb, var(--star-drop) 90%, transparent))
    drop-shadow(0 0 4px color-mix(in srgb, var(--star-glow) 90%, transparent));
}
.card__stars.is-radiant { min-height: 2.4em; grid-template-columns: minmax(0, 1fr); gap: 0; }
.card__stars.is-radiant::before, .card__stars.is-radiant::after { display: none; }
.card__stars.is-radiant i {
  position: relative; z-index: 1; isolation: isolate;
  display: flex; align-items: center; justify-content: center;
  padding: 4px 5px 6px;
  font-size: 1.29em; letter-spacing: 0;
  color: var(--star-hi);
}
/* The aura is its own layer, so neither the stars nor their box move while the light breathes. */
.card__stars.is-radiant i::before {
  content: ""; position: absolute; z-index: -1; inset: 5px 0 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--star-glow) 62%, transparent);
  filter: blur(5px); opacity: .34; pointer-events: none;
  animation: card-star-aura 1.35s ease-in-out infinite;
}
.card__stars.is-radiant .card__star { animation: card-star-turn 30s linear infinite; }
/* Thirteen to fifteen icons need the full row: tighten them rather than clip the achievement. */
.card__stars.is-radiant:is([data-stars="13"], [data-stars="14"], [data-stars="15"]) i { padding-inline: 2px; font-size: 1.14em; }
/* More than ten stars are two rows, ten across, from the left edge (the card is 17em wide on every
   screen). Shrink-wrapped, so the sparks hug the stars; the light is cast from the icons
   themselves, so it follows two rows instead of pooling in the gap between them. */
.card__stars[data-star-rows] { display: block; width: max-content; max-width: 100%; }
.card__stars[data-star-rows]::before, .card__stars[data-star-rows]::after { display: none; }
.card__stars[data-stars][data-star-rows] i { display: grid; grid-template-columns: repeat(10, auto); justify-content: start; row-gap: 2px; }
.card__stars[data-star-rows].is-radiant i::before { display: none; }
.card__stars[data-star-rows].is-radiant i { animation: card-star-aura-cast 1.35s ease-in-out infinite; }
.card__stars[data-star-rows] .card__star-particles { bottom: auto; height: 52%; }
.card__star-particles { position: absolute; inset: 2px 12%; display: block; pointer-events: none; }
.card__star-particles b {
  position: absolute; width: 2px; height: 2px;
  background: var(--star-hi); box-shadow: 0 0 4px var(--star-mid); opacity: .72;
}
.card__star-particles b:nth-child(1) { left: 4%; top: 18%; }
.card__star-particles b:nth-child(2) { left: 16%; bottom: 6%; }
.card__star-particles b:nth-child(3) { left: 29%; top: 2%; }
.card__star-particles b:nth-child(4) { left: 41%; bottom: 0; }
.card__star-particles b:nth-child(5) { left: 50%; top: 0; }
.card__star-particles b:nth-child(6) { right: 39%; bottom: 4%; }
.card__star-particles b:nth-child(7) { right: 27%; top: 3%; }
.card__star-particles b:nth-child(8) { right: 14%; bottom: 8%; }
.card__star-particles b:nth-child(9) { right: 3%; top: 20%; }
@keyframes card-star-aura {
  0%, 100% { filter: blur(5px); opacity: .3; }
  50% { filter: blur(8px); opacity: .9; }
}
@keyframes card-star-aura-cast {
  0%, 100% { filter: drop-shadow(0 0 2px color-mix(in srgb, var(--star-glow) 55%, transparent)) drop-shadow(0 0 6px color-mix(in srgb, var(--star-glow) 30%, transparent)); }
  50% { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--star-glow) 85%, transparent)) drop-shadow(0 0 10px color-mix(in srgb, var(--star-glow) 70%, transparent)); }
}
@keyframes card-star-turn {
  from { transform: translateY(-1px) rotate(0deg); }
  to { transform: translateY(-1px) rotate(360deg); }
}
/* A phone's card is short: the rating comes down to the card's own type. */
:where(html.m-mobile) .card__stars[data-stars] { min-height: 0; }
:where(html.m-mobile) .card__stars[data-stars] i { font-size: .75em; letter-spacing: 1px; padding-block: 1px; }
:where(html.m-mobile) .card__stars[data-stars] .card__star { margin-right: 1px; }
@media (prefers-reduced-motion: reduce) {
  .card__stars.is-radiant i::before, .card__stars.is-radiant .card__star, .card__stars[data-star-rows].is-radiant i { animation: none; }
}

/* ------------------------------------------------------------------ THE ITEM CARD'S SECTIONS (items.ts itemCard)
   Fixed order, one hairline between two sections and none inside one: the figures, the sockets, the
   meter, the item's own words, the foot. On the gear card the head is a band of its own, so the
   first section needs no line under it. */
.card__sec { margin-top: .5em; padding-top: .5em; border-top: 1px solid var(--hairline); }
.card--gear .card__head + .card__sec { margin-top: 0; padding-top: .15em; border-top: 0; }
/* Card sections set in a panel under its own titled hairline (the Exchange's gear well): the first
   needs no rule of its own, or the title reads double-ruled (owner, 2026-10-10). */
.well .card__sec:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.card__sec:not(.card__foot) > * + * { margin-top: .3em; }
.card__sec > .card__stars + .kv { margin-top: .15em; }
.card__effects .kv__val { color: var(--card-number); }

/* A meter (durability, charges, a vessel's capacity): the label, "left/max" at the right, a thin
   bar under both, filled to --share. Red from a fifth down. */
.card__meter { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: .8em; row-gap: .25em; align-items: baseline; }
.card__meter > b { font-weight: inherit; color: var(--ink); font-variant-numeric: tabular-nums; }
.card__meter > i { grid-column: 1 / -1; height: 3px; background: var(--navy-0); box-shadow: inset 0 0 0 1px var(--hairline); }
.card__meter > i::after { content: ""; display: block; width: var(--share, 0%); height: 100%; background: var(--brass); }
.card__meter.is-low > b { color: var(--error); }
.card__meter.is-low > i::after { background: var(--error); }

/* SOCKETS (items.ts socketRows): six holes on one line - a stone (filled), an open hole (a ring), a
   hole not yet punched (a faint dashed ring) - then a green row per stone, its picture first. */
.card__holes { display: flex; gap: .3em; }
.card__hole { width: .8em; height: .8em; border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--brass); }
.card__hole.is-full { background: var(--socket-full); box-shadow: inset 0 0 0 1.5px var(--brass-hi); }
.card__hole.is-shut { box-shadow: none; border: 1px dashed var(--ink-faint); }
.card__gem { display: flex; align-items: center; gap: .45em; color: var(--green); }
.card__gem > .ico { --ico-box: 1.3em; }
.card__gem > span { flex: 1; min-width: 0; }
.card__gem > b { font-weight: inherit; font-variant-numeric: tabular-nums; }
.card__gem.is-idle { color: var(--ink-mute); }
.card__sec > .card__gem + .card__gem { margin-top: .1em; }

/* THE ITEM'S OWN WORDS: muted prose, its "- " lines drawn as a list on a small brass diamond, the
   label of a "Label: value" line in the plain ink, a [bracketed] usage hint small and faint. */
.card__desc p { margin: 0; }
.card__desc ul { margin: 0; padding: 0; list-style: none; display: grid; gap: .1em; }
.card__desc li { position: relative; padding-left: .9em; }
.card__desc li::before { content: ""; position: absolute; left: .1em; top: .6em; width: .3em; height: .3em; background: var(--brass-dim); transform: rotate(45deg); }
.card__k { color: var(--ink); }
.card__hint { font-size: var(--fs-small); color: var(--ink-mute); }

/* WHAT A BOX HOLDS (items.ts boxBody, containers.json `peek`): a small tile, the name in its band's
   ink, the chance at the right. */
.card__box-head { color: var(--ink-mute); font-size: var(--fs-small); margin-bottom: .3em; }
.card__box-list { margin: 0; padding: 0; list-style: none; display: grid; gap: .25em; }
.card__box-list > li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .5em; }
.card__box-list .tile { font-size: .7em; }
.card__box-name { line-height: 1.2; }
.card__box-pct { color: var(--ink-mute); font-size: var(--fs-small); font-variant-numeric: tabular-nums; }

/* THE FOOT: small and quiet - what it takes to use (red when this character falls short), how it
   may change hands, and the stack at the right. */
.card__foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: .1em .5em; font-size: var(--fs-small); color: var(--ink-mute); }
.card__foot > span:not(.card__qty) + span:not(.card__qty)::before { content: "·"; margin-right: .5em; color: var(--ink-faint); }
.card__req { color: var(--ink); }
.card__req.is-unmet { color: var(--error); }
.card__qty { margin-left: auto; color: var(--ink); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ A PET'S CARD (chatlink.ts)
   The growth caption over its two columns, and the score in a fixed cell so numbers line up. */
.card__growth { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .6em; align-items: baseline; }
.card__score { display: inline-block; min-width: 2.6em; text-align: right; }

/* ------------------------------------------------------------------ WHERE THE NOTICES STAND (gui/notice.ts)
   The announcement ladder: centred under the top edge, lines stacking downward, never a click
   target (it prints over the world while the player walks through it); above the windows, under
   the scrim. */
.wins > .tl-cast {
  position: absolute;
  left: 50%;
  /* A little lower and a little larger than the page's 15px (owner, 2026-10-09). */
  top: 6.2em;
  font-size: 17px;
  transform: translateX(-50%);
  z-index: var(--z-cast);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .333em;
  width: max-content;
  max-width: min(41.333em, calc(var(--frame-w) - 2.133em));
  pointer-events: none;
}
/* The proclamation, edge to edge across the upper third (the wash has no edge of its own), one
   layer above the ladder (your own arrival outranks somebody else's luck), under the scrim. */
.tl-procl {
  position: fixed;
  left: 0;
  right: 0;
  top: 22%;
  z-index: var(--z-procl);
  pointer-events: none;
}
/* Mobile mode shrinks both by the height factor every window takes (--win-k), floored at 11px;
   your own notice stands below a four-line ladder and sets tighter. */
:where(html.m-mobile) .tl-procl { font-size: max(11px, calc(15px * var(--win-k, 1))); }
/* The ladder from its own 17px, floored at 12px, and under Chat Peek's four lines (40-hud.css). */
:where(html.m-mobile) .wins > .tl-cast { font-size: max(12px, calc(17px * var(--win-k, 1))); top: 7em; }
:where(html.m-mobile) .tl-procl { top: 34%; }
:where(html.m-mobile) .tl-procl .procl__text { font-size: 1.6em; letter-spacing: .08em; }

/* ------------------------------------------------------------------ A CARD IN MOBILE MODE
   A floating card (a tooltip, a menu) reads at the window size, never taller than the screen; a
   worn piece's stat lines lose most of their air. */
:where(html.m-mobile) .card { font-size: var(--win-size); max-height: calc(var(--frame-h) - 16px); overflow: hidden; }
:where(html.m-mobile) .card .kv > div { padding-block: .15em; }
:where(html.m-mobile) .card .item-row { padding-block: .15em; }
:where(html.m-mobile) .card .item-row--empty ~ .item-row--empty { display: none; }

/* The number pad (gui/c/input.ts, mobile mode): a card of keys in a bottom corner of the frame, away
   from the number field it types into, over every window and question. Placed by `translate`. Four
   rows of 2.5em: about a third of a landscape phone's height. Taps are square plates, a key that
   repeats when held is the stepper's round button, OK the full row. */
.numpad {
  font-size: var(--win-size, 14px);
  position: fixed; left: 0; top: 0; z-index: var(--z-menu);
  display: grid; grid-template-columns: repeat(5, 3.2em); grid-auto-rows: 2.5em; gap: .3em;
  width: max-content; padding: .4em;
  touch-action: none; user-select: none;
}
.numpad[hidden] { display: none; }
.numpad > .btn { padding: 0; font-size: 1.1em; font-variant-numeric: tabular-nums; }
.numpad > .ibtn { place-self: center; }
.numpad > .numpad__ok { grid-column: 1 / -1; font-size: 1em; }
/* The field the pad has just opened on: its first digit replaces this text. */
.field input.is-fresh { background: rgba(var(--brass-rgb), .16); }

/* ---- 30-win.css */
/* THE NEW WINDOWS (gui/win.ts) AND THE ONE LAYER TABLE (PLAN 2.3).

   A new window is 20-components.css's .win, standing in the `.wins` layer: a SAFE FRAME (ui/10-base.css
   .frame, the glass less the notch and the home bar, clipped; absolute with z auto, so the windows'
   z still interleaves with the body's menus, tips and curtains), each window placed by `translate` from the
   frame's top-left corner, z written by the shared stack (gui/z.ts). Its width is --w, in em of
   --win-size like every old window. A modal's veil is 20-components.css's .scrim, one layer under it,
   and with the page dock it is a .bleed: the whole glass. */
:where(.wins) > * { pointer-events: auto; }
.wins > .win {
  position: absolute; left: 0; top: 0;
  width: var(--w); max-width: calc(var(--frame-w) - 16px);
  max-height: calc(var(--frame-h) - 32px);
}
.wins > .bleed { position: fixed; }
.wins > .win > .win__body { min-height: 0; overflow: auto; overscroll-behavior: contain; }
/* The body's scrollbar in the window's brass (colour only: its width is the platform's). */
.wins > .win > .win__body { scrollbar-color: var(--scroll-thumb) transparent; }
/* Mobile mode: no window (the lobby's included) is taller than the screen less a gutter each end;
   its body is the scroller, so the head stays in reach to drag it by. */
:where(html.m-mobile) .win { max-height: calc(var(--frame-h) - 32px); }
:where(html.m-mobile) .win > .win__body { min-height: 0; overflow-y: auto; }
/* A finger on the head drags the window (tablet, mobile mode): the browser must not claim the touch
   as a pan, or it cancels the pointer a few px in (pointercancel) and the window stops. */
.wins > .win:not(.win--modal):not(.win--popup):not(.win--page) > .win__head { cursor: var(--cur-grab, grab); touch-action: none; }
html.is-dragging, html.is-dragging * { cursor: var(--cur-grabbing, grabbing) !important; }
.wins > .win:not([hidden]) { animation: win-in .12s ease-out; }
@keyframes win-in { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .wins > .win:not([hidden]) { animation: none; }
}

/* The question frame (gui/ask.ts, the system alert): the old dialog's own layout. Every dialog's
   action row (ask.ts questions, the popups: a party invitation, a fusion result) stands at the
   RIGHT, its last button flush with the body's right edge (the same 1.2em), the main button last -
   ask.ts and every popup append Cancel / Refuse first. A little more room underneath keeps the
   last button clear of the bottom-right crest, which the window-wide 2.9em inset used to dodge. */
.wins > .win:is(.win--ask, .win--popup) > .win__foot { justify-content: flex-end; padding-right: 1.2em; padding-bottom: 1.7em; }
.ask { display: grid; grid-template-columns: minmax(0, 1fr); gap: .7em; } /* never wider than the window */
.ask__q { display: grid; gap: .25em; }
.ask__count { display: flex; align-items: center; gap: .5em; }
/* The stepper takes what is left after the All button, which keeps its own width. */
.ask__count > .numf { flex: 1 1 auto; width: auto; min-width: 0; }
.ask__count > .numf > .field { min-width: 0; } /* .field's own 12em floor pushed All out of the window */
.ask__count > .btn { flex: none; }

/* Delete character (shell/home.ts remove): a size up from the alert, what is LOST leading. The
   name and level head it, the lost list is large and hot, the kept line is one quiet line, and
   Delete stands alone at the right of the foot like every dialog's main button (the close star is
   the way out). */
.win--delchar .ask { gap: 1em; font-size: var(--fs-heading); }
.win--delchar .note-line { font-size: var(--win-size); }
.delchar__head { display: grid; justify-items: center; gap: .2em; text-align: center; padding-bottom: .9em; border-bottom: 1px solid var(--hairline); }
.delchar__name { font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: calc(var(--fs-title) * 1.35); color: var(--ink-strong); line-height: 1.1; overflow-wrap: anywhere; }
.delchar__lv { color: var(--brass-hi); font-variant-numeric: tabular-nums; }
.delchar__cap { color: var(--error); font-size: var(--win-size); letter-spacing: .08em; text-transform: uppercase; margin-bottom: .2em; }
.delchar__lost .kv > div { padding: .55em .25em; }
.delchar__lost .kv dt { color: var(--ink); }
.delchar__lost .kv__val { font-size: var(--fs-title); white-space: nowrap; }
.delchar__confirm { display: grid; gap: .45em; }
.win--delchar [data-ok] { min-width: 12em; font-size: var(--fs-heading); }
.is-busy { cursor: var(--cur-busy, wait); }

/* THE LAYER TABLE, restated over the old sheets' numbers (gui/z.ts Z). Windows moved from 20+ to
   the shared stack's 100+; what stood between them and the old dialog layer moves with them, so the
   order on screen is the one it always was. */
.scrim { z-index: var(--z-modal); }                         /* a modal's veil */
.gfx { z-index: var(--z-fx); }                              /* the gacha pull (gacha-fx.ts) */
/* The curtains (loading, turn your device, offline) take their layers in ui/60-curtain.css. */

/* ---- 35-drag.css */
/* THE DRAG (gui/gesture.ts, PLAN 1.4): one engine for mouse, pen and finger.

   A drag source wears `data-drag`. A finger held on it must not pan the page: `touch-action: none`.
   In a list that scrolls (a bag grid on a phone sheet, a window's rows) it wears `data-drag="pan"`:
   a quick vertical swipe still scrolls, a held press (180 ms) carries, and the engine stops the pan
   once the press is armed (gesture.ts dragRoot). The grab hand is 20-components.css's [data-drag] rule.

   The ghost is a copy of what was pressed, riding the pointer by `transform` alone, over every
   window, card and menu (--z-ghost) and never in the way of the hit test under it. Under a mouse it
   IS the pointer: whole, not faint, centred on the hot spot, and the cursor is hidden while it
   rides (owner, 2026-10-09). The pressed thing itself stays put and goes faint while it is carried. */
html.is-carrying, html.is-carrying * { cursor: var(--cur-none, none) !important; }
[data-drag] { touch-action: none; }
[data-drag="pan"] { touch-action: pan-y; }

.ghost {
  position: fixed; left: 0; top: 0; z-index: var(--z-ghost);
  pointer-events: none; transform-origin: 0 0; will-change: transform;
}
.ghost * { pointer-events: none !important; }
/* The stand-ins carry their ancestors' classes for the rules only. A `display: contents` box still
   draws its ::before and ::after, which put a window's corner knot and head shadow on the copy. */
.ghost .ghost__stand::before, .ghost .ghost__stand::after { content: none !important; }
[data-drag].is-carried { opacity: .4; }
.is-carried [data-drag].is-carried { opacity: 1; } /* a row that fades as a whole (Pet Slot) */

/* ---- 40-hud.css */
/* THE ALWAYS-ON HUD, the blocks gui/hud/ builds (PLAN P09): the left rail (status line, your block,
   the Attack pet, the account fan), the boards' row and the run's reward, the minimap column (map,
   tile, tracker, presence), the menu tiles, and the bars (P10), the party rail (P12), the root, the stick and the put-away
   conversations: everything that was hud.css, with the same look.

   The language: no panels. No box, border or well on this layer: text and
   gauges straight on the world, a row on its own soft navy wash where it needs one, white type with
   a soft shadow, painted icons with a drop shadow. The Midnight Astrolabe's brass (owner, 2026-10-07)
   goes only where a box would not: a ring round a face and the minimap, a thin plate under a level
   and name, hairlines, the slot frames (tokens: 00-tokens.css "hud" block). Positions are keyed on the mode gui/mode.ts writes
   on <html> (html.m-mobile), never on a width. Every block is a direct child of .hud and wears
   its zoom (--hud-z), except the map column and the boards' row, which are drawn at screen pixels
   (the minimap grows by its backing store). */

/* --------------------------------------------------------------- THE ROOT */
/* The HUD's tokens (--hud-*) are ui/00-tokens.css's, on :root, so a detached chat window and the
   world plates read the same values as the rail. The root is a SAFE FRAME (ui/10-base.css .frame:
   the glass less the notch and the home bar, clipped): every block is placed from its edges, and
   only a .bleed child (the world plates, the drawer, the dial's bands) reaches the screen's edge. */
.hud {
  position: fixed;
  pointer-events: none;
  user-select: none;
  z-index: 10;
  font-family: var(--hud-face);
  font-weight: 400;
  font-size: 14px;
  line-height: 1.35;
  letter-spacing: .01em;
  color: var(--hud-text);
  text-shadow: var(--hud-shadow);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  --hud-menu-row: 50px;
}
/* The HUD's pictures are sampled, never smoothed (the look gs.css's `.gs img` gave it). */
.hud img { image-rendering: pixelated; }
/* Except the painted diamond icons: 256 px art drawn at 44 px aliases when sampled, so they smooth. */
.hud .hud-ibtn img, .hud .hud-tgl img { image-rendering: auto; }
.hud > * { position: absolute; pointer-events: auto; zoom: var(--hud-z); z-index: 1; }
/* A .bleed block escapes the frame's clip (ui/10-base.css): fixed, its box the whole glass. */
.hud > .bleed { position: fixed; }
/* ONE COMPOSITOR LAYER PER BLOCK, never one for the HUD. Left alone, the blocks over the canvas
   fall into one shared layer as big as their union, the whole glass (2462 x 1081 device px, 10 MB,
   on a 2.75x phone, 2026-10-11): each block is its own small layer instead, the size of what it
   paints. The bottom strip is the screen's width, so its halves and its plate are layers of their
   own. The strip and the plate layer are layers too though they paint nothing (their plates are
   each a layer already, 45-plates.css): left out, the HUD root stayed a full-screen layer to keep
   them in order over the canvas (phone, 2026-10-11). An empty layer costs no tiles. Nothing in a
   block is position: fixed, which this would pin to the block. */
.hud > *, .hud-strip__half, .hud-strip__plate { will-change: transform; }
.hud button { font: inherit; color: inherit; letter-spacing: inherit; text-shadow: inherit; }
/* Two thumbs on the glass - the stick and a slot - are play, not a pinch (a no-op for a mouse). */
.hud button { touch-action: none; }
/* Offline (main.ts, the freeze): the whole HUD is inert while the socket is down. Every block
   re-enables its own pointer events, so the kill has to reach them all. */
.hud.is-offline, .hud.is-offline * { pointer-events: none !important; }
/* No focus ring on the HUD or in a window: nothing here is reached with the keyboard (Tab walks the
   account's characters), and a ring left by a click reads as "still armed". The composer keeps
   its caret. */
.hud :focus, .hud :focus-visible, .win :focus, .win :focus-visible { outline: none; }

/* THE ROW: the one building block, a wash the height of its own content and nothing else. */
.hud-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 8px;
  background: var(--hud-wash);
}
.hud-row--rev { background: var(--hud-wash-rev); }

/* THE STICK (joystick.ts), mobile mode: a faint ring bottom left and a knob that follows the thumb;
   see-through at rest, a step brighter under a thumb. It stays while the chat is out: the chat
   opens at the bottom centre since 2026-10-11, clear of it. The whole ring is the hit box
   (joystick.ts measures it). */
:where(html.m-mobile) .hud .hud-joy {
  /* 120 px (owner, 2026-10-11: 165 was too big and sat over the party list), tucked into the
     corner the same 8 px off the frame's left and bottom edge, the mirror of the dial
     (owner, 2026-10-11). The edge is screen px here, so it is divided out of the zoom. */
  left: calc(var(--hud-edge) / var(--hud-z) + 8px); right: auto; top: auto; bottom: calc(var(--hud-edge) / var(--hud-z) + 8px);
  width: 120px; height: 120px; border-radius: 50%; z-index: 2; touch-action: none;
  background: radial-gradient(circle, rgba(var(--fx-white-rgb), .05) 0 55%, rgba(var(--fx-white-rgb), .12) 70%, rgba(var(--fx-white-rgb), .02) 71%);
  box-shadow: inset 0 0 0 2px rgba(var(--fx-white-rgb), .22), 0 0 0 1px rgba(var(--shadow-ink), .25);
  opacity: .7; transition: opacity .15s linear;
}
:where(html.m-mobile) .hud .hud-joy.is-on { opacity: 1; }
:where(html.m-mobile) .hud-joy__knob {
  --jx: 0%; --jy: 0%;
  position: absolute; left: 50%; top: 50%; width: 40%; height: 40%; border-radius: 50%; pointer-events: none;
  transform: translate(calc(-50% + var(--jx) * 2.5), calc(-50% + var(--jy) * 2.5));
  background: radial-gradient(circle at 40% 35%, rgba(var(--fx-white-rgb), .55), rgba(var(--fx-white-rgb), .22) 70%);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .35), 0 2px 6px rgba(var(--shadow-ink), .35);
}
:where(html.m-mobile) .hud-joy.is-on .hud-joy__knob { background: radial-gradient(circle at 40% 35%, rgba(var(--fx-white-rgb), .75), rgba(var(--fx-white-rgb), .35) 70%); }

/* The root's mobile numbers (every block's mobile layout is under MOBILE MODE below): the mode is
   the player's (gui/mode.ts), never a width; the safe area is the frame's (THE ROOT above). */
:where(html.m-mobile) .hud {
  --hud-edge: 6px;
  --hud-slot: 38px;
}

/* ------------------------------------------------------------- THE LEFT RAIL */
/* You and your team in one column, your deployed pet beside you, top-aligned. The pet column is
   fixed so your own gauges keep one width whether a pet is out or not. */
.hud-left {
  left: var(--hud-edge);
  top: var(--hud-edge);
  width: 540px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
/* Only the drawn blocks take a press. The row and the rail run as tall as the party list, so the
   open ground under the pet and under the party picture stays the world's. */
.hud-self, .hud-pet, .hud-party__frames, .hud-party__gear { pointer-events: auto; }
.hud-top { position: relative; display: flex; flex-direction: row; align-items: flex-start; gap: 6px; }
/* Sized as if the pet's 230px column and the gap were always there (owner, 2026-09-13). */
.hud-selfcol { flex: 0 0 calc(100% - 236px); min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hud-self { display: flex; flex-direction: column; gap: 2px; width: 100%; }

/* ------------------------------------------------------------- THE PARTY RAIL */
/* Your team under your own block (panels/party.ts): each teammate is your status block one size
   down, with no experience track and no class ring; the party picture stands right of the first.
   A press aims the next cast, right-click or a long press is the teammate's verbs. */
.hud-party { display: flex; flex-direction: column; gap: 2px; }
.hud-party__rail { display: flex; align-items: flex-start; gap: 4px; }
.hud-party__rail[hidden] { display: none; }
.hud-party__frames { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hud-party .hud-party__gear { flex: none; width: 40px; height: 40px; margin-top: 4px; }
.hud-party .hud-party__gear:disabled { opacity: .55; cursor: var(--cur-busy, progress); }
.hud-party__row { position: relative; cursor: var(--cur-hand, pointer); user-select: none; -webkit-touch-callout: none; }
/* The teammate the next cast is aimed at (in a fight): a light round the portrait. */
.hud-party__row.is-on .hud-portrait { box-shadow: 0 0 0 2px var(--hud-gold); }
/* The leader's star, in front of the level. */
.hud-party__crown { display: flex; margin-right: 3px; color: var(--hud-gold); }
.hud-party__crown[hidden] { display: none; }
.hud-party__crown .ic { width: 12px; height: 12px; fill: currentColor; }

/* The status block: a round portrait in its brass ring, and beside it one frame holding a slim
   header (level and name) over the two gauges, the figures inside the bar. The pet's is the same
   block a size down. A grid: the face and the frame centre on each other in the first row, the
   marks take a second row under the frame, so a buff never lifts the gauges (owner, 2026-10-10). */
.hud-self__block {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  row-gap: 2px;
  --hud-pic: 60px;
  --hud-pad-l: 17px;
  /* The gauges start on the rail's one column however big the face beside them is. */
  column-gap: calc(var(--hud-gauge-x) - var(--hud-pad-l) - var(--hud-pic));
  /* The left and top padding are the ring's: it overhangs the portrait and the screen edge is
     right there. */
  padding: 12px 8px 8px var(--hud-pad-l);
  background: none;
}
.hud-self__rows { display: contents; }
/* The level and the name always share a line: a long name stays on the right of the level and ends
   in an ellipsis, never dropping under it (owner, 2026-10-10). */
.hud-self__id { position: relative; z-index: 0; display: flex; align-items: baseline; gap: 0 7px; min-height: 20px; margin-bottom: 4px; }
/* The level and the name in the UI sans, like every name (owner, 2026-10-08). */
.hud-self__lvl { flex: none; font-family: var(--hud-face); font-weight: var(--face-weight); color: var(--hud-gold); font-size: var(--fs-lg); letter-spacing: .02em; }
.hud-self__name { flex: 1 1 0; min-width: 0; font-family: var(--face-ui); font-weight: var(--face-weight); font-size: var(--fs-lg); letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The Tamer Ticket emblem, a coin on the portrait's bottom-left edge, over the ring. */
.hud-self__ticket {
  --ico-box: 26px;
  position: absolute;
  z-index: 4;
  left: -9px;
  bottom: -9px;
  filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .85));
  cursor: var(--cur-hand, pointer);
}
.hud-self__ticket[hidden] { display: none; }

/* A gauge: one bar shape in the game, a rounded strip; the fill its own colour under the sheen and
   grain (--bar-sheen, on the line's ::after, above the fill); its plain word (HP, MP, owner
   2026-10-09) inside at the left and the figures centred over it. The fill is a transform (--v,
   0..1), never a width. */
.hud-vital { display: flex; align-items: center; }
.hud-vital__bar { position: relative; flex: 1; min-width: 0; height: 13px; }
.hud-vital__k {
  position: absolute;
  left: 7px;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  font-family: var(--hud-face);
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1;
  letter-spacing: .06em;
  color: rgba(var(--fx-white-rgb), .82);
  text-shadow: 0 1px 1px rgba(var(--shadow-ink), .9), 0 0 3px rgba(var(--shadow-ink), .7);
}
.hud-vital__line {
  position: absolute;
  inset: 0;
  background: rgba(var(--hud-well-rgb), .85);
  border: 1px solid rgba(var(--shadow-ink), .9);
  box-shadow: var(--hud-gauge-rim);
  border-radius: 99em;
  overflow: hidden;
  isolation: isolate;
}
.hud-vital__line::after { content: ""; position: absolute; inset: 0; background: var(--bar-sheen); mix-blend-mode: overlay; pointer-events: none; }
.hud-vital__fill { display: block; height: 100%; width: 100%; background: var(--bar-dim), var(--hud-hp); }
.hud-vital--mp .hud-vital__fill { background: var(--bar-dim), var(--hud-mp); }
.hud-vital__fill--x { width: 100%; transform: scaleX(var(--v, 0)); transform-origin: left center; transition: transform 200ms linear; }
@media (prefers-reduced-motion: reduce) { .hud-vital__fill--x { transition: none; } }
.hud-vital__v {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
  font-family: var(--hud-face);
  font-size: var(--fs-xs);
  line-height: 1;
  font-weight: 600;
  letter-spacing: .03em;
  color: var(--hud-text);
  text-shadow: 0 1px 1px rgba(var(--shadow-ink), .9);
}

/* The portrait: a round navy face in the brass astro ring alone (no class sigil: owner,
   2026-10-09), every one the same: black out to the ring, no drop shadow, no hairline inside it
   (owner, 2026-10-10). z 1: the face and its ring stand over the gauges' frame, which starts
   behind it. */
.hud-portrait {
  position: relative;
  z-index: 1;
  display: block;
  flex: none;
  width: 60px;
  height: 60px;
  padding: 4px;
  border-radius: 50%;
  cursor: var(--cur-arrow, default);
}
/* The black disc out to the ring's inner edge: the ring hangs past the well, and nothing of the
   world shows between them. */
.hud-portrait::before {
  content: "";
  position: absolute;
  inset: calc(2px - var(--hud-ring-out));
  border-radius: 50%;
  background: var(--void);
}
.hud-portrait::after {
  content: "";
  position: absolute;
  z-index: 3;
  inset: calc(-1 * var(--hud-ring-out));
  background: var(--art-ring) center / 100% 100% no-repeat;
  pointer-events: none;
}
.hud-portrait__face { position: relative; display: block; width: 100%; height: 100%; border-radius: 50%; overflow: hidden; background: rgba(var(--hud-ink-rgb), .8); }
/* A player's head is drawn on black, not on the well colour. */
.hud-portrait__face.is-dark { background: var(--void); }
.hud-portrait__face img { width: 100%; height: 100%; object-fit: cover; object-position: center 22%; }
/* A pet with no bust: its head icon, filling the well. */
.hud-portrait__face > .ico { width: 100%; height: 100%; }
/* The head crop is a square of head, neck and shoulders: blown up about the face for the big well. */
.hud-portrait__crop { position: absolute; inset: 0; transform: translateY(-11%) scale(1.38); transform-origin: 50% 36%; }
.hud-portrait--sm img { transform: scale(1.15); transform-origin: 50% 40%; }
.hud-portrait--sm { width: 44px; height: 44px; padding: 2px; }

/* THE GAUGES' FRAME (owner, 2026-10-09, after the old client's status plate): your health and mana,
   and your Attack pet's, stacked in one slim navy frame with a brass hairline, both solid (owner,
   2026-10-10), its left end tucked behind the portrait's ring, its right end a shallow point. The hairline is
   the frame's own ground showing 1px round the navy (::before), both cut to the same point. The
   bars inside lose their own rim: the frame is it. The party's rows keep the bare gauges. */
:is(.hud-self, .hud-pet) .hud-self__gauges {
  --tuck: 24px;
  --tip: 9px;
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-left: calc(-1 * var(--tuck));
  padding: 5px calc(var(--tip) + 5px) 5px calc(var(--tuck) + 4px);
  background: rgba(var(--brass-rgb), .55);
  clip-path: polygon(0 0, calc(100% - var(--tip)) 0, 100% 50%, calc(100% - var(--tip)) 100%, 0 100%);
}
:is(.hud-self, .hud-pet) .hud-self__gauges::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: rgba(var(--navy-2-rgb), .92);
  clip-path: polygon(0 0, calc(100% - var(--tip) + .5px) 0, 100% 50%, calc(100% - var(--tip) + .5px) 100%, 0 100%);
}
/* The frame's header: level and name on one slim line above the bars, ellipsised, no rule
   under it. */
:is(.hud-self, .hud-pet) .hud-self__gauges > .hud-self__id {
  flex-wrap: nowrap; align-items: center; min-height: 0; margin: 0; line-height: 1.1;
}
:is(.hud-self, .hud-pet) .hud-self__gauges > .hud-self__id :is(.hud-self__lvl, .hud-self__name) { font-size: var(--fs-md); text-shadow: 0 1px 1px rgba(var(--shadow-ink), .9); }
:is(.hud-self, .hud-pet) .hud-self__gauges > .hud-self__id .hud-self__name { min-width: 0; }
.hud-pet .hud-self__gauges > .hud-self__id :is(.hud-self__lvl, .hud-self__name) { font-size: var(--fs-sm); }
:is(.hud-self, .hud-pet) .hud-self__gauges .hud-vital__bar { height: 14px; }
:is(.hud-self, .hud-pet) .hud-self__gauges .hud-vital__line { border-color: rgba(var(--shadow-ink), .8); box-shadow: none; }
.hud-pet .hud-self__gauges { --tuck: 18px; --tip: 8px; gap: 2px; padding-top: 3px; padding-bottom: 3px; }
.hud-pet .hud-self__gauges .hud-vital__bar { height: 12px; }

/* The small block (the Attack pet): its gauges start a normal gap from its own portrait, one line
   for level and name. */
.hud-self__block--sm { --hud-pic: 44px; --hud-pad-l: 24px; column-gap: 8px; padding: 4px 8px 6px var(--hud-pad-l); }
.hud-self__block--sm .hud-self__id { flex-wrap: nowrap; min-height: 18px; margin-bottom: 3px; }
.hud-self__block--sm .hud-self__lvl, .hud-self__block--sm .hud-self__name { font-size: var(--fs-md); }
.hud-self__block--sm .hud-self__name { flex: 1 1 auto; min-width: 0; }
.hud-self__block--sm .hud-vital__bar { height: 13px; }
.hud-self__block--sm .hud-vital__v { font-size: var(--fs-xs); padding: 0 4px; }

/* The Attack pet's column, to the right of its owner. */
.hud-pet { flex: 0 0 230px; min-width: 0; max-width: 230px; display: flex; flex-direction: column; gap: 2px; }
.hud-pet[hidden] { display: none; }

/* THE MARKS: a body's buffs and your boons, the block's second row, under the gauges. A picture and a number:
   15px, the meaning on the rim (helpful green, hostile red, a boon gold) over a dark ring that
   keeps it legible on grass; the time left (or a boon's multiplier) in the corner, sized to its
   digits. The words are the card. */
.hud-buffs { grid-column: 2; display: flex; flex-wrap: wrap; gap: 5px; margin-top: 2px; }
.hud-buffs:empty { display: none; }
.hud-mark {
  position: relative;
  width: 15px;
  height: 15px;
  box-shadow: 0 0 0 1px var(--moss-400), 0 0 0 2px var(--border-hard);
  cursor: var(--cur-arrow, default);
}
.hud-mark.is-bad { box-shadow: 0 0 0 1px var(--ember-400), 0 0 0 2px var(--border-hard); }
.hud-mark.is-boon { box-shadow: 0 0 0 1px var(--hud-gold), 0 0 0 2px var(--border-hard); background: var(--border-hard); }
.hud-mark > .ico { --ico-box: 15px; }
/* A status with no picture of its own (a dozen buff rows carry none) is still a mark: the rim's
   colour fills it. */
.hud-mark > .ico[data-ico=""] { background-color: var(--moss-400); }
.hud-mark.is-bad > .ico[data-ico=""] { background-color: var(--ember-400); }
.hud-mark > img.g { display: block; width: 100%; height: 100%; object-fit: contain; }
.hud-mark > b {
  position: absolute;
  right: -3px;
  bottom: -3px;
  padding: 0 1px;
  border-radius: 2px;
  background: rgba(var(--hud-night-rgb), .82);
  color: var(--text);
  font-family: var(--face-ui);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1;
  text-shadow: none;
}
.hud-mark > b:empty { display: none; }

/* ------------------------------------------------- THE ACCOUNT FAN */
/* The character being played, as a face in the astro ring with the switch badge on its edge: on
   desktop right over the menu, in mobile mode under the minimap (MOBILE MODE below).
   Pressed, the account's other characters unroll in a row to its LEFT over the world on a night
   wash with two fading brass hairlines (no box), each named over its ring and a little dimmed, the
   + the last, smaller seat; the played face stays put, ringed and named in gold. gui/hud/left.ts
   writes each seat's --i and the count --n. */
.hud .hud-switch {
  left: auto; top: auto; right: var(--hud-edge); bottom: calc(var(--hud-edge) + var(--hud-menu-row) + 6px);
  width: 44px; height: 44px; z-index: 3; --fan-step: 62px;
}
.hud-switch__btn, .hud-account__body, .hud-account__add {
  appearance: none; display: block; width: 44px; height: 44px; padding: 0; margin: 0; border: 0;
  border-radius: 50%; background: none; font: inherit; color: inherit; cursor: var(--cur-hand, pointer);
}
.hud-switch__btn { position: relative; z-index: 1; filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)); }
.hud-switch__btn .hud-portrait, .hud-account__body .hud-portrait { cursor: inherit; transition: box-shadow .15s ease-out; }
.hud-switch__btn:hover .hud-portrait { box-shadow: 0 0 0 1px var(--hud-gold); }
/* The switch badge: the painted Switch tile, small, on the ring's lower left. */
.hud-switch__badge {
  position: absolute; z-index: 4; left: -9px; bottom: -7px; width: 22px; height: 22px;
  object-fit: contain; pointer-events: none; transition: opacity .15s ease-out;
}
.hud .hud-switch__badge { image-rendering: auto; }
.hud-switch.is-open .hud-switch__badge { opacity: 0; }
/* Open: the played face is lit gold (a glow past the ring), its name in gold behind a gold pip. */
.hud-switch.is-open .hud-switch__btn { filter: drop-shadow(0 0 5px rgba(var(--hud-gold-rgb), .85)) drop-shadow(0 0 2px rgba(var(--hud-gold-rgb), .6)); }
.hud-switch__btn .hud-account__name::before {
  content: ""; display: inline-block; width: 5px; height: 5px; margin: 0 4px 1px 0; vertical-align: middle;
  background: var(--hud-gold); transform: rotate(45deg); box-shadow: 0 0 4px rgba(var(--hud-gold-rgb), .9);
}
/* A name over each ring, shown only while the fan is open. */
.hud-account__name {
  position: absolute; left: 50%; bottom: calc(100% + 9px); max-width: calc(var(--fan-step) - 4px);
  transform: translateX(-50%); overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-family: var(--hud-face); font-size: var(--fs-xs); font-weight: 600; line-height: 1.2;
  color: var(--hud-text); text-shadow: var(--hud-shadow); pointer-events: none;
  opacity: 0; transition: opacity .15s ease-out;
}
.hud-switch.is-open .hud-account__name { opacity: .85; }
.hud-switch.is-open .hud-switch__btn .hud-account__name { opacity: 1; color: var(--hud-gold); }
/* The wash: night fading out to the left, a brass hairline along its top and bottom fading with it. */
.hud-switch::before {
  content: "";
  position: absolute; right: -10px; top: 50%; height: 82px;
  width: calc(var(--n, 1) * var(--fan-step) + 70px);
  transform: translateY(calc(-50% - 7px)) scaleX(.2); transform-origin: right center;
  background:
    linear-gradient(270deg, rgba(var(--hud-gold-rgb), .5), rgba(var(--hud-gold-rgb), 0)) top / 100% 1px no-repeat,
    linear-gradient(270deg, rgba(var(--hud-gold-rgb), .5), rgba(var(--hud-gold-rgb), 0)) bottom / 100% 1px no-repeat,
    linear-gradient(270deg, rgba(var(--hud-night-rgb), .82) 0%, rgba(var(--hud-night-rgb), .6) 65%, rgba(var(--hud-night-rgb), 0) 100%);
  opacity: 0; pointer-events: none; transition: transform .18s ease-out, opacity .18s ease-out;
}
.hud-switch.is-open::before { opacity: 1; transform: translateY(calc(-50% - 7px)) scaleX(1); }
.hud-account, .hud-account__seats { position: absolute; left: 50%; top: 50%; width: 0; height: 0; }
.hud-account__seats { left: 0; top: 0; }
.hud-account__body[hidden] { display: none; }
/* The others: a touch dimmed until pointed at. */
.hud-account__body .hud-portrait__face { filter: saturate(.6) brightness(.78); transition: filter .15s ease-out; }
.hud-account__body:hover .hud-portrait__face, .hud-account__body:focus-visible .hud-portrait__face { filter: none; }
.hud-account__body:hover .hud-portrait { box-shadow: 0 0 0 1px var(--hud-gold); }
.hud-account__body:hover .hud-account__name { opacity: 1; }
/* The +: a smaller empty well in a faint brass rim. */
.hud-account__add {
  display: grid; place-items: center; width: 34px; height: 34px; color: var(--hud-gold);
  background: var(--portrait-bg); box-shadow: 0 0 0 1px rgba(var(--hud-gold-rgb), .45), 0 2px 6px rgba(var(--shadow-ink), .7);
}
.hud-account__add > .ic { width: 14px; height: 14px; }
.hud-account__add:hover { box-shadow: 0 0 0 1px var(--hud-gold), 0 0 8px rgba(var(--hud-gold-rgb), .45); }
/* One seat every --fan-step to the left, nearest the button first; shut, folded behind it. */
.hud-account__body, .hud-account__add {
  position: absolute; left: 0; top: 0;
  transform: translate(-50%, -50%) scale(.4);
  opacity: 0; pointer-events: none;
  transition: transform .18s ease-out, opacity .18s ease-out;
  transition-delay: calc(var(--i, 0) * 20ms);
}
.hud-switch.is-open .hud-account__body, .hud-switch.is-open .hud-account__add {
  transform: translate(-50%, -50%) translateX(calc((var(--i, 0) + 1) * var(--fan-step) * -1));
  opacity: 1; pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  .hud-switch::before, .hud-account__body, .hud-account__add, .hud-account__name, .hud-switch__badge { transition: none; }
}

/* THE CONVERSATIONS' FACES (panels/direct-message.ts THE FACES). Over the account switch on a
   desktop, clear of the played face's name. Shut: the first three stacked, the front one whole on
   the left, the next two peeking out to its right, each a little dimmer. Open: a row unrolled to the
   LEFT, the newest against the corner, six in view and the rest a sideways scroll with no bar. In
   mobile mode the stack stands left of the chat button and opens to the left too, three in view. The list
   pads its own edge so the scroll never clips a ring or a count. */
.hud .hud-dmheads {
  --f: 40px; --gap: 6px; --peek: 10px;
  left: auto; top: auto; right: var(--hud-edge); bottom: calc(var(--hud-edge) + var(--hud-menu-row) + 6px + 44px + 24px);
  z-index: 3;
}
.hud .hud-dmheads[hidden] { display: none; }
.hud-dmheads__list { display: flex; align-items: center; padding: 6px; margin: -6px; }
.hud-dmheads__face {
  appearance: none; position: relative; flex: none; width: var(--f); height: var(--f); padding: 0; margin: 0;
  border: 0; border-radius: 50%; background: none; font: inherit; color: inherit; cursor: var(--cur-hand, pointer);
  filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7));
}
.hud-dmheads__face .hud-portrait { width: 100%; height: 100%; padding: 2px; cursor: inherit; transition: box-shadow .15s ease-out; }
.hud-dmheads__face:hover .hud-portrait { box-shadow: 0 0 0 1px var(--hud-gold); }
.hud-dmheads__n {
  position: absolute; z-index: 5; right: -5px; top: -5px; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--hud-hp); box-shadow: 0 0 0 1px var(--border-hard);
  font: 700 10px/16px var(--hud-face); font-style: normal; text-align: center; color: #fff; text-shadow: none;
}
.hud-dmheads__n[hidden] { display: none; }
.hud-dmheads:not(.is-open) .hud-dmheads__face:nth-child(n+4) { display: none; }
/* Shut, only the front face shows its count: the ones peeking behind would stack theirs on it. */
.hud-dmheads:not(.is-open) .hud-dmheads__face:nth-child(n+2) .hud-dmheads__n { display: none; }
.hud-dmheads:not(.is-open) .hud-dmheads__face + .hud-dmheads__face { margin-left: calc(var(--peek) - var(--f)); }
.hud-dmheads:not(.is-open) .hud-dmheads__face:nth-child(1) { z-index: 3; }
.hud-dmheads:not(.is-open) .hud-dmheads__face:nth-child(2) { z-index: 2; filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)) brightness(.8); }
.hud-dmheads:not(.is-open) .hud-dmheads__face:nth-child(3) { z-index: 1; filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)) brightness(.65); }
.hud-dmheads.is-open .hud-dmheads__list {
  flex-direction: row-reverse; gap: var(--gap); max-width: calc(6 * var(--f) + 5 * var(--gap) + 12px);
  overflow-x: auto; overscroll-behavior: contain; scrollbar-width: none;
}
.hud-dmheads__list::-webkit-scrollbar { display: none; }
:where(html.m-mobile) .hud .hud-dmheads { --f: 36px; right: calc(50% + 112px + 44px + 6px); bottom: calc(var(--hud-edge) + 4px); }
:where(html.m-mobile) .hud-dmheads.is-open .hud-dmheads__list { max-width: calc(3 * var(--f) + 2 * var(--gap) + 12px); }

/* ------------------------------------------------------------ THE MAP COLUMN */
/* The map's name, the minimap (the tile you stand on, the server clock and the client's own line
   in its corners), the quest tracker, presence: one column on the right edge. Not zoomed: the
   minimap is painted in raw screen pixels. */
.hud-mapcol {
  right: var(--hud-edge);
  top: var(--hud-edge);
  zoom: 1;
  pointer-events: none;
  width: var(--map-w, 216px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
.hud-mapcol > * { position: static; pointer-events: auto; }
/* The map's name and the map in one frame (owner, 2026-10-09): a soft navy wash the column's
   width, a brass hairline round it at low strength, so it sits with the world rather than over it. The name heads it in the display serif, between two
   hairlines that fade out from a small star. */
.hud-mapframe {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 2px 4px 4px;
  background: linear-gradient(180deg, rgba(var(--hud-ink-rgb), .62), rgba(var(--hud-ink-rgb), .38));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .22), 0 2px 10px rgba(var(--shadow-ink), .35);
  border-radius: 3px;
}
.hud-mapframe > .hud-mapname {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  font-family: var(--face-display);
  font-size: max(12px, calc(16px * var(--ui-scale)));
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: .06em;
  color: var(--brass-pale);
  text-shadow: var(--hud-shadow);
  white-space: nowrap;
  overflow: hidden;
  pointer-events: none;
  container-type: inline-size;
}
/* The name fits the frame by its length (owner, 2026-10-11): at most the full size, smaller as
   right.ts's --len grows, so the frame's width less the two stars and gaps holds about --len
   display-face letters (.8em each with the tracking); past the 9 px floor it ends in an ellipsis. */
.hud-mapname__t {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: max(9px, min(1em, calc((100cqi - 40px) / (var(--len, 8) * .8))));
}
/* The two hairlines either side of the name, each fading out from a small brass star. */
.hud-mapframe > .hud-mapname::before, .hud-mapframe > .hud-mapname::after {
  content: "\2726";
  flex: 1 1 0;
  min-width: 14px;
  font-family: var(--face-ui);
  font-size: 8px;
  line-height: 1;
  color: var(--brass-hi);
  text-shadow: none;
}
.hud-mapframe > .hud-mapname::before {
  text-align: right;
  background: linear-gradient(90deg, transparent, rgba(var(--brass-rgb), .55)) left center / calc(100% - 11px) 1px no-repeat;
}
.hud-mapframe > .hud-mapname::after {
  text-align: left;
  background: linear-gradient(270deg, transparent, rgba(var(--brass-rgb), .55)) right center / calc(100% - 11px) 1px no-repeat;
}
/* The whole map is the button that opens the world map. Square, a thin brass rim between two dark
   hairlines (the round ring's metal, drawn as lines), a soft shadow. Lit while hovered or open. */
.hud-map {
  --map-rim: rgba(var(--brass-rgb), .85);
  position: relative;
  appearance: none;
  padding: 0;
  border: 0;
  background: rgba(var(--hud-ink-rgb), .55);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .7), 0 0 0 2px var(--map-rim), 0 0 0 3px rgba(var(--shadow-ink), .8), 0 2px 10px rgba(var(--shadow-ink), .55);
  line-height: 0;
  cursor: var(--cur-hand, pointer);
  overflow: hidden;
}
.hud-map canvas { display: block; }
.hud-map:hover { --map-rim: var(--brass-hi); }
.hud-map.is-open { --map-rim: var(--brass-pale); }
/* The server clock top right inside the map and the tile you stand on top left (owner, 2026-10-10:
   the clock's look): big enough to read at a glance, on a small dark backing so bright ground never
   eats it. The clock's card is the time to the daily reset; both are inside the button, so a press
   on either opens the world map like the rest of the map. */
.hud-clock, .hud-coord {
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 2px 5px;
  border-radius: 2px;
  background: rgba(var(--shadow-ink), .55);
  font-family: var(--hud-face);
  font-size: var(--fs-md);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: .03em;
  font-variant-numeric: tabular-nums;
  color: var(--hud-text);
  text-shadow: var(--hud-shadow);
}
.hud-clock[hidden] { display: none; }
/* The tile you stand on: the clock mirrored into the top left. */
.hud-coord { right: auto; left: 4px; pointer-events: none; }
.hud-coord:empty { display: none; }
/* The client talking about itself, bottom left inside the map: FPS and latency, one quiet line
   on the clock's dark backing, a size up from the old line under the map. */
.hud-map > .hud-conn {
  position: absolute;
  left: 4px;
  bottom: 4px;
  display: flex;
  gap: 6px;
  max-width: calc(100% - 8px);
  padding: 2px 5px;
  border-radius: 2px;
  background: rgba(var(--shadow-ink), .55);
  font-family: var(--hud-face);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--hud-dim);
  text-shadow: var(--hud-shadow);
  pointer-events: none;
}
.hud-conn__ping.is-good { color: var(--hud-good); }
.hud-conn__ping.is-slow { color: var(--hud-gold); }
.hud-conn__ping.is-bad { color: var(--hud-hp); }

/* THE TRACKER: one slightly see-through black ground, square corners, no gradient washes (owner,
   2026-10-09). The head is quiet: the quest board's picture, the word in small semibold caps and a
   small target mark at the right, a thin divider under it; pressing it folds the list. Then each
   quest: its name, white (gold when pinned), and a line per objective in the muted ink - the step's
   picture, what to do, n/m right-aligned in tabular figures, and the walk mark. A full step turns
   gold with a check. The map column is drawn at screen pixels, so the tracker wears the HUD's
   zoom itself (owner, 2026-10-09: it read a size smaller than the rest of the HUD). Every line is
   the nameplate's one size (--plate-fs, owner 2026-10-10), the zoom divided back out so it lands
   at the same screen px. */
.hud-track {
  zoom: var(--hud-z);
  --track-fs: calc(var(--plate-fs) / var(--hud-z));
  font-size: var(--track-fs);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
  width: 100%;
  padding: 5px 9px 7px;
  background: rgba(0, 0, 0, .45);
  border-radius: 0;
}
.hud-track[hidden] { display: none; }
.hud-track__head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 0 4px;
  border-bottom: 1px solid rgba(var(--fx-white-rgb), .14);
  font-size: var(--track-fs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--hud-dim);
  cursor: var(--cur-hand, pointer);
}
.hud-track__head:hover { color: var(--hud-text); }
.hud-track.is-folded .hud-track__head { padding-bottom: 0; border-bottom-color: transparent; }
.hud-track__head::before {
  content: "";
  flex: none;
  width: 18px;
  height: 18px;
  background: url(/data/art/ui/hud-quests.ac8bf11c.webp) center / contain no-repeat;
  filter: drop-shadow(0 1px 1px rgba(var(--shadow-ink), .7));
}
.hud-track__head-title { flex: 1; min-width: 0; }
.hud-track__mark { flex: none; width: 14px; height: 14px; object-fit: contain; opacity: .6; }
.hud-track__list { display: flex; flex-direction: column; gap: 4px; }
.hud-track__list[hidden] { display: none; }
.hud-track__q {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.hud-track__title { font-size: var(--track-fs); font-weight: 600; line-height: 1.25; color: var(--hud-text); cursor: var(--cur-hand, pointer); }
.hud-track__title:hover .hud-track__name { color: var(--hud-gold); }
.hud-track__link { cursor: var(--cur-hand, pointer); }
.hud-track__name { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hud-track__q.is-pinned .hud-track__name { color: var(--hud-gold); }
.hud-track__objs { display: flex; flex-direction: column; }
.hud-track__obj {
  display: flex;
  gap: 6px;
  align-items: center;
  padding-left: 2px;
  font-size: var(--track-fs);
  line-height: 1.35;
  color: var(--hud-dim);
}
.hud-track__ic { width: 16px; height: 16px; flex: none; object-fit: contain; filter: drop-shadow(0 1px 1px rgba(var(--shadow-ink), .7)); }
.hud-track__obj.is-done { color: var(--hud-gold); }
.hud-track__what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hud-track__n { margin-left: auto; color: var(--hud-dim); white-space: nowrap; font-variant-numeric: tabular-nums; text-align: right; }
.hud-track__obj.is-done .hud-track__n { color: var(--hud-gold); }
/* A line about a creature finds it on the world map: underlined under the pointer. */
.hud-track__link:hover { text-decoration: underline; text-underline-offset: 2px; }
.hud-track__walk {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  line-height: 0;
  opacity: .7;
  cursor: var(--cur-hand, pointer);
}
.hud-track__walk > img.g { width: 16px; height: 16px; object-fit: contain; }
.hud-track__walk:hover { opacity: 1; }

/* Who walked in or out: a line on its own wash under the tracker, that removes itself. */
.hud-notes { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.hud-note {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 10px 2px 8px;
  background: var(--hud-wash-rev);
  font-size: var(--fs-sm);
  color: var(--hud-dim);
  white-space: nowrap;
  animation: hud-note-in 140ms ease-out;
}
.hud-note b { font-weight: 600; color: var(--hud-text); }
.hud-note__dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: rgba(var(--fx-white-rgb), .38); }
.hud-note.is-on .hud-note__dot { background: var(--hud-good); box-shadow: 0 0 6px rgba(var(--hud-good-glow-rgb), .7); }
@keyframes hud-note-in { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .hud-note { animation: none; } }

/* ------------------------------------------------------- THE BOARDS' COLUMN */
/* Bare painted icons down the map's left side, top to bottom, the run's reward under them. */
.hud-primary {
  right: calc(var(--hud-edge) + var(--map-w, 216px) + 8px);
  top: var(--hud-edge);
  zoom: 1;
  pointer-events: none; /* only its pieces take a press: the reward row widens the box over the world */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}
.hud-primary > * { position: static; pointer-events: auto; }
.hud-primary-menu { display: flex; flex-direction: column; gap: 4px; }
/* Mobile mode's one menu and its drawer (MOBILE MODE below); a desktop has its rows. */
.hud .hud-menubtn, .hud .hud-drawer { display: none; }

/* A tile: the diamond badge (its plate is in the painting); a drop shadow, a lift under the pointer, a dip when pressed,
   a gold glow while its window is open, and the red dot when something waits. */
.hud-ibtn {
  appearance: none;
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7));
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: var(--cur-hand, pointer);
  transition: transform 80ms ease;
}
.hud-ibtn[hidden] { display: none; }
.hud-ibtn > img.g { width: 100%; height: 100%; object-fit: contain; }
.hud-ibtn:hover { filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)) brightness(1.2); transform: translateY(-1px); }
.hud-ibtn.is-down { transform: translateY(1px) scale(.95); }
.hud-ibtn.is-open { filter: drop-shadow(0 0 4px rgba(var(--hud-gold-rgb), .85)); }
.hud-dot {
  position: absolute;
  right: 5px;
  top: 5px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--hud-hp);
  box-shadow: 0 0 0 1px var(--border-hard);
  pointer-events: none;
}
.hud-dot[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .hud-ibtn, .hud-ibtn:hover, .hud-ibtn.is-down { transition: none; transform: none; } }

/* THE LIVE EVENT'S TILE GLITTERS (owner, 2026-09-26): a gold glow breathing behind the painting
   and six motes rising off it, never catching the pointer. The drop shadow goes on the painting,
   not the button: on the button it re-blurred the moving glow every frame (2026-09-28 trace). */
.hud-ibtn--event { isolation: isolate; }
.hud .hud-ibtn--event, .hud .hud-ibtn--event:hover { filter: none; }
.hud-ibtn--event > img.g { filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)); }
.hud-ibtn--event:hover > img.g { filter: drop-shadow(0 1px 2px rgba(var(--shadow-ink), .7)) brightness(1.2); }
.hud-ibtn--event::before {
  content: ""; position: absolute; inset: -4px; z-index: -1; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--hud-event-hi-rgb), .55) 0%, rgba(var(--hud-event-rgb), .18) 45%, transparent 70%);
  animation: hud-event-glow 2.4s ease-in-out infinite;
  pointer-events: none;
}
.hud-motes { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.hud-motes i {
  position: absolute; bottom: 6px; width: 3px; height: 3px; border-radius: 50%;
  background: var(--hud-spark); box-shadow: 0 0 4px 1px rgba(var(--hud-spark-glow-rgb), .85);
  opacity: 0; animation: hud-event-mote 2.6s ease-out infinite;
}
.hud-motes i:nth-child(1) { left: 22%; animation-delay: 0s; }
.hud-motes i:nth-child(2) { left: 70%; animation-delay: .45s; width: 2px; height: 2px; }
.hud-motes i:nth-child(3) { left: 45%; animation-delay: .9s; }
.hud-motes i:nth-child(4) { left: 82%; animation-delay: 1.35s; width: 2px; height: 2px; }
.hud-motes i:nth-child(5) { left: 12%; animation-delay: 1.8s; width: 2px; height: 2px; }
.hud-motes i:nth-child(6) { left: 58%; animation-delay: 2.2s; }
.hud-motes i:nth-child(even) { --drift: -4px; }
@keyframes hud-event-glow { 0%, 100% { opacity: .55; transform: scale(.92); } 50% { opacity: 1; transform: scale(1.06); } }
@keyframes hud-event-mote {
  0% { opacity: 0; transform: translate(0, 0) scale(.6); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--drift, 3px), -30px) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .hud-ibtn--event::before { animation: none; opacity: .8; }
  .hud-motes { display: none; }
}
/* A finger's device (tier.ts isTouch, the test the art tier and the memory answer to, whatever the
   mode) keeps the glitter still: every running glow and mote is a compositor layer held for as long
   as the event is live. The glow at its middle breath, the motes caught along their rise. */
@media (hover: none) and (pointer: coarse) {
  .hud-ibtn--event::before { animation: none; opacity: .8; }
  .hud-motes i { animation: none; opacity: .75; transform: translate(var(--drift, 3px), var(--rise)); }
  .hud-motes i:nth-child(1) { --rise: -6px; }
  .hud-motes i:nth-child(2) { --rise: -17px; }
  .hud-motes i:nth-child(3) { --rise: -26px; opacity: .4; }
  .hud-motes i:nth-child(4) { --rise: -11px; }
  .hud-motes i:nth-child(5) { --rise: -21px; opacity: .55; }
  .hud-motes i:nth-child(6) { --rise: -3px; }
}

/* The run's reward: label and value rows on the reversed wash. */
.hud-reward { display: flex; flex-direction: column; gap: 2px; min-width: 190px; font-size: var(--fs-sm); }
.hud-reward[hidden] { display: none; }
.hud-reward .hud-row { justify-content: space-between; }
.hud-reward .hud-row span:last-child { color: var(--hud-gold); }
.hud-reward.is-shut .hud-row span:last-child { color: var(--hud-dim); }
.hud-reward__k { color: var(--hud-dim); }

/* ---------------------------------------------------------- THE BOTTOM STRIP */
/* gui/hud/strip.ts: one thin bar along the bottom edge, the screen's width. The left half is
   experience, filling from the left edge; the right half is energy, filling from the right edge;
   each half ticked every 10% from where it fills. A brass hairline on top. In the middle a small
   plate rises off it - a navy trapezoid on a brass hairline, a star between its two readouts - with
   the figures: EXP percent, the energy's charge of its bar and
   the bars payable today. Not zoomed as a block (the halves are percent of the screen); the
   thickness and the plate take --hud-z themselves. */
.hud-strip {
  left: 0; right: 0; bottom: 0; height: 0; zoom: 1;
  /* The plate is the one flex item, centred and standing up off the bottom edge. */
  display: flex; justify-content: center; align-items: flex-end;
  pointer-events: none;
}
.hud-strip__half {
  position: absolute;
  bottom: 0;
  width: 50%;
  height: calc(4px * var(--hud-z));
  overflow: hidden;
  background: rgba(var(--hud-well-rgb), .82);
  box-shadow: inset 0 1px 0 rgba(var(--brass-rgb), .4);
  pointer-events: auto;
}
.hud-strip__half--exp { left: 0; }
.hud-strip__half--en { right: 0; }
.hud-strip__fill {
  position: absolute;
  inset: 1px 0 0;
  transform: scaleX(var(--v, 0));
  transform-origin: left center;
  transition: transform 200ms linear;
  background: var(--hud-xp-fill);
}
.hud-strip__half--en .hud-strip__fill {
  transform-origin: right center;
  background: linear-gradient(270deg, var(--sky), rgba(var(--sky-rgb), .45));
}
/* The ticks, over the fill: one dark line every 10%, counted from the edge the half fills from. */
.hud-strip__half::after {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg, transparent 0 calc(10% - 1px), rgba(var(--shadow-ink), .85) calc(10% - 1px) 10%);
}
.hud-strip__half--en::after { background: repeating-linear-gradient(270deg, transparent 0 calc(10% - 1px), rgba(var(--shadow-ink), .85) calc(10% - 1px) 10%); }
/* A full bar is waiting for its nugget: it glows. Locked (under the level it opens at): dimmed. */
.hud-strip__half--en.is-full .hud-strip__fill { animation: hud-strip-full 1.8s ease-in-out infinite; }
.hud-strip__half--en.is-locked { opacity: .45; }
@keyframes hud-strip-full { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.35); } }
.hud-strip__plate {
  position: relative;
  flex: none;
  margin-bottom: 4px; /* zoomed by --hud-z like the bar: it stands on the bar, never over its ends */
  zoom: var(--hud-z);
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 17px;
  padding: 1px 15px 0;
  font-family: var(--hud-face);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--hud-dim);
  text-shadow: 0 1px 1px rgba(var(--shadow-ink), .9);
  pointer-events: auto;
}
/* The plate's brass edge: the trapezoid cut on a pseudo of its own, not on the plate. A clip-path
   over the twinkling star made the strip an offscreen pass every frame (2026-10-14 trace); cut on
   static paint beside the star it costs nothing. */
.hud-strip__plate::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background: rgba(var(--brass-rgb), .5);
  clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 100%, 0 100%);
}
.hud-strip__plate::before {
  content: "";
  position: absolute;
  inset: 1px 1px 0;
  z-index: -1;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .95), rgba(var(--navy-0-rgb), .95));
  clip-path: polygon(9.5px 0, calc(100% - 9.5px) 0, 100% 100%, 0 100%);
}
.hud-strip__side { display: flex; align-items: baseline; gap: 5px; }
.hud-strip__k { font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: rgba(var(--brass-rgb), .85); }
.hud-strip__n { font-weight: 700; color: var(--hud-text); }
.hud-strip__side--en .hud-strip__sub { color: var(--hud-text); }
/* A four-point astral star with a thin orbit ring round it, twinkling slowly between the readouts.
   The star is a picture, not a clip-path cut: an animated layer under a clip-path is an offscreen
   pass every frame, and on a Mali phone each pass reloads the whole screen's tiles (2026-10-14
   trace: this 13 px star and the dial's light were ~400 ms/s of the compositor's GPU thread). */
.hud-strip__star {
  position: relative;
  flex: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .45);
  background: radial-gradient(circle, rgba(var(--brass-hi-rgb), .45), transparent 65%); /* its glow, painted once */
}
.hud-strip__star::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3CradialGradient id='g' gradientUnits='userSpaceOnUse' cx='50' cy='50' r='70.7'%3E%3Cstop offset='.12' stop-color='%23fff'/%3E%3Cstop offset='.3' stop-color='%23e6cf9c'/%3E%3Cstop offset='1' stop-color='%23d4b57a' stop-opacity='.9'/%3E%3C/radialGradient%3E%3C/defs%3E%3Cpolygon fill='url(%23g)' points='50,0 58,42 100,50 58,58 50,100 42,58 0,50 42,42'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  animation: hud-strip-twinkle 3.2s ease-in-out infinite;
}
@keyframes hud-strip-twinkle {
  0%, 100% { transform: scale(.85) rotate(0deg); opacity: .85; }
  50% { transform: scale(1.1) rotate(45deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hud-strip__fill { transition: none; }
  .hud-strip__star::before { animation: none; }
  .hud-strip__half--en.is-full .hud-strip__fill { animation: none; filter: brightness(1.25); }
}

/* ---------------------------------------------------------------- THE MENU */
/* Bottom right: the systems in one line, folded into rows when the line would run into the hotbar
   (gui/hud/menu.ts writes --menu-cols from the cached viewport). A short last row stands against
   the right edge, under the full row's end (owner, 2026-10-14). */
.hud-menu {
  right: var(--hud-edge);
  bottom: var(--hud-edge);
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  width: calc(var(--menu-cols, 11) * 47px - 3px);
  gap: 3px;
}

/* ---------------------------------------------------------------- THE BARS */
/* Bottom centre (gui/hud/bar.ts, P10): your twelve loose square slots with a gap of world between
   them, the round toggles after the last one; the pet's strip a row above, its guide mark to its
   left. A slot is a navy well under the astro slot frame (--art-slot, faint on an empty slot), an
   empty one shows the star (slot-star; the pet strip's a paw, slot-paw), the key sits on the bottom edge on its small plate (slot-key), the count
   bottom right. THE DEFAULT (PLAN D10) is a light running round the rim of the one slot the
   server stored as the next turn's action: exactly one per bar. The cooldown and the turn clock are
   one dark cover each, wiped away by a compositor animation (gui/c/slots.ts), never per frame. */
.hud-bar { left: 50%; bottom: 28px; transform: translateX(-50%); display: flex; align-items: center; gap: 4px; }
.hud-bar__tray { display: flex; gap: 4px; }
.hud-bar__knobs { display: flex; align-items: center; gap: 4px; }
.hud-petbar { left: 50%; bottom: 82px; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; }
/* In the strip's own row, first cell: it belongs to the strip, never floats off on its own. */
.hud-petbar__help { position: static; flex: none; align-self: center; width: 28px; height: 28px; }

.hud .slot {
  position: relative;
  flex: none;
  width: var(--hud-slot);
  height: var(--hud-slot);
  background: var(--hud-slot-bg);
  box-shadow: var(--hud-slot-rim);
  cursor: var(--cur-hand, pointer);
  touch-action: none;
}
.hud .slot::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--art-slot) center / 100% 100% no-repeat;
  opacity: var(--hud-frame-op);
  filter: var(--hud-frame-tint);
  pointer-events: none;
}
.hud .slot.is-empty::before { opacity: var(--hud-frame-empty-op); }
.hud .slot.full:hover { box-shadow: var(--hud-slot-hover); }
.hud .slot.full:hover::before { filter: var(--hud-frame-tint) brightness(1.25); }
.hud .slot.is-empty { cursor: var(--cur-arrow, default); }
.hud .slot.is-empty::after {
  content: "";
  position: absolute;
  inset: 3px;
  background: url(/data/art/ui/slot-star.58379322.webp) center / 78% 78% no-repeat;
  opacity: .92;
}
/* The pet strip's empty slot shows a paw, not the star. */
.hud-petbar .slot.is-empty::after { background-image: url(/data/art/ui/slot-paw.94060037.webp); }
.hud .slot > .ico { position: absolute; inset: 0; margin: auto; --ico-box: calc(var(--hud-slot) - 6px); }
.hud .slot.is-locked > .ico { filter: grayscale(1) brightness(.6); }
/* A buff with a Smart Buff list (owner, 2026-10-14, panels/smart-buff.ts): a small green pip in
   the top-left corner, over the art and the frame, under nothing a press needs. */
.hud .slot.is-smart::after {
  content: ""; position: absolute; left: 4px; top: 4px; z-index: 4;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--green); box-shadow: 0 0 0 1.5px rgba(0, 0, 0, .65);
  pointer-events: none;
}
/* An item's quality, as the well's ground (00-tokens.css --q-bg-*), the same as a window's tile. */
.hud .slot.q-green { background: var(--q-bg-green); }
.hud .slot.q-blue { background: var(--q-bg-blue); }
.hud .slot.q-yellow { background: var(--q-bg-yellow); }
.hud .slot.q-gold { background: var(--q-bg-gold); }
.hud .slot.q-purple { background: var(--q-bg-purple); }
/* The two covers, the turn clock under the cooldown; both shrink upward off the art. */
.hud .slot > .tc, .hud .slot > .cd { position: absolute; inset: 3px; z-index: 1; background: var(--hud-cover); transform-origin: 50% 0; pointer-events: none; }
.hud .slot.cooling > .tc { display: none; }
.hud .slot > .cdn {
  position: absolute; inset: 0; z-index: 4;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-md); font-weight: 600; line-height: 1;
  color: var(--hud-text); text-shadow: var(--hud-shadow);
  pointer-events: none;
}
.hud .slot > .cdn:empty { display: none; }
/* THE DEFAULT'S LIGHT (owner, 2026-10-09: the travelling light of v0.13.36 / 079bd941 back in
   place of the painted frame). One bright arc runs round the default slot's rim, forever. The span
   is masked down to a ring as thick as the frame (padding keeps the padding box, the content box
   is cut out); the light is a conic gradient on its ::before, twice the slot's size, TURNED by
   transform so the compositor runs it - animating the gradient's own angle re-rastered the slot
   every frame on a Mali phone (2026-09-28 trace). The arc is painted in the pale brass at the
   strength a screen blend of the gold gave over the frame, so it reads as light, not as a paler
   patch of metal, with no blend: a blend made the whole bar an offscreen group on the GPU
   (2026-10-11). Dim while the slot cools; still with reduced motion. */
.hud .slot > .lit {
  display: none;
  position: absolute; inset: 0; z-index: 3;
  padding: 3px;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.hud .slot.opener.full:not(.is-locked) > .lit { display: block; }
.hud .slot > .lit::before {
  content: "";
  position: absolute;
  inset: -50%;
  background: conic-gradient(
    transparent 0 54%, rgba(var(--brass-pale-rgb), .24) 64%, rgba(var(--brass-pale-rgb), .78) 72%,
    #fffaf0 78%, rgba(var(--brass-pale-rgb), .78) 84%, rgba(var(--brass-pale-rgb), .24) 92%, transparent 100%);
  animation: hud-slot-lit 3.2s linear infinite;
}
@keyframes hud-slot-lit { to { transform: rotate(360deg); } }
.hud .slot.cooling > .lit { opacity: .28; }
@media (prefers-reduced-motion: reduce) {
  .hud .slot > .lit::before { animation: none; background: rgba(var(--brass-pale-rgb), .72); }
}
.hud .slot > .ct {
  position: absolute; right: 3px; bottom: 2px; z-index: 4;
  font-size: var(--fs-sm); font-weight: 600; line-height: 1;
  color: var(--hud-text); text-shadow: var(--hud-shadow);
  pointer-events: none;
}
.hud .slot > .ct:empty { display: none; }
.hud .slot > .k {
  position: absolute; left: 50%; bottom: -8px; z-index: 4;
  width: 16px; height: 16px; translate: -50% 0;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 600; line-height: 1;
  color: var(--hud-key); text-shadow: var(--hud-shadow);
  background: url(/data/art/ui/slot-key.5a6cd7d8.webp) center / 100% 100% no-repeat;
  pointer-events: none;
}
.hud-petbar .slot > .k { display: none; }

/* The toggles: painted diamond badges, the badge IS the button. Bright when on, dimmed when
   off; the pet's rank swaps its picture and is never dimmed (back is an answer, not "off"). Busy
   while the ask is out (gui/req.ts): the answer, not the click, lights it. */
.hud-tgl {
  appearance: none; position: relative; flex: none;
  width: 36px; height: 36px; margin: 0; padding: 0; border: 0; border-radius: 50%;
  background: none; box-shadow: none;
  display: flex; align-items: center; justify-content: center;
  cursor: var(--cur-hand, pointer);
}
.hud-tgl > img.g { width: 100%; height: 100%; display: block; object-fit: contain; transition: filter .15s; }
.hud-tgl:not(.is-on) > img.g { filter: brightness(.45) saturate(.6); }
.hud-tgl:not(.is-on):hover > img.g { filter: brightness(.7) saturate(.8); }
.hud-tgl.is-on:hover > img.g { filter: brightness(1.15); }
.hud-tgl--front > img.g, .hud-tgl--front:hover > img.g, .hud-petbar__help > img.g { filter: none; }
.hud-tgl.is-down { transform: scale(.94); }
.hud-tgl.is-busy { cursor: var(--cur-busy, progress); }
.hud-tgl.is-busy > img.g { opacity: .6; }
@media (prefers-reduced-motion: reduce) { .hud-tgl > img.g { transition: none; } .hud-tgl.is-down { transform: none; } }

/* ---------------------------------------------------------------- THE CHAT */
/* Bottom left (gui/hud/chat.ts), top to bottom: the channel tabs, the log, the composer. The tabs
   and the composer stand on the window face at full strength, the log between them on the same face
   at half, so the world shows through the talk and never through the controls (owner, 2026-10-10).
   It wears the window's rim and corner pieces the whole time. Talk is read as prose, so the whole
   block takes the UI face. */
.hud-chat {
  left: var(--hud-edge);
  bottom: var(--hud-edge);
  width: clamp(240px, calc((50vw - 8px) / var(--hud-z) - 340px), 460px);
  display: flex;
  flex-direction: column;
  isolation: isolate;
  font-family: var(--face-ui);
  font-weight: 600;
  transition: box-shadow .15s;
  --chat-log-h: 120px;
}
/* The bands reach the box's edges; each carries its own inset. Only in the corner: in the chatwin
   window the same nodes stand on the window's body. */
.hud-chat > .hud-chat__tabs { padding: 4px 6px 3px; background: var(--chat-bg); }
.hud-chat > .hud-log { padding: 3px 6px; }
.hud-chat > .hud-composer { padding: 3px 8px 6px 6px; background: var(--chat-bg); }
.hud-chat[data-size="m"] { --chat-log-h: 200px; }
.hud-chat[data-size="l"] { --chat-log-h: 320px; }
/* The log's half-strength face, under the whole box (the bands paint over it). No engraved inner
   plane: the chat is one flat face (owner, 2026-10-09). */
.hud-chat::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--chat-bg);
  pointer-events: none;
  opacity: .5;
}
/* The window's rim and its brass corner pieces (top-left and bottom-right) with the engraved
   plane, as every window on the window layer wears them (20-components.css `.wins > .win::after`),
   the whole time (owner, 2026-10-10). The corners at half a window's (owner, 2026-10-10: too big
   on a box this short), sized off --win-size, not the chat's own em. */
.hud-chat { box-shadow: var(--win-ring); }
.hud-chat::after {
  --plane: 5px;
  --cs: calc(4.16 * var(--win-size));
  content: ""; position: absolute; inset: 0; z-index: 4; pointer-events: none;
  background:
    var(--art-corner-tl) left top / var(--cs) var(--cs) no-repeat,
    var(--art-corner-br) right bottom / var(--cs) var(--cs) no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) top var(--plane) / calc(100% - 2 * var(--plane)) 1px no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) bottom var(--plane) / calc(100% - 2 * var(--plane)) 1px no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) left var(--plane) top var(--plane) / 1px calc(100% - 2 * var(--plane)) no-repeat,
    linear-gradient(var(--win-plane), var(--win-plane)) right var(--plane) top var(--plane) / 1px calc(100% - 2 * var(--plane)) no-repeat;
}
.hud-chat[hidden] { display: none; }
.hud-chat__tabs { display: flex; align-items: center; gap: 4px; min-width: 0; }

/* The tabs: a Lucide glyph each (svg.hud-glyph, chat.ts), stroked in its channel's ink, the name on hover. Quiet
   until read; the one being read is full strength in a glow of its channel's ink (no bar under it,
   owner 2026-10-09); a channel you are in neither of is grey and dim and says why on hover;
   a line waiting on a tab marks it with the HUD's red dot. */
.hud-chans {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.hud-chans::-webkit-scrollbar { display: none; }
.hud-ch, .hud-chat__btn, .hud-chatbtn {
  appearance: none;
  position: relative;
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: var(--cur-hand, pointer);
  transition: transform 80ms ease;
}
/* Narrow and gapless so all seven tabs fit the chat at its narrowest (240px desktop, 270px phone)
   beside the size and pop buttons: wider, Trade was cut off (owner, 2026-10-14). */
.hud-ch { width: 24px; height: 28px; }
.hud-chat__btn > img.g, .hud-chatbtn > img.g {
  width: 100%; height: 100%; object-fit: contain;
  filter: drop-shadow(var(--hud-lift));
  transition: filter .15s, opacity .15s;
}
/* The channel glyphs: Lucide strokes in currentColor, the channel's ink. */
.hud-glyph { display: block; flex: none; color: var(--chan-ink, var(--hud-text)); overflow: visible; }
.hud-ch > .hud-glyph, .hud-composer__ch > .hud-glyph { filter: drop-shadow(var(--hud-lift)); transition: filter .15s, opacity .15s; }
.hud-ch > .hud-glyph { width: 18px; height: 18px; opacity: .55; }
.hud-ch:not(.is-off):hover > .hud-glyph { opacity: .85; }
.hud-ch.is-on > .hud-glyph { opacity: 1; filter: drop-shadow(0 0 3px var(--chan-ink)) drop-shadow(var(--hud-lift)); }
.hud-ch.is-off { cursor: var(--cur-arrow, default); }
.hud-ch.is-off > .hud-glyph { opacity: .35; color: var(--hud-dim); }
.hud-ch.has-new::after {
  content: ""; position: absolute; right: 2px; top: 1px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--hud-hp); box-shadow: 0 0 0 1px var(--border-hard);
}
.hud-chat__btn { width: 22px; height: 22px; }
.hud-chat__btn:hover > img.g, .hud-chatbtn:hover > img.g, .hud-composer__ch:hover > .hud-glyph { filter: drop-shadow(var(--hud-lift)) brightness(1.2); }
.hud-ch.is-down, .hud-chat__btn.is-down, .hud-chatbtn.is-down { transform: scale(.92); }

/* One palette per channel (00-tokens.css --chan-*): the ink a line's name, a lit tab's bar and the
   field's placeholder read in, the same colour each tab's picture is painted in. */
[data-tone="all"] { --chan-ink: var(--chan-all); }
[data-tone="nearby"], .hud-line--nearby { --chan-ink: var(--chan-nearby); }
[data-tone="world"], .hud-line--world { --chan-ink: var(--chan-world); }
[data-tone="trade"], .hud-line--trade { --chan-ink: var(--chan-trade); }
[data-tone="team"], .hud-line--team { --chan-ink: var(--chan-team); }
[data-tone="guild"], .hud-line--guild { --chan-ink: var(--chan-guild); }
[data-tone="private"], .hud-line--private { --chan-ink: var(--chan-private); }
[data-tone="system"], .hud-line--sys { --chan-ink: var(--chan-system); }

/* The log scrolls and shows that it does: a hairline thumb, faint brass, on no track (owner,
   2026-10-09). Chrome and Safari draw the 3px webkit bar (setting scrollbar-width there would switch
   it off); Firefox, which has no such bar, takes its own thinnest one. The floor is the first child's auto
   margin (a column packed to its end would leave the backlog out of reach of the scroll), and on a
   channel tab, where that child may be hidden, a spacer does it. */
.hud-log {
  height: var(--chat-log-h);
  padding: 2px 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* chat.ts keeps the reader's place itself when old lines are trimmed (hold); the browser's own
     anchoring would move it a second time. */
  overflow-anchor: none;
  /* A linked item's band, in the inks lifted for text on the world. */
  --rarity-purple-text: var(--world-q-purple);
  --rarity-gold-text: var(--world-q-gold);
  --rarity-yellow-text: var(--world-q-yellow);
  --rarity-blue-text: var(--world-q-blue);
  --rarity-green-text: var(--world-q-green);
}
.hud-log::-webkit-scrollbar { width: 3px; }
.hud-log::-webkit-scrollbar-track { background: transparent; }
.hud-log::-webkit-scrollbar-thumb { background: var(--chat-thumb); border-radius: 2px; }
.hud-log::-webkit-scrollbar-thumb:hover { background: var(--chat-thumb-hi); }
/* scrollbar-color inherits (the window body sets one): reset it, or Chrome ignores the bar above. */
@supports selector(::-webkit-scrollbar) { .hud-log { scrollbar-width: auto; scrollbar-color: auto; } }
@supports not selector(::-webkit-scrollbar) {
  .hud-log { scrollbar-width: thin; scrollbar-color: var(--chat-thumb) transparent; }
}
.hud-log[hidden] { display: none; }
.hud-log > :first-child { margin-top: auto; }
.hud-log[data-view]:not([data-view=""])::before { content: ""; margin-top: auto; }
.hud-log[data-view]:not([data-view=""]) > :first-child { margin-top: 0; }
/* A channel tab shows that channel's lines alone; All shows every one but Trade. Hidden, not dropped.
   EXCEPTION (owner, 2026-10-14): Trade lines stay off All so sell/buy spam does not drown the rest;
   they show on the Trade tab alone (chat.ts showView, and its unread/peek check). */
.hud-log[data-view="all"] > .hud-line--trade,
.hud-log[data-view="nearby"] > .hud-line:not(.hud-line--nearby),
.hud-log[data-view="world"] > .hud-line:not(.hud-line--world),
.hud-log[data-view="trade"] > .hud-line:not(.hud-line--trade),
.hud-log[data-view="team"] > .hud-line:not(.hud-line--team),
.hud-log[data-view="guild"] > .hud-line:not(.hud-line--guild) { display: none; }
/* Another of this page's characters' party or guild (chat.ts is-other): named on All, hidden on the
   Party and Guild tabs, which are the played character's own. */
.hud-log[data-view="team"] > .hud-line.is-other,
.hud-log[data-view="guild"] > .hud-line.is-other { display: none; }
.hud-line__for { display: none; margin-right: .3em; color: var(--hud-dim); font-size: var(--fs-xs); }
.hud-line.is-other .hud-line__for { display: inline; flex: none; }

/* A phone's messenger (owner, 2026-10-10; chat.ts lineEl): everyone else down the left behind their
   round face, a bubble holding the channel's picture and their name in the channel's ink over their
   words; my own down the right, the bubble alone. Each bubble is tinted in its channel's ink (mine
   deeper) and opens on the channel's picture, so the channel reads on every tab (owner, 2026-10-10). The clock stands beside the bubble, at its foot. A system
   line (the System tab) is no bubble: the clock, then the words. */
.hud-line {
  flex: none;
  display: flex;
  align-items: flex-end;
  gap: 5px;
  padding: 1px 8px 1px 2px;
  font-size: var(--fs-md); /* the composer's size: a line reads as it was typed (owner, 2026-10-10) */
  line-height: 1.3;
  color: var(--hud-text);
  text-shadow: var(--hud-shadow);
}
.hud-line.is-mine { flex-direction: row-reverse; }
/* The face: the paperdoll's head blown up about the face, as the HUD portrait does it. Positioned,
   so it is the containing block of headEl's absolute <i> and clips it. */
.hud-line__face {
  position: relative;
  flex: none;
  width: 26px;
  height: 26px;
  overflow: hidden;
  border-radius: 50%;
  background: var(--chat-face-bg);
  box-shadow: 0 0 0 1px var(--chat-face-ring);
  display: grid;
  place-items: center;
  font-size: var(--fs-xs);
  color: var(--hud-dim);
}
.hud-line__face.is-dark { background: var(--void); }
/* A speaker's face opens their menu (main.ts chatFaceMenu): the hand, and its ring lit on hover. */
.hud-line__face[data-from] { cursor: var(--cur-hand, pointer); }
:where(html.m-desktop) .hud-line__face[data-from]:hover { box-shadow: 0 0 0 1px var(--brass, var(--chat-face-ring)); }
.hud-line__face > i { transform: translateY(-11%) scale(1.38); transform-origin: 50% 36%; }
.hud-line__bubble {
  min-width: 0;
  max-width: 78%;
  padding: 3px 8px 4px;
  border-radius: 3px 10px 10px 10px;
  background: color-mix(in srgb, var(--chan-ink) 28%, var(--chat-bubble));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--chan-ink) 40%, transparent);
  overflow-wrap: anywhere;
}
.hud-line.is-mine .hud-line__bubble { border-radius: 10px 3px 10px 10px; background: color-mix(in srgb, var(--chan-ink) 45%, var(--chat-bubble)); }
/* The bubble's head: the channel's picture (every tab, owner 2026-10-10: where it was said), the
   [character] tag, the name. My own bubble has no name: its picture stands inline before the words. */
.hud-line__head { display: flex; align-items: center; gap: 4px; min-width: 0; font-size: var(--fs-sm); line-height: 1.35; }
.hud-line.is-mine .hud-line__face, .hud-line.is-mine .hud-line__who { display: none; }
.hud-line.is-mine .hud-line__head { display: inline-flex; vertical-align: -1px; margin-right: 5px; }
.hud-line.is-mine .hud-line__body { display: inline; }
.hud-line__ch { flex: none; width: 13px; height: 13px; filter: drop-shadow(var(--hud-lift)); }
.hud-line__who { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--chan-ink, var(--hud-text)); }
.hud-line__body { display: block; }
.hud-line__at { flex: none; padding-bottom: 2px; font-size: var(--fs-xs); color: var(--hud-dim); font-variant-numeric: tabular-nums; }
.hud-line--sys { align-items: baseline; padding-left: 4px; }
.hud-line--sys .hud-line__text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.hud-line--sys .hud-line__body { display: inline; color: var(--chan-system); }
.hud-line--sys .hud-line__at { min-width: 2.6em; padding-bottom: 0; }
.hud-line .chat-link { text-shadow: inherit; }

/* The composer: the send channel's picture (a press picks another), the field (no underline: it
   stands on its own band; owner, 2026-10-10), its placeholder naming where the line goes ("Say to
   World...") in that channel's ink, and the send glyph. On System it is shut: dim, and nothing in it answers. */
.hud-composer { position: relative; display: flex; align-items: center; gap: 6px; padding: 2px 2px 0 0; }
/* New lines below while the reader is further up (chat.ts `fresh`): a small pill over the log's
   foot, centred on the composer's top edge; a press goes down to them. */
.hud-chat__new {
  position: absolute; left: 50%; bottom: calc(100% + 6px); z-index: 2; translate: -50% 0;
  display: flex; align-items: center; gap: 3px; height: 22px; padding: 0 6px 0 10px;
  border: 1px solid rgba(var(--brass-rgb), .55); border-radius: 11px;
  background: var(--chat-bubble); backdrop-filter: blur(3px);
  font: inherit; font-size: var(--fs-sm); color: var(--hud-text); text-shadow: var(--hud-shadow); white-space: nowrap;
  box-shadow: 0 2px 6px rgba(var(--shadow-ink), .6); cursor: var(--cur-hand, pointer);
}
.hud-chat__new[hidden] { display: none; }
.hud-chat__new > .hud-glyph { width: 14px; height: 14px; }
.hud-chat__new:hover { border-color: var(--hud-gold); }
.hud-composer__ch > .hud-glyph { width: 18px; height: 18px; }
/* Size, pop and send: Lucide glyphs in the HUD's text ink (chat.ts). */
.hud-chat__btn > .hud-glyph--btn { width: 18px; height: 18px; color: var(--hud-text); filter: drop-shadow(var(--hud-lift)); transition: filter .15s, opacity .15s; }
.hud-send > .hud-glyph--btn { width: 20px; height: 20px; }
/* The chat window's put-back (its head's close): a stroked glyph, never the .ic fill, flush with
   the right edge of the body's content (the button keeps its width to press; Lucide draws inside
   2..22 of its 24, so the last 2/24 is taken back too). */
.win__close:has(> .hud-glyph) { place-items: center end; }
.win__close > .hud-glyph { fill: none; color: var(--ink-strong); margin-right: calc(-1.75em * 2 / 24); }
.hud-chat__btn:hover > .hud-glyph--btn { filter: drop-shadow(var(--hud-lift)) brightness(1.2); }
.hud-composer.is-shut .hud-composer__ch, .hud-composer.is-shut .hud-send { cursor: var(--cur-arrow, default); }
.hud-composer.is-shut .hud-composer__ch > .hud-glyph, .hud-composer.is-shut .hud-send > .hud-glyph { opacity: .35; filter: grayscale(1) drop-shadow(var(--hud-lift)); }
.hud-composer .chat-input:disabled { cursor: var(--cur-arrow, default); }
.hud-send { width: 26px; height: 26px; }
.hud-composer .chat-input {
  flex: 1;
  min-width: 0;
  height: 26px;
  padding: 0 2px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  font: inherit;
  font-size: var(--fs-md);
  color: var(--hud-text);
  text-shadow: var(--hud-shadow);
  cursor: var(--cur-text, text);
}
.hud-composer .chat-input::-webkit-search-decoration, .hud-composer .chat-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.hud-composer .chat-input::placeholder { color: var(--chan-ink, var(--hud-dim)); opacity: .6; }
.hud-composer .chat-input:focus { outline: 0; }

/* The mobile mode's round chat button is built always and shown in mobile mode only. */
.hud-chatbtn { display: none; }

/* THE CHAT IN A WINDOW: the same nodes in 20-components.css's window, pulled to size by its corner; the log
   grows with it. */
.wins > .win.hud-chatwin {
  height: 22em;
  min-width: 17em;
  min-height: 12em;
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.wins > .win.hud-chatwin > .win__body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: .45em .7em .6em;
  font-family: var(--face-ui);
  font-weight: 600;
  color: var(--hud-text);
}
/* No engraved inner plane inside the chat's window: one flat face. Its lines are painted in
   --win-plane, so clearing the colour drops them and leaves the rim's corners to 20-components.css. */
.wins > .win.hud-chatwin { --win-plane: transparent; }
.hud-chatwin .hud-log { flex: 1; height: auto; min-height: 0; }
.hud-chatwin .hud-chans { flex: none; }
@media (prefers-reduced-motion: reduce) {
  .hud-chat, .hud-ch, .hud-chat__btn, .hud-chatbtn, .hud-ch > .hud-glyph, .hud-composer__ch > .hud-glyph, .hud-chat__btn > img.g, .hud-chatbtn > img.g { transition: none; }
  .hud-ch.is-down, .hud-chat__btn.is-down, .hud-chatbtn.is-down { transform: none; }
}

/* --------------------------------------------------------------- MOBILE MODE */
/* ONE LANDSCAPE LAYOUT for every phone (667 x 375 to 894 x 392 alike, B50), drawn at the size the
   player picked: your block top left with the party under it; the minimap top right with the tile
   and the status line under it; ONE menu tile left of the map, the run's reward beside it; the hotbar's dial in the bottom-right corner (THE DIAL below),
   the switch fan under the minimap, the chat's button on the strip at the bottom centre. The systems, the boards and
   the quest tracker stand in the menu's drawer, slid in from the right edge (gui/hud/menu.ts mountDrawer). The
   blocks stand --hud-edge in from the safe frame's edges (THE ROOT above). */
:where(html.m-mobile) .hud {
  /* THE DIAL's sizes (below): the round slot, the inner and the outer ring's radius through the
     slots' centres, and a toggle at a finger's size in HUD px (the HUD is zoomed). */
  --dial-s: 44px;
  --dial-ri: 120px;
  --dial-ro: 174px;
  --dial-t: calc(var(--touch) / var(--hud-z));
  /* The systems' row stands this far up off the bottom: the bottom strip's plate is under its middle. */
  --hud-strip-lift: 14px;
}
:where(html.m-mobile) .hud .hud-left { left: var(--hud-edge); top: var(--hud-edge); width: auto; }
:where(html.m-mobile) .hud .hud-selfcol { flex: 0 0 214px; }
/* Your block a size down (owner, 2026-10-11: too big on a phone): a smaller face, a narrower and
   slimmer frame, the header a step smaller; the figures stay in the bars. */
:where(html.m-mobile) .hud-self .hud-self__block { --hud-pic: 44px; --hud-pad-l: 11px; column-gap: 8px; padding: 8px 6px 5px var(--hud-pad-l); }
:where(html.m-mobile) .hud-self .hud-portrait { width: 44px; height: 44px; padding: 3px; }
:where(html.m-mobile) .hud-self .hud-self__ticket { --ico-box: 20px; left: -7px; bottom: -7px; }
:where(html.m-mobile) .hud-self .hud-self__gauges { --tuck: 18px; --tip: 7px; gap: 2px; padding-top: 3px; padding-bottom: 3px; }
:where(html.m-mobile) .hud-self .hud-self__gauges > .hud-self__id :is(.hud-self__lvl, .hud-self__name) { font-size: var(--fs-sm); }
:where(html.m-mobile) .hud-self .hud-self__gauges .hud-vital__bar { height: 11px; }
:where(html.m-mobile) .hud-self .hud-vital__k { left: 5px; }
/* The team: a face, a name and two thin gauges, the figures left to the fight. Every teammate is
   on the glass, never scrolled (owner, 2026-10-11: the fourth was cut off): a full party is five,
   so four rows (panels/party.ts), each this tight so the four end above the stick. */
:where(html.m-mobile) .hud-party .hud-party__frames { gap: 1px; }
:where(html.m-mobile) .hud-party .hud-party__row { --hud-pic: 26px; --hud-pad-l: 10px; column-gap: 7px; row-gap: 0; padding: 1px 4px 1px var(--hud-pad-l); }
:where(html.m-mobile) .hud-party__row .hud-portrait--sm { width: 26px; height: 26px; padding: 1px; }
:where(html.m-mobile) .hud-party__row .hud-self__id { min-height: 0; margin-bottom: 1px; line-height: 1; }
:where(html.m-mobile) .hud-party__row .hud-self__lvl, :where(html.m-mobile) .hud-party__row .hud-self__name { font-size: var(--fs-xs); }
:where(html.m-mobile) .hud-party__row .hud-vital + .hud-vital { margin-top: 1px; }
:where(html.m-mobile) .hud-party__row .hud-vital__bar { height: 4px; width: 84px; flex: none; }
:where(html.m-mobile) .hud-party__row .hud-vital__v, :where(html.m-mobile) .hud-party__row .hud-vital__k { display: none; }
:where(html.m-mobile) .hud-party__row .hud-buffs { display: none; }
:where(html.m-mobile) .hud-party .hud-party__gear { width: 40px; height: 40px; margin-top: 0; }
:where(html.m-mobile) .hud .hud-mapcol { right: var(--hud-edge); top: var(--hud-edge); }
:where(html.m-mobile) .hud .hud-primary { right: calc(var(--hud-edge) + var(--map-w, 134px) + 8px); top: var(--hud-edge); }
:where(html.m-mobile) .hud .hud-primary { flex-direction: row-reverse; align-items: flex-start; }
:where(html.m-mobile) .hud-primary .hud-reward { min-width: 0; max-width: 160px; }
/* The one menu (owner, 2026-10-11: every icon menu slides in from the right): its tile left of the
   map, and the drawer. The rows the tiles stand in on a desktop are empty here, and hidden. */
:where(html.m-mobile) .hud .hud-primary-menu, :where(html.m-mobile) .hud .hud-menu { display: none; }
:where(html.m-mobile) .hud .hud-menubtn { display: flex; flex: none; width: 46px; height: 46px; }
/* The drawer: full height on the right edge on a flat, see-through night (owner, 2026-10-11: no
   gradient), a brass hairline down its left edge, its X top right; the tiles four to a row, each
   named under it (a finger has no card to read). Shut, it waits off the edge. Its night reaches the
   screen's edge (owner, 2026-10-11): a .bleed, its tiles padded back inside the safe area (screen
   px, divided out of the zoom). */
:where(html.m-mobile) .hud .hud-drawer {
  display: flex; flex-direction: column; left: auto; right: 0; top: 0; bottom: 0; z-index: 5;
  padding: calc(var(--safe-t) / var(--hud-z) + 8px) calc(var(--safe-r) / var(--hud-z) + 12px)
    calc(var(--safe-b) / var(--hud-z) + 14px) 14px;
  overflow: hidden;
  background: rgba(var(--hud-night-rgb), .82);
  box-shadow: inset 1px 0 rgba(var(--hud-gold-rgb), .4);
  transform: translateX(100%); visibility: hidden; pointer-events: none;
  transition: transform .2s ease-out, visibility 0s linear .2s;
}
:where(html.m-mobile) .hud .hud-drawer.is-open { transform: none; visibility: visible; pointer-events: auto; transition: transform .2s ease-out; }
.hud-drawer__head { display: flex; justify-content: flex-end; margin-bottom: 4px; }
.hud-drawer__close {
  appearance: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 0;
  background: none; color: var(--hud-text); cursor: var(--cur-hand, pointer); opacity: .9;
}
.hud-drawer__close > * { width: 24px; height: 24px; transition: filter .15s, transform .1s; }
.hud-drawer__close:hover > *, .hud-drawer__close:focus-visible > * { filter: brightness(1.15) drop-shadow(0 0 4px rgba(var(--sky-rgb), .75)); }
.hud-drawer__close:active > * { transform: scale(.9); }
.hud-drawer__grid { display: grid; grid-template-columns: repeat(4, 62px); gap: 20px 4px; justify-items: center; padding-bottom: 16px; }
.hud-drawer__grid .hud-ibtn { width: 46px; height: 46px; }
.hud-drawer__grid .hud-ibtn::after {
  content: attr(aria-label); position: absolute; left: 50%; top: calc(100% + 1px); max-width: 62px;
  transform: translateX(-50%); overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-family: var(--hud-face); font-size: var(--fs-xs); font-weight: 600; line-height: 1.2;
  color: var(--hud-text); text-shadow: var(--hud-shadow); pointer-events: none;
}
.hud-drawer__grid .hud-ibtn.is-open::after { color: var(--hud-gold); }
@media (prefers-reduced-motion: reduce) { :where(html.m-mobile) .hud .hud-drawer, :where(html.m-mobile) .hud .hud-drawer.is-open { transition: none; } }
/* The switch under the minimap, at its right edge (owner, 2026-10-11; index.ts seats it in the map
   column, which is not zoomed, so it takes the HUD's zoom itself). The fan still unrolls to its
   left, across the open world under the menu tile; the gap above leaves room for the gold name. */
:where(html.m-mobile) .hud .hud-mapcol > .hud-switch {
  position: relative; left: auto; top: auto; right: auto; bottom: auto;
  align-self: flex-end; flex: none; margin: 14px 0 0; zoom: var(--hud-z);
}
/* The smaller map's corners a size down. */
:where(html.m-mobile) .hud-clock, :where(html.m-mobile) .hud-coord { top: 3px; padding: 1px 4px; font-size: var(--fs-sm); }
:where(html.m-mobile) .hud-clock { right: 3px; }
:where(html.m-mobile) .hud-coord { left: 3px; }
:where(html.m-mobile) .hud-map > .hud-conn { left: 3px; bottom: 3px; padding: 1px 4px; gap: 4px; font-size: var(--fs-xs); }
/* The tracker under the drawer's tiles (index.ts moves it), as wide as them, on the drawer's own
   night with a brass hairline over it. The drawer is zoomed already, so the tracker takes no zoom of
   its own; its text is still --plate-fs. Its head says it folds (minus out, plus folded). It takes
   the drawer's height under the tiles, and a long list scrolls inside it on the brass bar
   (10-base.css), the head and the tiles standing still (owner, 2026-10-11). */
:where(html.m-mobile) .hud-drawer .hud-track {
  zoom: 1; width: calc(4 * 62px + 3 * 4px); padding: 8px 2px 4px; gap: 3px;
  flex: 0 1 auto; min-height: 0;
  background: none; box-shadow: inset 0 1px rgba(var(--hud-gold-rgb), .4);
}
:where(html.m-mobile) .hud-drawer .hud-track__head { flex: none; }
:where(html.m-mobile) .hud-drawer .hud-track__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-right: 4px; }
:where(html.m-mobile) .hud-drawer .hud-drawer__head, :where(html.m-mobile) .hud-drawer .hud-drawer__grid { flex: none; }
:where(html.m-mobile) .hud-track .hud-track__head { gap: 4px; }
/* The fold mark stands where the target mark does. */
:where(html.m-mobile) .hud-track .hud-track__mark { display: none; }
:where(html.m-mobile) .hud-track .hud-track__head::before { width: 14px; height: 14px; }
:where(html.m-mobile) .hud-track .hud-track__head::after { content: ""; flex: none; width: 11px; height: 11px; margin-left: auto; background: url(/data/art/ui/ui-minus.66b1c0e7.webp) center / contain no-repeat; opacity: .75; }
:where(html.m-mobile) .hud-track.is-folded .hud-track__head::after { background-image: url(/data/art/ui/ui-plus.bdb08960.webp); }
:where(html.m-mobile) .hud-track .hud-track__obj { gap: 4px; }
:where(html.m-mobile) .hud-track .hud-track__ic, :where(html.m-mobile) .hud-track .hud-track__walk > img.g { width: 12px; height: 12px; }
/* The Attack pet is its face alone in its own portrait ring, on your portrait's centre line (the
   ringed face was 52 px at 3 px down; the bare one is 36, so 11 down), a size down with your block
   (owner, 2026-10-11: no health and mana rings round it, the tap shows them). A tap on the face
   turns its rows beside it on or off, and they stay until the next tap (left.ts .is-open): the same frame as your own block - level and name over health
   and mana in figures - and its marks under it. */
:where(html.m-mobile) .hud .hud-pet { flex: none; max-width: none; margin-top: 11px; position: relative; }
:where(html.m-mobile) .hud-pet .hud-self__rows { display: none; }
:where(html.m-mobile) .hud-pet.is-open .hud-self__rows {
  display: flex; flex-direction: column; gap: 2px; position: absolute; z-index: 0;
  left: calc(100% - 2px); top: 50%; width: 150px; transform: translateY(-50%);
}
:where(html.m-mobile) .hud-pet.is-open .hud-self__gauges { --tuck: 0px; padding-left: 7px; }
:where(html.m-mobile) .hud-pet.is-open .hud-buffs { margin-top: 0; }
:where(html.m-mobile) .hud-pet .hud-portrait { width: 36px; height: 36px; cursor: var(--cur-hand, pointer); }
:where(html.m-mobile) .hud-pet .hud-self__block--sm { padding: 0; gap: 0; }

/* THE DIAL: the bars in mobile mode (owner, 2026-10-11; gui/hud/bar.ts dial()). A quarter dial
   centred on the bottom-right corner: the inner ring is your twelve slots, the outer ring the pet's
   six (gone while no pet is out; owner, 2026-10-11: you under the thumb, the pet outside), round
   slots on their own (owner, 2026-10-14: no bands behind them), about three (inner) and four (outer) in the quarter and the rest past the
   edges. The root is a box on the corner; each ring is a disc round
   the corner (the disc is its hit box, the inner one over the outer), a slot stands on its ring at
   a0 + i * step, turned there and back so its art stays upright. THE TURN IS A SCROLL: each ring
   holds a hidden track whose scroll offset is the turn (0 = the first slot at the start, the end =
   the last slot at the end), and every slot's place is a transform animation on that track's
   timeline, so a drag writes one scroll offset a frame and the compositor moves the slots: no
   restyle, no layout, no paint (bar.ts dial(): a custom property restyled every slot's insides).
   The snap is a smooth scroll. A browser without scroll-driven animations reads --rot instead,
   with a 120 ms transition for the snap. Auto and Fly stand in the hub inside the inner ring, under the
   thumb; the pet's two switches just left of the dial's foot. The bars' desktop rows and the pet
   guide are not drawn here. */
.hud .hud-dial { display: none; }
:where(html.m-mobile) .hud .hud-bar, :where(html.m-mobile) .hud .hud-petbar { display: none; }
:where(html.m-mobile) .hud .hud-dial {
  /* ON THE FRAME'S CORNER, clipped by its own box: none of a ring's slots stands beside a notch or
     over the home bar. Not by the frame: Safari draws the slots, moved by the scroll timeline on the
     GPU, past the frame's clip (iPhone, 2026-10-15). The box reaches the outer ring's top and the
     pet's switches; it takes no press itself. */
  display: block; left: auto; top: auto; right: 0; bottom: 0;
  width: calc(var(--dial-ro) + var(--dial-s) / 2 + 30px + 2 * var(--dial-t));
  height: calc(var(--dial-ro) + var(--dial-s) / 2 + 8px);
  overflow: hidden; pointer-events: none; --hud-slot: var(--dial-s);
}
:where(html.m-mobile) .hud .hud-dial > * { pointer-events: auto; }
.hud-dial__ring {
  /* --step and --span (the turn from the first slot to the last) are bar.ts's, written once. */
  --r: var(--dial-ro); --a0: 12deg; --rot: 0deg;
  --d: calc(2 * (var(--r) + var(--dial-s) / 2 + 6px));
  timeline-scope: --hud-dial;
  position: absolute; left: calc(100% - var(--d) / 2); top: calc(100% - var(--d) / 2); width: var(--d); height: var(--d);
  border-radius: 50%; touch-action: none; will-change: transform;
}
.hud-dial__ring--in { --r: var(--dial-ri); --a0: 15deg; --d: calc(2 * (var(--r) + var(--dial-s) / 2 + 5px)); }
/* A drag past an end pulls the ring (bar.ts writes its rotate); it springs back on release. */
.hud-dial__ring.is-back { transition: transform .18s ease-out; }
/* The ends: while slots wait past one, a brass rail along the ring (between the rings for the inner
   one, outside the outer one) and a thumb on it that moves with the turn: a curved scrollbar, start
   at the low end of the arc. The thumb rides the slots' own scroll timeline; without one it stays
   hidden and the rail alone says the ring turns. */
.hud-dial__rail, .hud-dial__thumb { position: absolute; pointer-events: none; opacity: 0; transition: opacity .2s linear; }
.hud-dial__ring { --rail: calc(var(--r) + var(--dial-s) / 2 + 5px); }
.hud-dial__rail {
  left: calc(50% - var(--rail)); top: calc(50% - var(--rail)); width: calc(2 * var(--rail)); height: calc(2 * var(--rail));
  border-radius: 50%; box-shadow: inset 0 0 0 1.5px rgba(var(--hud-gold-rgb), .3), 0 0 0 1px rgba(var(--shadow-ink), .25);
}
.hud-dial__thumb {
  --t: 10deg; left: calc(50% - 2.5px); top: calc(50% - 9px); width: 5px; height: 18px; border-radius: 3px;
  background: rgba(var(--hud-gold-rgb), .95); box-shadow: 0 0 4px rgba(var(--shadow-ink), .9);
  transform: rotate(var(--t)) translateX(calc(-1 * var(--rail)));
}
.hud-dial__ring:is(.has-before, .has-after) > :is(.hud-dial__rail, .hud-dial__thumb) { opacity: .9; }
@supports (animation-timeline: scroll()) {
  .hud .hud-dial__thumb { animation: hud-dial-thumb linear both; animation-timeline: --hud-dial; }
}
@supports not (animation-timeline: scroll()) { .hud .hud-dial__thumb { display: none; } }
@keyframes hud-dial-thumb {
  from { transform: rotate(10deg) translateX(calc(-1 * var(--rail))); }
  to { transform: rotate(80deg) translateX(calc(-1 * var(--rail))); }
}
@media (prefers-reduced-motion: reduce) { .hud-dial__ring.is-back, .hud-dial__rail, .hud-dial__thumb { transition: none; } }
.hud-dial__track {
  position: absolute; left: 0; top: 0; width: 10px; height: 1px; overflow: hidden;
  visibility: hidden; pointer-events: none; scroll-timeline: --hud-dial x;
}
.hud-dial__track > i { display: block; width: 1010px; height: 1px; }
.hud .hud-dial__ring > .slot {
  --b: calc(var(--a0) + var(--i) * var(--step));
  --a: calc(var(--b) + var(--rot));
  position: absolute; left: calc(50% - var(--dial-s) / 2); top: calc(50% - var(--dial-s) / 2);
  transform: rotate(var(--a)) translateX(calc(-1 * var(--r))) rotate(calc(-1 * var(--a)));
  will-change: transform;
  border-radius: 50%; overflow: hidden;
}
@supports (animation-timeline: scroll()) {
  .hud .hud-dial__ring > .slot { animation: hud-dial-turn linear both; animation-timeline: --hud-dial; }
}
@keyframes hud-dial-turn {
  from { transform: rotate(var(--b)) translateX(calc(-1 * var(--r))) rotate(calc(-1 * var(--b))); }
  to { transform: rotate(calc(var(--b) + var(--span))) translateX(calc(-1 * var(--r))) rotate(calc(-1 * (var(--b) + var(--span)))); }
}
.hud-dial__ring.is-snap > .slot { transition: transform .12s ease-out; }
@media (prefers-reduced-motion: reduce) { .hud-dial__ring.is-snap > .slot { transition: none; } }
/* Round: a brass hairline in place of the square frame art, the default's light a round rim, the
   count at the crown (the corners are cut off). No key caps (owner, 2026-10-11): a phone has no
   number keys. */
.hud .hud-dial .slot::before { background: none; border-radius: 50%; box-shadow: inset 0 0 0 1.5px rgba(var(--hud-gold-rgb), .9); }
/* The round rim's light is the arc drawn once as a ring picture and turned whole, with no mask: the
   conic under a ring mask was an offscreen pass every frame on a Mali phone (2026-10-14 trace, see
   .hud-strip__star). Turning a ring is the same travelling light, which the square bar cannot do. */
.hud .hud-dial .slot > .lit { -webkit-mask: none; mask: none; padding: 0; }
/* The round clip only while a cover shows (cooldown, turn clock): the turning light under a rounded
   clip was an offscreen pass every frame (2026-10-14 trace). The art rounds itself instead. */
.hud .hud-dial__ring > .slot:not(:has(> .tc:not([hidden]), > .cd:not([hidden]))) { overflow: visible; }
.hud .hud-dial .slot > .ico { border-radius: 50%; overflow: hidden; }
.hud .hud-dial .slot > .lit::before { inset: 0; border-radius: 50%; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3Ccircle id='c' cx='50' cy='50' r='45.5' pathLength='100'/%3E%3C/defs%3E%3Cg fill='none' stroke='%23f4e6c4' stroke-width='9' stroke-dasharray='2.2 97.8' transform='rotate(-90 50 50)'%3E%3Cuse href='%23c' stroke-opacity='0.05' stroke-dashoffset='-55'/%3E%3Cuse href='%23c' stroke-opacity='0.1' stroke-dashoffset='-57'/%3E%3Cuse href='%23c' stroke-opacity='0.14' stroke-dashoffset='-59'/%3E%3Cuse href='%23c' stroke-opacity='0.19' stroke-dashoffset='-61'/%3E%3Cuse href='%23c' stroke-opacity='0.24' stroke-dashoffset='-63'/%3E%3Cuse href='%23c' stroke-opacity='0.38' stroke-dashoffset='-65'/%3E%3Cuse href='%23c' stroke-opacity='0.51' stroke-dashoffset='-67'/%3E%3Cuse href='%23c' stroke-opacity='0.65' stroke-dashoffset='-69'/%3E%3Cuse href='%23c' stroke-opacity='0.78' stroke-dashoffset='-71'/%3E%3Cuse href='%23c' stroke-opacity='0.85' stroke-dashoffset='-73'/%3E%3Cuse href='%23c' stroke-opacity='0.93' stroke-dashoffset='-75'/%3E%3Cuse href='%23c' stroke-opacity='1' stroke-dashoffset='-77' stroke='%23fffaf0'/%3E%3Cuse href='%23c' stroke-opacity='0.93' stroke-dashoffset='-79' stroke='%23fffaf0'/%3E%3Cuse href='%23c' stroke-opacity='0.85' stroke-dashoffset='-81'/%3E%3Cuse href='%23c' stroke-opacity='0.78' stroke-dashoffset='-83'/%3E%3Cuse href='%23c' stroke-opacity='0.65' stroke-dashoffset='-85'/%3E%3Cuse href='%23c' stroke-opacity='0.51' stroke-dashoffset='-87'/%3E%3Cuse href='%23c' stroke-opacity='0.38' stroke-dashoffset='-89'/%3E%3Cuse href='%23c' stroke-opacity='0.24' stroke-dashoffset='-91'/%3E%3Cuse href='%23c' stroke-opacity='0.18' stroke-dashoffset='-93'/%3E%3Cuse href='%23c' stroke-opacity='0.12' stroke-dashoffset='-95'/%3E%3Cuse href='%23c' stroke-opacity='0.06' stroke-dashoffset='-97'/%3E%3C/g%3E%3C/svg%3E") center / 100% 100% no-repeat; }
.hud .hud-dial .slot > .k { display: none; }
.hud .hud-dial .slot > .ct { right: auto; left: 50%; bottom: auto; top: 3px; translate: -50% 0; }
.hud .hud-dial__ring--out .slot.is-empty::after { background-image: url(/data/art/ui/slot-paw.94060037.webp); }
.hud-dial__hub { position: absolute; left: 100%; top: 100%; }
.hud .hud-dial .hud-tgl { width: var(--dial-t); height: var(--dial-t); }
.hud .hud-dial__hub > .hud-tgl {
  --r: calc(var(--dial-ri) - var(--dial-s) / 2 - 3px - var(--dial-t) / 2);
  position: absolute; left: calc(var(--dial-t) / -2); top: calc(var(--dial-t) / -2);
  transform: rotate(var(--a)) translateX(calc(-1 * var(--r))) rotate(calc(-1 * var(--a)));
}
.hud .hud-dial__hub > .hud-tgl.is-down { scale: .94; }
.hud-dial__hub > .hud-tgl--auto { --a: 22deg; }
.hud-dial__hub > .hud-tgl--fly { --a: 68deg; }
.hud-dial__hub.is-solo > .hud-tgl--auto { --a: 45deg; }
.hud-dial__pet { position: absolute; right: calc(var(--dial-ro) + var(--dial-s) / 2 + 10px); bottom: 8px; }

/* The chat in mobile mode is the round button at the bottom centre, standing on the strip just left
   of its plate (owner, 2026-10-11: beside the experience and energy, the bottom-left corner is the
   stick's); the menu tile's look: a painting, no well. Lit while the chat is out and marked when a
   line lands behind it; the chat stands centred over the strip, narrower and shorter, and folded is
   gone. Out, it wears its frame the whole time: a phone has no hover, and the chat was opened to be
   read. The plate is about 200px wide in every locale, so the button clears its half and a margin
   for a longer figure. */
:where(html.m-mobile) .hud .hud-chatbtn { display: flex; left: calc(50% - 112px - 44px); bottom: var(--hud-edge); width: 44px; height: 44px; z-index: 3; }
:where(html.m-mobile) .hud-chatbtn.is-on > img.g { filter: drop-shadow(0 0 4px var(--hud-gold)) drop-shadow(var(--hud-lift)); }
:where(html.m-mobile) .hud-chatbtn.has-new::after {
  content: ""; position: absolute; right: 3px; top: 3px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--hud-hp); box-shadow: 0 0 0 1px var(--border-hard);
}
:where(html.m-mobile) .hud .hud-chat { left: 50%; bottom: calc(var(--hud-edge) + 50px); transform: translateX(-50%); width: min(270px, 40vw); --chat-log-h: 84px; }
:where(html.m-mobile) .hud .hud-chat[data-size="m"] { --chat-log-h: 140px; }
:where(html.m-mobile) .hud .hud-chat[data-size="l"] { --chat-log-h: min(220px, 40dvh); }
:where(html.m-mobile) .hud .hud-chat.is-folded { display: none; }
:where(html.m-mobile) .hud-chat .hud-ch { width: 26px; height: 32px; }
/* CHAT PEEK (gui/hud/chat.ts): the last four said lines, bottom centre where the chat opens, over
   the chat button (owner, 2026-10-11), on the chat box's own half-strength face and ring so it reads as a corner of the same
   chat. "Name:" in its channel's ink, then the words, one ellipsised line each. One button that
   opens the chat; nothing inside is its own target. Ten seconds without a line it fades out
   (.is-idle) and takes no presses. Built always, shown in mobile mode only. */
.hud-chatpeek { display: none; }
:where(html.m-mobile) .hud .hud-chatpeek:not([hidden]) {
  display: grid; justify-items: start; gap: 1px; position: absolute; left: 50%; top: auto; bottom: calc(var(--hud-edge) + 50px); transform: translateX(-50%); z-index: 3;
  width: min(36vw, 26em); padding: 5px 9px 6px; isolation: isolate; border: 0; border-radius: 8px;
  box-shadow: var(--win-ring); background: none; color: var(--hud-text); text-align: left;
  font: 600 var(--fs-sm)/1.3 var(--face-ui); cursor: var(--cur-hand, pointer);
  transition: opacity .4s ease-out, visibility 0s;
}
:where(html.m-mobile) .hud-chatpeek::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background: var(--chat-bg); opacity: .5; pointer-events: none;
}
/* The open chat stands where the peek does, and shows the same lines. */
:where(html.m-mobile) .hud:has(.hud-chat:not(.is-folded):not([hidden])) .hud-chatpeek { display: none; }
:where(html.m-mobile) .hud .hud-chatpeek.is-idle { opacity: 0; visibility: hidden; transition: opacity .4s ease-out, visibility 0s .4s; }
.hud-chatpeek * { pointer-events: none; }
.hud-chatpeek__line { max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-shadow: var(--hud-shadow); }
.hud-chatpeek__line > b { font-weight: 700; margin-right: .3em; color: var(--chan-ink, var(--hud-text)); }
.hud-chatpeek__line.is-mine { justify-self: end; }
@media (prefers-reduced-motion: reduce) { .hud .hud-chatpeek, .hud .hud-chatpeek.is-idle { transition: none; } }
:where(html.m-mobile) .hud-chat .hud-chat__btn { width: 30px; height: 30px; }
:where(html.m-mobile) .hud-chat .hud-composer .chat-input { height: 30px; }
:where(html.m-mobile) .hud-line__face { width: 22px; height: 22px; }

/* ---- 45-plates.css */
/* THE PLATE LAYER: the DOM over the world. The world's own nameplates - a character's name, a
   monster's, an NPC's and a pet's name, the costume and season boards, the fight mark - are not
   here: glplates.ts draws them on the canvas. What is left is what takes the pointer or is one of
   a few: the fight's plates (combat.ts, built by plate.ts buildPlate), the shop board over a
   trading body and your own gathering bar (main.ts stallBoardOn / gatherSwing), both placed on
   top of the body's drawn plate. Not a surface the windows dress: there is no face behind it, the
   background is whatever the world is painting.

   PX, and only here (with 50-battle.css): main.ts and combat.ts place at raw toScreen() pixels
   over a body whose size is a fact about the camera; the layer is never HUD-scaled.
   `html.world-far` is the phone camera (main.ts resize), which stands back: everything comes down
   with its bodies.

   A fight plate is the world's look in DOM: the colour of the NAME says what the body is to you,
   the level trails it in the quiet gold. */

/* Never scaled; z 0 so a plate paints under an open window instead of over it. */
.hud-plates { inset: 0; pointer-events: none; zoom: 1; z-index: var(--z-plates, 0); }
/* The plate layer goes with the swirl and with the wait before the arena is staged (main.ts). */
.hud-plates.cb-swap { opacity: 0; transition: opacity 120ms linear; }

/* Any text sitting directly on the world carries this; text inside a window does not. */
.gs-on-world { text-shadow: var(--world-shadow); }

.gs-plate { width: 92px; text-align: center; }
/* Placed by plate.ts placePlate with a transform from the layer's corner, on its own compositor
   layer, so walking and fighting never lay out or repaint it. `width: auto` so a long name never
   wraps; 92px stays the minimum, and it is the width every bar under the name is fixed to. */
.hud-plates .gs-plate {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform;
  transform: translate(-50%, -100%);
  width: auto;
  min-width: 92px;
  white-space: nowrap;
}
.gs-plate__id { display: flex; align-items: center; justify-content: center; }
.gs-plate__lines { min-width: 0; }
/* The name row: the nickname and the level on one baseline, in the UI sans like every name
   (owner, 2026-10-08). 600: the one weight shipped. The four-sided rim carries the row over grass and snow. */
.gs-plate__name {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 4px;
  font-family: var(--face-ui);
  font-size: var(--plate-fs);
  font-weight: 600;
  color: var(--gs-plate-ink, var(--plate-ink));
  text-shadow: var(--plate-name-shadow);
}
/* The level AFTER the name ("Lv. 85", owner 2026-10-10), same face and size, in the world's gold:
   a fact about the body, not part of its name, and never the name's own hue. */
.gs-plate__lvl { color: var(--world-gold); font-weight: 600; }
.gs-plate__lvl[hidden] { display: none; }

/* The shop board over a trading body (main.ts stallBoardOn): a small paper sign in the world
   voice, and a BUTTON - pressing it walks up to the counter. The one thing in the layer that takes
   the pointer outside a fight. Its own compositor layer, placed by placePlate on top of the body's
   drawn plate. Spelled under .hud-plates: `.hud button` outranks a lone class. */
.hud-plates .gs-plate__stall {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform;
  display: block;
  max-width: 150px;
  padding: 1px 8px 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border: 1px solid var(--plate-rim);
  border-radius: 5px;
  font-family: var(--face-ui);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--plate-stall-ink);
  text-shadow: none;
  background: var(--plate-ink);
  box-shadow: var(--plate-lift);
  pointer-events: auto;
  cursor: var(--cur-hand, pointer);
}
.hud-plates .gs-plate__stall:hover { background: var(--plate-stall-hover); }

/* Who this body is to the reader, one token per kind, used by the name and the bar's ring. The
   target is said twice so it beats the plain kinds on the plate about to be hit. */
.gs-plate--ally, .gs-plate--player { --gs-plate-ink: var(--plate-ink); }
.gs-plate--hostile { --gs-plate-ink: var(--plate-foe); }
.gs-plate.is-targeted,
.hud-plates .gs-plate.is-targeted { --gs-plate-ink: var(--gold-bright); }

/* THE PLATE'S BAR (a fight's health, combat.ts): the game's one bar shape at a plate's height. The
   fill is full width drawn through scaleX, so a blow is a compositor animation, never a layout. */
.plate-bar {
  position: relative;
  height: 6px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--bar-track);
  border: var(--border-width, 1px) solid var(--border-hard);
}
.plate-bar__fill {
  position: absolute;
  inset: 0;
  border-radius: 2px;
  transform-origin: left;
  background: var(--bar-wash), var(--ember-500);
  box-shadow: inset 0 1px 0 0 var(--ember-400);
  transition: transform 180ms ease-out;
}
.gs-plate--ally .plate-bar__fill { background: var(--bar-wash), var(--moss-500); box-shadow: inset 0 1px 0 0 var(--moss-400); }
.gs-plate.is-targeted .plate-bar { box-shadow: inset 0 0 0 1px var(--gold-bright); }

/* The gathering cycle over your own head (main.ts gatherSwing): gui/c/marks.ts `bar` in gold, a
   clock counting up, not a pool draining. Fixed 92px, placed by placePlate on top of your plate,
   never a readout of name length. */
.hud-plates .gs-plate__siege { position: absolute; left: 0; top: 0; will-change: transform; width: 92px; height: 6px; }
.gs-plate__siege > .bar__fill { transition: none; }

/* THE FAR CAMERA (a phone, main.ts resize): bodies are half a desktop's height. Names come down
   through --plate-fs (10px), the shop board a size down with them. */
html.world-far .hud-plates .gs-plate__stall { max-width: 96px; padding: 0 5px 1px; border-radius: 3.5px; font-size: 9px; }

/* ---- 50-battle.css */
/* THE FIGHT'S DOM (combat.ts): the fight plate's bar and status marks, the floating numbers
   (numbers.ts, also the world's), the critical burst, the soul orb and the result banner. All of it lives in the plate
   layer (45-plates.css), never zoomed, at raw toScreen() pixels: px here, as there. The plate
   itself is the world's (plate.ts). */

.cb-plate,
.cb-crit {
  position: absolute;
  pointer-events: none;
}
/* The death clip ends on an empty frame, so a corpse has no body left to label. */
.cb-plate.cb-dead { display: none; }

/* Health under the name: ONE width for every body in the fight (92px, a plate's own width), never
   a readout of name length; a boss (Fighter.boss, a flag on the wire) wears 1.5x, still fixed. */
.cb-plate .cb-hp { margin-top: 2px; width: 92px; margin-inline: auto; }
.cb-plate.cb-boss .cb-hp { width: 138px; }

/* THE STATUS MARKS, under the health bar over every body in the fight. A mark is the buff's own
   picture, its seconds in the corner, its name, what it does and the time left in its card
   (gui/c/status.ts markCard, hover or a tap): the marks take the pointer for that, and nothing
   else on a plate does. Direction is the rim - the ally green, the
   hostile red - drawn outside the box over a dark ring, never a tint over the art. Wrapping beats
   clipping; an empty row has no height. */
.cb-plate .cb-status { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px 4px; }
.cb-plate .cb-status:not(:empty) { margin-top: 4px; }
.cb-mark {
  position: relative;
  display: block;
  width: 15px;
  height: 15px;
  pointer-events: auto;
  cursor: var(--cur-arrow, default);
}
.cb-mark--up { box-shadow: 0 0 0 1px var(--moss-400), 0 0 0 2px var(--border-hard); }
.cb-mark--down { box-shadow: 0 0 0 1px var(--ember-400), 0 0 0 2px var(--border-hard); }
/* The 32px atlas cell ZOOMED into the mark, never resized: --ico-box is the one knob. */
.cb-mark__ico { --ico-box: 15px; display: block; }
/* A status with no picture still makes a mark: filled with its rim's colour. */
.cb-mark--bare.cb-mark--up .cb-mark__ico { background: var(--moss-400); }
.cb-mark--bare.cb-mark--down .cb-mark__ico { background: var(--ember-400); }
/* The seconds, hanging off the corner on a ground sized to the digits (two fit beside a mark). */
.cb-mark__n {
  position: absolute;
  right: -3px;
  bottom: -3px;
  padding: 0 1px;
  border-radius: 2px;
  background: var(--mark-count-bg);
  color: var(--text);
  font-family: var(--face-ui);
  font-size: 8px;
  font-weight: 600;
  line-height: 1.25;
}

/* THE FAR CAMERA (a phone, main.ts resize): its fight stands back and bodies are half their
   desktop height; a 92px bar was wider than two bodies side by side. Same one-width rule. */
html.world-far .cb-plate .cb-hp { width: 52px; height: 4px; }
html.world-far .cb-plate.cb-boss .cb-hp { width: 78px; }
html.world-far .cb-plate .cb-status { gap: 2px 3px; }
html.world-far .cb-mark { width: 11px; height: 11px; }
html.world-far .cb-mark__ico { --ico-box: 11px; }
/* An 11px mark is too small for a finger: the tap lands on a wider, unseen square around it. */
html.world-far .cb-mark::after { content: ""; position: absolute; inset: -5px -1px; }

/* THE FLOATING NUMBERS (numbers.ts): every damage, heal, mana and "miss" word, one component for
   the fight and the world. Pooled elements in their own layer over the plates; numbers.ts moves
   them with Web Animations on transform and opacity only, so nothing here animates. Legibility
   first: a hard dark outline and a soft halo cut every number out of sand, sea or a battle
   backdrop; the colour is the theme's. Tamer Sans ships at 600 alone, so a critical's weight is a
   thicker outline and a bigger size, never a faux bold. */
.fn-layer { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.fn {
  --fn-ink: var(--fn-hit);
  --fn-rim: var(--fn-outline);
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
}
.fn[hidden], .fn [hidden] { display: none; }
.fn__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  transform: translate(-50%, 0);
  opacity: 0;
  font-family: var(--face-ui);
  font-weight: 600;
  line-height: 1;
  color: var(--fn-ink);
  text-shadow:
    -1.5px -1.5px 0 var(--fn-rim), 1.5px -1.5px 0 var(--fn-rim), -1.5px 1.5px 0 var(--fn-rim), 1.5px 1.5px 0 var(--fn-rim),
    0 2px 0 var(--fn-rim), 0 0 6px rgba(0, 0, 0, .85), 0 2px 10px rgba(0, 0, 0, .6);
}
.fn__row { display: flex; align-items: center; gap: 4px; }
.fn__val { position: relative; font-size: 32px; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
/* The white the critical lands in, over its gold, faded off by numbers.ts. */
.fn__flash { position: absolute; inset: 0; color: var(--fn-flash); opacity: 0; }
.fn__pic { --ico-box: 22px; flex: none; }

.fn--heal { --fn-ink: var(--fn-heal); }
.fn--heal .fn__val, .fn--mana .fn__val { font-size: 26px; }
.fn--mana { --fn-ink: var(--world-mana); }
.fn--zero { --fn-ink: var(--fn-zero); }
.fn--zero .fn__val { font-size: 21px; letter-spacing: 0.04em; }

/* THE CRITICAL: bigger, a 2px outline, a red and a cyan edge either side (the chromatic fringe),
   the white flash, the tag over it and a ring of light behind it on the slam. */
.fn--crit { --fn-ink: var(--fn-crit); }
.fn--crit .fn__body {
  text-shadow:
    -2px -2px 0 var(--fn-rim), 2px -2px 0 var(--fn-rim), -2px 2px 0 var(--fn-rim), 2px 2px 0 var(--fn-rim),
    0 2.5px 0 var(--fn-rim), -3px 0 0 var(--fn-fringe-a), 3px 0 0 var(--fn-fringe-b),
    0 0 8px rgba(0, 0, 0, .9), 0 0 18px var(--fn-crit-glow);
}
.fn--crit .fn__val { font-size: 50px; letter-spacing: 0.01em; }
.fn--crit .fn__flash {
  text-shadow:
    -2px -2px 0 var(--fn-rim), 2px -2px 0 var(--fn-rim), -2px 2px 0 var(--fn-rim), 2px 2px 0 var(--fn-rim),
    0 0 14px rgba(255, 255, 255, .9);
}
.fn__ring {
  position: absolute;
  left: 50%;
  top: 58%;
  width: 84px;
  height: 84px;
  margin: -42px 0 0 -42px;
  border-radius: 50%;
  border: 3px solid var(--fn-crit);
  box-shadow: 0 0 12px var(--fn-crit-glow), inset 0 0 10px var(--fn-crit-glow);
  opacity: 0;
}

/* The far camera (a phone): everything comes down with its bodies. */
html.world-far .fn__val { font-size: 24px; }
html.world-far .fn--heal .fn__val, html.world-far .fn--mana .fn__val { font-size: 20px; }
html.world-far .fn--zero .fn__val { font-size: 16px; }
html.world-far .fn--crit .fn__val { font-size: 37px; }
html.world-far .fn__ring { width: 62px; height: 62px; margin: -31px 0 0 -31px; }
html.world-far .fn__pic { --ico-box: 17px; }

/* Mobile mode (a landscape phone, ~390 css px tall): the numbers are sized for the short screen,
   about half the desktop size, whatever the camera. After world-far and as specific, so it wins. */
html.m-mobile .fn__val { font-size: 18px; }
html.m-mobile .fn--heal .fn__val, html.m-mobile .fn--mana .fn__val { font-size: 15px; }
html.m-mobile .fn--zero .fn__val { font-size: 13px; }
html.m-mobile .fn--crit .fn__val { font-size: 27px; }
html.m-mobile .fn__ring { width: 46px; height: 46px; margin: -23px 0 0 -23px; border-width: 2px; }
html.m-mobile .fn__pic { --ico-box: 13px; }


/* THE CRITICAL BURST: a flat star of light out of the body a critical landed on, behind every
   number and plate (combat.ts prepends it). A 0x0 point at the impact; `--r` the longest spike,
   `--spin` the star's turn, each spike its own `--a` angle and `--l` length. Screen-blended, so
   crossing spikes brighten. */
.gs-crit-burst {
  --r: 44px;
  --spin: 0deg;
  width: 0;
  height: 0;
  mix-blend-mode: screen;
}
.gs-crit-burst__core {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 0.62);
  height: calc(var(--r) * 0.62);
  border-radius: 50%;
  background: radial-gradient(circle,
    rgb(var(--fx-white-rgb)) 0 9%,
    var(--burst-cream) 20%,
    rgba(var(--burst-amber-rgb), 0.55) 34%,
    rgba(var(--burst-orange-rgb), 0.22) 52%,
    rgba(var(--burst-ember-rgb), 0) 70%);
  animation: gs-crit-core 340ms cubic-bezier(0.1, 0.72, 0.3, 1) forwards;
}
.gs-crit-burst i {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * var(--l, 1));
  height: calc(var(--r) * 0.065);
  margin-top: calc(var(--r) * -0.0325);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, rgba(var(--fx-white-rgb), 0.55) 0, rgb(var(--fx-white-rgb)) 22%, var(--burst-spike) 58%, rgba(var(--burst-tip-rgb), 0) 100%);
  clip-path: polygon(0 46%, 34% 0, 100% 50%, 34% 100%);
  animation: gs-crit-spike 300ms cubic-bezier(0.08, 0.78, 0.28, 1) forwards;
}
@keyframes gs-crit-core {
  from { transform: translate(-50%, -50%) scale(0.3); opacity: 1; }
  40%  { opacity: 0.85; }
  to   { transform: translate(-50%, -50%) scale(1.15); opacity: 0; }
}
/* The rotation restated in every frame: a keyframe replaces the whole transform. */
@keyframes gs-crit-spike {
  from { transform: rotate(calc(var(--spin) + var(--a))) scaleX(0.12); opacity: 0.85; }
  18%  { opacity: 1; }
  to   { transform: rotate(calc(var(--spin) + var(--a))) scaleX(1); opacity: 0; }
}

/* THE SOUL ORB: what a caught creature leaves where it stood - held, not killed. Violet, the one
   cold thing on a warm battlefield; rises as it fades; one shockwave ring. */
.gs-soul-orb {
  --r: 26px;
  width: 0;
  height: 0;
  mix-blend-mode: screen;
}
.gs-soul-orb__core {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  background: radial-gradient(circle,
    rgb(var(--fx-white-rgb)) 0 12%,
    var(--orb-lilac) 26%,
    rgba(var(--orb-violet-rgb), 0.72) 46%,
    rgba(var(--orb-deep-rgb), 0.30) 68%,
    rgba(var(--orb-night-rgb), 0) 82%);
  animation: gs-soul-orb-core 620ms cubic-bezier(0.16, 0.72, 0.3, 1) forwards;
}
.gs-soul-orb__ring {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  border: 2px solid rgba(var(--orb-ring-rgb), 0.85);
  animation: gs-soul-orb-ring 420ms cubic-bezier(0.1, 0.7, 0.3, 1) forwards;
}
@keyframes gs-soul-orb-core {
  from { transform: translate(-50%, -50%) scale(0.25); opacity: 0; }
  22%  { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
  55%  { transform: translate(-50%, -62%) scale(0.92); opacity: 0.95; }
  to   { transform: translate(-50%, -120%) scale(0.55); opacity: 0; }
}
@keyframes gs-soul-orb-ring {
  from { transform: translate(-50%, -50%) scale(0.2); opacity: 0.9; }
  to   { transform: translate(-50%, -50%) scale(1.9); opacity: 0; }
}
/* Reduced motion keeps the FACT and drops the flourish: the orb is a still point, the burst goes
   (the number still turns gold and the body still flinches; numbers.ts drops its own slam). */
@media (prefers-reduced-motion: reduce) {
  .gs-soul-orb__ring { display: none; }
  .gs-soul-orb__core { animation: gs-soul-orb-still 500ms linear forwards; }
  @keyframes gs-soul-orb-still {
    from { transform: translate(-50%, -50%) scale(1); opacity: 1; }
    to   { transform: translate(-50%, -50%) scale(1); opacity: 0; }
  }
  .gs-crit-burst { display: none; }
}

/* THE RESULT, modal over the field: the veil is the click trap (dismiss is the only thing a click
   can do while it is up), the banner the readout over the middle. A defeat colours its veil. */
.cb-veil {
  position: absolute;
  inset: 0;
  z-index: 2; /* over the numbers' layer */
  pointer-events: auto;
  background: var(--scrim);
  animation: cb-veil-in 200ms linear;
}
.cb-veil--loss { background: var(--world-veil-loss); }
@keyframes cb-veil-in { from { opacity: 0; } to { opacity: 1; } }
.gs-banner {
  font-family: var(--face-ui);
  font-size: 28px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold-bright);
  text-shadow: var(--world-shadow);
}
.gs-banner--result {
  position: absolute;
  left: 50%;
  top: 34%;
  transform: translate(-50%, -50%);
  text-align: center;
  pointer-events: none;
}
.gs-banner--loss { color: var(--ember-300); }
.gs-banner--flee { color: var(--text-mute); }
/* A gain line: a picture and a number (exp, coin), or a drop: the item's tile, its name in its
   quality's world ink, the count. One centred list under the verdict. */
.gs-banner__gain {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 14px;
  letter-spacing: 0;
  text-transform: none;
  margin-top: 4px;
  color: var(--stone-50);
}
.cb-gain__ic { width: 20px; height: 20px; flex: none; }
.cb-drop__ico { --ico-box: 24px; flex: none; }
.gs-banner__gain--drop { cursor: var(--cur-loot, default); }
.cb-drop__name--green  { color: var(--world-q-green); }
.cb-drop__name--blue   { color: var(--world-q-blue); }
.cb-drop__name--yellow { color: var(--world-q-yellow); }
.cb-drop__name--gold   { color: var(--world-q-gold); }
.cb-drop__name--purple { color: var(--world-q-purple); }

/* MANUAL TARGETING (combat.ts drawAim, vault note 262). Owner 2026-10-17: a ring under the feet and
   an arrow over the head of the body the next turn hits, nothing else on the body. Placed like a
   plate, by translate3d at raw screen px in the plate layer, so no texture and no GPU memory.
   A faint dashed ring marks a pick that will NOT be hit (covered, a random or an all-enemies skill)
   and the body under the pointer (desktop). */
.hud-plates .cb-aim { position: absolute; left: 0; top: 0; width: 0; height: 0; pointer-events: none; will-change: transform; }
.hud-plates .cb-aim[hidden] { display: none; }
.cb-ring::before {
  content: ""; position: absolute; left: -46px; top: -16px; width: 92px; height: 32px; box-sizing: border-box;
  border-radius: 50%; border: 3px solid var(--brass-hi);
  box-shadow: 0 0 14px rgba(var(--brass-hi-rgb), .55), inset 0 0 10px rgba(var(--brass-hi-rgb), .35);
  animation: cb-ring-pulse 1.4s ease-in-out infinite;
}
.cb-ring--ally::before { border-color: var(--moss-400); box-shadow: 0 0 14px rgba(78, 145, 48, .55), inset 0 0 10px rgba(78, 145, 48, .35); }
.cb-ring--ghost::before, .cb-ring--hover::before { border: 2px dashed rgba(var(--brass-pale-rgb), .6); box-shadow: none; animation: none; }
.cb-ring--hover::before { border-color: rgba(var(--brass-pale-rgb), .4); }
/* Manual battle: every body the skill can be pointed at (owner 2026-10-17). Quieter than the pick. */
.cb-ring--option::before { border: 2px solid rgba(var(--brass-pale-rgb), .32); box-shadow: none; animation: none; }
.cb-ring__tag {
  position: absolute; left: 0; top: 18px; transform: translateX(-50%); white-space: nowrap;
  font-size: 12px; font-weight: 700; color: var(--brass-pale); text-shadow: var(--world-shadow);
}
/* Over the plate (placePlate stands it on the head, translate(-50%,-100%)), bobbing gently. */
.cb-arrow::before {
  content: ""; position: absolute; left: -10px; top: -62px; width: 0; height: 0;
  border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 13px solid var(--brass-hi);
  filter: drop-shadow(0 0 4px rgba(var(--brass-hi-rgb), .7));
  animation: cb-arrow-bob 1s ease-in-out infinite;
}
.cb-arrow--low::before { top: 4px; }
html.world-far .cb-ring::before { left: -30px; top: -11px; width: 60px; height: 22px; }
@keyframes cb-ring-pulse { 50% { transform: scale(1.1); opacity: .7; } }
@keyframes cb-arrow-bob { 50% { transform: translateY(-5px); } }
@media (prefers-reduced-motion: reduce) { .cb-ring::before, .cb-arrow::before { animation: none; } }

/* THE TARGET CARD on the HUD: a face, the name and the health BAR only - never a figure (owner
   2026-10-17). Desktop: centred over the hotbar. Mobile: left of the dial, with two step buttons
   that move the pick (a body is a small thing to tap on a phone). */
.hud > .cb-target {
  display: flex; align-items: center; gap: 8px; box-sizing: border-box; min-width: 190px; max-width: 250px;
  padding: 6px 10px; border-radius: 8px; background: rgba(var(--navy-1-rgb), .92);
  border: 1px solid rgba(var(--brass-rgb), .5);
  left: 50%; bottom: 150px; transform: translateX(-50%); pointer-events: none;
}
.hud > .cb-target[hidden] { display: none; }
.cb-target__face { flex: none; width: 26px; height: 26px; box-sizing: border-box; border-radius: 50%; background: #5a2a2a; border: 2px solid var(--plate-foe); }
.cb-target__face.is-ally { background: var(--navy-3); border-color: var(--moss-400); }
.cb-target__body { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
.cb-target__name { font-size: 13px; font-weight: 700; color: var(--brass-hi); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cb-target__bar { height: 6px; border-radius: 2px; background: var(--navy-0); overflow: hidden; }
.cb-target__fill { display: block; height: 100%; transform-origin: 0 50%; background: var(--plate-foe); }
.cb-target.is-ally .cb-target__fill { background: var(--moss-500); }
.cb-target__prompt { display: none; font-size: 12px; color: var(--ink-mute); }
.cb-target.is-empty .cb-target__prompt { display: block; }
.cb-target.is-empty :is(.cb-target__face, .cb-target__name, .cb-target__bar) { display: none; }
.cb-target__step { display: none; }
:where(html.m-mobile) .hud > .cb-target {
  left: auto; transform: none; bottom: calc(var(--hud-edge) + 24px); right: calc(var(--hud-edge) + var(--dial-ro) + 12px);
  min-width: 0; width: 250px; pointer-events: auto; padding: 4px 6px; gap: 6px;
}
:where(html.m-mobile) .cb-target__name { font-size: 12px; }
:where(html.m-mobile) .cb-target__prompt { font-size: 11px; line-height: 1.25; }
:where(html.m-mobile) .cb-target__step {
  display: flex; align-items: center; justify-content: center; flex: none; width: 44px; height: 44px;
  border-radius: 8px; border: 1px solid rgba(var(--brass-rgb), .5); background: var(--navy-3);
  color: var(--brass-hi); font-size: 22px; line-height: 1; cursor: var(--cur-hand, pointer);
}
:where(html.m-mobile) .cb-target__step:focus-visible { outline: 2px solid var(--brass-hi); outline-offset: 1px; }

/* ---- 55-levelup.css */
/* YOUR LEVEL-UP: the centre medallion (apps/game/client/levelup-medal.ts), ported from the theme
   lab's #lvup (docs/ui-overhaul/themes css/fx.css, js/fx.js; t03 Midnight Astrolabe). The world
   ring under every levelling body is levelup.ts and stays.

   Every size is a share of --lvup-d, the medallion's diameter (the lab's 520 px on a 1440x813
   stage): 64% of the viewport's height, so a phone in landscape (390 high) gets 250 px. The
   number in the hollow is a medal number, out of the type scale on purpose (00-tokens.css). The
   lab's world-screenshot tunnel is a radial navy veil here. */

.lvup {
  --lvup-d: min(calc(520px * var(--ui-scale)), calc(100dvh * .64));
  position: fixed; inset: 0; z-index: var(--z-fx);
  display: none; overflow: hidden;
  cursor: var(--cur-hand); touch-action: none; user-select: none;
}
html.m-mobile .lvup { --lvup-d: calc(100dvh * .7); }
.lvup.is-on { display: block; }
/* Fading out it takes no press: the world is the player's again. */
.lvup.is-out { pointer-events: none; animation: lvup-out .5s forwards; }
@keyframes lvup-out { to { opacity: 0; } }
@keyframes lvup-fade { from { opacity: 0; } }

.lvup__veil {
  position: absolute; inset: 0; animation: lvup-fade .35s;
  background:
    radial-gradient(circle at 50% 49%, rgba(var(--navy-1-rgb), .7) 0, rgba(var(--navy-2-rgb), .5) 30%,
      rgba(var(--sky-rgb), .16) 48%, rgba(var(--wash-rgb), .8) 80%),
    linear-gradient(rgba(var(--navy-3-rgb), .35), rgba(var(--navy-3-rgb), .35));
}
.lvup__rays { position: absolute; inset: 0; width: 100%; height: 100%; }
.lvup__flash {
  position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 50%, rgba(var(--ink-strong-rgb), .9), rgba(var(--brass-pale-rgb), 0) 45%);
  animation: lvup-flash .7s ease-out forwards;
}
@keyframes lvup-flash { from { opacity: 1; } to { opacity: 0; } }

/* The ring of words, turning slowly round the medallion. Sizes inside the svg are its own units
   (a 560 box), scaled with it. */
.lvup__runes {
  position: absolute; left: 50%; top: 50%;
  width: calc(var(--lvup-d) * 1.385); height: calc(var(--lvup-d) * 1.385);
  translate: -50% -50%;
  animation: lvup-runes 1s ease-out both;
}
.lvup__runes svg { display: block; width: 100%; height: 100%; animation: lvup-spin 60s linear infinite; }
.lvup__runes text {
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 16px;
  text-transform: uppercase;
  fill: rgba(var(--ink-strong-rgb), .5);
}
.lvup__runes circle { fill: none; stroke: rgba(var(--ink-strong-rgb), .28); stroke-width: 1; }
@keyframes lvup-runes { from { opacity: 0; transform: scale(.85) rotate(-20deg); } }
@keyframes lvup-spin { to { transform: rotate(360deg); } }

.lvup__medal, .lvup__spks {
  position: absolute; left: 50%; top: 50%;
  width: var(--lvup-d); height: var(--lvup-d);
  margin: calc(var(--lvup-d) / -2) 0 0 calc(var(--lvup-d) / -2);
}
.lvup__medal {
  background: url(/data/art/ui/lvup-medallion.f2787336.webp) center / contain no-repeat;
  filter: drop-shadow(0 0 18px rgba(var(--shadow-ink), .6)) drop-shadow(0 0 2px rgba(var(--brass-pale-rgb), .45));
  animation: lvup-medal .75s cubic-bezier(.16, 1.3, .4, 1) both;
}
@keyframes lvup-medal { 0% { opacity: 0; transform: scale(1.9); filter: blur(10px); } 60% { opacity: 1; filter: blur(0); } }

/* The word and the new number, in the hollow. */
.lvup__core {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -46%);
  text-align: center; white-space: nowrap;
  animation: lvup-core .6s .35s both;
}
@keyframes lvup-core { from { opacity: 0; transform: translate(-50%, -30%); } }
.lvup__core small {
  display: block;
  font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: calc(var(--lvup-d) * .05); line-height: 1.35;
  letter-spacing: .03em;
  color: var(--ink-strong);
  text-shadow: 0 2px 8px rgba(var(--shadow-ink), 1);
}
.lvup__core b {
  position: relative; display: block;
  margin-top: 2px;
  font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: calc(var(--lvup-d) * .215); line-height: 1;
  background: linear-gradient(180deg, var(--ink-strong) 0, var(--brass-pale) 46%, var(--brass-hi) 54%, var(--brass-pale) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 2px 3px rgba(var(--shadow-ink), .9)) drop-shadow(0 0 14px rgba(var(--brass-hi-rgb), .35));
}
/* The glint under the number. */
.lvup__core b::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 6px;
  width: 120%; height: 2px; margin: 0 auto;
  transform: translateX(-8%);
  background: radial-gradient(closest-side, rgba(var(--brass-pale-rgb), .95), transparent);
  filter: blur(.5px);
  animation: lvup-glint 1.2s .8s both;
}
@keyframes lvup-glint { from { opacity: 0; transform: translateX(-8%) scaleX(.2); } }

/* Sparkles twinkling on the spike tips and the ring (placed by levelup-medal.ts in % of the box). */
.lvup__spk {
  position: absolute;
  width: calc(var(--lvup-d) * .05); height: calc(var(--lvup-d) * .05);
  translate: -50% -50%;
  background: radial-gradient(circle, var(--ink-strong) 0 6%, rgba(var(--brass-pale-rgb), .6) 14%, transparent 40%);
  opacity: 0;
  animation: lvup-twinkle 1.6s infinite;
}
.lvup__spk::before, .lvup__spk::after {
  content: ""; position: absolute; left: 50%; top: 0;
  width: 2px; height: 100%; margin-left: -1px;
  background: linear-gradient(transparent, var(--ink-strong), transparent);
}
.lvup__spk::after { transform: rotate(90deg); }
@keyframes lvup-twinkle { 0%, 100% { opacity: 0; transform: scale(.4); } 40% { opacity: 1; transform: scale(1); } }

/* Less motion: no rays, no flash, no sparkles, nothing turning; the veil, the medallion and the
   number simply fade in. */
@media (prefers-reduced-motion: reduce) {
  .lvup__rays, .lvup__flash, .lvup__spks { display: none; }
  .lvup__veil, .lvup__runes, .lvup__medal, .lvup__core { animation: lvup-fade .4s both; }
  .lvup__runes svg, .lvup__core b::after { animation: none; }
}

/* ---- 60-curtain.css */
/* THE CURTAINS (P32): what goes over the whole page, each on its own layer (gui/z.ts Z).

   - The loading curtain (loading-screen.ts): 20-components.css's `.ldg`; here its layer and its fade (the
     transition is FADE_MS in loading-screen.ts, which takes it out of the page when it ends).
   - The turn curtain (orientation.ts, `.ldg--turn`): the same look, over the loading curtain, on a
     phone held upright: its title is the Play now button where the page can go fullscreen, and the
     install offer (shell/install.ts installOffer) stands at its foot.
   - The fullscreen ask (orientation.ts, `.fs-ask`): Play full screen in the middle of a phone held
     sideways in a browser tab, under the turn curtain.
   - The offline veil (main.ts setOffline, `.offline-veil`): the freeze. The world and HUD stay on
     screen, greyed, with one line whose dots keep moving, over everything.
   Tokens only. */
.ldg { z-index: var(--z-loading); transition: opacity .18s linear; }
.ldg.is-going { opacity: 0; }
.ldg.ldg--turn { z-index: var(--z-orient-curtain); }
/* Play now stands apart from the logo, and the note sits under it as its caption. */
.ldg--turn .ldg__band > .btn { margin: 1.2em 0 .35em; padding-inline: 2.6em; }
.ldg[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .ldg { transition: none; }
}

/* THE FULLSCREEN ASK: one plate in the middle of the screen, the frame's middle (padding = the safe
   area); only the plate takes the press, the world around it stays live. */
.fs-ask {
  inset: 0; z-index: var(--z-fs-ask);
  padding: var(--safe-t) var(--safe-r) var(--safe-b) var(--safe-l);
  display: grid; place-items: center;
  pointer-events: none;
}
.fs-ask[hidden] { display: none; }
.fs-ask__card {
  pointer-events: auto;
  display: grid; justify-items: center; gap: .6em;
  padding: 1.1em 1.6em 1.2em;
  border-radius: .8em;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .97), rgba(var(--navy-1-rgb), .98));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .4), inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), 0 .4em 1.6em rgba(var(--shadow-ink), .55);
}
.fs-ask__line { font: var(--face-weight) var(--fs-md)/1.3 var(--face-ui); color: var(--brass-pale); }
.offline-veil {
  position: fixed; inset: 0; z-index: var(--z-offline);
  display: grid; place-items: center;
  background: var(--offline-wash);
}
.offline-veil__text {
  font: 600 var(--fs-2xl) var(--face-ui);
  letter-spacing: .04em; color: var(--hud-text);
  text-shadow: var(--hud-shadow);
}
/* The dots keep moving so the page reads as waiting rather than hung: opacity only, one per dot. */
.offline-veil__dots { display: inline-block; width: 1.2em; }
.offline-veil__dots > i { font-style: normal; opacity: 0; animation: offline-dot-1 1.2s steps(1, end) infinite; }
.offline-veil__dots > i:nth-child(2) { animation-name: offline-dot-2; }
.offline-veil__dots > i:nth-child(3) { animation-name: offline-dot-3; }
@keyframes offline-dot-1 { 0% { opacity: 0; } 25%, 100% { opacity: 1; } }
@keyframes offline-dot-2 { 0% { opacity: 0; } 50%, 100% { opacity: 1; } }
@keyframes offline-dot-3 { 0% { opacity: 0; } 75%, 100% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .offline-veil__dots > i { animation: none; opacity: 1; }
}

/* ---- 90-shell.css */
/* THE SHELL (P34): sign in, the home screen, the forge and the corner spinner
   (apps/game/client/shell/*). Moved from public/shell.css.

   The windows here are design-system components - `.win`, `.band`, `.rows`, `.item-row`, `.well`,
   `.field`, `.tabs`, `.btn`, `.doll-box`, `.chip` - and this sheet places them. What the shell
   draws itself is the front end's own staging on the night key art: the orrery behind the logo,
   the dais under the hero, the name plate and the roster lines. No corner ticks on any of it
   (owner rule): rings, hairlines that fade, starlight. Tokens only: colours, type (--fs-*) and
   layers (--z-*). Phone rules are `html.m-mobile` (gui/mode.ts), never a width query; a finger's
   screen in desktop mode (a tablet) is `(pointer: coarse)`; a stage too narrow for three columns
   (a tablet held upright, a small window) is the `shell` container query at the foot. The account
   menu is the game's one context menu (gui/menu.ts) and every question the one dialog
   (gui/ask.ts), so neither is placed here. */

.shell {
  position: fixed;
  inset: 0;
  z-index: var(--z-shell);           /* over the canvas, under every window and question */
  font: var(--face-weight) var(--fs-md)/1.35 var(--face-ui);
  color: var(--ink);
}
.shell[hidden] { display: none; }

/* The stage: the night vista, and whatever stands on it. Full viewport, no page scroll and no page
   chrome - this screen is the whole window, not a document in one. The key art (data/art/landing/
   shell-night, Midnight Astrolabe, 2026-10-09) is drawn `cover`; the 1x tier on a phone. The night
   closes in at the top, the foot and the edges (::before) so type laid straight on the art reads. */
.shell__stage {
  position: absolute;
  inset: 0;
  display: grid;
  overflow: hidden;
  isolation: isolate;
  /* Clear of a notch or a rounded corner: the art still runs under them, the content does not. */
  padding: var(--safe-t) var(--safe-r) var(--safe-b) var(--safe-l);
  container: shell / inline-size;
  background: var(--shell-sky) url("/data/art/landing/shell-night.7dfe29ad.webp") center 42% / cover no-repeat;
}
html.m-mobile .shell__stage { background-image: url("/data/art/landing/shell-night-1x.533db8a8.webp"); }
.shell__stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(var(--navy-0-rgb), .72), rgba(var(--navy-0-rgb), 0) 16%),
    linear-gradient(0deg, rgba(var(--navy-0-rgb), .9), rgba(var(--navy-0-rgb), 0) 30%),
    radial-gradient(ellipse 85% 80% at 50% 46%, transparent 52%, rgba(var(--navy-0-rgb), .55));
}
/* The logo and the box under it, stacked and centred as a pair; the build line is pinned to the
   foot of the screen on its own, so it does not pull the pair off centre. */
.shell__stage--signin {
  place-items: center; align-content: center; gap: 1.1em;
  padding: max(1em, var(--safe-t)) max(1em, var(--safe-r)) max(2.6em, var(--safe-b)) max(1em, var(--safe-l));
}   /* the foot's padding keeps the pair off the build line */
/* Two rows: the top bar and the vista. The strip lies OVER the vista in the bottom-right corner
   rather than in a row of its own, so its coming and going never moves the hero or the roster. */
.shell__stage--home { grid-template-rows: auto minmax(0, 1fr); }
.shell__stage--home > .shell__band { grid-row: 1; }
.shell__stage--home > .shell__vista { grid-row: 2; }

/* ------------------------------------------------------------- sign in */

/* THE ORRERY: a faint astrolabe on the night sky, centred on the logo and the box. Three thin
   rings: the outer one plain, the middle one a dial of fine ticks, the inner one turning slowly
   with two stars riding it. Quiet starlight and brass at low alpha: it frames, it never competes. */
.shell__orrery {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(94vh, 74vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  z-index: -1;
  pointer-events: none;
}
.shell__orrery > i { position: absolute; border-radius: 50%; }
.shell__orrery > i:nth-child(1) {
  inset: 0;
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .2), 0 0 0 1px rgba(var(--navy-0-rgb), .25);
}
.shell__orrery > i:nth-child(2) {
  inset: 4.5%;
  background: repeating-conic-gradient(rgba(var(--brass-pale-rgb), .32) 0 .25deg, transparent .25deg 3deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 9px), #000 calc(100% - 8px), #000 calc(100% - 1px), transparent);
  mask: radial-gradient(closest-side, transparent calc(100% - 9px), #000 calc(100% - 8px), #000 calc(100% - 1px), transparent);
}
.shell__orrery > i:nth-child(3) {
  inset: 15%;
  box-shadow: inset 0 0 0 1px rgba(var(--sky-rgb), .13);
  animation: shell-turn 240s linear infinite;
}
.shell__orrery > i:nth-child(3)::before,
.shell__orrery > i:nth-child(3)::after {
  content: "";
  position: absolute;
  width: 5px;
  height: 5px;
  margin: -2.5px;
  border-radius: 50%;
  background: var(--brass-pale);
  box-shadow: 0 0 6px 2px rgba(var(--brass-hi-rgb), .45);
}
.shell__orrery > i:nth-child(3)::before { left: 14.64%; top: 14.64%; }
.shell__orrery > i:nth-child(3)::after { left: 100%; top: 50%; width: 3px; height: 3px; margin: -1.5px; background: var(--ink-strong); }
@keyframes shell-turn { to { rotate: 360deg; } }

/* The sign-in box is a HEADLESS window: the design system's .win minus the title bar, because a box
   that is the whole screen has nothing to be closed to. It holds the buttons and the two lines
   under them (why this box is up, whether the doors are open) and nothing else, so it is only as
   wide as the longest label on one line. */
.shell__signin { width: min(23em, 100%); min-width: 0; }
.shell__signin > .win__body { display: grid; gap: .8em; padding: 1.2em 1.1em 1.1em; }
.shell__signin .note-line { text-align: center; }
.shell__signin .note-line[hidden], .shell__signin .btn[hidden] { display: none; }
/* THE LOGO stands on the night sky, over the box, at title-screen size, a starlight halo behind it
   so its warm gold sits on the navy rather than on a hole in it. */
.shell__crest {
  display: block;
  width: min(30em, 100%);
  height: auto;
  filter: drop-shadow(0 .6em 1.4em rgba(var(--shadow-ink), .6)) drop-shadow(0 0 2.2em rgba(var(--sky-rgb), .14));
  user-select: none;
}
/* Each button is the primary plate, full width, with the provider's own mark in it on a white chip
   (Google's branding asks for the four-colour G unaltered). The label never wraps. */
.shell__signin .btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .55em;
  white-space: nowrap;
  font-size: var(--fs-lg);
}
.shell__provider {
  width: 1.45em;
  height: 1.45em;
  padding: .2em;
  box-sizing: border-box;
  background: var(--provider-chip);
  border-radius: 50%;
  box-shadow: 0 .06em .12em rgba(var(--shadow-ink), .3);
  flex: none;
}
/* Shut (sign-in closed, maintenance, a provider with no keys): still the primary plate, only faded.
   The busy one (its consent window is up) keeps its face too, with the busy cursor. */
.shell__signin .btn:disabled { filter: none; opacity: .55; }
.shell__signin .btn.is-busy { opacity: .75; }
/* The build number, OUTSIDE the box at the foot of the screen: it is about the client, not about
   signing in. Letter-spaced starlight on the darkened foot. */
.shell__build {
  position: absolute;
  left: 0;
  right: 0;
  bottom: max(.9em, var(--safe-b));
  margin: 0;
  text-align: center;
  font-size: var(--fs-sm);
  letter-spacing: .12em;
}
.shell__build .note-line {
  display: inline;
  color: var(--vista-ink);
  opacity: .7;
  text-shadow: 0 .06em .25em rgba(var(--shadow-ink), .7);
}

/* ---------------------------------------------------------- home: band */

/* THE ACCOUNT'S SHELF, straight across the top of the stage and frameless: no line under it
   (owner, 2026-10-09). Three cells, the outer two equal, so the middle sits on the screen's centre
   line. The purse and the account are the same small capsule: navy glass, a faint brass hairline,
   round ends. */
.shell__band {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  margin: 0;
  min-height: 3.4em;
  padding: .7em 1.8em;
  gap: .7em;
  color: var(--ink);
  background: none;
  box-shadow: none;
}
.shell__band .band__mid { margin: 0; justify-self: center; }
/* The right cell is its whole column, packed to the right end: on a narrow phone the account's name
   is what gives way (ellipsis) when the Install capsule stands beside it, never the middle line. */
.shell__band .band__action { display: flex; align-items: center; justify-content: flex-end; gap: .6em; justify-self: stretch; min-width: 0; margin: 0; }
.shell__purse, .shell__who {
  display: inline-flex; align-items: center; min-width: 0;
  height: 2.5em; margin: 0; border: 0; border-radius: 1.25em;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .78), rgba(var(--navy-1-rgb), .82));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .32), inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), 0 2px 10px rgba(var(--shadow-ink), .45);
  color: var(--ink-strong);
}
/* The purse: the coin standing out of the capsule's left end, the number, the + as a brass disc
   in its right end. */
.shell__purse { justify-self: start; gap: .5em; padding: 0 .3em 0 .25em; font-size: var(--fs-lg); }
.shell__purse .coin { order: -1; }
.shell__purse .chip { font-variant-numeric: tabular-nums; font-weight: 700; letter-spacing: .02em; min-width: 2.5em; }
.shell__purse .chip .coin, .shell__purse .coin { width: 1.9em; height: 1.9em; }
.shell__purse > .ibtn { width: 1.8em; height: 1.8em; border-radius: 50%;
  background: radial-gradient(circle at 50% 35%, rgba(var(--brass-hi-rgb), .35), rgba(var(--brass-rgb), .12));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .6); }
.shell__purse > .ibtn:hover { box-shadow: inset 0 0 0 1px var(--brass-hi), 0 0 .6em rgba(var(--brass-hi-rgb), .35); }
/* Who is signed in, at the far right, and a button: it opens the account's menu. A brass-ringed
   disc with the account's first letter leads it. */
.shell__acct { position: relative; display: inline-flex; min-width: 0; }
.shell__who { gap: .55em; max-width: 22em; padding: 0 .9em 0 .25em; font: inherit; cursor: var(--cur-hand, pointer); }
.shell__who:hover, .shell__who.is-down { box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .7), 0 0 .8em rgba(var(--brass-hi-rgb), .2), 0 2px 10px rgba(var(--shadow-ink), .45); }
.shell__who > .ic { width: 1em; height: 1em; flex: none; color: var(--brass-hi); }
.shell__who-mark {
  display: grid; place-items: center; flex: none; width: 2em; height: 2em; border-radius: 50%;
  font: 700 var(--fs-md)/1 var(--face-display); color: var(--brass-pale); text-transform: uppercase;
  background: radial-gradient(circle at 50% 30%, rgba(var(--navy-4-rgb), 1), rgba(var(--navy-0-rgb), 1));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .75), 0 0 0 2px rgba(var(--navy-0-rgb), .6);
}
/* The address in the UI's face, not the display face: an email in a blackletter reads as a name it
   is not (user, 2026-09-24). */
.shell__who-name {
  font: var(--face-weight) var(--fs-lg)/1.1 var(--face-ui);
  color: var(--ink-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ------------------------------------------------------ home: the strip */

/* Up only when something is wrong: a newer build is live, the server did not answer, or the device
   has no room for the world. A quiet pill in the bottom-right corner, the twin of the download
   pill in the bottom-left (.shell__fetch): the same card face and ring, muted small type, the
   button the only bright thing in it. Under the roster's Delete, clear of Enter world. */
.shell__world {
  position: absolute;
  right: max(1em, var(--safe-r));
  bottom: max(1em, var(--safe-b));
  z-index: 2;
  max-width: min(32em, calc(100% - 2em));
  min-height: 0;
  flex-wrap: nowrap;
  gap: .7em;
  padding: .3em .3em .3em .9em;
  border-radius: 999px;
  background: var(--card-bg);
  box-shadow: var(--card-ring);
  font-size: var(--fs-sm);
}
.shell__world[hidden] { display: none; }
.shell__world .band__label { color: var(--ink-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.shell__world .band__mid { flex: 1 1 auto; margin: 0; min-width: 0; color: var(--ink-mute); }
.shell__world .band__mid .note-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shell__world .band__action { flex: none; margin-left: 0; }
/* A short screen (a phone held sideways) has the roster reaching the foot: while the pill is up the
   roster ends above it, scrolling its places inside as it already does when short. */
@media (max-height: 600px) {
  .shell__stage--home:has(> .shell__world:not([hidden])) .shell__roster { margin-bottom: 2.6em; max-height: calc(100% - 2.6em); }
}

/* ---------------------------------------------------- home: install */

/* THE INSTALL CAPSULE (shell/install.ts), on a phone or tablet in the browser: on the shelf, left
   of the account and the same capsule (owner 2026-10-14: a banner row under the shelf squashed the
   character). The app's icon standing out of the left end, Install, and Not now as a brass x disc
   in the right end, the purse's + mirrored. It never shrinks; the account's name gives way first.
   What there is to read drops as a card under it, over the vista, so nothing below it moves. */
.shell__install {
  position: relative; display: inline-flex; align-items: center; flex: none; gap: .3em;
  height: 2.5em; padding: 0 .25em 0 .2em; border-radius: 1.25em;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .78), rgba(var(--navy-1-rgb), .82));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .32), inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), 0 2px 10px rgba(var(--shadow-ink), .45);
}
.shell__install[hidden], .shell__install-card[hidden], .shell__install-note[hidden] { display: none; }
.shell__install-go {
  display: inline-flex; align-items: center; gap: .45em; height: 100%; margin: 0; padding: 0 .2em 0 0;
  border: 0; background: none; color: var(--brass-pale);
  font: 700 var(--fs-md)/1 var(--face-ui); white-space: nowrap; cursor: var(--cur-hand, pointer);
}
.shell__install-go:hover { color: var(--brass-hi); }
.shell__install-icon {
  flex: none; width: 2em; height: 2em; border-radius: 22%;
  background: url("/data/art/site/apple-touch-icon.9989ff01.png") center / cover no-repeat;
  box-shadow: 0 0 0 1px rgba(var(--brass-rgb), .45);
}
.shell__install > .ibtn { flex: none; width: 1.8em; height: 1.8em; border-radius: 50%;
  background: radial-gradient(circle at 50% 35%, rgba(var(--brass-hi-rgb), .35), rgba(var(--brass-rgb), .12));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .6); }
.shell__install > .ibtn .ic { width: .9em; height: .9em; }
/* The card: the strip's card face, under the capsule's right end, growing leftwards. */
.shell__install-card {
  position: absolute; top: calc(100% + .5em); right: 0; z-index: 3;
  display: grid; gap: .2em; width: 20em; padding: .6em .8em;
  border-radius: .6em; background: var(--card-bg); box-shadow: var(--card-ring);
  color: var(--brass-pale); font-size: var(--fs-sm); line-height: 1.3; white-space: normal;
}
.shell__install-title { font: 700 var(--fs-md)/1.2 var(--face-ui); color: var(--brass-pale); }
.shell__install-line b { color: var(--ink-strong); }
.shell__install-note { font-size: var(--fs-xs); color: var(--ink-mute); }
/* THE TURN CURTAIN'S OFFER (installOffer, orientation.ts): at the foot of the curtain where the
   loading screen has its meter, one line of why and the square Install with the app's icon in it,
   a size under Play now. The steps open above it, so the button never moves. */
.turn-install {
  position: absolute; left: 50%; bottom: max(7vh, 1.6em); translate: -50% 0;
  display: grid; justify-items: center; gap: .7em;
  width: min(22em, 84vw);
  text-align: center;
}
.turn-install[hidden] { display: none; }
.turn-install__pitch { font: var(--face-weight) var(--fs-md)/1.4 var(--face-ui); color: var(--brass-pale); text-wrap: balance; }
.btn.turn-install__go { display: inline-flex; align-items: center; gap: .55em; white-space: nowrap; }
.turn-install__go .shell__install-icon { width: 1.4em; height: 1.4em; }
.turn-install .shell__install-card { top: auto; right: auto; bottom: calc(100% + .6em); left: 0; width: 100%; text-align: left; }
.turn-install .shell__install-title { display: none; } /* the button above already says it */
/* The OS's own button, drawn in the line where the words name it. */
.shell__install-mark { display: inline-block; width: 1.3em; height: 1.3em; margin: 0 .15em; vertical-align: -.3em; color: var(--ink-strong); }

/* --------------------------------------------------------- home: vista */

/* Three columns on one centre line: an empty left (the key art's own), the hero in the middle,
   the roster against the right edge. The outer two are equal so the hero stands on the screen's
   centre line whatever the roster's width. */
.shell__vista {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(12em, 30em) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 1.4em;
  min-height: 0;
  padding: .8em 2em 1.4em;
}
.shell__vista-mid {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  min-height: 0;
}
.shell__verbs { display: grid; justify-items: center; margin-top: 1em; }

/* THE HERO: the doll as tall as the stage allows (a figure is 120 x 218 of the front canvas, so
   the box keeps that ratio and the feet sit on its foot), the dais under its feet, the name plate
   under the dais. */
.shell__hero {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  width: 100%;
  min-height: 0;
}
.shell__hero-stage {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: 100%;
  min-height: 0;
  padding-bottom: 2.4em;            /* the circle's near half, under the feet */
}
.shell__hero-doll, .shell__draft {
  position: relative;
  height: min(100%, 31em);
  width: auto;
  aspect-ratio: 120 / 218;
}
.shell__hero-doll > .doll, .shell__draft > .doll {
  filter: url(#doll-ink) drop-shadow(0 0 1.4em rgba(var(--sky-rgb), .18));
}
body.no-ink .shell__hero-doll > .doll, body.no-ink .shell__draft > .doll { filter: none; }

/* THE DAIS: an astrolabe laid flat under the feet. A pool of brass light, two thin elliptic rings
   and a dial of fine ticks turning slowly (a circle turned, then flattened, so its ticks ride the
   ellipse). The doll stands in front of it. On the home screen with no character it is the way to
   the forge: the painted plus floats in it. */
.shell__dais {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: min(100%, 22em);
  aspect-ratio: 3.4;
  translate: -50% 0;
  padding: 0;
  border: 0;
  background: none;
  z-index: 0;
  pointer-events: none;
}
.shell__hero-doll, .shell__draft { z-index: 1; }
.shell__dais > i { position: absolute; border-radius: 50%; pointer-events: none; }
.shell__dais-glow {
  inset: -60% -10% -20%;
  background: radial-gradient(closest-side, rgba(var(--brass-hi-rgb), .3), rgba(var(--sky-rgb), .08) 55%, transparent);
}
.shell__dais-ring {
  inset: 0;
  box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .62), 0 0 .9em rgba(var(--brass-rgb), .25), inset 0 0 1.4em rgba(var(--brass-rgb), .2);
  background: radial-gradient(closest-side, rgba(var(--navy-0-rgb), .35), rgba(var(--navy-0-rgb), .1) 70%, transparent);
}
.shell__dais-ring--in { inset: 20% 24%; background: none; box-shadow: inset 0 0 0 1px rgba(var(--brass-pale-rgb), .32); }
.shell__dais-orbit {
  left: 4%;
  right: 4%;
  top: 50%;
  aspect-ratio: 1;
  translate: 0 -50%;
  background: repeating-conic-gradient(rgba(var(--brass-pale-rgb), .45) 0 .5deg, transparent .5deg 7.5deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 7%), #000 calc(100% - 6.4%), #000 calc(100% - 1%), transparent);
  mask: radial-gradient(closest-side, transparent calc(100% - 7%), #000 calc(100% - 6.4%), #000 calc(100% - 1%), transparent);
  transform: scaleY(.294) rotate(0deg);
  animation: shell-dial 120s linear infinite;
}
@keyframes shell-dial { to { transform: scaleY(.294) rotate(360deg); } }
/* THE MAGIC CIRCLE (shell/circle.ts): the painted astrolabe in perspective on a three.js canvas,
   behind the doll, wider than the dais so the rim glow and the ticket's column of light have room.
   The canvas is 10:9 and draws the circle's centre 72% down it (circle.ts FEET), so the translate
   puts that point on the dais's centre line, where the feet stand. Once it is up the CSS rings
   above go; a device with no WebGL keeps them. */
.shell__circle {
  position: absolute;
  left: 50%;
  bottom: var(--feet, 4.1em);
  width: min(130%, 30em);
  aspect-ratio: 10 / 9;
  translate: -50% 28%;
  z-index: 0;
  pointer-events: none;
}
.shell__circle-gl { display: block; width: 100%; height: 100%; }
.shell__hero-stage:has(.shell__circle-gl) .shell__dais > i { display: none; }
.shell__dais > .shell__plus { display: none; }
.shell__hero.is-empty .shell__dais { pointer-events: auto; cursor: var(--cur-hand, pointer); }
.shell__hero.is-empty .shell__dais > .shell__plus {
  display: block; position: absolute; left: 50%; top: 50%; width: 3.2em; height: 3.2em;
  translate: -50% -78%; opacity: .8; object-fit: contain;
}
.shell__hero.is-empty .shell__dais:hover > .shell__plus { opacity: 1; }
.shell__hero.is-empty .shell__plate { display: none; }

/* The name plate: the nickname in the display face, pale brass with a starlight bloom; a hairline
   fading at both ends; level, class and the ticket mark in letter-spaced UI caps; where it stands. */
.shell__plate { position: relative; z-index: 1; display: grid; justify-items: center; gap: .35em; margin-top: 1.6em; text-align: center; max-width: 100%; }  /* room between the circle's rim and the name */
.shell__hero-name {
  font: var(--face-display-weight) clamp(1.6em, 5.2vh, 2.7em)/1.05 var(--face-display);
  letter-spacing: .06em;
  color: var(--brass-pale);
  text-shadow: 0 .06em 0 rgba(var(--shadow-ink), .9), 0 0 .8em rgba(var(--brass-hi-rgb), .3);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shell__hero-sub {
  display: inline-flex;
  align-items: center;
  gap: .6em;
  padding-top: .35em;
  font: var(--face-weight) var(--fs-md)/1.2 var(--face-ui);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink);
  background: linear-gradient(90deg, transparent, rgba(var(--brass-rgb), .5), transparent) center top / 100% 1px no-repeat;
}
.shell__hero-lv { color: var(--brass-hi); }
.shell__dot { width: 3px; height: 3px; border-radius: 50%; background: rgba(var(--brass-hi-rgb), .7); }
.shell__hero-place { font-size: var(--fs-sm); letter-spacing: .08em; color: var(--ink-mute); }
.shell__hero-place:empty { display: none; }

/* --------------------------------------------------------- home: roster */

/* The roster down the right edge: a caption on the theme's section head (the brass hairline), the
   five places, Delete at the foot. Never taller than the stage: a screen too short for the five
   scrolls them inside the roster, caption and Delete standing. */
.shell__roster {
  align-self: center;
  justify-self: end;
  display: flex;
  flex-direction: column;
  gap: .55em;
  width: min(100%, 20.5em);
  max-height: 100%;
  min-height: 0;
}
.shell__roster-head {
  display: flex;
  align-items: baseline;
  gap: .6em;
  padding: 0 1.9em .35em .1em;
  background: var(--divider-bg);
  font: var(--face-display-weight) var(--fs-md)/1.2 var(--face-display);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brass-hi);
  text-shadow: 0 1px 0 rgba(var(--shadow-ink), .9), 0 0 .6em rgba(var(--shadow-ink), .8);
}
.shell__roster > .shell__roster-head { font-size: calc(var(--fs-md) * 1.12); }
.shell__rank { display: grid; gap: .45em; flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; padding-left: .4em; margin-left: -.4em; }   /* the picked line's step left stays inside the scroller */
.shell__rank::-webkit-scrollbar { display: none; }
.shell__roster-foot { display: flex; justify-content: flex-end; padding-top: .3em; }
/* One place: the face filling the line's left end, the nickname over level and class, the ticket at
   the end. A navy line with a faint brass edge; the picked one is lit with starlight, a brighter rim
   and a brass bar down its left edge. The rim and the bar are drawn over the line (::after, ::before)
   so the face, which runs to the edge, never covers them. */
.shell__card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: .7em;
  min-height: 3.5em;
  padding: .4em .8em .4em .45em;
  border: 0;
  border-radius: 0;
  overflow: hidden;
  font: inherit;
  color: inherit;
  text-align: left;
  background: linear-gradient(90deg, rgba(var(--navy-1-rgb), .95), rgba(var(--navy-1-rgb), .88));  /* near solid: the vista's statues showed through and ate the text */
  box-shadow: 0 .4em 1em rgba(var(--shadow-ink), .35);
  cursor: var(--cur-hand, pointer);
  transition: background-color 120ms ease-out, box-shadow 120ms ease-out, translate 120ms ease-out;
}
.shell__card::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .16);
  transition: box-shadow 120ms ease-out;
}
.shell__card:hover::after { box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .34); }
.shell__card.is-picked {
  translate: -.4em 0;
  background: linear-gradient(90deg, rgba(var(--navy-4-rgb), .97), rgba(var(--navy-2-rgb), .95) 70%);
  box-shadow: 0 0 1.2em rgba(var(--brass-hi-rgb), .14), 0 .4em 1em rgba(var(--shadow-ink), .45);
}
.shell__card.is-picked::after { box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .55); }
.shell__card.is-picked::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  z-index: 1;
  width: 2px;
  background: var(--brass-hi);
  pointer-events: none;
}
/* The roster's lines a size up from the forge's class lines (owner, 2026-10-09), and one fixed
   height, so a line that gains its "in the world" word does not push the others. */
.shell__roster .shell__card { height: 4em; min-height: 0; padding: 0 .9em 0 0; gap: .75em; }
.shell__roster .shell__card-name { font-size: var(--fs-xl); }
.shell__roster .shell__card-meta { font-size: calc(var(--fs-sm) * 1.12); }
/* THE FACE: no well and no ring - the head, blown up about the face, fills the line's left end from
   its top to its foot, clipped by the line's own box and fading into the line at its right. */
.shell__face {
  position: relative;
  display: grid;
  place-items: center;
  align-self: stretch;
  width: 4.3em;
  overflow: hidden;
  background: radial-gradient(ellipse 70% 80% at 45% 40%, rgba(var(--navy-4-rgb), .7), transparent);
  -webkit-mask: linear-gradient(90deg, #000 70%, transparent);
  mask: linear-gradient(90deg, #000 70%, transparent);
}
.shell__face > .head { inset: 0 auto auto 0; width: 100%; height: auto; aspect-ratio: 1; border-radius: 0; transform: translateY(-20%) scale(1.45); transform-origin: 50% 34%; }
.shell__card-text { display: grid; gap: 0; min-width: 0; }
.shell__card-name {
  font: var(--face-weight) var(--fs-lg)/1.1 var(--face-ui);
  color: var(--ink-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.shell__card-meta { font-size: var(--fs-sm); letter-spacing: .06em; color: var(--ink-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shell__card-away { font-size: var(--fs-xs); letter-spacing: .04em; color: var(--btn-ok-rim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shell__card-away[hidden] { display: none; }
.shell__card.is-picked .shell__card-name { color: var(--brass-pale); }
.shell__card.is-picked .shell__card-meta { color: var(--ink); }
/* An empty place: the same line, emptier - more see-through, the painted plus where the face would be. */
.shell__card--empty { background: rgba(var(--navy-1-rgb), .8); }
.shell__card--empty::after { box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .1); }
.shell__card--empty .shell__face { background: radial-gradient(ellipse 60% 70% at 50% 50%, rgba(var(--navy-0-rgb), .6), transparent); }
.shell__card--empty .shell__card-name { color: var(--ink-mute); font-size: var(--fs-lg); }
.shell__card--empty:hover .shell__card-name { color: var(--ink); }
.shell__card .shell__plus { width: 1.6em; height: 1.6em; object-fit: contain; opacity: .7; }
.shell__card--empty:hover .shell__plus { opacity: 1; }

.shell__ticket { --ico-box: 1.8em; width: 1.8em; height: 1.8em; flex: none; filter: drop-shadow(0 0 .35em rgba(var(--brass-hi-rgb), .45)); }
.shell__ticket[hidden] { display: none; }

/* ---------------------------------------------------- home: the over cell */

/* The forge and the profile open in the VISTA'S OWN CELL, and while one is up the hero, the roster
   and the strip are gone (not dimmed: a place peering out from under a page read as a second thing
   to press). No wash behind it: the window stands straight on the vista. */
.shell__over {
  display: grid;
  place-items: center;
  gap: .8em;
  padding: .8em 1.6em 1.2em;
  overflow: auto;
  min-height: 0;
}
.shell__over[hidden] { display: none; }
.shell__stage--home.is-over:not(.is-side) .shell__vista,
.shell__stage--home.is-over:not(.is-side) .shell__world { display: none; }
.shell__stage--home.is-over .shell__over { grid-area: 2 / 1 / 3 / 2; }

/* THE FORGE, a character creator on the vista's three columns: the classes and Back at the left,
   the draft on the magic circle in the middle with its look under its feet, the read, the name and
   Create at the right. Nothing in it has a close star: Back (and Escape) is the one way out. */
.shell__desk {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(12em, 30em) minmax(0, 1fr);   /* equal sides: the draft on the centre line */
  gap: 1.6em;
  width: min(100%, 84em);
  height: 100%;
  min-height: 0;
}
.shell__forge-side { display: grid; align-content: center; width: min(100%, 18em); min-height: 0; }
/* Back is the game's secondary button at the foot of the classes, where Delete stands under the
   roster: the outer edge, the way out of the screen. */
.shell__forge-foot { justify-content: flex-start; }
/* The classes: the roster's own lines, the class sigil where the face would be. An unpicked sigil
   is dimmed; the picked line steps toward the stage and its sigil burns at full colour. */
.shell__classes { align-self: center; display: grid; gap: .45em; }
.shell__class { grid-template-columns: auto minmax(0, 1fr); gap: .8em; min-height: 3.7em; padding-left: .55em; }
.shell__class.is-picked { translate: .4em 0; }
.shell__sigil { display: block; flex: none; width: 2.8em; height: 2.8em; background: center / contain no-repeat; }
.shell__sigil[data-cls="warrior"] { background-image: url(/data/art/ui/sigil-warrior.bdf39f22.webp); }
.shell__sigil[data-cls="mage"] { background-image: url(/data/art/ui/sigil-mage.e69b7ab3.webp); }
.shell__sigil[data-cls="priest"] { background-image: url(/data/art/ui/sigil-priest.99fb5271.webp); }
.shell__sigil[data-cls="ranger"] { background-image: url(/data/art/ui/sigil-ranger.04e5640f.webp); }
.shell__class > .shell__sigil { filter: saturate(.45) brightness(.75); transition: filter 160ms ease-out; }
.shell__class:hover > .shell__sigil { filter: saturate(.8) brightness(.95); }
.shell__class.is-picked > .shell__sigil { filter: drop-shadow(0 0 .45em rgba(var(--brass-hi-rgb), .35)); }

/* The middle column: the draft as tall as the column lets it, the look capsule under its feet. */
.shell__look {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  gap: .9em;
  height: 100%;
  min-height: 0;
}
.shell__draft { height: min(100%, 31em); }
.shell__look > .shell__hero-stage:has(.shell__circle-gl) .shell__dais > i { display: none; }
/* THE LOOK: one square navy bar with a brass hairline. The body is a square two-way toggle, the
   lit half ringed in brass; hair and face are carousels, a chevron either side of the part's
   name over its number; the reroll is the brass disc at the end. Hairlines part the cells. */
.shell__lookbar {
  display: flex;
  align-items: center;
  height: 3.3em;
  padding: 0 .35em;
  border-radius: 0;                  /* square, as the body toggle in it (owner, 2026-10-09) */
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .84), rgba(var(--navy-1-rgb), .9));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .32), inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), 0 .35em 1.1em rgba(var(--shadow-ink), .5);
}
.shell__lookbar > * { position: relative; }
.shell__lookbar > * + *::before {
  content: ""; position: absolute; left: 0; top: 18%; bottom: 18%; width: 1px; pointer-events: none;
  background: linear-gradient(180deg, transparent, rgba(var(--brass-rgb), .45), transparent);
}
.shell__lookbar > .shell__reroll::before { content: none; }
.shell__sexes { display: flex; gap: .15em; padding-right: .35em; border-radius: 0; }
.shell__sex {
  height: 2.5em; padding: 0 1em; border: 0; border-radius: 0;
  background: none; color: var(--ink-mute); font: inherit; letter-spacing: .04em;
  cursor: var(--cur-hand, pointer); transition: color 120ms ease-out, box-shadow 120ms ease-out;
}
.shell__sex:hover { color: var(--ink); }
.shell__sex.is-on {
  color: var(--brass-pale);
  background: radial-gradient(ellipse at 50% 20%, rgba(var(--brass-hi-rgb), .22), rgba(var(--brass-rgb), .05));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .55);
}
.shell__turn { display: flex; align-items: center; padding: 0 .3em; }
.shell__turn > .ibtn { width: 1.9em; height: 1.9em; border: 0; background: none; opacity: .75; }
.shell__turn > .ibtn::after { content: none; }
.shell__turn > .ibtn .ic { width: .9em; height: .9em; }
.shell__turn > .ibtn:hover { opacity: 1; filter: drop-shadow(0 0 .35em rgba(var(--brass-hi-rgb), .5)); }
.shell__turn-val { display: grid; justify-items: center; min-width: 3.4em; line-height: 1.05; }
.shell__turn-val > small { font-size: var(--fs-xs); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-mute); }
.shell__turn-val > b { font: 700 var(--fs-lg)/1.1 var(--face-ui); color: var(--ink-strong); font-variant-numeric: tabular-nums; }
.shell__reroll.ibtn {
  width: 2.5em; height: 2.5em; margin-left: .3em; border: 0;
  background: radial-gradient(circle at 50% 35%, rgba(var(--brass-hi-rgb), .35), rgba(var(--brass-rgb), .12));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .6);
}
.shell__reroll.ibtn::after { content: none; }
.shell__reroll .ic { width: 1.35em; height: 1.35em; }
.shell__reroll.ibtn:hover { box-shadow: inset 0 0 0 1px var(--brass-hi), 0 0 .6em rgba(var(--brass-hi-rgb), .35); }

/* THE READ: a headless window - the sigil and the class name in the display face at its head, the
   description, the level-1 sheet in two columns of painted stat icons; the name and the one Create
   in the foot, which never scrolls away. */
.shell__forge--read { align-self: center; justify-self: end; width: min(100%, 25em); max-height: 100%; min-height: 0; min-width: 0; }
.shell__forge--read > .win__body { display: grid; align-content: start; gap: .8em; padding: 1.3em 1.4em .8em; min-height: 0; overflow: auto; }
.shell__forge--read > .win__foot { flex-direction: column; align-items: stretch; gap: .6em; padding: .5em 1.4em 1.4em; }
.shell__read-head { display: flex; align-items: center; gap: .8em; min-width: 0; }
.shell__read-head > .shell__sigil { width: 3.6em; height: 3.6em; filter: drop-shadow(0 0 .5em rgba(var(--brass-hi-rgb), .3)); }
.shell__read-name {
  min-width: 0;
  font: var(--face-display-weight) 1.85em/1.05 var(--face-display);
  letter-spacing: .05em;
  color: var(--brass-pale);
  text-shadow: 0 .06em 0 rgba(var(--shadow-ink), .9), 0 0 .8em rgba(var(--brass-hi-rgb), .3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shell__forge--read .shell__roster-head { margin-top: .2em; font-size: var(--fs-sm); text-shadow: none; }
.shell__sheet { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 0 1em; }   /* the right column holds the long words */
.shell__sheet-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .5em;
  padding: .38em 0; border-bottom: 1px solid var(--line);
}
.shell__sheet-row > .g { width: 1.3em; height: 1.3em; object-fit: contain; }
.shell__sheet-row > span { color: var(--ink-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shell__sheet-row > b { color: var(--ink-strong); font-variant-numeric: tabular-nums; }

/* PROFILE & SETTINGS is a side window, not a page: a normal-sized window at the right edge under
   the top bar, near the name it opens from, as tall as its content (the body scrolls past the
   stage's height). The stage stays as it is behind it (.is-side); the over cell lets presses
   through everywhere but the window. */
/* Pinned to the one column too, so the window's cell lies over them rather than pushing them aside. */
.shell__stage--home.is-side > .shell__world,
.shell__stage--home.is-side > .shell__vista { grid-column: 1; }
.shell__stage--home.is-side .shell__over {
  grid-area: 2 / 1 / 3 / 2;
  place-items: start end;
  padding: .2em 1.8em 1.2em;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
.shell__section {
  width: min(30em, 100%);
  max-height: 100%;
  min-height: 0;
  overflow: hidden;
  pointer-events: auto;
}
.shell__section > .win__body {
  display: grid;
  align-content: start;
  gap: .7em;
  padding: 1.1em 1.2em .8em;
  overflow: auto;
  min-height: 0;
}
.shell__section > .win__body > * { min-width: 0; max-width: 100%; }
.shell__section > .win__foot { justify-content: flex-end; }
.shell__section .tabs { flex-wrap: wrap; max-width: 100%; height: auto; }
.shell__section .note-line { white-space: normal; }
.shell__lumi { display: inline-flex; align-items: center; gap: .3em; }

/* The Enter world list (a question, gui/ask.ts): the other characters, one tick each. */
.shell__bring { gap: .55em; margin: .7em 0; }

/* ---------------------------------------------------------- mobile mode */

/* A landscape phone: the short height is the constraint. The shelf and the paddings tighter, the
   roster narrower and its lines shorter, the name a size down; the doll still takes whatever height
   is left. The forge's three columns narrow. Everything a thumb presses is at least --touch tall
   (the type is shrunk to the height, a finger is not). */
html.m-mobile .shell__band { min-height: 2.6em; padding: .25em 1em; }
html.m-mobile .shell__vista { padding: .3em 1em .6em; gap: .8em; grid-template-columns: minmax(0, 1fr) minmax(9em, 16em) minmax(0, 1fr); }
html.m-mobile .shell__verbs { margin-top: .4em; }
html.m-mobile .shell__hero-stage { padding-bottom: 1.3em; }
html.m-mobile .shell__circle { --feet: 2.5em; width: min(150%, 21em); }
html.m-mobile .shell__hero-name { font-size: clamp(1.25em, 6vh, 1.7em); }
html.m-mobile .shell__hero-sub { font-size: var(--fs-sm); padding-top: .2em; }
html.m-mobile .shell__plate { gap: .15em; margin-top: .8em; }
html.m-mobile .shell__roster { width: min(100%, 16em); gap: .3em; }
html.m-mobile .shell__rank { gap: .25em; }
html.m-mobile .shell__card { min-height: 2.6em; padding: .2em .6em .2em .3em; gap: .5em; }
html.m-mobile .shell__roster .shell__card { height: var(--touch); min-height: 0; padding: 0 .6em 0 0; }
html.m-mobile .shell__face { width: 3.2em; }
html.m-mobile .shell__card-name { font-size: var(--fs-md); }
html.m-mobile .shell__roster-head { font-size: var(--fs-sm); padding-bottom: .2em; }
html.m-mobile .shell__roster-foot { padding-top: 0; }
html.m-mobile .shell__over { padding: .4em 1em .6em; }
html.m-mobile .shell__stage--home.is-side .shell__over { padding: 0 1em .4em; }
html.m-mobile .shell__section > .win__body { gap: .45em; padding: .6em 1em .4em; }
html.m-mobile .shell__desk { grid-template-columns: 16em minmax(8em, 1fr) 16em; gap: .8em; }
html.m-mobile .shell__forge-side { width: 12em; }
html.m-mobile .shell__forge--read { width: 16em; }
html.m-mobile .shell__forge-side { gap: .4em; }
html.m-mobile .shell__classes { gap: .25em; }
html.m-mobile .shell__class { min-height: var(--touch); padding-left: .35em; gap: .55em; }
html.m-mobile .shell__sigil { width: 2.1em; height: 2.1em; }
html.m-mobile .shell__look { gap: .4em; }
html.m-mobile .shell__lookbar { height: calc(var(--touch) + 4px); font-size: var(--fs-sm); }
html.m-mobile .shell__sex { height: var(--touch); padding: 0 .9em; }
html.m-mobile .shell__turn { padding: 0; }
html.m-mobile .shell__turn > .ibtn { width: calc(var(--touch) * .7); height: var(--touch); }
html.m-mobile .shell__reroll.ibtn { width: calc(var(--touch) - 4px); height: calc(var(--touch) - 4px); }
html.m-mobile .shell__turn-val { min-width: 2.4em; }
html.m-mobile .shell__forge--read > .win__body { gap: .45em; padding: .8em 1em .4em; }
html.m-mobile .shell__forge--read > .win__foot { gap: .4em; padding: .3em 1em .9em; }
html.m-mobile .shell__read-head > .shell__sigil { width: 2.4em; height: 2.4em; }
html.m-mobile .shell__read-name { font-size: 1.4em; }
html.m-mobile .shell__forge--read .field { height: var(--touch); }
/* The sheet's words go to the tooltip on a phone: icon and figure, four across. */
html.m-mobile .shell__sheet { grid-template-columns: repeat(4, auto); justify-content: space-between; gap: 0 .6em; }
html.m-mobile .shell__sheet-row { grid-template-columns: auto auto; gap: .3em; padding: .2em 0; }
html.m-mobile .shell__sheet-row > span { display: none; }

/* A finger, phone or tablet: the small verbs (Delete, Back, Sign out, Close), the shelf's two
   capsules and the purse's +, the language tabs and the name field are all a fingertip tall. */
html.m-mobile .shell .btn, html.m-mobile .shell__purse, html.m-mobile .shell__who,
html.m-mobile .shell__section .tabs__tab { min-height: var(--touch); }
html.m-mobile .shell__purse, html.m-mobile .shell__who, html.m-mobile .shell__install { height: var(--touch); border-radius: calc(var(--touch) / 2); }
html.m-mobile .shell__install > .ibtn { width: calc(var(--touch) - 10px); height: calc(var(--touch) - 10px); }
html.m-mobile .shell__purse > .ibtn { width: calc(var(--touch) - 10px); height: calc(var(--touch) - 10px); }
html.m-mobile .shell__roster-foot > .btn { min-width: 6.5em; }
/* The logo gives way to the box on a short screen: the buttons and the build line keep their room. */
html.m-mobile .shell__crest { width: auto; max-width: min(30em, 100%); height: min(12em, 36vh); }
html.m-mobile .shell__band { padding-block: .3em; }
@media (pointer: coarse) {
  .shell .btn, .shell__section .tabs__tab, .shell__forge--read .field { min-height: var(--touch); }
  .shell__roster-foot > .btn { min-width: 6.5em; }
  .shell__purse, .shell__who, .shell__install { height: max(2.5em, var(--touch)); border-radius: max(1.25em, calc(var(--touch) / 2)); }
  .shell__install > .ibtn { width: max(1.8em, calc(var(--touch) - 8px)); height: max(1.8em, calc(var(--touch) - 8px)); }
  .shell__purse > .ibtn { width: max(1.8em, calc(var(--touch) - 8px)); height: max(1.8em, calc(var(--touch) - 8px)); }
  .shell__turn > .ibtn { width: max(1.9em, calc(var(--touch) * .75)); height: var(--touch); }
}

/* A stage too narrow for three columns on one centre line (a tablet held upright, a small desktop
   window; a phone is always wide enough): the empty left column goes, the hero takes the room
   and the roster keeps its own; the forge stacks the classes over the read at the right of the
   draft. Desktop mode only: mobile mode's narrower columns above still fit. */
@container shell (max-width: 60em) {
  html.m-desktop .shell__vista { grid-template-columns: minmax(0, 1fr) minmax(0, 18em); padding-inline: 1.2em; }
  html.m-desktop .shell__vista-side { display: none; }
  html.m-desktop .shell__roster { width: 100%; }
  html.m-desktop .shell__desk {
    grid-template-columns: minmax(0, 1fr) minmax(0, 19em);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas: "look side" "look read";
    gap: 1.2em;
  }
  html.m-desktop .shell__forge-side { grid-area: side; width: 100%; align-content: start; }
  html.m-desktop .shell__look { grid-area: look; }
  html.m-desktop .shell__forge--read { grid-area: read; align-self: start; width: 100%; }
  html.m-desktop .shell__over { padding-inline: 1.2em; }
}

/* ---------------------------------------------------- the corner spinner */

/* The download, in the bottom-left corner of the sign-in box and the home screen, from the moment
   the game page opens until the pack is down and verified: the painted busy ring, turning, and the
   share downloaded beside it while bytes are moving. The step and the MB are its card (gui/tip.ts).
   Over the shell, under every window and the curtain. */
.shell__fetch {
  position: fixed;
  left: max(1em, var(--safe-l));
  bottom: max(1em, var(--safe-b));
  z-index: calc(var(--z-shell) + 10);
  display: flex;
  align-items: center;
  gap: .4em;
  padding: .3em .55em .3em .3em;
  border-radius: 999px;
  background: var(--card-bg);
  box-shadow: var(--card-ring);
  font: var(--face-weight) var(--fs-md)/1 var(--face-ui);
  color: var(--ink);
  cursor: var(--cur-arrow, default);
}
.shell__fetch[hidden] { display: none; }
.shell__fetch-ring { width: 1.5em; height: 1.5em; object-fit: contain; animation: shell-fetch-spin 1.2s linear infinite; }
@keyframes shell-fetch-spin { to { transform: rotate(360deg); } }
.shell__fetch-pct { font-variant-numeric: tabular-nums; }
.shell__fetch-pct:empty { display: none; }
.shell__fetch:has(.shell__fetch-pct:empty) { padding-right: .3em; }

@media (prefers-reduced-motion: reduce) {
  .shell__card { transition: none; }
  .shell__fetch-ring, .shell__orrery > i, .shell__dais-orbit { animation: none; }
}

/* ---- w-bag.css */
/* THE BACKPACK AND THE WAREHOUSE (gui/bag.ts): two gui windows, `.win.bagw[data-win=bag|warehouse]`.
   Placed by gui/win.ts (the bag docks right, the warehouse stands beside it); this sheet sizes them.

   THE CELL FOLLOWS THE WINDOW. The grid sits in a size container (.bagw__wrap) and a cell is the
   design's --tile (10em / 3, 40px at the 12px window size) or the share of the container's width
   its column gets, whichever is smaller: a warehouse beside a Backpack on a laptop, or either one
   on a phone sheet, shrinks its cells instead of hanging off the screen. The column count is the
   protocol's (PAGE_COLS, written on the wrap as --page-cols); mobile mode lays the warehouse's 200
   cells ten wide and the body scrolls. */
.bagw__body { display: grid; gap: .7em; align-content: start; }
.bagw__wrap { container-type: inline-size; min-width: 0; }
.bagw .tiles {
  --cols: var(--page-cols);
  --tile: min(calc(10em / 3), calc((100cqw - (var(--cols) - 1) * .4em) / var(--cols)));
  justify-content: center;
}
/* The pack strip: one tab per pack, each an equal share of the window, however many there are. */
.bagw .tabs--text .tabs__tab { min-width: 0; padding: 0 .6em; white-space: nowrap; }
.bagw .tabs__tab > .ic { width: 1em; height: 1em; margin-left: .3em; }
.bagw .tabs__tab > .ic[hidden] { display: none; }
/* An empty cell on a shut pack is the padlock, centred and faint (20-components.css .tile--locked), not
   the corner badge an occupied one wears. */
.bagw .tile--locked.tile--barred > .ic {
  inset: 0; margin: auto; width: 1.5em; height: 1.5em; padding: 0;
  background: none; color: rgba(var(--ink-wash), .22); opacity: .35;
}
.bagw .win__foot { flex-wrap: wrap; row-gap: .5em; }
.bagw__gap { flex: 1; }

/* The warehouse: twenty columns, as wide as the screen leaves beside the Backpack (42em and three
   gutters), and on a short screen its ten rows give way first. */
.wins > .win[data-win="warehouse"] { width: min(calc(20 * 10em / 3 + 19 * .4em + 2.93em), calc(var(--frame-w) - 42em - 42px)); }
.win[data-win="warehouse"] .tiles {
  --tile: min(calc(10em / 3), calc((100cqw - (var(--cols) - 1) * .4em) / var(--cols)), calc((var(--frame-h) - 32px - 14.5em) / 10 - .4em));
}

/* MOBILE MODE: two sheets side by side, the Backpack on the right and the warehouse beside it, each
   half the screen, so a stack drags straight across. Each is as tall as the screen allows and its
   body scrolls, so every row is reachable; the foot stays put. */
html.m-mobile .wins > .win.bagw { width: calc(.5 * var(--frame-w) - 12px); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .wins > .win[data-win="bag"] { width: min(42em, calc(.5 * var(--frame-w) - 12px)); }
html.m-mobile .win[data-win="warehouse"] .tiles { --cols: 10; --tile: min(calc(10em / 3), calc((100cqw - 9 * .4em) / 10)); }
/* A carry over the bag lights the one cell it would land in (bag.ts lightCell), never the grid. */
.bagw__grid.is-over { outline: none; }

/* The Unlock mode button (owner, 2026-10-09): the same padlock as Lock, struck through, since there is
   no painted open-padlock icon yet. */
.bagw .bagw__unlock::before {
  content: ""; position: absolute; left: 50%; top: 50%; width: 1.25em; height: .14em;
  background: currentColor; border-radius: .1em; transform: translate(-50%, -50%) rotate(-45deg);
  box-shadow: 0 0 0 .06em var(--cream-rim); pointer-events: none;
}

/* THE AETHER DRAWER (bag.ts aetherDrawer, owner 2026-10-15): the three vessel banks OUTSIDE the
   Backpack, hung on its left edge so it moves with the window. The drawer is a clip as tall as the
   window; the panel inside wears the window's own band and arc, and its head is a .win__head so
   AETHER stands level with "Backpack". Hidden, the panel slides right, in behind the window's edge
   (the clip hides it), and a left-arrow tab on that edge slides it back out. Three upright bars - the
   HUD's .bar stood on end, --hp / --mp / --sage - each with its icon and held figure under it. */
.aeth { position: absolute; top: 0; bottom: 0; right: 100%; width: 8.6em; overflow: hidden; pointer-events: none; }
.aeth__panel {
  position: absolute; inset: 0 0 0 0; display: flex; flex-direction: column; padding: 3px;
  background: var(--win-arc), var(--win-bg); box-shadow: var(--win-ring); pointer-events: auto;
  transform: translateX(0); transition: transform .28s ease;
}
.aeth.is-in > .aeth__panel { transform: translateX(100%); pointer-events: none; }
.aeth__head { gap: .4em; }
.aeth__head > .ibtn { margin-left: auto; }
.aeth__bars { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .3em; padding: .8em .5em .7em; }
.aeth__row { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; justify-items: center; gap: .3em; min-width: 0; }
.bar.bar--v { width: 1.15em; height: auto; align-self: stretch; }
.bar.bar--v > .bar__fill { inset: auto 0 0 0; width: 100%; height: 100%; transform-origin: bottom; transition: transform .2s linear; }
/* The character sheet's paint, turned on its side (owner, 2026-10-15): the sheen's gloss, shade and
   one-tile-high grain are drawn for a lying bar, so the overlay is laid out lying (the bar's height
   as its width) and turned a quarter, gloss down the left edge, grain tiled up the bar. */
.bar.bar--v { container-type: size; }
.bar.bar--v::after {
  inset: auto; left: calc(50% - 50cqh); top: calc(50% - 50cqw); width: 100cqh; height: 100cqw; transform: rotate(-90deg);
}
.aeth__ic { width: 1.6em; height: 1.6em; object-fit: contain; }
.aeth__n { font-size: var(--fs-small); color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The tab that brings it back: a brass pull on the Backpack's left edge, under the head, pointing
   left (the hide arrow turned round). Only while the drawer is in. */
.aeth__tab {
  position: absolute; right: 100%; top: 3.1em; display: grid; place-items: center; width: 1.6em; height: 3.2em;
  padding: 0; border: 1px solid rgba(var(--brass-hi-rgb), .4); border-right: 0; border-radius: .45em 0 0 .45em;
  background: var(--win-bg); box-shadow: var(--win-ring); cursor: var(--cur-hand, pointer);
  opacity: 0; transform: translateX(.6em); pointer-events: none; transition: opacity .2s ease .12s, transform .2s ease .12s;
}
.aeth__tab.is-on { opacity: 1; transform: translateX(0); pointer-events: auto; }
.aeth__tab-ic { width: 1em; height: 1em; object-fit: contain; transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .aeth__panel, .aeth__tab { transition: none; } }

/* MOBILE MODE: the same drawer, a smaller title so the hide arrow stays inside the panel, and
   tighter columns so three figures sit apart. */
html.m-mobile .aeth { width: 9.4em; }
html.m-mobile .aeth__head { gap: .2em; }
html.m-mobile .aeth__head > .win__title { font-size: 1em; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
html.m-mobile .aeth__head > .ibtn { flex: none; }
html.m-mobile .aeth__bars { gap: .15em; padding: .6em .2em .5em; }
html.m-mobile .aeth__ic { width: 1.35em; height: 1.35em; }
html.m-mobile .aeth__n { font-size: .78em; }
html.m-mobile .aeth__tab { width: var(--touch); }

/* ---- w-char.css */
/* THE CHARACTER WINDOW (panels/character-sheet.ts, `.win.csw[data-win=char]`) AND THE PLAYER WINDOW
   (panels/profile.ts, `.win.pfw[data-win=profile]`): the figure between its two frame columns on
   the left, numbers (or the deployed pets) on the right. Placed by gui/win.ts; this sheet lays
   them out. 20-components.css draws every part (tile, doll, stat row, band, kv, item row). */
.csw__body, .pfw__body { display: grid; grid-template-columns: 19em minmax(0, 1fr); gap: 1em; align-items: start; }
.csw__side { display: grid; gap: .6em; min-width: 0; align-content: start; }

/* The two frame columns STRETCH to the figure between them and space their five frames over its
   height, so the feet never stand in a gap beside the last frame. */
.csw__doll { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .5em; align-items: stretch; justify-items: center; }
.eqcol { display: grid; gap: .45em; align-content: space-between; }
.csw__fig { display: grid; gap: .2em; justify-items: center; align-content: start; min-width: 0; max-width: 100%; }
.csw__fig .doll-box { --doll-w: 10em; }
.csw__nick { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--face-ui); }
.csw__fig > .note-line { max-width: 100%; text-align: center; overflow-wrap: anywhere; }

/* Who, over the vitals: the class sigil, "Lv 31" with the figure in the strong ink, a thin rule,
   the class name. No caption: the sigil and the figure say it. */
.csw__who { display: flex; align-items: center; gap: .55em; min-width: 0; padding-bottom: .35em; border-bottom: 1px solid var(--line); }
.csw__sigil { width: 2em; height: 2em; flex: none; background: center / contain no-repeat; filter: drop-shadow(0 0 .06em rgba(var(--shadow-ink), .45)); }
.csw__sigil[data-cls="warrior"] { background-image: url(/data/art/ui/sigil-warrior.bdf39f22.webp); }
.csw__sigil[data-cls="mage"] { background-image: url(/data/art/ui/sigil-mage.e69b7ab3.webp); }
.csw__sigil[data-cls="priest"] { background-image: url(/data/art/ui/sigil-priest.99fb5271.webp); }
.csw__sigil[data-cls="ranger"] { background-image: url(/data/art/ui/sigil-ranger.04e5640f.webp); }
.csw__lv { display: inline-flex; align-items: baseline; gap: .25em; white-space: nowrap; }
.csw__lv > small { color: var(--ink-mute); font-size: var(--fs-small); letter-spacing: .06em; }
.csw__lv > b { color: var(--ink-strong); font-size: 1.33em; font-variant-numeric: tabular-nums; line-height: 1; }
.csw__cls { min-width: 0; padding-left: .55em; border-left: 1px solid var(--line); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Health, mana, experience: the word (HP, MP, EXP), then the bar with its figures. */
.csw__vitals { display: grid; gap: .35em; }
.csw__vital { display: grid; grid-template-columns: 2.4em minmax(0, 1fr); gap: .45em; align-items: center; }
.csw__vlab { font-size: var(--fs-small); font-weight: 700; letter-spacing: .04em; color: var(--ink-mute); white-space: nowrap; }
.csw__sic { width: 1.4em; height: 1.4em; object-fit: contain; }

/* The attribute rows: icon, base (+ staged), gear, total, the -/+ pair. No word column: the icon
   is the stat and its card names it. The caption row's last cell is as wide as the pair. */
.csw .stat, .csw .stats__head { grid-template-columns: 1.6em repeat(3, minmax(0, 1fr)) auto; }
.csw .stats__head span:first-child { grid-column: auto; }
.csw__stepcap { width: 3.85em; }
.csw__acts { display: inline-flex; gap: .5em; }
.csw__acts[hidden] { display: none; }

/* The combat table: two columns of pairs in a short scroller. */
.csw__combat { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 1em; max-height: 9em; }

/* Set bonuses: the set on the left, what it lifts on the right, one stat to a line. */
.csw__sets { display: grid; gap: .45em; min-width: 0; }
.csw__set { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .2em 1em; align-items: start; color: var(--green); }
.csw__setvals { display: grid; justify-items: end; white-space: nowrap; }

/* A deployed pet: its head, name and level, the position's icon (the word in its card). The row
   opens the pet's sheet; the Player window's empty row opens nothing. */
.csw__pet { cursor: var(--cur-hand, pointer); }
.pfw .csw__pet.item-row--empty { cursor: var(--cur-arrow, default); }
.csw__pet .pos-ic { width: 1.6em; height: 1.6em; object-fit: contain; }
.csw__pet .pos-ic.off { opacity: .35; }

/* DESKTOP: the sheet ends above the hotbar's band (mode.ts hudRects: 70 HUD px and the 10 px edge,
   at the HUD's zoom), the body scrolling under the head when the screen is shorter (B161). */
html.m-desktop .wins > .win.csw { max-height: calc(.92 * var(--frame-h) - 88px * var(--hud-z, 1)); }

/* MOBILE: a narrower figure, so the numbers keep their width on a landscape phone sheet. */
html.m-mobile .csw__body, html.m-mobile .pfw__body { grid-template-columns: 15em minmax(0, 1fr); gap: .7em; }
html.m-mobile .csw__fig .doll-box { --doll-w: 7em; }

/* ---- w-compound.css */
/* COMPOUNDING (panels/compounding.ts, `.win.cmpw[data-win=compound]`, P29): the tool (tile, name,
   charges), the deal on one dashed drop (material tile with its pile > the next quality's tile with
   the count this attempt makes, the name and the step under it), the odds (slider and the server's
   rate), and the foot (the bench's last word, Compound). Placed by gui/win.ts beside the Backpack;
   moved from 25-card.css. Tokens only. */
.cmpw [hidden] { display: none !important; }
.wins > .win.cmpw > .win__body { display: grid; gap: .7em; align-content: start; min-width: 0; }

.cmpw__tool { display: flex; align-items: center; gap: .6em; min-width: 0; }
.cmpw__tool > .tile { flex: none; }
.cmpw__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmpw__charges { flex: none; color: var(--ink-mute); font-variant-numeric: tabular-nums; }

.cmpw__drop { gap: .5em; min-height: 7.5em; }
.cmpw__deal { display: flex; align-items: center; justify-content: center; gap: .8em; }
.cmpw__to { width: 1.4em; height: 1.4em; object-fit: contain; opacity: .7; }
.cmpw__named { display: grid; justify-items: center; gap: .15em; min-width: 0; max-width: 100%; }
.cmpw__named > b { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }

.cmpw__odds { display: flex; align-items: center; gap: .6em; min-width: 0; }
.cmpw__ic { width: 1.3em; height: 1.3em; object-fit: contain; flex: none; }
.cmpw__range { flex: 1; min-width: 0; width: auto; }
.cmpw__rate { margin-inline-start: auto; min-width: 3.6em; font-size: var(--fs-sm); }
.cmpw__rate.is-dim { opacity: .55; }

.cmpw .win__foot { gap: .6em; }
.cmpw__line { flex: 1; min-width: 0; }
.cmpw .win__foot > .btn { flex: none; white-space: nowrap; }

/* The result dialog (ask.form body): the haul as its tile, its name and step, then the line. */
.cmpw__result { display: grid; gap: .6em; justify-items: center; text-align: center; }
.cmpw__got { display: grid; justify-items: center; gap: .3em; }

/* Phones (landscape): the line under the bench may wrap in the small size. */
html.m-mobile .cmpw__line { font-size: var(--fs-xs); }
html.m-mobile .cmpw__drop { min-height: 6em; }

/* ---- w-dealer.css */
/* THE PET DEALER (panels/pet-dealer/*, `.win.pdw[data-win=petdealer]`, P27) and the growth table a
   fusion leaves (`.win.pgw[data-win=petgrowth]`). 20-components.css draws every part (tabs, tile, band,
   row, well, note line); this sheet lays them out and draws the bench diagram (moved here from
   20-components.css's Fusion block). Tokens only. */

.pdw [hidden], .pgw [hidden] { display: none !important; }
.wins > .win.pdw > .win__body { display: flex; flex-direction: column; gap: .6em; min-height: 0; }
/* One height whatever the page (no jump on a tab press), ending above the hotbar's band; the page
   scrolls inside it. */
html.m-desktop .wins > .win.pdw { height: min(40em, calc(.92 * var(--frame-h) - 88px * var(--hud-z, 1))); }
.pdw .tabs.is-dim, .pdw .tabs__tab.is-dim { opacity: .55; }

.pdw__page { display: grid; gap: .6em; align-content: start; min-width: 0; }
.pdw__bench { display: grid; grid-template-columns: minmax(0, 26em) minmax(0, 1fr); gap: .9em; align-items: start; min-width: 0; }
.pdw__side { display: grid; gap: .55em; align-content: start; min-width: 0; }
.pdw__side > .well { display: grid; gap: .35em; }
.pdw__cap { display: flex; align-items: baseline; gap: .5em; }
.pdw__capn { color: var(--ink-mute); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.pdw__picker { grid-template-columns: repeat(auto-fill, var(--tile)); justify-content: start; }
.pdw__picker .tile[data-pid] { cursor: var(--cur-hand, pointer); }
/* A pet already on the bench: dimmed and ticked by its frame, still pickable to move it. */
.pdw__picker .tile.pdw__used { opacity: .45; }
/* Fusion's picker: Pet Slot's pets then the pen's, in one box that takes what the bench leaves of
   the window and scrolls inside it (never the window past the frame); at least one row of faces. */
.pdw__fusion { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.pdw__fusion > .pdw__bench { flex: none; }
.pdw__pool { --fade: .6em; flex: 1 1 auto; min-height: calc(var(--tile) + 2 * var(--fade)); display: grid; gap: .5em; align-content: start; overscroll-behavior: contain; }
.pdw__pool-sec { display: grid; gap: .3em; min-width: 0; }
.pdw__stones { --cols: 6; justify-content: start; }
.pdw__stones .tile { cursor: var(--cur-hand, pointer); }
.pdw__recipes .item-row { cursor: var(--cur-hand, pointer); }
.pdw__list { max-height: 16em; }
.pdw__sets { display: grid; gap: .7em; }
.pdw__set { display: grid; gap: .35em; }
.pdw__cards { --cols: 8; justify-content: start; }
.pdw .btn.is-dim { opacity: .55; }
.pdw__bench--album { grid-template-columns: auto minmax(0, 1fr); align-items: center; }
.pdw__extract { display: grid; justify-items: center; gap: .3em; padding: .6em 1em; cursor: var(--cur-hand, pointer); }

/* Each page's foot: its own line (a refusal, else what the bench still needs) and its commit. */
.pdw .win__foot { gap: .6em; }
.pdw__foot { display: flex; align-items: center; gap: .6em; flex: 1; min-width: 0; }
.pdw__foot > .note-line { flex: 1; min-width: 0; }
.pdw__foot > .btn { margin-left: auto; flex: none; }

/* ---------------------------------------------------------------- THE BENCH DIAGRAM
   Four corners (two above, two below) and the middle across the centre row, with the energy
   running from the four into the one: what fusion (and a stone combination) does, said once. The
   three rows are one height each, so the lines (drawn once in the diagram's own 0..100 box,
   kit.ts) meet the tiles at any width. */
.fuse {
  position: relative; display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(3, 5.4em);
  place-items: center; gap: .4em 3em; padding: .8em 1.2em;
  border: 1px solid var(--hairline); border-radius: .5em; background: var(--paper-2);
}
.fuse__node { position: relative; display: grid; justify-items: center; gap: .25em; min-width: 0; max-width: 100%; cursor: var(--cur-hand, pointer); }
.fuse__node--major { grid-column: 1 / -1; grid-row: 2; }
.fuse__node--nw { grid-column: 1; grid-row: 1; }
.fuse__node--ne { grid-column: 2; grid-row: 1; }
.fuse__node--sw { grid-column: 1; grid-row: 3; }
.fuse__node--se { grid-column: 2; grid-row: 3; }
.fuse__node.is-over .tile { box-shadow: 0 0 0 2px var(--fuse-glow); }
.fuse__seat { position: relative; display: grid; }
.fuse__role { font-size: var(--fs-sm); color: var(--ink-mute); max-width: 9em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fuse__node--major .fuse__role:not([class*="card__title--"]) { color: var(--brass); }

.fuse__energy { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; fill: none; stroke-linecap: round; }
.fuse__energy-base { stroke: var(--fuse-ink); stroke-width: 2; opacity: .3; }
.fuse__energy-flow {
  stroke: var(--fuse-ink); stroke-width: 2; stroke-dasharray: 8 22;
  filter: drop-shadow(0 0 3px var(--fuse-glow));
  animation: fuse-flow 1.2s linear infinite;
}
@keyframes fuse-flow { to { stroke-dashoffset: -30; } }
/* The charge: the second between the confirm and the send, the energy racing into the middle. */
.fuse.is-charging .fuse__energy-flow { stroke: var(--fuse-glow); stroke-width: 3; animation-duration: .35s; }
.fuse.is-charging .fuse__node--major .fuse__aura { filter: drop-shadow(0 0 6px var(--fuse-glow)); }
/* The bolts: one per FILLED corner, racing its line into the middle again and again while the charge
   lasts; the sacrifices drain and the Major swells as it takes them in. */
.fuse__comet {
  stroke: var(--fuse-glow); stroke-width: 5; stroke-dasharray: 18 120; stroke-dashoffset: 18; opacity: 0;
  filter: drop-shadow(0 0 6px var(--fuse-glow)) drop-shadow(0 0 2px #fff);
}
.fuse.is-charging .fuse__comet { animation: fuse-comet .55s cubic-bezier(.5, 0, .9, .6) infinite; }
.fuse.is-charging:has(.fuse__node--nw.is-full) .fuse__comet--nw,
.fuse.is-charging:has(.fuse__node--ne.is-full) .fuse__comet--ne,
.fuse.is-charging:has(.fuse__node--sw.is-full) .fuse__comet--sw,
.fuse.is-charging:has(.fuse__node--se.is-full) .fuse__comet--se { opacity: 1; }
@keyframes fuse-comet { from { stroke-dashoffset: 18; } to { stroke-dashoffset: -100; } }
.fuse.is-charging .fuse__node:not(.fuse__node--major).is-full .tile { filter: brightness(.6) saturate(.5); transition: filter .9s ease-in; }
.fuse.is-charging .fuse__node--major .fuse__seat { animation: fuse-take .55s ease-in-out infinite; }
@keyframes fuse-take { 50% { transform: scale(1.08); filter: drop-shadow(0 0 10px var(--fuse-glow)) brightness(1.25); } }

/* The ring around a filled node: the same two layers at tile scale. Off on an empty node. */
.fuse__aura { position: absolute; left: -.18em; top: -.18em; width: calc(100% + .36em); height: calc(100% + .36em); overflow: visible; pointer-events: none; fill: none; }
.fuse__aura-base { stroke: var(--fuse-ink); stroke-width: 1; opacity: .4; }
.fuse__aura-flow {
  stroke: var(--fuse-ink); stroke-width: 1.6; stroke-dasharray: 16 34;
  filter: drop-shadow(0 0 3px var(--fuse-glow));
  animation: fuse-orbit 2.4s linear infinite;
}
.fuse__node--major .fuse__aura-base { opacity: .7; stroke-width: 1.5; }
.fuse__node--major .fuse__aura-flow { stroke-width: 2; }
@keyframes fuse-orbit { to { stroke-dashoffset: -100; } }
.fuse__node:not(.is-full) .fuse__aura { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .fuse__energy-flow, .fuse__aura-flow, .fuse.is-charging .fuse__comet, .fuse.is-charging .fuse__node--major .fuse__seat { animation: none; }
  .fuse.is-charging .fuse__comet { stroke-dashoffset: -41; }
}

/* ---------------------------------------------------------------- THE GROWTH TABLE */
.pgw__who { display: flex; align-items: center; gap: .6em; }
.pgw__who > b { font-family: var(--face-ui); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.pgw__rows { display: grid; gap: .2em; }
.pgw__row { display: grid; grid-template-columns: 1.4em 3.5em 1em 3.5em 3em; align-items: center; gap: .5em; padding: .15em .4em; border-radius: .3em; font-variant-numeric: tabular-nums; }
.pgw__row.is-on { background: var(--row-on); }
.pgw__ic { width: 1.3em; height: 1.3em; object-fit: contain; }
.pgw__n { text-align: right; }
.pgw__to { width: .9em; height: .9em; opacity: .6; }
.pgw__d { color: var(--green); text-align: right; }
.pgw__d--down { color: var(--error); }
.pgw .win__foot { justify-content: flex-end; }

/* MOBILE MODE: the counter is the whole sheet; the bench and its side stay two columns (the short
   height is the constraint), the picker under them, rows and tiles a finger's size. */
/* Its own desktop width in the phone's em, never past 80% of the frame (owner, 2026-10-11: no full-width blocks). */
html.m-mobile .wins > .win.pdw { width: min(var(--w), .8 * var(--frame-w)); max-height: calc(var(--frame-h) - 16px); }
/* With Pet Slot up beside it (Pet Room, Fusion), the pair shares the screen: the counter 58%, Pet
   Slot 42%, a pet dragged straight across (index.ts moves the counter to the left edge). */
html.m-mobile .wins:has(> .win.petw:not([hidden])) > .win.pdw { width: calc(.58 * var(--frame-w) - 14px); }
html.m-mobile .wins:has(> .win.pdw:not([hidden])) > .win.petw { width: calc(.42 * var(--frame-w) - 14px); }
html.m-mobile .wins > .win.pdw > .win__body { gap: .45em; }
/* The diagram is a picture of four into one, not a form: it takes the narrower column, its rows and
   gaps tight, its names a size down; the side - the numbers and the choices - gets the room
   (owner, 2026-10-11: the bench looked forced, all air round the diagram). */
html.m-mobile .pdw__bench { grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: .5em; }
html.m-mobile .fuse { grid-template-rows: repeat(3, 3.9em); gap: .1em 1em; padding: .35em .6em; }
html.m-mobile .fuse__role { font-size: var(--fs-xs); max-width: 8em; }
html.m-mobile .pdw__side { gap: .4em; }
html.m-mobile .pdw__side > .well { padding: .4em .6em; gap: .25em; }
html.m-mobile .pdw__recipes .item-row { min-height: var(--touch); }
html.m-mobile .pdw__foot > .btn { min-height: var(--touch); }

/* ---- w-dm.css */
/* DIRECT MESSAGE (panels/direct-message.ts, `.win.dmw-win[data-win=dm]`): the rail of
   conversations on the left (face, name, last line, unread count; New message under it), the
   conversation on the right (who it is with, the log read left and right in 20-components.css's `.talk`,
   the field and the send button, a refusal under them). Placed by gui/win.ts; this sheet lays it
   out. Moved from 25-card.css (P18), tokens only. */
.dmw { display: grid; grid-template-columns: 15em minmax(0, 1fr); gap: .8em; align-items: stretch; min-width: 0; }
.dmw__rail { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: .5em; min-width: 0; }
.dmw__rail > .scroll { height: 19em; }
.dmw__rail > .btn > img.g { width: 1.3em; height: 1.3em; }
.dmw__threads > .item-row { cursor: var(--cur-hand, pointer); }
.dmw__threads > .item-row .item-row__sub { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.dmw__talk { display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; gap: .5em; min-width: 0; }
.dmw__talk[hidden], .dmw__none[hidden], .dmw__talk > .note-line[hidden] { display: none; }
.dmw__who { display: flex; align-items: center; gap: .5em; min-width: 0; }
.dmw__who-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: var(--fs-lg); }
.dmw__log { height: 19em; }
.dmw__log > .note-line[hidden] { display: none; }
.dmw__say { display: flex; align-items: center; gap: .5em; min-width: 0; }
.dmw__say > .field { flex: 1; min-width: 0; }
.dmw__face { width: 3.2em; height: 3.2em; }
.dmw__none { display: grid; gap: .3em; justify-items: center; align-content: center; text-align: center; }
.dmw__none > img.g { width: 3em; height: 3em; }

/* A face: the paperdoll's head crop on the dark well, else the initial. */
.dm-face { overflow: hidden; }
.dm-face.is-dark { background: var(--void); }
.dm-letter { display: grid; place-items: center; width: 100%; height: 100%; font-size: var(--fs-lg); color: var(--ink-mute); }

/* Mobile mode: a narrower rail, and both columns take the sheet's height instead of a fixed one. */
html.m-mobile .dmw { grid-template-columns: 11em minmax(0, 1fr); }
html.m-mobile .dmw__rail > .scroll, html.m-mobile .dmw__log { height: calc(var(--frame-h) - 13em); min-height: 6em; }
html.m-mobile .dmw__say > .ibtn { min-width: var(--touch); min-height: var(--touch); }

/* ---- w-door.css */
/* THE DOORKEEPER (instance-entry.ts, `.win.door[data-win=instance]`, P32): the day's entries in the
   band, the zone's plate and name beside its facts, the reward paths (a wrapping text strip with the
   picks left as a pill), what the path pays (a grade line, quality dots on the equip painting, the
   goods' tiles two shelves deep at most), the no-key warning, the skip's roster (a name and three
   paintings, dark when not ready) and the refusal line. Placed by gui/win.ts; moved from 25-card.css.
   Tokens only. */
.door [hidden] { display: none !important; }
.wins > .win.door > .win__body { display: grid; grid-template-columns: minmax(0, 1fr); gap: .7em; align-content: start; min-width: 0; }
.door .band__num { font-variant-numeric: tabular-nums; }

/* The plate and its name, beside the facts. A grid item's automatic minimum is its content: every
   column says min-width 0, and a long value falls under its own label rather than out of the frame. */
.door__top { display: grid; grid-template-columns: 13.6em minmax(0, 1fr); gap: .8em; align-items: start; min-width: 0; }
.door__pic { display: grid; gap: .4em; justify-items: center; min-width: 0; }
.door__pic > .well { display: grid; justify-items: center; }
.door__plate { display: block; width: 12em; height: 8.2em; object-fit: cover; }
.door .kv { min-width: 0; }
.door .kv > div { flex-wrap: wrap; }

.door__types, .door__loot, .door__skip { display: grid; gap: .5em; min-width: 0; }
.door__head { display: flex; align-items: center; gap: .5em; }
.door__head > .pill { font-variant-numeric: tabular-nums; }
.door .tabs--block { flex-wrap: wrap; }
.door .tabs--text .tabs__tab { min-width: 5.6em; padding: 0 1.1em; }

/* The goods: tiles in a wrapping shelf, the card names each one. */
.door__goods { display: flex; flex-wrap: wrap; gap: .35em; }
.door__gear { position: relative; display: inline-grid; place-items: center; width: 2.4em; height: 2.4em; border-radius: .3em; background: rgba(var(--ink-wash), .06); box-shadow: inset 0 0 0 1px var(--hairline); }
.door__gear > img.g { width: 1.7em; height: 1.7em; object-fit: contain; }
.door__gear::after { content: ""; position: absolute; right: .2em; bottom: .2em; width: .6em; height: .6em; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 1px var(--win-rim); }
.door__gear[data-q="green"]::after { background: var(--world-q-green); }
.door__gear[data-q="blue"]::after { background: var(--world-q-blue); }
.door__gear[data-q="yellow"]::after { background: var(--world-q-yellow); }
.door__gear[data-q="gold"]::after { background: var(--world-q-gold); }
.door__gear[data-q="purple"]::after { background: var(--world-q-purple); }

/* The skip's roster: who, then a ticket, a clear and a key, lit when ready. */
.door__crew { display: grid; gap: .25em; }
.door__member { display: flex; align-items: center; gap: .5em; }
.door__who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.door__mark { flex: none; display: inline-grid; place-items: center; width: 1.6em; height: 1.6em; }
.door__mark > img.g { width: 1.4em; height: 1.4em; object-fit: contain; }
.door__mark > .ico { --ico-box: 1.4em; }
.door__mark.is-off { opacity: .35; filter: grayscale(1); }

.door .win__foot > .door__gap { flex: 1; }

html.m-mobile .door__top { grid-template-columns: 10em minmax(0, 1fr); }
html.m-mobile .door__plate { width: 9em; height: 6.2em; }

/* ---- w-ev-gacha.css */
/* A GACHA MACHINE inside the Events hub (panels/ev-gacha.ts), under .evw__body[data-kind="gacha"]:
   left the main pane - the band (the key art as a backdrop, the featured prizes standing on its foot
   over a scrim) and the prize pool under it on a solid ground, headed by the pity rule; right the
   deck - the pity as it stands, the last pull, the two pulls. The accent, ground and motif are the
   machine's own (--acc-rgb, --deep, --motif from w-events.css). The header strip is a plain bar here
   (back, title, the purse in its slot). Then THE PULL (gacha-fx.ts, `.gfx` on <body> at --z-fx,
   30-win.css): its canvas, the cards, the hint and the bar. Everything in em of --win-size on a
   .4em step (--gs); the cards in --cw, a share of the screen. */

/* ---- the header strip and the rail card: a plain bar, no art, no time pill (a machine has none) */
.evw__view[data-kind="gacha"] .evw__thumb {
  background: linear-gradient(90deg, rgba(var(--acc-rgb), .1), transparent 45%), var(--navy-2);
}
.evw__view[data-kind="gacha"] .evw__head::after { content: none; }
.evw__item[data-kind="gacha"] .evw__words { justify-content: flex-end; }
.evw__view[data-kind="gacha"] .gcv__purse, .evw__view[data-kind="gacha"] .gcv__lumi { display: inline-flex; align-items: center; gap: .4em; }
.evw__view[data-kind="gacha"] .gcv__purse [hidden] { display: none !important; }

/* ---- the view */
.evw__body[data-kind="gacha"] [hidden] { display: none !important; }
.evw__body[data-kind="gacha"] > .gcv {
  --gs: .4em;                                               /* the spacing step */
  --deck-w: clamp(20em, 30vw, 27em);                        /* the right side, wider (owner, 2026-10-10) */
  --ground: color-mix(in srgb, var(--deep) 38%, var(--navy-1));
  flex: 1; min-height: 0; position: relative; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr) var(--deck-w); grid-template-rows: minmax(0, 1fr);
  background: var(--ground);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 .2em rgba(var(--navy-1-rgb), .96),
    0 0 0 calc(.2em + 1px) rgba(var(--acc-rgb), .45), var(--win-shadow);
}

/* ---- the main pane: the band over the pool. Sized off its own box (cq units), so the band takes
   a share of the height whatever the width, and the pool keeps the rest. */
.gcv__main {
  position: relative; min-width: 0; min-height: 0; container-type: size;
  display: grid; grid-template-rows: auto minmax(0, 1fr);
}
.gcv__band {
  position: relative; overflow: hidden;
  height: clamp(9em, 40cqh, 22em);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 calc(var(--gs) * 4) calc(var(--gs) * 3);
}
.gcv__art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%; }
.gcv__motif { position: absolute; inset: 0; background: var(--motif); opacity: .4; pointer-events: none; }
/* the scrim: solid ground under the featured row, the art clear above it, a soft edge at the sides */
.gcv__shade {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(0deg, var(--ground) 0, rgba(var(--wash-rgb), .82) 30%, rgba(var(--wash-rgb), .35) 62%, transparent 85%),
    linear-gradient(90deg, rgba(var(--wash-rgb), .45), transparent 22%, transparent 78%, rgba(var(--wash-rgb), .45));
}
.gcv__feat { position: relative; display: grid; gap: calc(var(--gs) * 1.5); }
.gcv__feat-tag {
  font-size: var(--fs-small); letter-spacing: .16em; text-transform: uppercase;
  color: rgb(var(--acc-rgb)); text-shadow: 0 1px 2px rgba(var(--shadow-ink), .95);
}
/* One row of cards, as many as fit: the rest wrap onto a second line that the row's height cuts off. */
.gcv__show {
  --card-w: 5.8em;
  display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 0 calc(var(--gs) * 3);
  height: 9.8em; overflow: hidden;
}
.gcv__card {
  position: relative; width: var(--card-w); height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: var(--gs);
  text-align: center;
}
.gcv__card > .tile { box-shadow: inset 0 0 0 max(1px, .1em) var(--tile-rim), 0 0 0 1px rgba(var(--acc-rgb), .35), 0 .3em .8em rgba(var(--shadow-ink), .6); }
.gcv__card-name {
  width: 100%; min-height: 2.3em; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  font-size: var(--fs-small); line-height: 1.15; color: var(--ink-strong);
  text-shadow: 0 1px 2px rgba(var(--shadow-ink), .95);
}
/* the grand prize: the focal card - the tallest, the accent's halo, its tag over it */
.gcv__card.is-grand { --card-w: 7.6em; }
.gcv__card.is-grand > .tile {
  font-size: 1.9em;
  box-shadow: inset 0 0 0 max(1px, .06em) var(--tile-rim), 0 0 0 max(1px, .06em) rgb(var(--acc-rgb)),
    0 0 .9em rgba(var(--acc-rgb), .55), 0 .2em .5em rgba(var(--shadow-ink), .6);
}
.gcv__card.is-grand > .gcv__card-name { color: rgb(var(--acc-rgb)); font-weight: 700; }

/* the pool: a solid ground, its head (the name and the pity rule), one row per tier */
.gcv__pool { min-height: 0; display: flex; flex-direction: column; background: var(--ground); }
.gcv__pool-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--gs) calc(var(--gs) * 3);
  padding: calc(var(--gs) * 2) calc(var(--gs) * 4) calc(var(--gs) * 1.5);
  box-shadow: inset 0 -1px 0 var(--hairline);
}
.gcv__pool-head > b {
  font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: var(--fs-heading); color: var(--ink-strong); letter-spacing: .04em;
}
.gcv__rule { font-size: var(--fs-small); color: var(--ink-mute); }
.gcv__tiers {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
  display: flex; flex-direction: column;
  padding: 0 calc(var(--gs) * 4) var(--gs);
}
/* A row grows into the room left, so a short pool fills the pane instead of ending in a gap. Each
   row is in its rarest prize's rarity colour (--q): a stripe at its left edge, a wash fading from
   it, and the figure in the same ink; the grand tier's wash is a step stronger. No names. */
.gcv__tier {
  --q: var(--ink-mute);
  flex: 1 0 auto;
  display: grid; grid-template-columns: 7.2em minmax(0, 1fr); align-items: center; gap: calc(var(--gs) * 3);
  margin: 0 calc(var(--gs) * -4); padding: calc(var(--gs) * 2) calc(var(--gs) * 4);
  background: linear-gradient(90deg, color-mix(in srgb, var(--q) 9%, transparent), transparent 45%);
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--q) 70%, transparent);
}
.gcv__tier[data-q="purple"] { --q: var(--world-q-purple); }
.gcv__tier[data-q="gold"] { --q: var(--world-q-gold); }
.gcv__tier[data-q="yellow"] { --q: var(--world-q-yellow); }
.gcv__tier[data-q="blue"] { --q: var(--world-q-blue); }
.gcv__tier[data-q="green"] { --q: var(--world-q-green); }
.gcv__tier + .gcv__tier { box-shadow: inset 3px 0 0 color-mix(in srgb, var(--q) 70%, transparent), inset 0 1px 0 rgba(var(--sky-rgb), .08); }
.gcv__tier.is-grand {
  background: linear-gradient(90deg, color-mix(in srgb, var(--q) 20%, transparent), color-mix(in srgb, var(--q) 5%, transparent) 60%, transparent);
  box-shadow: inset 3px 0 0 var(--q);
}
.gcv__tier-head { display: grid; justify-items: start; gap: calc(var(--gs) * .5); line-height: 1.1; }
.gcv__tier-pct { font-size: var(--fs-heading); color: var(--q); font-variant-numeric: tabular-nums; white-space: nowrap; }
.gcv__tier.is-grand .gcv__tier-pct { font-size: calc(var(--fs-heading) * 1.2); }
.gcv__tier-each { font-size: var(--fs-small); color: var(--ink-mute); }
.gcv__tier-items { display: flex; flex-wrap: wrap; gap: calc(var(--gs) * 1.5); }
.gcv__tier-items .tile { font-size: 1.1em; }
.gcv__odd { display: grid; justify-items: center; gap: calc(var(--gs) * .5); }
.gcv__odd > i { font-style: normal; font-size: var(--fs-badge); color: var(--ink-mute); font-variant-numeric: tabular-nums; }

/* ---- the deck: one solid pane, its three blocks ruled apart; the pulls at its foot */
.gcv__deck {
  position: relative; min-height: 0;
  display: flex; flex-direction: column; gap: calc(var(--gs) * 3);
  padding: calc(var(--gs) * 4) calc(var(--gs) * 4) calc(var(--gs) * 3.5);
  overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
  background: linear-gradient(180deg, var(--navy-2), var(--navy-1));
  box-shadow: inset 1px 0 0 rgba(var(--acc-rgb), .35);
}
.gcv__deck::-webkit-scrollbar { display: none; }

.gcv__pity { flex: none; display: flex; flex-direction: column; gap: calc(var(--gs) * 2); }
/* THE DIAL (owner, 2026-10-10: radial, not a bar chart): a 270-degree ring from the lower left round
   to the lower right, one segment per pull up to the pity (ev-gacha.ts sets --n, --done, --soft).
   Done in the machine's accent, the next one white, the plain stretch a faint track, soft pity in
   brass, the guaranteed last one bright gold. The gaps between segments and the ring's hollow are
   the mask's: the deck shows through them. */
.gcv__dial {
  --a: 270deg; --seg: calc(var(--a) / var(--n, 24)); --w: 1.3em;
  position: relative; width: min(100%, 13em); aspect-ratio: 1; align-self: center;
  display: grid; place-items: center;
}
.gcv__ring {
  position: absolute; inset: 0; border-radius: 50%;
  background:
    conic-gradient(from 225deg, transparent 0 calc(var(--seg) * var(--done)), var(--ink-strong) 0 calc(var(--seg) * (var(--done) + 1)), transparent 0),
    conic-gradient(from 225deg,
      rgb(var(--acc-rgb)) 0 calc(var(--seg) * var(--done)),
      rgba(var(--sky-rgb), .16) 0 calc(var(--seg) * var(--soft)),
      rgba(var(--brass-hi-rgb), .5) 0 calc(var(--seg) * (var(--n) - 1)),
      var(--brass-pale) 0 var(--a), transparent 0);
  -webkit-mask:
    radial-gradient(farthest-side, transparent calc(100% - var(--w)), #000 calc(100% - var(--w) + 1px)),
    repeating-conic-gradient(from 225deg, #000 0 calc(var(--seg) - 1.6deg), transparent 0 var(--seg));
  -webkit-mask-composite: source-in;
  mask:
    radial-gradient(farthest-side, transparent calc(100% - var(--w)), #000 calc(100% - var(--w) + 1px)),
    repeating-conic-gradient(from 225deg, #000 0 calc(var(--seg) - 1.6deg), transparent 0 var(--seg));
  mask-composite: intersect;
  filter: drop-shadow(0 0 .4em rgba(var(--acc-rgb), .35));
}
.gcv__tally { position: relative; display: inline-flex; align-items: baseline; gap: .3em; font-variant-numeric: tabular-nums; }
.gcv__count {
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 3.4em; line-height: 1;
  color: var(--ink-strong); text-shadow: 0 0 .6em rgba(var(--acc-rgb), .45);
}
.gcv__of { font-size: var(--fs-heading); color: var(--ink-mute); }
.gcv__guar { margin: 0; text-align: center; font-size: var(--fs-heading); line-height: 1.25; color: var(--ink-strong); }
/* The pity prize (choosePity): a slot the player fills with the grand prize the pity pull gives; empty, a + that asks for it. */
.gcv__pick-row { display: flex; align-items: center; justify-content: space-between; gap: var(--gs); }
.gcv__pick-row[hidden], .gcv__hint[hidden] { display: none; }
.gcv__pick-cap { color: var(--ink-mute); font-size: var(--fs-small); }
.gcv__pick { all: unset; cursor: inherit; border-radius: .3em; }
.gcv__pick:focus-visible { outline: 1px solid rgb(var(--acc-rgb)); outline-offset: 2px; }
.gcv__plus { display: grid; place-items: center; color: rgb(var(--acc-rgb)); }
.gcv__pick.is-empty .gcv__plus { box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .7), 0 0 .8em rgba(var(--acc-rgb), .35); animation: gcv-plus 1.6s ease-in-out infinite; }
@keyframes gcv-plus { 50% { box-shadow: inset 0 0 0 1px rgb(var(--acc-rgb)), 0 0 1.2em rgba(var(--acc-rgb), .55); } }
@media (prefers-reduced-motion: reduce) { .gcv__pick.is-empty .gcv__plus { animation: none; } }
.gcv__hint { margin: 0; color: rgb(var(--acc-rgb)); font-size: var(--fs-small); text-align: center; }
.gcv__choose { display: grid; gap: calc(var(--gs) * 2); }
.gcv__choose-line { margin: 0; color: var(--ink); }
.gcv__choose-row { display: flex; flex-wrap: wrap; justify-content: center; gap: calc(var(--gs) * 2); }
.gcv__choice { all: unset; cursor: inherit; display: grid; justify-items: center; gap: var(--gs); width: 7.5em; padding: var(--gs); border-radius: .4em; text-align: center; }
.gcv__choice:hover, .gcv__choice:focus-visible { background: rgba(var(--acc-rgb), .12); }
.gcv__choice.is-on { box-shadow: inset 0 0 0 1px rgb(var(--acc-rgb)); }
.gcv__choice-name { font-size: var(--fs-small); color: var(--ink-strong); line-height: 1.2; }
.gcv__next { margin: 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--gs); font-size: var(--fs-small); color: var(--ink-mute); }
.gcv__next > b { font-size: var(--fs-heading); color: rgb(var(--acc-rgb)); font-variant-numeric: tabular-nums; }

/* The pull history: the deck's spare height, scrolling inside itself. One row per press: when, then
   its prizes in pull order (a grand one ringed in the accent). Empty, one quiet line says so. */
.gcv__hist {
  flex: 1 1 0; min-height: 6em; display: flex; flex-direction: column; gap: calc(var(--gs) * 1.5);
  padding-top: calc(var(--gs) * 3); box-shadow: inset 0 1px 0 var(--hairline);
}
.gcv__hist-list {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
  display: flex; flex-direction: column; gap: calc(var(--gs) * 1.5);
}
.gcv__hist-row { display: grid; gap: calc(var(--gs) * .75); padding-bottom: calc(var(--gs) * 1.5); box-shadow: inset 0 -1px 0 var(--hairline); }
.gcv__hist-at { font-size: var(--fs-small); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.gcv__hist-got { display: flex; flex-wrap: wrap; gap: var(--gs); }
.gcv__hist-got > .tile { --tile: 2.6em; }
.gcv__hist-none { margin: 0; font-size: var(--fs-small); color: var(--ink-mute); }
.gcv__won-grand { --tile-ring: rgb(var(--acc-rgb)); }

/* The two pulls: one family (the square, two lines: the count, the price); x5 is the lit one - wider,
   the accent's rim and face, a glow. */
.gcv__pulls { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: calc(var(--gs) * 2); padding-top: calc(var(--gs) * 3); box-shadow: inset 0 1px 0 var(--hairline); }
.gcv__pull.btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: calc(var(--gs) * .75);
  min-width: 0; padding: calc(var(--gs) * 2) var(--gs); white-space: nowrap;
  --btn-rim: rgba(var(--acc-rgb), .5); --btn-face: var(--navy-3);
}
.gcv__pn { font-size: var(--fs-heading); font-weight: 700; letter-spacing: .04em; }
.gcv__price .chip { min-width: 0; height: 1.7em; padding: 0 .6em; background: rgba(var(--navy-0-rgb), .55); color: var(--ink); pointer-events: none; }
.gcv__pull--5.btn {
  --btn-rim: rgb(var(--acc-rgb));
  --btn-face: color-mix(in srgb, rgb(var(--acc-rgb)) 26%, var(--navy-2));
  box-shadow: 0 0 1.2em rgba(var(--acc-rgb), .32);
}
.gcv__pull--5 .gcv__pn { color: var(--ink-strong); }
.gcv__pull.is-busy { opacity: .75; }
/* The same two pulls paid in an event good (gacha.json `alt`), under the Gold ones, no rule between. */
.gcv__pulls--alt { padding-top: calc(var(--gs) * 1.5); box-shadow: none; }
.gcv__pulls--alt[hidden], .gcv__tok[hidden] { display: none; }
.gcv__good > .ico { --ico-box: 1.4em; }
.gcv__said { margin: 0; color: var(--error); font-size: var(--fs-small); text-align: center; }

/* ---- a tablet's narrower pane: the deck keeps its width, the main pane's cards step down */
@media (max-width: 1240px) {
  .evw__body[data-kind="gacha"] > .gcv { --deck-w: clamp(18em, 31vw, 22em); }
  .gcv__band { padding-inline: calc(var(--gs) * 3); }
  .gcv__pool-head, .gcv__tiers { padding-inline: calc(var(--gs) * 3); }
  .gcv__tier { margin-inline: calc(var(--gs) * -3); padding-inline: calc(var(--gs) * 3); }
  .gcv__tier { grid-template-columns: 6.2em minmax(0, 1fr); gap: calc(var(--gs) * 2); }
  .gcv__tier-items .tile { font-size: 1em; }
}

/* ---- a landscape phone (the height is the constraint): the band keeps the tiles, the names go;
   the pool's tiles shrink; the deck tightens and the last pull goes small */
@media (max-height: 560px) {
  .evw__body[data-kind="gacha"] > .gcv { --deck-w: clamp(15em, 28vw, 18em); }
  .gcv__band { height: clamp(6.6em, 33cqh, 10em); padding: 0 calc(var(--gs) * 2.5) calc(var(--gs) * 2); }
  .gcv__feat-tag, .gcv__card-name { display: none; }
  .gcv__show { height: 4.6em; gap: 0 calc(var(--gs) * 2); }
  .gcv__card { width: auto; }
  .gcv__card > .tile { font-size: 1em; }
  .gcv__card.is-grand > .tile { font-size: 1.25em; }
  .gcv__pool-head { padding: var(--gs) calc(var(--gs) * 2.5); }
  .gcv__tiers { padding: 0 calc(var(--gs) * 2.5) calc(var(--gs) * .5); }
  .gcv__tier { grid-template-columns: 5em minmax(0, 1fr); gap: calc(var(--gs) * 2); padding: var(--gs) calc(var(--gs) * 2.5); margin-inline: calc(var(--gs) * -2.5); }
  .gcv__tier-pct { font-size: 1em; }
  .gcv__tier-items { gap: var(--gs); }
  .gcv__tier-items .tile { font-size: .82em; }
  .gcv__deck { padding: calc(var(--gs) * 2) calc(var(--gs) * 2.5); gap: calc(var(--gs) * 1.5); }
  .gcv__pity { gap: var(--gs); }
  .gcv__count { font-size: 1.8em; }
  .gcv__guar { font-size: 1em; }
  .gcv__dial { width: min(100%, 7.5em); --w: .9em; }
  .gcv__hist { padding-top: calc(var(--gs) * 1.5); gap: var(--gs); min-height: 4em; }
  .gcv__hist-got > .tile { --tile: 2.1em; }
  .gcv__pulls { padding-top: calc(var(--gs) * 1.5); gap: var(--gs); }
  .gcv__pulls--alt { padding-top: var(--gs); }
  .gcv__pull.btn { padding: var(--gs) calc(var(--gs) * .5); gap: calc(var(--gs) * .5); }
  .gcv__pn { font-size: 1em; }
  .gcv__price .chip { height: 1.5em; padding: 0 .4em; }
}

/* ======================================================================= THE PULL (gacha-fx.ts) */
.gfx {
  --gap: clamp(.6em, 1.6vw, 1.6em);
  position: fixed; inset: 0; overflow: hidden; contain: strict;
  font-size: var(--win-size); color: var(--ink); cursor: var(--cur-arrow, default);
  background: radial-gradient(ellipse at 50% 50%, #0d1530, #04070f 72%);
  user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.gfx:not(.is-live) { opacity: 0; pointer-events: none; }
.gfx [hidden] { display: none !important; }
.gfx__gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.gfx__flash {
  position: absolute; inset: 0; pointer-events: none; opacity: 0; z-index: 2;
  background: radial-gradient(circle at 50% 50%, #fff, rgba(255, 248, 232, .85) 30%, rgba(255, 240, 210, 0) 78%);
}

/* The row: one card large, five fitted to the width and to the short height alike. The card box is
   the card itself (2:3): both faces are cut to their edge, nothing stands out of it. */
.gfx--one { --cw: min(34vw, calc(56vh * .6667), 19em); }
/* Five in a row: the gap is a share of the card (.16), so five cards and four gaps fill at most the
   row's width (5 + 4 x .16 = 5.64). */
.gfx--many { --cw: min(calc(94vw / 5.7), calc(54vh * .6667), 15em); --gap: calc(var(--cw) * .16); }
.gfx__row {
  position: absolute; inset: 0 0 5em; z-index: 1; pointer-events: none;
  display: flex; align-items: center; justify-content: center; gap: var(--gap); padding: 0 3vw;
}
.gfx__card { position: relative; flex: none; width: var(--cw); aspect-ratio: 2 / 3; opacity: 0; pointer-events: auto; cursor: var(--cur-hand, pointer); }
.gfx__card.is-in { opacity: 1; }
.gfx__card.is-up { cursor: inherit; }
/* the idle: a little float, each card on its own beat */
.gfx__float {
  position: absolute; inset: 0; z-index: 1; perspective: calc(var(--cw) * 4);
  animation: gfx-float 3.4s ease-in-out infinite alternate; animation-delay: calc(var(--i) * -.8s);
}
@keyframes gfx-float {
  from { transform: translate3d(0, calc(var(--cw) * -.022), 0); }
  to { transform: translate3d(0, calc(var(--cw) * .022), 0); }
}
.gfx__flip { position: absolute; inset: 0; transform-style: preserve-3d; transform: rotateY(180deg); }
.gfx__card.is-up .gfx__flip { transform: none; }
.gfx__face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.gfx__face--back { transform: rotateY(180deg); }
/* Both faces are the card to its edge (alpha-cut, rounded): the glow is a drop-shadow that follows
   that edge, never a box around the image (owner, 2026-10-10: no border outside the card). */
.gfx__back, .gfx__frame { display: block; position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.gfx__face { filter: drop-shadow(0 0 .35em rgba(var(--brass-hi-rgb), .45)) drop-shadow(0 .6em .9em rgba(0, 0, 0, .55)); }
.gfx__frame { z-index: 1; }
.gfx__win { position: absolute; z-index: 2; left: 50%; top: 53%; width: 56%; aspect-ratio: 1; translate: -50% -50%; display: grid; place-items: center; }
.gfx__glow { position: absolute; inset: -8%; border-radius: 50%; background: radial-gradient(closest-side, rgba(var(--rim-rgb), .6), rgba(var(--rim-rgb), 0)); }
.gfx__ico { position: relative; --ico-box: calc(var(--cw) * .25); image-rendering: auto; filter: drop-shadow(0 .2em .3em rgba(0, 0, 0, .6)); }
.gfx__qty {
  position: absolute; right: 2%; bottom: 4%;
  font: 700 calc(var(--cw) * .08) / 1 var(--face-ui); color: var(--ink-strong);
  text-shadow: 0 1px 0 #000, 0 0 .4em #000;
}
.gfx__card { --rim-rgb: var(--sky-rgb); --tier-rgb: var(--sky-rgb); }
.gfx__card[data-rim="purple"] { --rim-rgb: 179, 147, 214; }
.gfx__card[data-rim="blue"] { --rim-rgb: 134, 168, 208; }
.gfx__card[data-rim="gold"] { --rim-rgb: 240, 150, 74; }
.gfx__card[data-rim="yellow"] { --rim-rgb: 227, 196, 86; }
.gfx__card[data-rim="green"] { --rim-rgb: 147, 194, 131; }
.gfx__card[data-rank="1"] { --tier-rgb: 110, 214, 255; }
.gfx__card[data-rank="2"] { --tier-rgb: 255, 198, 92; }
/* purple is the top (owner, 2026-10-10), as on the quality ladder */
.gfx__card[data-rank="3"] { --tier-rgb: 186, 120, 255; }

/* the light behind a card once it is up, in its tier's colour. A sibling of the floating card, at
   z-index 0 in the row's stacking context while every card stands at 1: the light of one card stays
   behind ALL the cards, never over its neighbours (owner, 2026-10-10). */
.gfx__aura {
  position: absolute; z-index: 0; inset: -16% -24%; border-radius: 50%; pointer-events: none; opacity: 0;
  background: radial-gradient(closest-side, rgba(var(--tier-rgb), .8), rgba(var(--tier-rgb), .25) 55%, rgba(var(--tier-rgb), 0));
  transition: opacity .4s ease-out;
}
.gfx__card.is-up[data-rank="1"] .gfx__aura { opacity: .4; }
.gfx__card.is-up[data-rank="2"] .gfx__aura { opacity: .75; }
.gfx__card.is-up[data-rank="3"] .gfx__aura { opacity: 1; }
.gfx__rays {
  position: absolute; z-index: 0; inset: -42% -60%; pointer-events: none; opacity: 0;
  background: repeating-conic-gradient(from 0deg, rgba(var(--tier-rgb), .26) 0 5deg, transparent 5deg 15deg);
  -webkit-mask: radial-gradient(closest-side, #000 25%, transparent 100%); mask: radial-gradient(closest-side, #000 25%, transparent 100%);
  animation: gfx-spin 16s linear infinite paused;
}
.gfx__card.is-up[data-rank="3"] .gfx__rays { opacity: 1; animation-play-state: running; transition: opacity .6s ease-out .45s; }
@keyframes gfx-spin { to { transform: rotate(1turn); } }

/* the words and the buttons along the foot */
.gfx__hint {
  position: absolute; z-index: 3; left: 0; right: 0; bottom: max(1.7em, var(--safe-b));
  text-align: center; pointer-events: none; opacity: 0; transition: opacity .3s;
  font-size: var(--fs-heading); letter-spacing: .16em; text-transform: uppercase; color: rgba(var(--sky-rgb), .85);
  text-shadow: 0 0 1em rgba(var(--sky-rgb), .35);
}
.gfx__hint.is-on { opacity: 1; animation: gfx-breathe 2.4s ease-in-out infinite; }
@keyframes gfx-breathe { 50% { opacity: .5; } }
.gfx__foot {
  position: absolute; z-index: 3; left: max(1.6em, var(--safe-l)); right: max(1.6em, var(--safe-r));
  bottom: max(1.2em, var(--safe-b)); display: flex; justify-content: center; pointer-events: none;
}
.gfx__foot > * { pointer-events: auto; }
.gfx__bar { display: flex; gap: 1.2em; align-items: center; }
.gfx__bar .btn { min-width: 9em; }

@media (max-height: 560px) {
  .gfx--one { --cw: min(30vw, calc(60vh * .6667), 15em); }
  .gfx--many { --cw: min(calc(92vw / 5.7), calc(58vh * .6667)); }
  .gfx__row { inset: 0 0 3.6em; }
  .gfx__hint { font-size: 1em; bottom: max(1em, var(--safe-b)); }
  .gfx__foot { bottom: max(.7em, var(--safe-b)); }
}
@media (prefers-reduced-motion: reduce) {
  .gfx__float, .gfx__rays, .gfx__hint.is-on { animation: none; }
  .gfx__aura { transition: none; }
}

/* While the pull covers the screen the page under it rests: nothing beneath is painted. */
html.gfx-cover .hud, html.gfx-cover .wins { visibility: hidden; }

/* ---- w-ev-journey.css */
/* TAMER'S JOURNEY view inside the Events hub (panels/ev-journey.ts), under .evw__body[data-kind=journey].
   Left the ladder, one row per level gift: the level in a box, the name and where it stands, the goods.
   A rung received is greyed under a check; one on its way wears the accent; the next one is lit and
   framed; a locked one is dim. Right the tally, the next gift with the climb to it, the rules, the
   mailbox button. The ladder scrolls; the side never does. */
.evw__body[data-kind="journey"] .jrv [hidden] { display: none !important; }
.evw__body[data-kind="journey"] .jrv {
  flex: 1; min-height: 0; min-width: 0;
  display: grid; gap: 1em;
  grid-template-columns: minmax(0, 46em) minmax(15em, 22em); grid-template-rows: minmax(0, 1fr);
  justify-content: center;
}
.evw__body[data-kind="journey"] :is(.jrv__ladder, .jrv__side) {
  position: relative; min-width: 0; min-height: 0;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .8), rgba(var(--navy-1-rgb), .94));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 2px rgba(var(--brass-rgb), .28),
    inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), var(--win-shadow);
}

/* ---- the ladder */
.evw__body[data-kind="journey"] .jrv__ladder { overflow-y: auto; padding: .7em; display: flex; flex-direction: column; gap: .4em; }
.evw__body[data-kind="journey"] .jrv__rung {
  position: relative; flex: none; display: grid; align-items: center; gap: .8em;
  grid-template-columns: 3.2em minmax(7em, 1fr) auto 1.4em;
  padding: .45em .7em; border-radius: .35em;
  background: rgba(var(--navy-4-rgb), .55); box-shadow: inset 0 0 0 1px rgba(var(--sky-rgb), .08);
}
.evw__body[data-kind="journey"] .jrv__lvbox {
  display: grid; justify-items: center; line-height: 1; padding: .3em 0; border-radius: .3em;
  background: rgba(var(--shadow-ink), .35); color: var(--ink-mute);
}
.evw__body[data-kind="journey"] .jrv__lvbox > small { font-size: var(--fs-badge); letter-spacing: .08em; text-transform: uppercase; }
.evw__body[data-kind="journey"] .jrv__lv { font-family: var(--face-display); font-size: var(--fs-title); color: var(--brass-pale); }
.evw__body[data-kind="journey"] .jrv__text { display: grid; gap: .15em; min-width: 0; }
.evw__body[data-kind="journey"] .jrv__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
.evw__body[data-kind="journey"] .jrv__status { font-size: var(--fs-small); color: var(--ink-mute); white-space: nowrap; }
.evw__body[data-kind="journey"] .jrv__items { display: flex; flex-wrap: wrap; gap: .3em; justify-content: flex-end; }
.evw__body[data-kind="journey"] .jrv__check { width: 1.1em; height: 1.1em; justify-self: center; }

.evw__body[data-kind="journey"] .jrv__rung[data-state="sent"] .jrv__items .tile { opacity: .55; filter: saturate(.4); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="sent"] .jrv__status { color: var(--brass-hi); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="sent"] .jrv__check {
  background: var(--brass-hi);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 8.5l4 4 8-9' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.evw__body[data-kind="journey"] .jrv__rung[data-state="owed"] { box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .5); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="owed"] .jrv__status { color: rgb(var(--acc-rgb)); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="next"] {
  background: linear-gradient(90deg, rgba(var(--acc-rgb), .18), rgba(var(--navy-4-rgb), .6) 70%);
  box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .75), 0 0 1em rgba(var(--acc-rgb), .2);
}
.evw__body[data-kind="journey"] .jrv__rung[data-state="next"] .jrv__lvbox { background: rgba(var(--acc-rgb), .2); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="next"] .jrv__status { color: rgb(var(--acc-rgb)); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="locked"] :is(.jrv__name, .jrv__lv) { color: var(--ink-mute); }
.evw__body[data-kind="journey"] .jrv__rung[data-state="locked"] .jrv__items .tile { opacity: .7; }

/* ---- the side */
.evw__body[data-kind="journey"] .jrv__side { display: flex; flex-direction: column; gap: .9em; padding: 1em 1.2em 1.2em; overflow: hidden; }
.evw__body[data-kind="journey"] .jrv__tally { display: flex; align-items: baseline; gap: .35em; flex-wrap: wrap; }
.evw__body[data-kind="journey"] .jrv__count { font-family: var(--face-display); font-size: 2.6em; line-height: 1; color: var(--brass-pale); }
.evw__body[data-kind="journey"] .jrv__of { font-family: var(--face-display); font-size: var(--fs-title); color: var(--ink-mute); }
.evw__body[data-kind="journey"] .jrv__tally > small { flex-basis: 100%; font-size: var(--fs-small); color: var(--ink-mute); }
.evw__body[data-kind="journey"] .jrv__next {
  display: grid; gap: .35em; padding: .6em .7em; border-radius: .35em;
  background: var(--band-bg); box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .35);
}
.evw__body[data-kind="journey"] .jrv__kicker {
  display: flex; align-items: center; gap: .4em; font-size: var(--fs-small); letter-spacing: .08em;
  text-transform: uppercase; color: rgb(var(--acc-rgb));
}
.evw__body[data-kind="journey"] .jrv__kicker > img.g { width: 1.1em; height: 1.1em; object-fit: contain; }
.evw__body[data-kind="journey"] .jrv__nextname { font-size: var(--fs-heading); color: var(--ink); }
.evw__body[data-kind="journey"] .jrv__nextat { font-size: var(--fs-small); color: var(--ink-mute); }
.evw__body[data-kind="journey"] .jrv__next .jrv__items { justify-content: flex-start; margin: .2em 0; }
.evw__body[data-kind="journey"] .jrv__done { margin: 0; color: var(--brass-hi); }
.evw__body[data-kind="journey"] .jrv__rules {
  margin: 0; padding: 0 0 0 1.1em; display: grid; align-content: start; gap: .4em; flex: 1; min-height: 0;
  font-size: var(--fs-small); color: var(--ink-mute);
}
.evw__body[data-kind="journey"] .jrv__rules > li::marker { color: rgb(var(--acc-rgb)); }

/* ---- short screens (a landscape phone): a tighter frame, the goods smaller */
@media (max-height: 560px) {
  .evw__body[data-kind="journey"] .jrv { grid-template-columns: minmax(0, 1fr) minmax(11.5em, 15em); gap: .6em; }
  .evw__body[data-kind="journey"] .jrv__ladder { padding: .4em; gap: .3em; }
  .evw__body[data-kind="journey"] .jrv__rung { padding: .3em .45em; gap: .5em; grid-template-columns: 2.6em minmax(5em, 1fr) auto 1.1em; }
  .evw__body[data-kind="journey"] .jrv__lv { font-size: var(--fs-heading); }
  .evw__body[data-kind="journey"] .jrv__side { padding: .5em .6em .6em; gap: .45em; }
  .evw__body[data-kind="journey"] .jrv__count { font-size: 1.8em; }
  .evw__body[data-kind="journey"] .jrv__rules { display: none; }
}

/* ---- w-ev-pass.css */
/* THE TAMER PASS view inside the Events hub (panels/ev-pass.ts), under .evw__body[data-kind=pass]:
   the level strip, the Rewards and Missions pages in one cell (the page switch sits in the hub's
   header slot), the foot; then the three questions it asks in ask.ts's dialog (.psv-ask).

   One spacing step (--g, .6em) and its multiples. Every surface is solid and readable: the strip
   and the detail are navy plates, the switch a well with a brass-filled pick, the board two
   colour-coded bands - Unbound silver on slate, Starward gold on indigo, each led by its emblem
   plate (data/art/ui/pass-unbound, pass-starward on pass-field-*). The board fills the height; its
   tiles grow with it (container units on .psv__board), capped by --tmax, so a tall screen gets
   bigger cards and never taller empty boxes. Everything is in em of --win-size, so a short phone
   shrinks it with the windows. Motion is transform and opacity only. */

[data-kind="pass"] .psv {
  --g: .6em;                          /* the spacing step */
  --side: 10.5em;                     /* the tier plates */
  --rh0: 3em;                         /* the level numbers' row */
  --rg: .35em;                        /* between the board's rows */
  --tmax: 6.6em;                      /* the largest a rung's tile grows */
  --silver-rgb: 206, 220, 244;
  --gold-rgb: var(--brass-hi-rgb);
  flex: 1; min-height: 0; display: flex; flex-direction: column; gap: calc(var(--g) * 1.5);
}

/* ---- the level strip: a solid plate. The level as a figure, the meter with its figure beside it
   (never on the fill), My Level and Up Level. */
[data-kind="pass"] .psv__bar {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: calc(var(--g) * 2);
  padding: var(--g) calc(var(--g) * 1.5) var(--g) var(--g);
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .97), rgba(var(--navy-2-rgb), .97));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), inset 0 0 0 1px rgba(var(--brass-rgb), .2),
    inset 0 1px 0 rgba(var(--brass-pale-rgb), .14), 0 .5em 1.4em rgba(var(--shadow-ink), .45);
}
[data-kind="pass"] .psv__lv {
  display: grid; justify-items: center; align-content: center; gap: .1em;
  min-width: 6.2em; padding: .15em calc(var(--g) * 1.5) .2em;
  border-right: 1px solid rgba(var(--brass-rgb), .22);
}
[data-kind="pass"] .psv__lvcap { font-size: var(--fs-badge); letter-spacing: .14em; color: var(--brass-hi); white-space: nowrap; }
[data-kind="pass"] .psv__lvrow { display: flex; align-items: baseline; gap: .3em; }
[data-kind="pass"] .psv__lvnum {
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 2.5em; line-height: 1;
  color: var(--ink-strong); font-variant-numeric: tabular-nums;
  text-shadow: 0 0 .5em rgba(var(--sky-rgb), .35), 0 .05em .1em rgba(var(--shadow-ink), .9);
}
[data-kind="pass"] .psv__lvof { color: var(--ink-mute); font-variant-numeric: tabular-nums; }
[data-kind="pass"] .psv__track { display: grid; gap: calc(var(--g) * .75); min-width: 0; }
[data-kind="pass"] .psv__mtop { display: flex; align-items: baseline; justify-content: space-between; gap: var(--g); min-width: 0; }
[data-kind="pass"] .psv__season {
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: var(--fs-heading);
  color: var(--ink-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[data-kind="pass"] .psv__mnums { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The meter: a slim trough with no words in it. Brass rising into star-white (00-tokens --pass-*). */
[data-kind="pass"] .psv__meter {
  --bar: var(--pass-fill); height: .7em; border-radius: 999px; background: var(--navy-0);
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .32), var(--pass-glow);
}
[data-kind="pass"] .psv__meter > .bar__text { display: none; }
[data-kind="pass"] .psv__acts { display: flex; align-items: center; gap: var(--g); }
[data-kind="pass"] .psv__acts > .btn[hidden] { display: none; }
[data-kind="pass"] .psv__acts .btn { display: inline-flex; align-items: center; gap: .35em; white-space: nowrap; }
.psv__star { width: 1.1em; height: 1.1em; object-fit: contain; }

/* ---- the page switch, in the hub's header slot: a solid well, the picked page filled in brass
   with navy words, the other quiet ink. Readable over any key art. */
[data-kind="pass"] .psv__switch {
  display: flex; gap: .2em; padding: .25em; border-radius: .45em;
  background: var(--navy-0);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .95), inset 0 0 0 1px rgba(var(--brass-rgb), .42), 0 .3em .9em rgba(var(--shadow-ink), .55);
}
[data-kind="pass"] .psv__sw {
  position: relative; display: inline-flex; align-items: center; gap: .4em;
  padding: .5em 1.3em; border: 0; border-radius: .3em; background: transparent;
  font: inherit; font-family: var(--face-display); font-weight: var(--face-display-weight); letter-spacing: .06em;
  color: var(--ink-mute); cursor: var(--cur-hand, pointer); white-space: nowrap;
}
[data-kind="pass"] .psv__sw:hover:not(.is-on) { color: var(--ink-strong); background: rgba(var(--sky-rgb), .07); }
[data-kind="pass"] .psv__sw.is-on {
  color: var(--navy-1);
  background: linear-gradient(180deg, var(--brass-pale), var(--brass-hi) 45%, var(--brass));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), inset 0 -1px 0 rgba(var(--shadow-ink), .25), 0 0 .9em rgba(var(--brass-rgb), .35);
  cursor: var(--cur-arrow, default);
}
/* What waits behind a switch or a heading: a small green count beside its word. */
.psv__count {
  display: inline-grid; place-items: center; min-width: 1.45em; padding: 0 .3em; margin-left: .4em;
  border-radius: 1em; font-family: var(--face-ui); font-weight: 700; font-size: var(--fs-badge); line-height: 1.45; letter-spacing: 0;
  background: var(--green); color: var(--navy-1); box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .6);
}
.psv__count[hidden] { display: none; }

/* ---- the pages: both in one cell, so the switch's morph never reflows */
[data-kind="pass"] .psv__stage { flex: 1; min-height: 0; display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); }
[data-kind="pass"] .psv__page { grid-area: 1 / 1; min-width: 0; min-height: 0; }
[data-kind="pass"] .psv__page[hidden] { display: none; }
[data-kind="pass"] .psv__rw { display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr) 16em; gap: calc(var(--g) * 1.5); }

/* ---- the board: the ruler row, then the two tier bands; plates outside the scroller */
[data-kind="pass"] .psv__board {
  position: relative; min-height: 0; container-type: size;
  display: grid; grid-template-columns: var(--side) minmax(0, 1fr); grid-template-rows: var(--rh0) minmax(0, 1fr) minmax(0, 1fr);
  row-gap: var(--rg);
}
[data-kind="pass"] .psv__band { grid-column: 1 / -1; position: relative; }
[data-kind="pass"] .psv__band--free {
  grid-row: 2;
  background: linear-gradient(90deg, rgba(var(--silver-rgb), .15), rgba(var(--silver-rgb), .07) 45%, rgba(var(--silver-rgb), .05)), #17223a;
  box-shadow: inset 0 2px 0 rgba(var(--silver-rgb), .45), inset 0 -1px 0 rgba(var(--shadow-ink), .6), 0 0 0 1px rgba(var(--shadow-ink), .8);
}
[data-kind="pass"] .psv__band--prem {
  grid-row: 3;
  background: linear-gradient(90deg, rgba(var(--gold-rgb), .2), rgba(var(--gold-rgb), .07) 40%, rgba(134, 104, 236, .16)), #1b1446;
  box-shadow: inset 0 2px 0 rgba(var(--gold-rgb), .75), inset 0 -1px 0 rgba(var(--shadow-ink), .6), 0 0 0 1px rgba(var(--shadow-ink), .8),
    0 0 1.6em rgba(var(--gold-rgb), .1);
}

[data-kind="pass"] .psv__side { grid-column: 1; grid-row: 1 / -1; position: relative; z-index: 1; display: grid; grid-template-rows: subgrid; }
/* The ruler's label over the plates, where the level numbers start. */
[data-kind="pass"] .psv__corner {
  align-self: end; justify-self: end; padding: 0 var(--g) .55em 0;
  font-size: var(--fs-badge); letter-spacing: .14em; color: var(--ink-mute);
}
/* A plate: the emblem over the tier's name and its state, centred, so the name never runs under
   the road whatever the language. */
[data-kind="pass"] .psv__tier {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: calc(var(--g) * .6);
  min-width: 0; padding: 0 calc(var(--g) * .5); text-align: center;
  border: 0; font: inherit; color: var(--ink);
  box-shadow: inset -1px 0 0 rgba(var(--tier-rgb), .35);
}
[data-kind="pass"] .psv__tier--free {
  --tier-rgb: var(--silver-rgb); grid-row: 2;
  background: linear-gradient(90deg, rgba(var(--wash-rgb), .25), rgba(var(--wash-rgb), .55)), url(/data/art/ui/pass-field-unbound.ab0d560f.webp) left bottom / cover no-repeat, #1d2940;
}
[data-kind="pass"] .psv__tier--prem {
  --tier-rgb: var(--gold-rgb); grid-row: 3; cursor: var(--cur-hand, pointer);
  background: linear-gradient(90deg, rgba(var(--wash-rgb), .1), rgba(var(--wash-rgb), .5)), url(/data/art/ui/pass-field-starward.660ecdec.webp) left center / cover no-repeat, #211852;
}
[data-kind="pass"] .psv__tier--prem:disabled { cursor: inherit; }
[data-kind="pass"] .psv__emblem { position: relative; flex: none; width: 4em; height: 4em; object-fit: contain; filter: drop-shadow(0 0 .6em rgba(var(--tier-rgb), .5)); }
[data-kind="pass"] .psv__tier--prem:not(:disabled):hover .psv__emblem { transform: scale(1.06); }
[data-kind="pass"] .psv__tier--prem .psv__emblem { transition: transform .15s; }
[data-kind="pass"] .psv__tname { position: relative; display: grid; justify-items: center; gap: .2em; min-width: 0; max-width: 100%; }
[data-kind="pass"] .psv__tname > b {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 1.15em; letter-spacing: .08em;
  color: rgb(var(--tier-rgb)); text-shadow: 0 0 .7em rgba(var(--tier-rgb), .4), 0 .06em .12em rgba(var(--shadow-ink), .9);
}
[data-kind="pass"] .psv__tstate { display: inline-flex; align-items: center; gap: .3em; font-size: var(--fs-small); color: var(--ink); line-height: 1.25; }
[data-kind="pass"] .psv__tstate .ic { width: 1em; height: 1em; }
[data-kind="pass"] .psv__tier--prem:disabled .psv__tstate { color: var(--green); }

/* The road: one column per level, snapped sideways. No browser bar: a horizontal one would eat
   height the plates beside it cannot see; a wheel, a swipe and My Level move it. The tile is sized
   off the board's height here: what a band leaves after the card's name and state, capped. */
[data-kind="pass"] .psv__road {
  --rowh: calc((100cqh - var(--rh0) - 2 * var(--rg)) / 2);
  --tc: clamp(2.4em, calc(var(--rowh) - 4.4em), var(--tmax));
  --cw: calc(var(--tc) + 2.1em);
  grid-column: 2; grid-row: 1 / -1; position: relative; z-index: 1; min-width: 0;
  display: flex; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; scrollbar-width: none;
  padding: 0 .3em;
}
[data-kind="pass"] .psv__road::-webkit-scrollbar { display: none; }
[data-kind="pass"] .psv__col {
  position: relative; flex: none; width: var(--cw);
  display: grid; grid-template-rows: var(--rh0) minmax(0, 1fr) minmax(0, 1fr); row-gap: var(--rg);
  scroll-snap-align: start;
  content-visibility: auto; contain-intrinsic-size: auto 7em auto 20em;
}
/* A landmark column wears a faint gold band; the player's own column a sky one, the whole height. */
[data-kind="pass"] .psv__col.is-big::before,
[data-kind="pass"] .psv__col.is-here::before {
  content: ""; position: absolute; inset: 0 .08em; pointer-events: none;
  background: linear-gradient(180deg, rgba(var(--gold-rgb), .2), rgba(var(--gold-rgb), .05) 30%, rgba(var(--gold-rgb), .07));
  box-shadow: inset 0 0 0 1px rgba(var(--gold-rgb), .22);
}
[data-kind="pass"] .psv__col.is-here::before {
  background: linear-gradient(180deg, rgba(var(--sky-rgb), .26), rgba(var(--sky-rgb), .09) 30%, rgba(var(--sky-rgb), .11));
  box-shadow: inset 0 0 0 1px rgba(var(--sky-rgb), .55), 0 0 1.2em rgba(var(--sky-rgb), .18);
}

/* The ruler: each level's number over a rail that is lit up to the player's level, a node on it. */
[data-kind="pass"] .psv__num { position: relative; display: grid; place-items: start center; padding-top: .2em; }
[data-kind="pass"] .psv__num::before {          /* the rail through every column */
  content: ""; position: absolute; left: 0; right: 0; bottom: .55em; height: 2px; background: var(--navy-0);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .5);
}
[data-kind="pass"] .psv__num::after {           /* the node */
  content: ""; position: absolute; left: 50%; bottom: .55em; width: .5em; height: .5em; translate: -50% 50%; rotate: 45deg;
  background: var(--navy-1); box-shadow: 0 0 0 1px rgba(var(--brass-rgb), .5);
}
[data-kind="pass"] .psv__num > b {
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 1.35em; line-height: 1.15;
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
[data-kind="pass"] .psv__col.is-reached .psv__num::before { background: linear-gradient(90deg, var(--brass), var(--brass-hi)); }
[data-kind="pass"] .psv__col.is-reached .psv__num::after { background: var(--brass-hi); box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .8); }
[data-kind="pass"] .psv__col.is-reached .psv__num > b { color: var(--ink); }
[data-kind="pass"] .psv__col.is-big .psv__num > b { color: rgb(var(--gold-rgb)); text-shadow: 0 0 .6em rgba(var(--gold-rgb), .45); }
/* Where the player stands: the number on a sky plate, the node larger and lit. */
[data-kind="pass"] .psv__col.is-here .psv__num > b {
  padding: 0 .4em; border-radius: .25em; color: var(--navy-1); background: rgb(var(--sky-rgb)); text-shadow: none;
  box-shadow: 0 0 .8em rgba(var(--sky-rgb), .6);
}
[data-kind="pass"] .psv__col.is-here .psv__num::after {
  width: .7em; height: .7em; background: rgb(var(--sky-rgb));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 .7em rgba(var(--sky-rgb), .9);
}

/* A rung: a card in its band. The picture, the name, the state line; the tier tints it. */
[data-kind="pass"] .psv__cell--free { --tier-rgb: var(--silver-rgb); }
[data-kind="pass"] .psv__cell--prem { --tier-rgb: var(--gold-rgb); }
[data-kind="pass"] .psv__cell,
[data-kind="pass"] .psv__slot {
  position: relative; z-index: 1; place-self: center;
  width: calc(var(--cw) - .5em); height: calc(100% - .6em); max-height: calc(var(--tc) + 5em);
  border-radius: .4em;
}
[data-kind="pass"] .psv__cell {
  display: grid; grid-template-rows: minmax(0, 1fr) auto auto; justify-items: center; align-items: center; gap: .2em;
  padding: .4em .25em .3em;
  background: linear-gradient(180deg, rgba(var(--tier-rgb), .1), rgba(var(--wash-rgb), .55) 70%);
  box-shadow: inset 0 0 0 1px rgba(var(--tier-rgb), .26), 0 .2em .5em rgba(var(--shadow-ink), .35);
  cursor: var(--cur-hand, pointer);
  transition: transform .15s;
}
[data-kind="pass"] .psv__cell:hover { transform: translateY(-.15em); }
[data-kind="pass"] .psv__art { position: relative; display: flex; flex-wrap: wrap; gap: .2em; justify-content: center; align-items: center; }
[data-kind="pass"] .psv__art > .tile { --tile: var(--tc); }
[data-kind="pass"] .psv__art--n > .tile { --tile: calc(var(--tc) * .56); }
[data-kind="pass"] .psv__art .tile__text { font-size: var(--fs-badge); }
[data-kind="pass"] .psv__name {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-badge); line-height: 1.3; color: var(--ink);
}
[data-kind="pass"] .psv__state {
  display: grid; place-items: center; width: 100%; min-height: 1.55em;
  font-size: var(--fs-badge); font-weight: 700; letter-spacing: .06em; line-height: 1;
}
[data-kind="pass"] .psv__state:empty { visibility: hidden; }
/* The mark over the picture: a check on a claimed rung, a padlock on Starward not bought. */
[data-kind="pass"] .psv__mark { position: absolute; z-index: 3; display: grid; place-items: center; border-radius: 50%; pointer-events: none; }
[data-kind="pass"] .psv__mark[hidden] { display: none; }
[data-kind="pass"] .psv__mark .ic { width: 62%; height: 62%; }
[data-kind="pass"] .psv__mark--taken {
  left: 50%; top: 50%; translate: -50% -50%; width: 1.9em; height: 1.9em;
  background: color-mix(in srgb, var(--green) 62%, var(--navy-0));
  box-shadow: 0 0 0 1.5px var(--green), 0 0 0 3px rgba(var(--shadow-ink), .75), 0 .15em .5em rgba(var(--shadow-ink), .7);
}
[data-kind="pass"] .psv__mark--taken .ic { filter: drop-shadow(0 1px 1px rgba(var(--shadow-ink), .8)); }
[data-kind="pass"] .psv__mark--shut {
  right: -.4em; top: -.4em; width: 1.75em; height: 1.75em;
  background: var(--navy-0); box-shadow: 0 0 0 1px rgba(var(--gold-rgb), .7), 0 .1em .3em rgba(var(--shadow-ink), .8);
}
/* Claimed: the card sinks back, the name greys, the check says it. */
[data-kind="pass"] .psv__cell.is-taken { background: rgba(var(--wash-rgb), .5); box-shadow: inset 0 0 0 1px rgba(var(--tier-rgb), .1); }
[data-kind="pass"] .psv__cell.is-taken .psv__name { color: var(--ink-faint); }
[data-kind="pass"] .psv__cell.is-taken .psv__state { color: var(--green); font-weight: 600; }
/* Starward not bought: the picture behind a veil of the band, the padlock on its corner. */
[data-kind="pass"] .psv__cell.is-shut .psv__art > .tile { filter: saturate(.4) brightness(.62); }
[data-kind="pass"] .psv__cell.is-shut .psv__name { color: var(--ink-mute); }
/* Ready: the brass ring, a breathing light, the state line a filled Claim. */
[data-kind="pass"] .psv__cell.is-can {
  background: linear-gradient(180deg, rgba(var(--gold-rgb), .24), rgba(var(--wash-rgb), .5) 75%);
  box-shadow: inset 0 0 0 2px rgba(var(--brass-pale-rgb), .9), 0 0 1.1em rgba(var(--gold-rgb), .5);
}
[data-kind="pass"] .psv__cell.is-can .psv__state {
  border-radius: .25em; color: var(--navy-1); letter-spacing: .1em;
  background: linear-gradient(180deg, var(--brass-pale), var(--brass-hi) 50%, var(--brass));
}
[data-kind="pass"] .psv__cell.is-focus { box-shadow: inset 0 0 0 2px rgb(var(--sky-rgb)), 0 0 1em rgba(var(--sky-rgb), .45); }
/* An empty level: a quiet slot - only the picture's place, a faint outline round a faint point,
   so the eye skips it and lands on the levels that hold something. */
[data-kind="pass"] .psv__slot {
  display: grid; place-items: center;
  height: auto; aspect-ratio: 1; width: calc(var(--tc) + .4em);
  background: rgba(var(--wash-rgb), .16); box-shadow: inset 0 0 0 1px rgba(var(--tier-rgb), .1);
}
[data-kind="pass"] .psv__slot > i { width: .35em; height: .35em; rotate: 45deg; background: rgba(var(--tier-rgb), .22); }

/* A rung that can be taken: a breathing light and four points rising through it. */
[data-kind="pass"] .psv__glow { position: absolute; inset: 0; z-index: 0; overflow: hidden; border-radius: inherit; pointer-events: none; }
[data-kind="pass"] .psv__glow[hidden] { display: none; }
[data-kind="pass"] .psv__glow::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 1.4em rgba(var(--gold-rgb), .6);
  animation: psv-breathe 1.8s ease-in-out infinite alternate;
}
[data-kind="pass"] .psv__glow i {
  position: absolute; bottom: -.4em; left: var(--x); width: var(--s); height: var(--s); border-radius: 50%;
  background: var(--mote); box-shadow: 0 0 .45em .08em var(--mote-glow);
  opacity: 0; animation: psv-mote var(--d) ease-in var(--w) infinite;
}
[data-kind="pass"] .psv__glow i:nth-child(1) { --x: 18%; --s: .45em; --d: 2.4s; --w: 0s; }
[data-kind="pass"] .psv__glow i:nth-child(2) { --x: 42%; --s: .34em; --d: 2s;   --w: -.8s; }
[data-kind="pass"] .psv__glow i:nth-child(3) { --x: 64%; --s: .5em;  --d: 2.8s; --w: -1.6s; }
[data-kind="pass"] .psv__glow i:nth-child(4) { --x: 82%; --s: .3em;  --d: 2.2s; --w: -.4s; }
@keyframes psv-breathe { from { opacity: .35; } to { opacity: 1; } }
@keyframes psv-mote {
  0%   { transform: translateY(0) scale(.5); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: translateY(-6em) scale(1); opacity: 0; }
}

/* ---- the detail: the reward under the pointer, the one tapped, or the grand prize; the board's height */
[data-kind="pass"] .psv__detail {
  --tier-rgb: var(--silver-rgb);
  min-height: 0;
  display: grid; grid-template-rows: auto minmax(5em, 1fr) auto minmax(0, auto) auto; gap: var(--g);
  padding: calc(var(--g) * 1.5);
  background:
    radial-gradient(ellipse 80% 40% at 50% 40%, rgba(var(--tier-rgb), .16), transparent 70%),
    linear-gradient(180deg, rgba(var(--navy-3-rgb), .97), rgba(var(--navy-1-rgb), .97));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), inset 0 2px 0 rgba(var(--tier-rgb), .6), inset 0 0 0 1px rgba(var(--tier-rgb), .22), var(--win-shadow);
}
[data-kind="pass"] .psv__detail[data-track="premium"] { --tier-rgb: var(--gold-rgb); }
[data-kind="pass"] .psv__detail[hidden] { display: none; }
[data-kind="pass"] .psv__dhead { display: flex; align-items: center; justify-content: space-between; gap: var(--g); }
[data-kind="pass"] .psv__dtag {
  padding: .15em .8em; border-radius: .25em; font-size: var(--fs-small); letter-spacing: .12em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
  font-family: var(--face-display); font-weight: var(--face-display-weight);
  color: rgb(var(--tier-rgb)); background: rgba(var(--tier-rgb), .12); box-shadow: inset 0 0 0 1px rgba(var(--tier-rgb), .5);
}
[data-kind="pass"] .psv__dlv { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
[data-kind="pass"] .psv__dstage { display: grid; place-items: center; min-height: 0; overflow: hidden; container-type: size; }
/* The figure as big as the stage holds (the box is twice as tall as wide). */
[data-kind="pass"] .psv__dstage .doll-box { --doll-w: min(10em, 50cqh, 90cqw); }
[data-kind="pass"] .psv__dtiles { display: flex; flex-wrap: wrap; gap: var(--g); justify-content: center; }
[data-kind="pass"] .psv__dtiles .tile { filter: drop-shadow(0 0 1em rgba(var(--tier-rgb), .4)); }
[data-kind="pass"] .psv__dpicks:empty { display: none; }
[data-kind="pass"] .psv__dpicks .tabs__tab { flex: 1 1 0; min-width: 0; padding: 0 .5em; }
[data-kind="pass"] .psv__dcard { min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
[data-kind="pass"] .psv__dcard > .card { width: auto; padding: 0; background: none; box-shadow: none; }
[data-kind="pass"] .psv__dfoot { display: grid; }
[data-kind="pass"] .psv__dfoot > [hidden] { display: none; }
[data-kind="pass"] .psv__dstate {
  display: flex; align-items: center; justify-content: center; gap: .35em; min-height: 2.4em;
  border-radius: .3em; background: rgba(var(--wash-rgb), .55); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .14);
  color: var(--ink-mute); font-size: var(--fs-small);
}
[data-kind="pass"] .psv__dstate .ic { width: 1.1em; height: 1.1em; }
[data-kind="pass"] .psv__dstate[data-k="taken"] { color: var(--green); }

/* ---- the missions: one list - Daily under its head, then the Weekly divider, then Weekly */
[data-kind="pass"] .psv__ms {
  overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent;
  padding: calc(var(--g) * 1.5);
  background: linear-gradient(180deg, rgba(var(--navy-2-rgb), .96), rgba(var(--navy-1-rgb), .97));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), inset 0 0 0 1px rgba(var(--brass-rgb), .18), var(--win-shadow);
}
[data-kind="pass"] .psv__shead {
  display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(10em, 17em); align-items: center; gap: calc(var(--g) * 2);
  margin-bottom: var(--g); padding: calc(var(--g) * .8) var(--g) calc(var(--g) * .8) calc(var(--g) * 1.25);
  background: linear-gradient(90deg, rgba(var(--brass-rgb), .16), rgba(var(--navy-3-rgb), .9) 45%, rgba(var(--navy-2-rgb), .85));
  box-shadow: inset 3px 0 0 var(--brass-hi), 0 0 0 1px rgba(var(--shadow-ink), .7), inset 0 -1px 0 rgba(var(--brass-rgb), .25);
}
[data-kind="pass"] .psv__shead[hidden] { display: none; }
[data-kind="pass"] .psv__shead--div { margin-top: calc(var(--g) * 2.5); }
[data-kind="pass"] .psv__stitle {
  margin: 0; display: inline-flex; align-items: center;
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: var(--fs-title); line-height: 1.1;
  color: var(--ink-strong); letter-spacing: .04em;
}
[data-kind="pass"] .psv__reset { min-width: 0; color: var(--ink); font-size: var(--fs-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
[data-kind="pass"] .psv__spts { display: flex; align-items: center; gap: var(--g); min-width: 0; }
[data-kind="pass"] .psv__spts > .bar { flex: 1; min-width: 3em; }
[data-kind="pass"] .psv__mgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(23em, 1fr)); gap: var(--g); }
[data-kind="pass"] .psv__mgrid:empty { display: none; }
[data-kind="pass"] .psv__mission {
  display: grid; grid-template-columns: 3.6em minmax(0, 1fr) auto; align-items: center; gap: calc(var(--g) * 1.4);
  min-height: 5.2em; padding: calc(var(--g) * 1.2) calc(var(--g) * 1.4) calc(var(--g) * 1.2) var(--g);
  background: linear-gradient(90deg, rgba(var(--navy-3-rgb), .96), rgba(var(--navy-2-rgb), .94));
  box-shadow: inset 3px 0 0 rgba(var(--brass-pale-rgb), .2), 0 0 0 1px rgba(var(--shadow-ink), .7), inset 0 0 0 1px rgba(var(--brass-rgb), .1);
}
[data-kind="pass"] .psv__mission.is-ready { box-shadow: inset 3px 0 0 var(--green), 0 0 0 1px rgba(var(--shadow-ink), .7), inset 0 0 0 1px rgba(134, 196, 106, .45), 0 0 1em rgba(134, 196, 106, .15); }
[data-kind="pass"] .psv__mission.is-taken { opacity: .5; }
[data-kind="pass"] .psv__pts {
  display: grid; place-items: center; width: 3.3em; height: 3.3em; justify-self: center; border-radius: 50%;
  font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 1.05em; color: var(--brass-hi);
  background: radial-gradient(circle, rgba(var(--brass-rgb), .2), rgba(var(--brass-rgb), .05) 70%);
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .5);
}
[data-kind="pass"] .psv__mbody { display: grid; gap: .4em; min-width: 0; }
[data-kind="pass"] .psv__mtitle { font-weight: 600; font-size: var(--fs-heading); color: var(--ink-strong); line-height: 1.25; }
[data-kind="pass"] .psv__mline { display: flex; align-items: center; gap: var(--g); }
[data-kind="pass"] .psv__mline > .bar { flex: 1; min-width: 0; }
[data-kind="pass"] .psv__nums { color: var(--ink-mute); font-size: var(--fs-small); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---- the foot: a refusal at the left, Buy Starward and Claim all at the right */
[data-kind="pass"] .psv__foot { display: flex; align-items: center; gap: var(--g); min-height: 2.6em; }
[data-kind="pass"] .psv__foot > .note-line { flex: 1; min-width: 0; }
[data-kind="pass"] .psv__foot > .note-line[hidden] { display: block; visibility: hidden; }
[data-kind="pass"] .psv__fbtns { display: flex; align-items: center; gap: var(--g); margin-left: auto; }
[data-kind="pass"] .psv__fbtns > .btn[hidden] { display: none; }
[data-kind="pass"] .psv__buy { display: inline-flex; align-items: center; gap: .45em; }
.psv__bemblem { width: 1.6em; height: 1.6em; margin: -.3em 0; object-fit: contain; filter: drop-shadow(0 0 .4em rgba(var(--brass-hi-rgb), .6)); }

/* ---- the questions (Up Level, Starward, a rung of alternatives), in ask.ts's dialog: the amount,
   a summary well, the choices as equal cards, then the dialog's own action row */
.psv-ask { display: grid; gap: .8em; }
.psv-ask__lbl { margin-bottom: -.3em; font-size: var(--fs-small); color: var(--ink-mute); }
.psv-sum {
  margin: 0; display: grid; gap: .35em; padding: .6em .8em;
  border-radius: .3em; background: rgba(var(--navy-0-rgb), .7); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .16);
}
.psv-sum > div { display: flex; align-items: center; justify-content: space-between; gap: .8em; }
.psv-sum dt { color: var(--ink-mute); }
.psv-sum dd { margin: 0; display: inline-flex; align-items: center; gap: .4em; font-weight: 700; color: var(--ink-strong); font-variant-numeric: tabular-nums; }
.psv-sum dd .ic { width: .9em; height: .9em; opacity: .7; }
.psv-sum__from { color: var(--ink-mute); font-weight: 600; }
.psv-sum__to { color: var(--brass-hi); }
.psv-opts { display: grid; grid-template-columns: repeat(var(--n, 2), minmax(0, 1fr)); gap: .6em; }
.psv-opt {
  display: grid; justify-items: center; align-content: center; gap: .3em; min-width: 0;
  padding: .8em .5em .7em; border: 0; border-radius: .35em;
  background: rgba(var(--navy-3-rgb), .85); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .22);
  font: inherit; color: var(--ink); text-align: center; cursor: var(--cur-hand, pointer);
}
.psv-opt:hover:not(.is-on) { background: rgba(var(--navy-4-rgb), .85); }
.psv-opt.is-on {
  background: radial-gradient(ellipse 90% 70% at 50% 0%, rgba(var(--brass-rgb), .24), transparent 75%), rgba(var(--navy-4-rgb), .95);
  box-shadow: inset 0 0 0 2px var(--brass-hi), 0 0 1em rgba(var(--brass-rgb), .3);
  color: var(--ink-strong); cursor: var(--cur-arrow, default);
}
.psv-opt__coin { width: 2em; height: 2em; object-fit: contain; }
.psv-opt__num { font-size: 1.45em; line-height: 1.1; color: var(--ink-strong); font-variant-numeric: tabular-nums; }
.psv-opt__cap { font-size: var(--fs-small); color: var(--ink-mute); }
.psv-opt.is-on .psv-opt__cap { color: var(--brass-hi); }
.psv-opt__emblem { width: 3.2em; height: 3.2em; object-fit: contain; filter: drop-shadow(0 0 .7em rgba(var(--brass-hi-rgb), .45)); }
.psv-opt__name { font-weight: 700; line-height: 1.2; overflow-wrap: anywhere; }
.psv-opt__price { display: inline-flex; align-items: center; gap: .35em; font-variant-numeric: tabular-nums; }
.psv-opt__price > .coin { width: 1.2em; height: 1.2em; }

/* ---- an iPad (and any narrow desk): narrower plates and detail give the road room */
@media (max-width: 1200px) {
  [data-kind="pass"] .psv { --side: 8.6em; --tmax: 4.8em; }
  [data-kind="pass"] .psv__rw { grid-template-columns: minmax(0, 1fr) 13.5em; gap: var(--g); }
  [data-kind="pass"] .psv__emblem { width: 2.9em; height: 2.9em; }
  [data-kind="pass"] .psv__tname > b { font-size: .95em; letter-spacing: .06em; }
  [data-kind="pass"] .psv__lv { min-width: 5em; padding-inline: var(--g); }
  [data-kind="pass"] .psv__mgrid { grid-template-columns: repeat(auto-fill, minmax(17em, 1fr)); }
  [data-kind="pass"] .psv__shead { grid-template-columns: auto minmax(0, 1fr) minmax(8em, 13em); gap: var(--g); }
}

/* ---- a landscape phone (390, 375, 360 tall): the plates become emblems, the card drops its name,
   the strip and the foot tighten; the road keeps scrolling with its snap */
@media (max-height: 560px) {
  [data-kind="pass"] .psv { --g: .45em; --side: 4.4em; --rh0: 2.3em; --rg: .25em; --tmax: 4.4em; gap: var(--g); }
  [data-kind="pass"] .psv__bar { padding: .3em .6em .3em .3em; gap: .8em; }
  [data-kind="pass"] .psv__lv { min-width: 0; padding: 0 .6em 0 .3em; }
  [data-kind="pass"] .psv__lvcap { display: none; }
  [data-kind="pass"] .psv__lvnum { font-size: 1.8em; }
  [data-kind="pass"] .psv__season { font-size: 1em; }
  [data-kind="pass"] .psv__mnums { font-size: var(--fs-small); }
  [data-kind="pass"] .psv__acts .btn { font-size: .88em; }
  [data-kind="pass"] .psv__sw { padding: .35em .8em; font-size: .9em; }
  [data-kind="pass"] .psv__rw { grid-template-columns: minmax(0, 1fr) 11em; }
  [data-kind="pass"] .psv__road { --tc: clamp(2.2em, calc(var(--rowh) - 2.6em), var(--tmax)); --cw: calc(var(--tc) + 1.3em); }
  [data-kind="pass"] .psv__corner { display: none; }
  [data-kind="pass"] .psv__tier { flex-direction: column; justify-content: center; gap: .15em; padding: 0 .2em; }
  [data-kind="pass"] .psv__emblem { width: 2.4em; height: 2.4em; }
  [data-kind="pass"] .psv__tname { justify-items: center; }
  [data-kind="pass"] .psv__tname > b { font-size: .6em; letter-spacing: .03em; }
  [data-kind="pass"] .psv__tstate { display: none; }
  [data-kind="pass"] .psv__num > b { font-size: 1.1em; }
  [data-kind="pass"] .psv__num::before, [data-kind="pass"] .psv__num::after { bottom: .35em; }
  [data-kind="pass"] .psv__cell { width: calc(var(--cw) - .35em); height: calc(100% - .4em); max-height: calc(var(--tc) + 3em); }
  [data-kind="pass"] .psv__cell { padding: .3em .2em .2em; gap: .15em; grid-template-rows: minmax(0, 1fr) auto; }
  [data-kind="pass"] .psv__name { display: none; }
  [data-kind="pass"] .psv__state { min-height: 1.3em; }
  [data-kind="pass"] .psv__mark--taken { width: 1.5em; height: 1.5em; }
  [data-kind="pass"] .psv__mark--shut { width: 1.2em; height: 1.2em; right: -.25em; top: -.25em; }
  [data-kind="pass"] .psv__detail { padding: .5em .6em .6em; gap: .35em; grid-template-rows: auto minmax(3.4em, 1fr) auto minmax(0, auto) auto; }
  [data-kind="pass"] .psv__dtag { padding: .1em .45em; letter-spacing: .04em; font-size: var(--fs-badge); }
  [data-kind="pass"] .psv__dtiles .tile--lg { font-size: 1.1em; }
  [data-kind="pass"] .psv__dcard { font-size: .9em; }
  /* A costume shows only its name on a short screen (its card is one stock line): the figure keeps
     the room. An item keeps its card, scrolled, over a smaller picture. */
  [data-kind="pass"] .psv__detail.is-doll .psv__dcard .card__body { display: none; }
  [data-kind="pass"] .psv__dstate { min-height: 2em; }
  [data-kind="pass"] .psv__dfoot .btn { font-size: .9em; }
  [data-kind="pass"] .psv__shead { grid-template-columns: auto minmax(0, 1fr) minmax(7em, 11em); gap: .7em; padding: .35em .5em .35em .7em; }
  [data-kind="pass"] .psv__stitle { font-size: var(--fs-heading); }
  [data-kind="pass"] .psv__mgrid { grid-template-columns: repeat(auto-fill, minmax(15em, 1fr)); }
  [data-kind="pass"] .psv__mission { grid-template-columns: 2.4em minmax(0, 1fr) auto; padding: .4em .45em .4em .4em; min-height: 0; gap: .5em; }
  [data-kind="pass"] .psv__ms { padding: .45em; }
  [data-kind="pass"] .psv__mtitle { font-size: 1em; }
  [data-kind="pass"] .psv__pts { width: 2.3em; height: 2.3em; font-size: .9em; }
  [data-kind="pass"] .psv__foot { min-height: 2.2em; }
  [data-kind="pass"] .psv__fbtns .btn { font-size: .9em; }
  /* The questions fit a short screen without scrolling: tighter wells, a choice laid out in a row. */
  .psv-ask { gap: .5em; }
  .psv-sum { padding: .35em .6em; gap: .2em; }
  .psv-opt { display: flex; align-items: center; justify-content: center; gap: .45em; padding: .5em .5em; }
  .psv-opt__coin { width: 1.5em; height: 1.5em; }
  .psv-opt__num { font-size: 1.2em; }
  .psv-opt__emblem { width: 2em; height: 2em; }
  .psv-opt:has(.psv-opt__emblem) { display: grid; gap: .2em; }
}

@media (prefers-reduced-motion: reduce) {
  [data-kind="pass"] .psv__glow::before { animation: none; }
  [data-kind="pass"] .psv__glow i { animation: none; opacity: .85; bottom: auto; top: 30%; }
  [data-kind="pass"] .psv__glow i:nth-child(odd) { top: 62%; }
  [data-kind="pass"] .psv__cell, [data-kind="pass"] .psv__tier--prem .psv__emblem { transition: none; }
}

/* ---- w-ev-signin.css */
/* THE DAILY SIGN-IN view inside the Events hub (panels/ev-signin.ts), under .evw__body[data-kind=signin].
   Left the month: one title line, then the days as true squares (--S, the side the view fits in px;
   --cols 7 unless another count gives a clearly bigger square). Each cell: a big faint day number in its top-left corner,
   the reward centred with its count. One look per state, nothing else borders a cell:
     future   the plain navy square;
     today    a bright brass frame and a breathing glow while it is still to take;
     claimed  the reward greyed under a clean white check (css mask, no art);
     missed   sunk dark, the reward faint under a small padlock;
     Ticket   a missed day a Tamer Ticket still collects: the event's coral rim and number;
     milestone  a small brass diamond in the top-right corner, whatever its state.
   Right the day in focus large in the astrolabe ring (it grows into the room the side has), the next
   milestone, the Ticket line, the reset countdown, the button. Motion is transform and opacity only. */
.evw__body[data-kind="signin"] .sgv [hidden] { display: none !important; }
.evw__body[data-kind="signin"] .sgv {
  --side-min: 16em;
  flex: 1; min-height: 0; min-width: 0;
  display: grid; gap: 1em;
  grid-template-columns: max-content minmax(var(--side-min), 24em); grid-template-rows: minmax(0, 1fr);
  justify-content: center;
}
.evw__body[data-kind="signin"] :is(.sgv__cal, .sgv__side) {
  position: relative; min-width: 0; min-height: 0; overflow: hidden;
  background: linear-gradient(180deg, rgba(var(--navy-3-rgb), .8), rgba(var(--navy-1-rgb), .94));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 2px rgba(var(--brass-rgb), .28),
    inset 0 1px 0 rgba(var(--brass-pale-rgb), .12), var(--win-shadow);
}

/* ---- the month: the title line and the grid, centred together in the column */
.evw__body[data-kind="signin"] .sgv__cal {
  display: flex; flex-direction: column; justify-content: center; gap: .8em; padding: 1em 1.2em;
}
.evw__body[data-kind="signin"] .sgv__top { display: flex; align-items: baseline; gap: .9em; min-width: 0; flex: none; }
.evw__body[data-kind="signin"] .sgv__title {
  margin: 0; flex: none; font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: var(--fs-title); line-height: 1.2; color: var(--brass-pale); white-space: nowrap;
}
.evw__body[data-kind="signin"] .sgv__flavour {
  margin: 0; flex: 1; min-width: 0; font-size: var(--fs-small); color: var(--ink-mute);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  contain: inline-size; /* never widens the month's column past its grid */
}

.evw__body[data-kind="signin"] .sgv__days {
  flex: none; display: grid; gap: .4em;
  grid-template-columns: repeat(var(--cols, 7), var(--S, 4em));
  grid-auto-rows: var(--S, 4em);
}
/* A cell is S by S; everything in it is in em of S/6, so it all scales with the square. */
.evw__body[data-kind="signin"] .sgv__day {
  position: relative; width: var(--S, 4em); height: var(--S, 4em);
  font-size: calc(var(--S, 4em) / 6);
  border-radius: .35em;
  background: rgba(var(--navy-4-rgb), .55);
  box-shadow: inset 0 0 0 1px rgba(var(--sky-rgb), .08);
}
.evw__body[data-kind="signin"] .sgv__num {
  position: absolute; left: .25em; top: .05em; pointer-events: none;
  font-family: var(--face-display); font-weight: 700; font-size: 2.5em; line-height: 1;
  color: rgba(var(--sky-rgb), .16);
}
/* The reward: the picture alone (no well, no rim), a little below centre so the number reads. */
.evw__body[data-kind="signin"] .sgv__day > .tile {
  --tile: 3.7em;
  position: absolute; left: 50%; top: 50%; translate: -50% -42%;
  background: none; box-shadow: none; overflow: visible;
}
.evw__body[data-kind="signin"] .sgv__day > .tile > img {
  inset: 0; width: 100%; height: 100%;
  filter: drop-shadow(0 .12em .15em rgba(var(--shadow-ink), .7));
}
.evw__body[data-kind="signin"] .sgv__day > .tile.is-dim > img { filter: grayscale(1); opacity: .4; }
.evw__body[data-kind="signin"] :is(.sgv__day, .sgv__ring) .tile__count {
  right: -.15em; bottom: -.1em; padding: 0; background: none;
  font-weight: 700; color: var(--ink-strong);
  text-shadow: 0 0 .25em rgb(var(--shadow-ink)), 0 .06em .1em rgb(var(--shadow-ink));
}
.evw__body[data-kind="signin"] .sgv__day > .tile.is-dim .tile__count { color: var(--ink-faint); }

/* The milestone: one small brass diamond in the top-right corner. */
.evw__body[data-kind="signin"] .sgv__pip {
  position: absolute; right: .55em; top: .55em; width: .6em; height: .6em; rotate: 45deg; display: none;
  background: linear-gradient(135deg, var(--brass-pale), var(--brass));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .8), 0 0 .5em rgba(var(--brass-rgb), .5);
}
.evw__body[data-kind="signin"] .sgv__day.is-milestone .sgv__pip { display: block; }

/* The check over a day taken (and the hero when it is taken): white, round-capped, a soft shadow
   on the wrapper (a mask would clip a filter on the same box). */
.evw__body[data-kind="signin"] .sgv__check {
  position: absolute; left: 50%; top: 50%; width: 3.2em; height: 3.2em; translate: -50% -42%;
  pointer-events: none; opacity: 0;
  filter: drop-shadow(0 .08em .12em rgba(var(--shadow-ink), .95)) drop-shadow(0 0 .3em rgba(var(--shadow-ink), .6));
}
.evw__body[data-kind="signin"] .sgv__check::before {
  content: ""; position: absolute; inset: 0; background: var(--ink-strong);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4.5 12.8l4.8 4.7L19.6 6.8' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4.5 12.8l4.8 4.7L19.6 6.8' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* The padlock over a day gone by. */
.evw__body[data-kind="signin"] .sgv__lock {
  position: absolute; left: 50%; top: 50%; width: 1.5em; height: 1.5em; translate: -50% -30%;
  padding: .3em; box-sizing: content-box; border-radius: 50%; pointer-events: none; display: none;
  background: rgba(var(--shadow-ink), .75); box-shadow: 0 0 0 1px rgba(var(--sky-rgb), .18);
  opacity: .9;
}

/* Claimed: sunk a step, the reward grey, the check on it. */
.evw__body[data-kind="signin"] .sgv__day.is-claimed { background: rgba(var(--navy-0-rgb), .55); }
.evw__body[data-kind="signin"] .sgv__day.is-claimed .sgv__num { color: rgba(var(--sky-rgb), .08); }
.evw__body[data-kind="signin"] .sgv__day.is-claimed .sgv__check { opacity: 1; }
.evw__body[data-kind="signin"] .sgv__day.is-fresh .sgv__check { animation: sgv-check .4s cubic-bezier(.2, .9, .3, 1.3); }
@keyframes sgv-check { from { opacity: 0; scale: 1.8; } to { opacity: 1; scale: 1; } }

/* Missed: dark, the reward faint, the padlock. */
.evw__body[data-kind="signin"] .sgv__day.is-gone { background: rgba(var(--navy-0-rgb), .8); box-shadow: none; }
.evw__body[data-kind="signin"] .sgv__day.is-gone .sgv__num { color: rgba(var(--sky-rgb), .06); }
.evw__body[data-kind="signin"] .sgv__day.is-gone > .tile > img { opacity: .22; }
.evw__body[data-kind="signin"] .sgv__day.is-gone .sgv__lock { display: block; }

/* A missed day the Tamer Ticket still collects: the event's coral, the reward in full colour. */
.evw__body[data-kind="signin"] .sgv__day.is-late {
  background: radial-gradient(circle at 50% 60%, rgba(var(--acc-rgb), .14), transparent 70%), rgba(var(--navy-4-rgb), .55);
  box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .75);
}
.evw__body[data-kind="signin"] .sgv__day.is-late .sgv__num { color: rgba(var(--acc-rgb), .4); }

/* Today: the brightest square on the page. A brass frame, a lit ground, and while it is still to
   take a glow that breathes (opacity on its own layer). */
.evw__body[data-kind="signin"] .sgv__day.is-now {
  z-index: 1;
  background: radial-gradient(circle at 50% 58%, rgba(var(--brass-hi-rgb), .3), transparent 72%), var(--navy-4);
  box-shadow: inset 0 0 0 2px var(--brass-hi), 0 0 0 1px rgba(var(--shadow-ink), .9);
}
.evw__body[data-kind="signin"] .sgv__day.is-now .sgv__num { color: rgba(var(--brass-pale-rgb), .6); }
.evw__body[data-kind="signin"] .sgv__day.is-now.is-claimed {
  background: rgba(var(--navy-3-rgb), .9);
  box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .55);
}
.evw__body[data-kind="signin"] .sgv__day.is-now.is-claimed .sgv__num { color: rgba(var(--brass-pale-rgb), .25); }
.evw__body[data-kind="signin"] .sgv__day.is-now.is-open::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  box-shadow: 0 0 1.1em .1em rgba(var(--brass-hi-rgb), .75), inset 0 0 .9em rgba(var(--brass-pale-rgb), .35);
  animation: sgv-breathe 1.6s ease-in-out infinite alternate;
}
@keyframes sgv-breathe { from { opacity: .35; } to { opacity: 1; } }

/* The day tapped into focus (not today): a starlight ring. */
.evw__body[data-kind="signin"] .sgv__day.is-focus { box-shadow: inset 0 0 0 1px var(--sky), 0 0 .6em rgba(var(--sky-rgb), .3); }
body.play .evw__body[data-kind="signin"] .sgv__day { cursor: var(--cur-hand, pointer); }
body.play .evw__body[data-kind="signin"] .sgv__day.is-busy { cursor: var(--cur-busy); }
.evw__body[data-kind="signin"] .sgv__day.is-busy > .tile { opacity: .6; }
@media (hover: hover) {
  .evw__body[data-kind="signin"] .sgv__day:not(.is-now, .is-late, .is-gone, .is-focus):hover {
    box-shadow: inset 0 0 0 1px rgba(var(--sky-rgb), .32);
  }
}

/* ---- the side: the day in focus (it takes every bit of height the rest leaves), the next
   milestone, the foot */
.evw__body[data-kind="signin"] .sgv__side { display: flex; flex-direction: column; gap: .9em; padding: 1em 1.2em 1.2em; }
.evw__body[data-kind="signin"] .sgv__hero { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; gap: .6em; }
.evw__body[data-kind="signin"] .sgv__lbls { align-self: stretch; display: flex; align-items: center; justify-content: space-between; gap: .5em; flex: none; }
.evw__body[data-kind="signin"] .sgv__lbl {
  padding: .2em .8em; border-radius: 999px; white-space: nowrap;
  font-size: var(--fs-small); letter-spacing: .06em; text-transform: uppercase;
  color: var(--navy-1); background: linear-gradient(180deg, var(--brass-hi), var(--brass));
}
.evw__body[data-kind="signin"] .sgv__state { font-size: var(--fs-small); color: var(--ink-mute); white-space: nowrap; }
.evw__body[data-kind="signin"] .sgv__stage {
  flex: 1; min-height: 4em; align-self: stretch; container-type: size;
  display: grid; place-items: center;
}
.evw__body[data-kind="signin"] .sgv__ring {
  --ring: min(100cqw, 100cqh, 15em);
  position: relative; width: var(--ring); height: var(--ring); display: grid; place-items: center;
  font-size: calc(var(--ring) / 10);
  background: radial-gradient(circle, rgba(var(--brass-rgb), .2), rgba(var(--navy-0-rgb), .2) 52%, transparent 64%),
    var(--art-ring) center / contain no-repeat;
}
.evw__body[data-kind="signin"] .sgv__ring > .tile {
  --tile: 4.6em; font-size: 1em;
  box-shadow: inset 0 0 0 max(1px, .08em) var(--tile-rim), 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 1.2em rgba(var(--brass-rgb), .3);
}
.evw__body[data-kind="signin"] .sgv__ring .tile__count { right: .15em; bottom: .05em; font-size: 1.1em; }
.evw__body[data-kind="signin"] .sgv__ring > .sgv__check { width: 3.6em; height: 3.6em; translate: -50% -50%; }
.evw__body[data-kind="signin"] .sgv__ring > .sgv__lock { width: 1.4em; height: 1.4em; translate: -50% -50%; }
.evw__body[data-kind="signin"] .sgv__hero.is-claimed .sgv__check { opacity: 1; }
.evw__body[data-kind="signin"] .sgv__hero.is-gone .sgv__lock { display: block; }
.evw__body[data-kind="signin"] .sgv__hero.is-gone .sgv__ring > .tile > img { opacity: .25; }
.evw__body[data-kind="signin"] .sgv__hero.is-now .sgv__ring > .tile {
  box-shadow: inset 0 0 0 2px var(--brass-hi), 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 1.6em rgba(var(--brass-hi-rgb), .55);
}
.evw__body[data-kind="signin"] .sgv__hero.is-late .sgv__ring > .tile {
  box-shadow: inset 0 0 0 2px rgba(var(--acc-rgb), .85), 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 1.4em rgba(var(--acc-rgb), .4);
}
.evw__body[data-kind="signin"] .sgv__caption { flex: none; display: grid; justify-items: center; gap: .15em; min-width: 0; max-width: 100%; }
.evw__body[data-kind="signin"] .sgv__name {
  min-width: 0; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  font-size: var(--fs-heading); line-height: 1.25; color: var(--ink-strong); text-align: center;
}
.evw__body[data-kind="signin"] .sgv__qty { flex: none; font-family: var(--face-display); font-size: var(--fs-title); color: var(--brass-hi); }

/* The next milestone: one band, its diamond the grid's. */
.evw__body[data-kind="signin"] .sgv__next {
  flex: none; display: flex; align-items: center; gap: .7em; padding: .5em .7em; min-width: 0;
  border-radius: .35em; background: var(--band-bg); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .18);
}
.evw__body[data-kind="signin"] .sgv__ntext { display: grid; gap: .2em; min-width: 0; }
.evw__body[data-kind="signin"] .sgv__mshead { display: flex; align-items: center; gap: .45em; font-size: var(--fs-small); color: var(--brass-hi); white-space: nowrap; }
.evw__body[data-kind="signin"] .sgv__mspip {
  width: .5em; height: .5em; flex: none; rotate: 45deg;
  background: linear-gradient(135deg, var(--brass-pale), var(--brass)); box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .8);
}
.evw__body[data-kind="signin"] .sgv__nname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }

/* The foot: the Ticket line, the countdown, the button, a refusal. */
.evw__body[data-kind="signin"] .sgv__foot { flex: none; display: grid; gap: .5em; }
.evw__body[data-kind="signin"] .sgv__catch { margin: 0; text-align: center; font-size: var(--fs-small); color: var(--ink-mute); }
.evw__body[data-kind="signin"] .sgv__catch.is-on { color: rgb(var(--acc-rgb)); }
.evw__body[data-kind="signin"] .sgv__reset {
  margin: 0; display: flex; align-items: center; justify-content: center; gap: .45em;
  font-size: var(--fs-small); color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.evw__body[data-kind="signin"] .sgv__reset > b { font-weight: inherit; }
.evw__body[data-kind="signin"] .sgv__clock {
  position: relative; width: 1em; height: 1em; flex: none; border-radius: 50%;
  box-shadow: inset 0 0 0 .1em var(--brass-hi);
}
.evw__body[data-kind="signin"] .sgv__clock::after {
  content: ""; position: absolute; left: 45%; top: 22%; width: 30%; height: 33%;
  border-left: .1em solid var(--brass-hi); border-bottom: .1em solid var(--brass-hi);
}
.evw__body[data-kind="signin"] .sgv__go .sgv__gomark { width: 1.1em; height: 1.1em; margin-right: .35em; vertical-align: -.15em; }
.evw__body[data-kind="signin"] .sgv__go.is-done { opacity: 1; filter: saturate(.6); }

/* ---- an iPad-wide room: a narrower side, so the week gets the width */
@media (max-width: 1260px) {
  .evw__body[data-kind="signin"] .sgv { --side-min: 13.5em; }
}

/* ---- short screens (a landscape phone): a tighter frame and side */
@media (max-height: 560px) {
  .evw__body[data-kind="signin"] .sgv { --side-min: 11.5em; grid-template-columns: max-content minmax(var(--side-min), 15em); gap: .6em; }
  .evw__body[data-kind="signin"] .sgv__cal { padding: .5em .6em; gap: .4em; }
  .evw__body[data-kind="signin"] .sgv__title { font-size: var(--fs-heading); }
  .evw__body[data-kind="signin"] .sgv__days { gap: .25em; }
  .evw__body[data-kind="signin"] .sgv__side { padding: .5em .6em .6em; gap: .45em; }
  .evw__body[data-kind="signin"] .sgv__hero { gap: .3em; }
  .evw__body[data-kind="signin"] .sgv__stage { min-height: 3em; }
  .evw__body[data-kind="signin"] .sgv__lbl { font-size: var(--fs-badge); padding: .15em .6em; }
  .evw__body[data-kind="signin"] .sgv__name { font-size: 1em; }
  .evw__body[data-kind="signin"] .sgv__qty { font-size: var(--fs-heading); }
  .evw__body[data-kind="signin"] .sgv__next { padding: .3em .45em; gap: .45em; }
  .evw__body[data-kind="signin"] .sgv__foot { gap: .3em; }
}

@media (prefers-reduced-motion: reduce) {
  .evw__body[data-kind="signin"] .sgv__day.is-fresh .sgv__check,
  .evw__body[data-kind="signin"] .sgv__day.is-now.is-open::before { animation: none; }
}

/* ---- w-events.css */
/* THE EVENTS HUB (panels/events.ts, window `events`): a page, not a window. No frame: the world goes
   dark (and soft, on a desktop) behind a timeline rail of banner cards on the left and the picked
   event's spread on the right - its back plate, its key art, the strip of rewards over the art and
   the title plate with the call to action over the plate's edge. Each event its own accent, deep
   ground and motif (00-tokens.css --ev-*), keyed on data-ev; the brass is the fallback. Everything
   is in em of --win-size, so a short phone (the height is the constraint) shrinks it with the windows. */

/* ---- each event's accent, ground and motif: the rail's card and the spread both read these */
.evw [data-ev] {
  --acc-rgb: var(--ev-brass-rgb); --deep: var(--ev-brass-deep);
  --motif: radial-gradient(circle at 80% 40%, rgba(var(--acc-rgb), .18), transparent 60%);
}
.evw [data-ev="starlight_wardrobe"] {
  --acc-rgb: var(--ev-wardrobe-rgb); --deep: var(--ev-wardrobe-deep);
  /* a star field: two offset grids of points, one finer and brighter */
  --motif:
    radial-gradient(circle, rgba(var(--acc-rgb), .7) 0 1px, transparent 1.6px) 0 0 / 2.3em 2.3em,
    radial-gradient(circle, rgba(var(--acc-rgb), .35) 0 1px, transparent 1.6px) 1.1em 1.4em / 3.3em 3.3em,
    radial-gradient(ellipse at 70% 0%, rgba(var(--acc-rgb), .25), transparent 65%);
}
.evw [data-ev="tamers_fortune"] {
  --acc-rgb: var(--ev-fortune-rgb); --deep: var(--ev-fortune-deep);
  /* the sun rising behind the temple: rays out of the bottom edge, a vermilion glow under them */
  --motif:
    repeating-conic-gradient(from -90deg at 50% 118%, rgba(var(--acc-rgb), .16) 0 3deg, transparent 3deg 11deg),
    radial-gradient(ellipse at 50% 110%, rgba(var(--ev-fortune-2-rgb), .45), transparent 70%);
}
.evw [data-ev="tamers_codex"] {
  --acc-rgb: var(--ev-codex-rgb); --deep: var(--ev-codex-deep);
  /* a page of lore: faint ruled lines, a glyph's glow rising off the open book at the foot */
  --motif:
    repeating-linear-gradient(0deg, transparent 0 1.9em, rgba(var(--acc-rgb), .12) 1.9em calc(1.9em + 1px)),
    radial-gradient(ellipse at 50% 115%, rgba(var(--ev-codex-2-rgb), .4), transparent 65%);
}
.evw [data-ev="first_topup"] {
  --acc-rgb: var(--ev-topup-rgb); --deep: var(--ev-topup-deep);
  /* a night sky through a bedroom window: soft stars, a warm lamp glow rising from the lower left */
  --motif:
    radial-gradient(circle, rgba(var(--ev-topup-2-rgb), .55) 0 1px, transparent 1.6px) .4em .8em / 2.9em 2.9em,
    radial-gradient(ellipse at 15% 110%, rgba(var(--acc-rgb), .4), transparent 60%);
}
.evw [data-ev="wishing_well"] {
  --acc-rgb: var(--ev-well-rgb); --deep: var(--ev-well-deep);
  /* ripples spreading from a coin dropped below the left edge, a sakura blush in the far corner */
  --motif:
    repeating-radial-gradient(circle at 24% 125%, transparent 0 1.7em, rgba(var(--acc-rgb), .24) 1.7em calc(1.7em + 1px), transparent calc(1.7em + 1px) 2.6em),
    radial-gradient(ellipse at 100% 0%, rgba(var(--ev-well-2-rgb), .3), transparent 60%);
}
.evw [data-ev="pass"] {
  --acc-rgb: var(--ev-pass-rgb); --deep: var(--ev-pass-deep);
  /* orbits: rings round a point near the right edge, cut off by it */
  --motif:
    repeating-radial-gradient(circle at 92% 30%, transparent 0 2.8em, rgba(var(--acc-rgb), .26) 2.8em calc(2.8em + 1px), transparent calc(2.8em + 1px) 4.6em),
    radial-gradient(ellipse at 20% 50%, rgba(var(--acc-rgb), .22), transparent 60%);
}
.evw [data-ev="signin"] {
  --acc-rgb: var(--ev-signin-rgb); --deep: var(--ev-signin-deep);
  /* a calendar page: a grid ruled from the top-left corner, dawn light from the left */
  --motif:
    linear-gradient(rgba(var(--acc-rgb), .15) 1px, transparent 1px) 0 0 / 100% 2.2em,
    linear-gradient(90deg, rgba(var(--acc-rgb), .15) 1px, transparent 1px) 0 0 / 2.2em 100%,
    radial-gradient(ellipse at 0% 60%, rgba(var(--acc-rgb), .3), transparent 65%);
}
.evw [data-ev="journey"] {
  --acc-rgb: var(--ev-journey-rgb); --deep: var(--ev-journey-deep);
  /* milestones up a slope: a dotted line climbing to the top-right, the summit's glow above it */
  --motif:
    repeating-linear-gradient(-28deg, transparent 0 2.4em, rgba(var(--acc-rgb), .22) 2.4em calc(2.4em + 2px), transparent calc(2.4em + 2px) 4.8em),
    radial-gradient(ellipse at 88% 8%, rgba(var(--acc-rgb), .32), transparent 60%);
}

/* ---- the page */
.wins > .win.evw {
  inset: 0; width: auto; max-width: none; height: auto; max-height: none; min-width: 0;
  display: grid; grid-template-columns: minmax(14em, 21em) minmax(0, 76em); grid-template-rows: minmax(0, 1fr); gap: 2.5em;
  justify-content: center;
  padding: max(2.5em, var(--safe-t)) max(4.5em, var(--safe-r))
    max(2em, var(--safe-b)) max(1.6em, var(--safe-l));
  background: var(--ev-veil); box-shadow: none;
  animation: evw-in .16s ease-out;
}
.wins > .win.evw::before, .wins > .win.evw::after { content: none; }
/* The world softened behind it on a desktop; a phone only dims it (a full-screen blur over the
   canvas is paid every frame). */
html.m-desktop .wins > .win.evw { backdrop-filter: blur(6px); }
/* A phone played in desktop mode (tier.ts isTouch) is still a phone: no blur, the veil alone. */
@media (hover: none) and (pointer: coarse) { html.m-desktop .wins > .win.evw { backdrop-filter: none; } }
@keyframes evw-in { from { opacity: 0; } }

.evw__close { position: absolute; z-index: 5; top: max(.8em, var(--safe-t)); right: max(1em, var(--safe-r)); font-size: 1.3em; }

/* ---- the rail: a hairline down the very left with a short tick capping each end, a ring per card
   on it (the picked one filled and haloed), the cards stacked to its right (owner, 2026-10-10, after
   a reference). Any number of cards scrolls, with no scrollbar: the stack fades out at its top and
   bottom edges, so a card leaving the view goes see-through rather than being cut. */
.evw__rail { position: relative; display: flex; min-height: 0; --node-x: 1.2em; --fade: 4em; }
.evw__line {
  position: absolute; left: var(--node-x); top: .9em; bottom: .9em; width: 1px; translate: -50% 0;
  background: linear-gradient(rgba(var(--sky-rgb), .1), rgba(var(--sky-rgb), .45) 12%, rgba(var(--sky-rgb), .45) 88%, rgba(var(--sky-rgb), .1));
}
.evw__line::before, .evw__line::after {   /* the end ticks, standing off the line */
  content: ""; position: absolute; left: 50%; width: 2px; height: .55em; translate: -50% 0;
  background: var(--ink-strong); opacity: .8;
}
.evw__line::before { top: -.9em; }
.evw__line::after { bottom: -.9em; }
.evw__stack {
  display: flex; flex-direction: column; gap: 1.1em; width: 100%;
  overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
  padding-right: .6em;              /* the top and bottom are .fade-edges' (events.ts) */
}
.evw__stack::-webkit-scrollbar { display: none; }
.evw__stack > :first-child { margin-top: auto; }
.evw__stack > :last-child { margin-bottom: auto; }
.evw__item { display: grid; grid-template-columns: calc(var(--node-x) * 2) minmax(0, 1fr); column-gap: 1.2em; align-items: center; flex: none; }
.evw__node {                        /* a white ring on the line, the night showing through it */
  justify-self: center; position: relative; z-index: 1; box-sizing: border-box;
  width: .8em; height: .8em; border-radius: 50%;
  background: var(--navy-0); border: 2px solid var(--ink-strong);
  box-shadow: 0 0 .3em rgba(var(--shadow-ink), .8);
  transition: box-shadow .15s, background .15s, border-color .15s;
}
.evw__item.is-on .evw__node {        /* the picked one: filled in its accent, in a faint halo */
  background: rgb(var(--acc-rgb)); border-color: var(--ink-strong);
  box-shadow: 0 0 0 .45em rgba(var(--ink-strong-rgb), .12), 0 0 0 .5em rgba(var(--ink-strong-rgb), .18), 0 0 1em rgba(var(--acc-rgb), .8);
}

.evw__card {
  position: relative; display: block; width: 100%; aspect-ratio: 3.4 / 1; min-height: 4.4em;
  padding: 0; border: 0; border-radius: 0; overflow: hidden;
  background: var(--deep); color: var(--ink); text-align: left; font: inherit;
  cursor: var(--cur-hand, pointer);
  box-shadow: 0 0 0 1px rgba(var(--brass-rgb), .3), 0 .4em 1em rgba(var(--shadow-ink), .5);
  filter: var(--ev-dim);
  transition: filter .15s, transform .15s, box-shadow .15s;
}
.evw__card:hover { filter: saturate(.8) brightness(.92); }
.evw__item.is-on .evw__card {
  filter: none; transform: translateX(.35em);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 .15em rgb(var(--acc-rgb)), 0 0 1.4em rgba(var(--acc-rgb), .45), 0 .4em 1em rgba(var(--shadow-ink), .5);
}
/* The strip's picture: the banner, or the motif with the painted tile standing at its right end. */
.evw__pic {
  position: absolute; inset: 0;
  background: var(--motif), var(--deep);   /* a machine's banner is written over it inline */
}
.evw__pic > img.g { position: absolute; right: 4%; top: 50%; height: 130%; translate: 0 -50%; opacity: .9; filter: drop-shadow(0 0 .8em rgba(var(--acc-rgb), .5)); }
.evw__card::after {                 /* the words' shade, from the left */
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, rgba(var(--wash-rgb), .88), rgba(var(--wash-rgb), .4) 58%, transparent);
}
.evw__words {
  position: absolute; z-index: 2; left: .8em; right: 1.6em; top: .55em; bottom: .5em;
  display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start;
}
.evw__pill {
  font-size: var(--fs-badge); line-height: 1.5; padding: 0 .7em; border-radius: 999px;
  background: rgba(var(--wash-rgb), .78); color: var(--ink); white-space: nowrap;
  box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .5); font-variant-numeric: tabular-nums;
}
.evw__name {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--face-ui); font-weight: 700; font-size: var(--fs-heading); line-height: 1.15;
  color: var(--ink-strong); text-shadow: var(--win-title-shadow);
}
.evw__item.is-on .evw__name { color: rgb(var(--acc-rgb)); }
.evw__badge { z-index: 3; right: .5em; top: .5em; }

/* ---- the spread: a stage centred in what is left, sized from both sides of it (container units),
   so the art fits a tall desk and a short phone alike. Nothing clips it: the art's tilt, its frame
   ring and glow stand a few px past the stage's left edge (into the gap by the rail), the back plate
   and the call to action past its right (into the page's margin), and a clip there sliced the
   banner's left side straight off. */
.evw__spread { position: relative; min-width: 0; min-height: 0; container-type: size; display: grid; place-items: center start; }
.evw__none { grid-row: 1; grid-column: 2; place-self: center; color: var(--ink-mute); font-size: var(--fs-heading); }
.evw__stage {
  --tilt: -1.2deg;
  position: relative; width: min(94cqw, calc((100cqh - 12em) * 3.125), 72em);
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-rows: auto auto;
  animation: evw-stage .22s ease-out;
}
@keyframes evw-stage { from { opacity: 0; transform: translateX(1.2em); } }

.evw__back {                        /* the plate under it all: the event's ground and motif, askew */
  grid-column: 2 / 13; grid-row: 1 / 3; margin: 1.6em -1.4em -1em 0;
  background: var(--motif), var(--deep);
  box-shadow: 0 0 0 1px rgba(var(--acc-rgb), .35), var(--win-shadow);
  transform: rotate(1.6deg);
}
.evw__art {
  grid-column: 1 / 13; grid-row: 1; position: relative; overflow: hidden;
  aspect-ratio: 3.125 / 1;
  background: var(--deep);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 .3em rgba(var(--navy-1-rgb), .96),
    0 0 0 calc(.3em + 1px) rgba(var(--acc-rgb), .6), 0 0 2.4em rgba(var(--acc-rgb), .18), var(--win-shadow);
  transform: rotate(var(--tilt));
}
.evw__art::after {                  /* the engraved plane inside the frame */
  content: ""; position: absolute; inset: .5em; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(var(--brass-pale-rgb), .28);
}
.evw__img { display: block; width: 100%; height: 100%; object-fit: cover; }
.evw__motif { position: absolute; inset: 0; background: var(--motif); }
.evw__art--comp { background: radial-gradient(circle at 20% 50%, rgba(var(--acc-rgb), .3), transparent 55%), linear-gradient(115deg, var(--deep), var(--navy-0)); }
/* The Pass's and the Sign-in's key art: the painted tile as a medal, the figure under it. */
.evw__medal { position: absolute; left: 5%; top: 8%; bottom: 8%; aspect-ratio: 1; display: grid; place-items: center; }
.evw__medal > img.g { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 0 1.4em rgba(var(--acc-rgb), .55)); }
.evw__lv {
  position: absolute; bottom: -2%; left: 50%; translate: -50% 0;
  display: flex; align-items: baseline; gap: .35em; white-space: nowrap;
  padding: .05em .8em; background: rgba(var(--wash-rgb), .82); box-shadow: inset 0 0 0 1px rgba(var(--acc-rgb), .55);
}
.evw__lv > b { font-family: var(--face-display); font-weight: var(--face-display-weight); font-size: 1.9em; line-height: 1.1; color: rgb(var(--acc-rgb)); font-variant-numeric: tabular-nums; }

/* The rewards over the art: a machine's grand prizes over its bottom-left corner; the Pass's next
   landmarks and the Sign-in's week across the art's right side, beside the medal. */
.evw__strip {
  grid-column: 1 / 9; grid-row: 1; align-self: end; justify-self: start; z-index: 2;
  margin: 0 0 -1.6em 1.6em;
  display: flex; gap: .5em; padding: .55em .7em;
  background: rgba(var(--wash-rgb), .78);
  box-shadow: 0 0 0 1px rgba(var(--acc-rgb), .4), 0 .5em 1.2em rgba(var(--shadow-ink), .6);
  transform: rotate(var(--tilt));
}
.evw__stage[data-kind="pass"] .evw__strip, .evw__stage[data-kind="signin"] .evw__strip, .evw__stage[data-kind="timed"] .evw__strip {
  grid-column: 4 / 13; align-self: center; justify-self: center; margin: 0 0 1.4em;
}
.evw__rung { display: grid; justify-items: center; gap: .2em; }
.evw__rung > i { font-style: normal; font-size: var(--fs-badge); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.evw__rung.is-today > i { color: rgb(var(--acc-rgb)); font-weight: 700; }

/* The title plate: the event's name, its line, its figures and the call to action standing over
   its bottom edge. The theme's corner brackets grow from its top-left and bottom-right corners; an
   orbit engraving runs off its top-right edge. */
.evw__plate {
  grid-column: 6 / 13; grid-row: 2; position: relative; z-index: 3;
  margin: -4.6em -1em 1.4em 0;
  display: grid; gap: .45em; justify-items: start; align-content: start;
  padding: 1.5em 2.2em 0 2.2em;
  background: linear-gradient(160deg, rgba(var(--navy-2-rgb), .97), rgba(var(--navy-0-rgb), .97));
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .92), 0 0 0 2px rgba(var(--acc-rgb), .5), var(--win-shadow);
}
.evw__plate::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    var(--art-corner-tl) left top / var(--corner-size) var(--corner-size) no-repeat,
    var(--art-corner-br) right bottom / var(--corner-size) var(--corner-size) no-repeat;
}
.evw__plate::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-radial-gradient(circle at 104% -18%, transparent 0 5em, rgba(var(--acc-rgb), .16) 5em calc(5em + 1px), transparent calc(5em + 1px) 7.4em);
}
.evw__plate > * { position: relative; z-index: 1; }
.evw__kicker { font-size: var(--fs-small); letter-spacing: .16em; text-transform: uppercase; color: rgb(var(--acc-rgb)); min-height: 1em; }
.evw__title {
  margin: 0; font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: calc(var(--win-size) * 2.5); line-height: 1.05; color: var(--ink-strong);
  text-shadow: 0 0 1.1em rgba(var(--acc-rgb), .5), 0 .06em .12em rgba(var(--shadow-ink), .9);
}
.evw__pitch { margin: 0; max-width: 28em; color: var(--ink); font-size: var(--fs-heading); line-height: 1.3; }
.evw__info { display: flex; flex-wrap: wrap; align-items: center; gap: .6em 1em; min-height: 1.8em; margin-top: .2em; }
.evw__info > .bar { width: 17em; }
.evw__fig { display: inline-flex; align-items: baseline; gap: .45em; }
.evw__fig > b { color: var(--ink-strong); font-variant-numeric: tabular-nums; }
.evw__fig > .tile { align-self: center; }
.evw__go.btn--special {
  --btn-rim: rgb(var(--acc-rgb));
  justify-self: end; margin: .5em -1.6em -1.3em 0;
  filter: drop-shadow(0 0 .7em rgba(var(--acc-rgb), .55)) drop-shadow(0 .2em .3em rgba(var(--shadow-ink), .6));
}
/* Two ways in (Go to quest beside the exchange): one row of the same special button, the row
   taking the overhang a lone button has. */
.evw__ctas { justify-self: end; display: flex; align-items: center; gap: .5em; margin: .5em -1.6em -1.3em 0; }
.evw__ctas > .evw__go.btn--special { margin: 0; }
/* Go to quest's question: Story or Daily, side by side. */
.ask__go { display: flex; gap: .6em; justify-content: center; padding: .4em 0; }
.ask__go > .btn { flex: 1 1 0; }
/* The First Top Up Prize's Claim: dim until the account has paid, then it breathes (events.ts is-ready). */
.evw__go.btn--special:disabled { filter: grayscale(.7) brightness(.7); }
.evw__go.btn--special.is-ready { animation: evw-ready 1.8s ease-in-out infinite; }
@keyframes evw-ready {
  50% { filter: drop-shadow(0 0 1.3em rgba(var(--acc-rgb), .95)) drop-shadow(0 .2em .3em rgba(var(--shadow-ink), .6)) brightness(1.12); }
}
@media (prefers-reduced-motion: reduce) { .evw__go.btn--special.is-ready { animation: none; } }
.evw__go.btn--special:hover { filter: drop-shadow(0 0 1em rgba(var(--acc-rgb), .8)) drop-shadow(0 .2em .3em rgba(var(--shadow-ink), .6)) brightness(1.08); }

/* ---- inside an event (panels/events.ts enter/leave): the header strip over the view's host, in the
   spread's cell. The morph itself is WAAPI on translate/scale/opacity only; these are the end states.
   During the morph the banner stands over the view and takes no presses. */
.evw__spread, .evw__view { grid-area: 1 / 2; }
.evw__spread[hidden], .evw__view[hidden] { display: none; }
.evw__spread.is-leaving, .evw__spread.is-coming { z-index: 2; pointer-events: none; }
.evw__view {
  position: relative; min-width: 0; min-height: 0;
  display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 1em;
}
/* The strip the key art shrank into: the banner (or the motif and the painted tile) under a shade
   from the left, the back arrow, the title and the time left, the view's own slot at the right end. */
.evw__head {
  position: relative; display: flex; align-items: center; gap: .9em;
  min-height: 5.2em; padding: .5em 1.2em .5em .5em; overflow: hidden;
  background: var(--deep);
  box-shadow: 0 0 0 1px rgba(var(--shadow-ink), .9), 0 0 0 .2em rgba(var(--navy-1-rgb), .96),
    0 0 0 calc(.2em + 1px) rgba(var(--acc-rgb), .6), 0 0 2em rgba(var(--acc-rgb), .16), var(--win-shadow);
}
.evw__thumb { position: absolute; inset: 0; background: var(--motif), var(--deep); }
.evw__thumb > img.g {
  position: absolute; right: 18%; top: 50%; height: 150%; translate: 0 -50%; opacity: .85;
  filter: drop-shadow(0 0 .8em rgba(var(--acc-rgb), .5));
}
.evw__head::after {                 /* the words' shade, from the left */
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, rgba(var(--wash-rgb), .92), rgba(var(--wash-rgb), .55) 45%, rgba(var(--wash-rgb), .15));
}
.evw__head > :not(.evw__thumb) { position: relative; z-index: 1; }
.evw__backbtn { margin-left: 0; flex: none; }
.evw__backbtn .ic { scale: -1 1; }
.evw__htext { display: flex; align-items: baseline; gap: .9em; min-width: 0; }
.evw__vtitle {
  margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--face-display); font-weight: var(--face-display-weight);
  font-size: calc(var(--win-size) * 1.9); line-height: 1.1; color: var(--ink-strong);
  text-shadow: 0 0 1.1em rgba(var(--acc-rgb), .5), 0 .06em .12em rgba(var(--shadow-ink), .9);
}
.evw__slot { margin-left: auto; display: flex; align-items: center; gap: .6em; }
.evw__slot:empty { display: none; }
/* The view's host: the view fills it and scrolls inside itself when it must. */
.evw__host { position: relative; min-width: 0; min-height: 0; display: flex; }
.evw__body { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
/* Inside an event the page keeps the banners' grid to the pixel (owner, 2026-10-10): the view takes
   the banner's column, never a wider one, so the rail on the left does not move. */
.evw__todo { margin: auto; color: var(--ink-mute); font-size: var(--fs-heading); }

/* ---- short screens (a landscape phone: 390, 375, 360 tall): the rail narrows, the margins and
   the type step down, the pitch keeps one line */
@media (max-height: 560px) {
  .wins > .win.evw {
    grid-template-columns: minmax(11em, 15em) minmax(0, 1fr); gap: 1.4em;
    padding: max(1em, var(--safe-t)) max(4em, var(--safe-r))
      max(.8em, var(--safe-b)) max(1em, var(--safe-l));
  }
  .evw__rail { --fade: 2em; }
  .evw__stack { gap: .7em; }
  .evw__card { min-height: 3.6em; }
  .evw__name { font-size: 1em; }
  .evw__stage { width: min(96cqw, calc((100cqh - 10.5em) * 3.125)); }
  .evw__plate { grid-column: 4 / 13; margin-top: -3.4em; padding: 1em 1.4em 0; gap: .3em; }
  .evw__title { font-size: calc(var(--win-size) * 1.9); }
  .evw__pitch { font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
  .evw__info > .bar { width: 13em; }
  .evw__go.btn--special { font-size: 1.15em; margin-right: -1em; }
  .evw__ctas { margin-right: -1em; }
  .evw__ctas > .evw__go.btn--special { margin: 0; }
  .evw__strip { margin: 0 0 -1.2em 1em; padding: .4em .5em; gap: .35em; }
  .evw__strip .tile:not(.tile--sm) { --tile: 2.6em; }
  .evw__lv > b { font-size: 1.4em; }
  .evw__plate::before { background-size: 6em 6em, 6em 6em; }
  .evw__view { gap: .6em; }
  .evw__head { min-height: 3.4em; padding: .3em .8em .3em .3em; gap: .6em; }
  .evw__vtitle { font-size: calc(var(--win-size) * 1.4); }
}

/* ---- the promo banner (panels/events.ts, window `promobanner`): the same page and stage, no rail - the
   stage centred, and under it the page dots (two cards or more) and "Do not show again today". */
.wins > .win.evw.evw--promo {
  grid-template-columns: minmax(0, 76em); grid-template-rows: minmax(0, 1fr) auto; gap: 1.6em;
  padding-left: max(4.5em, var(--safe-l));
}
.evw--promo .evw__spread { grid-area: 1 / 1; place-items: center; }
.promo__foot { display: flex; align-items: center; justify-content: center; gap: 2em; }
.promo__dots { display: flex; gap: .7em; }
.promo__dots:empty { display: none; }
.promo__dot {
  width: .8em; height: .8em; padding: 0; border-radius: 50%; box-sizing: border-box;
  background: var(--navy-0); border: 2px solid var(--ink-strong); cursor: var(--cur-hand, pointer);
}
.promo__dot.is-on { background: var(--ink-strong); box-shadow: 0 0 .6em rgba(var(--ink-strong-rgb), .6); }
.promo__hide { color: var(--ink); text-shadow: 0 .06em .12em rgba(var(--shadow-ink), .9); }
@media (max-height: 560px) {
  .wins > .win.evw.evw--promo { grid-template-columns: minmax(0, 1fr); gap: .8em; padding-left: max(4em, var(--safe-l)); }
}
/* On a phone the hub's stage filled the screen; the popup's stands at about two thirds of it, the
   world showing round it (owner, 2026-10-18). Its text is sized off --win-size, so the whole stage
   is zoomed rather than its em; the foot keeps its size, to stay easy to tap. */
html.m-mobile .evw--promo .evw__stage { zoom: .66; }
@media (max-height: 560px) { html.m-desktop .evw--promo .evw__stage { zoom: .66; } }

@media (prefers-reduced-motion: reduce) {
  .wins > .win.evw, .evw__stage { animation: none; }
  .evw__card, .evw__node { transition: none; }
}

/* ---- w-gather.css */
/* GATHERING (panels/gathering.ts, `.win.gthw[data-win=gather]`, P29): the activity band, then
   either the empty state (no node underfoot) or the node's facts, the tool (tile, name, durability
   bar), the cycle (its clock, the bar filling over the server's duration, seconds left), and the last
   finds as tiles. The foot: the state badge, the line saying why, Stop, Start. Placed by gui/win.ts;
   moved from 25-card.css and 20-components.css. Tokens only. */
.gthw [hidden] { display: none !important; }
.wins > .win.gthw > .win__body { display: grid; gap: .7em; align-content: start; min-width: 0; }

.gthw__head .ic { width: 1.5em; height: 1.5em; object-fit: contain; }
.gthw__head .band__num { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gthw .gui-empty { display: grid; justify-items: center; gap: .3em; padding: 1.2em 1em; text-align: center; }
.gthw .gui-empty > img.g { width: 3em; height: 3em; object-fit: contain; }

.gthw__here { display: grid; gap: .7em; min-width: 0; }
.gthw .kv__val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gthw .kv__val > .is-short { color: var(--error); }

.gthw__tool { display: flex; align-items: center; gap: .6em; min-width: 0; }
.gthw__tool > .tile { flex: none; }
.gthw__toolcopy { display: grid; gap: .3em; flex: 1; min-width: 0; }
.gthw__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.gthw__cyclerow { display: flex; align-items: center; gap: .6em; min-width: 0; }
.gthw__cycle { display: inline-flex; align-items: center; gap: .3em; flex: none; font-variant-numeric: tabular-nums; }
.gthw__ic { width: 1.2em; height: 1.2em; object-fit: contain; }
.gthw__cyclerow > .bar { flex: 1; min-width: 0; }

.gthw__findsbox { display: grid; gap: .4em; min-width: 0; }
.gthw__finds { --cols: 6; }

/* The foot in two rows (QA 2026-10-09): the line saying why goes first, the whole width; the pill and
   the two buttons under it. In one row a 26em window squeezed the line to a word a row and pushed
   Start past the right edge. An empty line takes no row. */
.gthw .win__foot { gap: .6em; flex-wrap: wrap; row-gap: .45em; }
.gthw__state { flex: none; }
.gthw__line { flex: 1 0 100%; order: -1; min-width: 0; }
.gthw__line:empty { display: none; }
.gthw .win__foot > .btn { flex: none; white-space: nowrap; }
.gthw .win__foot > .btn:nth-last-child(2) { margin-left: auto; }
.gthw .btn__ic { width: 1.2em; height: 1.2em; object-fit: contain; }
/* Start is the errand: the gathering cursor. */
body.play .gthw .win__foot > .btn:last-child:not(:disabled) { cursor: var(--cur-gather); }

/* Phones (landscape, the short height): the finds fold to fewer columns, the line may wrap. */
html.m-mobile .gthw__finds { --cols: 4; }
html.m-mobile .gthw__line { font-size: var(--fs-xs); }

/* ---- w-guild.css */
/* THE GUILD WINDOW (panels/guild/*.ts, `.win.gw[data-win=guild]`, P25; Astro Night 2026-10-10) and
   THE GUILD MASTER (panels/guild-found.ts, `.win.gf[data-win=guildfound]`). The guild window is
   night like every window (the paper pages are gone, owner 2026-10-10): under the head, the hero
   (the guild's banner or the painted landscape, crest, name and the figures), the page strip, and
   the page on show, sitting on the window's own ground in wells. 20-components.css draws every
   component; tokens only. */

.gw [hidden], .gf [hidden] { display: none !important; }
.wins > .win.gw > .win__body { display: flex; flex-direction: column; gap: .6em; min-height: 0; overflow: hidden; }
/* DESKTOP: one height whatever the page, ending above the hotbar's band; the page scrolls. */
html.m-desktop .wins > .win.gw { height: min(46em, calc(.9 * var(--frame-h) - 88px * var(--hud-z, 1))); }

/* --- the head: the hero edge to edge, then the bar: the page tabs on the left, the reader's own
   name, position and Merit on the right (owner 2026-10-10: the hero carries only the guild's). */
.gw__top { display: flex; flex-direction: column; gap: .6em; flex: none; min-width: 0; }
.gw__bar { display: flex; align-items: center; justify-content: space-between; gap: .6em 1em; min-width: 0; }
.gw__tabs { min-width: 0; overflow-x: auto; scrollbar-width: thin; }
.gw__me { display: flex; align-items: center; gap: .6em; flex: none; font-size: var(--fs-md); }
.gw__me-name { font-family: var(--face-ui); color: var(--ink-strong); white-space: nowrap; }
.gw__tab { display: inline-flex; align-items: center; gap: .4em; }
.gw__tab > img.g { width: 1.6em; height: 1.6em; object-fit: contain; }
.gw__wait { margin-left: .1em; }

/* --- the hero: the banner behind the crest and the words, washed to dusk from the left so the
   words read on any picture. Full bleed: straight under the window head, out to both sides of the
   body (its padding undone), no frame. `--gw-banner` is the guild's own upload (index.ts), unset
   for the painted default. */
.gw__hero {
  position: relative; isolation: isolate; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: 1em; min-height: 8.5em; margin: -.9em -1.2em 0; padding: .8em 3.4em .8em 1.2em; box-sizing: border-box; overflow: hidden;
  color: var(--ink-strong);
}
.gw__hero::before { content: ""; position: absolute; inset: 0; z-index: -2;
  background: var(--gw-banner, url(/data/art/ui/guild/hero.50924a5a.webp)) center 54% / cover no-repeat; }
.gw__hero::after { content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg, rgba(var(--wash-rgb), .86), rgba(var(--wash-rgb), .5) 45%, rgba(var(--wash-rgb), .08) 85%),
    linear-gradient(transparent 55%, rgba(var(--navy-1-rgb), .7)); }
.gw__crest { display: grid; place-items: center; width: 5.4em; height: 5.4em;
  background: radial-gradient(circle, rgba(var(--sky-rgb), .16), transparent 70%); }
.gw__crest > img { grid-area: 1 / 1; width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 .15em .4em rgba(var(--shadow-ink), .8)); }
.gw__crest > .gw__crest-mark { width: 80%; height: 80%; }
.gw__hero-copy { display: grid; gap: .35em; min-width: 0; justify-items: start; }
.gw__name { max-width: 100%; font-family: var(--face-ui); font-weight: var(--face-weight); font-size: var(--fs-2xl); line-height: 1.15;
  color: var(--ink-strong); overflow-wrap: anywhere; text-shadow: var(--hud-shadow); }
.gw__meta { display: flex; flex-wrap: wrap; align-items: center; gap: .45em .9em; font-size: var(--fs-md); text-shadow: var(--hud-shadow); }
.gw__level { color: var(--brass-hi); font-family: var(--face-ui); }
.gw__fig { display: inline-flex; align-items: center; gap: .35em; font-variant-numeric: tabular-nums; }
.gw__fig > img.g { width: 1.25em; height: 1.25em; object-fit: contain; }
.gw__found { display: inline-grid; grid-template-columns: 8em auto; align-items: center; gap: .6em; font-size: var(--fs-sm);
  color: var(--ink-mute); font-variant-numeric: tabular-nums; text-shadow: var(--hud-shadow); }
.gw__cz { position: absolute; top: .5em; right: .6em; }

/* --- the page: on the window's ground, scrolling under the head */
.gw__pages { flex: 1 1 auto; min-height: 0; }
.gw__page { display: grid; gap: .7em; align-content: start; min-width: 0; padding: .1em .2em; }
.gw__wellhead { display: flex; align-items: center; justify-content: space-between; gap: .6em; min-height: 1.8em; }
.gw__sect { display: inline-flex; align-items: center; gap: .45em; }
.gw__sect > img.g { width: 1.2em; height: 1.2em; object-fit: contain; }
.gw__tools { display: flex; flex-wrap: wrap; align-items: center; gap: .5em; }
.gw__tools > .field--search { flex: 1 1 10em; min-width: 9em; }
.gw__acts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5em; }
.gw__hint { color: var(--ink-mute); }
.gw__dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: var(--ink-faint); flex: none; }
.gw__dot--1 { background: var(--orange); }
.gw__dot--2 { background: var(--green); box-shadow: 0 0 .4em rgba(var(--hud-good-glow-rgb), .6); }
.gw__face { font-size: .58em; }
.gw__bld-ic, .gw__log-ic { width: 2em; height: 2em; object-fit: contain; }
.gw__person, .gw__bld, .gw__quest, .gw__offer { min-height: 3.2em; }
.gw__person .item-row__end, .gw__bld .item-row__end { gap: .4em; }
.gw__merit .coin { width: 1.1em; height: 1.1em; }
.gw__offer-text { overflow-wrap: anywhere; }

/* Overview: the announcement and the quests on the left, the history on the right. */
.gw__ov { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: .8em; align-items: start; }
.gw__ov-main, .gw__ov-side { display: grid; gap: .8em; min-width: 0; }
.gw__notice-text { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); }
.gw__notice-text.is-empty { color: var(--ink-mute); }
.gw__reset { font-size: var(--fs-sm); color: var(--ink-mute); font-variant-numeric: tabular-nums; text-transform: none; letter-spacing: 0; }
.gw__log-box { max-height: 22em; }
.gw__log .item-row__name b { white-space: normal; overflow-wrap: anywhere; }
.gw__quest { flex-wrap: wrap; }
.gw__quest > .bar { grid-column: 1 / -1; }
.gw__qsub { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .5em; }
.gw__quest .item-row__name b { white-space: normal; }

/* Members: Roster | Recruit over the view; the roster's head and rows are two grids on the same
   tracks (fractions only, so both come out the same width). */
.gw__views { justify-self: start; }
.gw__rosterview, .gw__recruitview { display: grid; gap: .6em; min-width: 0; }
.gw__roster { --cols: minmax(0, 1.6fr) minmax(0, .5fr) minmax(0, 1fr) minmax(0, .7fr) minmax(0, .9fr); }
.gw__roster-box { max-height: 22em; }
.gw-m { cursor: var(--cur-hand, pointer); }
.gw-m__name { gap: .5em; }
.gw-m > :is(:nth-child(2), :nth-child(4), :nth-child(5)) { justify-content: flex-end; font-variant-numeric: tabular-nums; }
.gw-m:hover > * { background: rgba(var(--ink-wash), .05); }
.gw__roster--head .table__row > :is(:nth-child(2), :nth-child(4), :nth-child(5)) { justify-content: flex-end; }
/* The column words as written (Name, Level, Position...), not the table's small caps (owner 2026-10-10). */
.gw__roster--head .table__head > * { font-variant: normal; letter-spacing: 0; }
.gw__invite .gw__tools > .field { flex: 1 1 10em; }
/* Accept applications: two lit choices that follow the server. */
.gw__recruit { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .3em .8em; padding: 0 .2em; }
.gw__recruit > .note-line { grid-column: 1 / -1; }
.gw__switch { display: inline-flex; gap: .3em; }
.gw__switch > .btn.is-on { --btn-face: var(--tan); color: var(--tan-text); }

/* Buildings: the buildings beside the treasury; the next step's price, red where the guild is short. */
.gw__bldgrid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: .8em; align-items: start; }
.gw__bld-box { display: grid; gap: .2em; }
.gw__lv { font-variant-numeric: tabular-nums; white-space: nowrap; }
.gw__cost { white-space: normal; }
.gw__short { color: var(--error); }
.gw__purse { display: inline-flex; align-items: center; gap: .35em; font-family: var(--face-ui); font-size: var(--fs-lg); }
.gw__purse .coin { width: 1.1em; height: 1.1em; }
.gw__store { display: grid; grid-template-columns: repeat(auto-fill, minmax(11em, 1fr)); gap: .3em .8em; margin: .5em 0; }
.gw__stock { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .5em; }
.gw__stock-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gw__n { font-variant-numeric: tabular-nums; }
.gw__donate { display: grid; gap: .4em; }
.gw__donate .field { width: 7em; }

/* Shop and pharmacy: the list, then the pick strip with the one verb and why it is grey. */
.gw__quotas { display: flex; flex-wrap: wrap; gap: .35em; flex: 1 1 auto; }
.gw__counter { display: grid; gap: .5em; }
.gw__offer-box { max-height: 20em; }
.gw__offers { display: grid; grid-template-columns: repeat(auto-fill, minmax(17em, 1fr)); gap: 0 1em; }
.gw__offer { cursor: var(--cur-hand, pointer); }
.gw__offer .item-row__sub { white-space: normal; }
/* The pick strip stays in reach under a long list: it sticks to the bottom of the page's scroll. */
.gw__pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .6em;
  position: sticky; bottom: 0; z-index: 1; background: var(--win-face); box-shadow: 0 -.4em .8em rgba(var(--shadow-ink), .5); }
.gw__pick-act { display: flex; align-items: center; gap: .6em; }

/* The foot: the line on the left, the one way out on the right. */
.wins > .win.gw .win__foot { gap: .5em; }
.gw__said { margin-right: auto; min-width: 0; overflow-wrap: anywhere; }

/* An empty page (no guild): no box, the picture on a soft brass glow, a title and one line. */
.gw .gui-empty { display: grid; justify-items: center; gap: .45em; padding: 1.4em 1em; text-align: center; background: none; border: 0; box-shadow: none; }
.gw .gui-empty > img.g { width: 4.4em; height: 4.4em; object-fit: contain; filter: drop-shadow(0 0 1.1em rgba(var(--brass-hi-rgb), .28)); }
.gw .gui-empty > b { font-family: var(--face-display); font-size: var(--fs-heading); color: var(--brass-pale); }
.gw__crest-sm { width: 2em; height: 2em; object-fit: contain; }

/* --- Customize (a dialog, ask.ts): the emblem and the banner, each with its rules. */
.gw__cz-body { display: grid; gap: .7em; }
.gw__cz-row { display: flex; align-items: center; gap: .8em; margin-bottom: .4em; }
.gw__cz-crest { display: grid; place-items: center; width: 4em; height: 4em; flex: none; }
.gw__cz-crest > img { grid-area: 1 / 1; width: 100%; height: 100%; object-fit: contain; }
.gw__cz-crest > .gw__crest-mark { width: 80%; height: 80%; }
.gw__cz-banner { aspect-ratio: 3 / 1; margin-bottom: .5em; border: 1px solid rgba(var(--brass-rgb), .4); border-radius: .4em;
  background: var(--gw-banner, url(/data/art/ui/guild/hero.50924a5a.webp)) center / cover no-repeat; }
.gw__cz-body .gw__acts { justify-content: flex-start; margin-bottom: .3em; }

/* MOBILE MODE: a sheet in proportion, never the whole screen (owner, 2026-10-11: it stretched the
   full height whatever the page held): its own desktop width up to 78% of the frame and one height up to 32em,
   the same on every page so a tab press does not jump. The hero is one short row with the strip
   beside it, pictures only; the page scrolls inside; rows keep a thumb's height. */
html.m-mobile .wins > .win.gw { width: min(var(--w), .78 * var(--frame-w)); height: min(32em, calc(var(--frame-h) - 24px)); }
html.m-mobile .gw__top { gap: .4em; }
html.m-mobile .gw__hero { min-height: 0; padding: .3em 2.8em .3em .6em; gap: .6em; }
html.m-mobile .gw__me { font-size: var(--fs-sm); }
html.m-mobile .gw__me-name { display: none; }
html.m-mobile .gw__crest { width: 2.4em; height: 2.4em; }
html.m-mobile .gw__hero-copy { display: flex; flex-wrap: nowrap; align-items: center; gap: .6em; overflow: hidden; }
html.m-mobile .gw__name { font-size: var(--fs-lg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; }
html.m-mobile .gw__meta { flex-wrap: nowrap; font-size: var(--fs-sm); }
html.m-mobile :is(.gw__fig--members, .gw__found-n) { display: none; }
html.m-mobile .gw__found { grid-template-columns: 5em; }
html.m-mobile .gw__cz { top: 50%; transform: translateY(-50%); }
html.m-mobile .gw__tab-l { display: none; }
html.m-mobile .gw__tabs .tabs__tab { min-width: var(--touch); min-height: var(--touch); padding: 0 .6em; }
html.m-mobile :is(.gw__ov, .gw__bldgrid) { grid-template-columns: minmax(0, 1fr); }
html.m-mobile .gw__notice-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
html.m-mobile :is(.gw__roster-box, .gw__offer-box, .gw__log-box) { max-height: none; }
html.m-mobile :is(.gw__person, .gw__quest, .gw__offer), html.m-mobile .gw-m > * { min-height: var(--touch); }
html.m-mobile .gw__roster { --cols: minmax(0, 1.6fr) minmax(0, .6fr) minmax(0, 1fr) minmax(0, .9fr); }
html.m-mobile .gw__roster .table__row > :nth-child(4) { display: none; }

/* --- THE GUILD MASTER: a founding form over the board of every guild */
.wins > .win.gf > .win__body { display: flex; flex-direction: column; gap: .6em; min-height: 0; overflow: hidden; }
.gf__head { display: flex; justify-content: space-between; align-items: baseline; }
.gf__box { height: 18em; flex: 1 1 auto; min-height: 0; }
.gf__row { cursor: var(--cur-hand, pointer); min-height: 3.2em; }
.gf__pic { display: inline-flex; align-items: center; gap: .4em; }
.gf__rank { min-width: 1.6em; text-align: right; font-family: var(--face-ui); font-size: var(--fs-lg); font-variant-numeric: tabular-nums; color: var(--ink-mute); }
.gf__crest { width: 2.2em; height: 2.2em; object-fit: contain; }
.gf__why { font-size: var(--fs-sm); color: var(--ink-mute); white-space: nowrap; }
.wins > .win.gf .win__foot { gap: .5em; }
.gf__said { margin-right: auto; min-width: 0; overflow-wrap: anywhere; }
/* Guild Rankings (owner, 2026-10-08): the first three on a podium over the rows, the first raised in
   the middle; every ranked line carries its count over a thin brass bar. */
.gf__podium { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: end; gap: .6em; padding: .8em;
  border: 1px solid rgba(var(--brass-rgb), .4); border-radius: .5em; flex: none;
  background: radial-gradient(ellipse at 50% 0%, rgba(var(--sky-rgb), .14), transparent 70%), var(--navy-0); }
.gf__place { appearance: none; font: inherit; color: var(--ink); min-width: 0; cursor: var(--cur-hand, pointer);
  display: grid; justify-items: center; gap: .2em; padding: .6em .4em .7em;
  border: 1px solid rgba(var(--brass-rgb), .35); border-radius: .5em; background: linear-gradient(var(--navy-3), var(--navy-1));
  transition: border-color .15s, transform .15s; }
.gf__place:disabled { cursor: var(--cur-arrow, default); }
.gf__place:hover:not(:disabled) { border-color: var(--brass-hi); transform: translateY(-.1em); }
.gf__place--1 { order: 2; padding-top: 1em; border-color: var(--brass); box-shadow: 0 0 .9em rgba(var(--brass-rgb), .25); }
.gf__place--2 { order: 1; }
.gf__place--3 { order: 3; }
.gf__place.is-mine { box-shadow: inset 0 0 0 .12em var(--sky); }
.gf__place-crest { width: 3.2em; height: 3.2em; object-fit: contain; }
.gf__place--1 .gf__place-crest { width: 4.2em; height: 4.2em; }
.gf__medal { display: grid; place-items: center; width: 1.7em; height: 1.7em; margin-top: -.9em; border-radius: 50%;
  font-family: var(--face-ui); font-variant-numeric: tabular-nums; color: var(--navy-0);
  background: linear-gradient(#eef1f5, #9aa6b4); box-shadow: 0 .1em .25em rgba(var(--shadow-ink), .5); }
.gf__medal--1 { background: linear-gradient(var(--brass-pale), var(--brass)); }
.gf__medal--3 { background: linear-gradient(#e7a877, #a8622f); }
.gf__place-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--face-ui); font-size: var(--fs-lg); color: var(--ink-strong); }
.gf__place--1 .gf__place-name { color: var(--brass-hi); }
.gf__place-lv { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); color: var(--ink-mute); }
.gf__fill { display: inline-grid; gap: .2em; min-width: 4.6em; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; text-align: right; }
.gf__fill b { font-weight: inherit; }
.gf__bar { display: block; height: .25em; border-radius: 1em;
  background: linear-gradient(90deg, var(--brass) var(--v), rgba(var(--sky-rgb), .14) var(--v)); }
.gf__fullness:empty { display: none; }
html.m-mobile .wins > .win.gf { width: min(36em, calc(var(--frame-w) - 16px)); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .gf__podium { padding: .5em; gap: .4em; }
html.m-mobile .gf__place-crest, html.m-mobile .gf__place--1 .gf__place-crest { width: 2.4em; height: 2.4em; }
@media (prefers-reduced-motion: reduce) { .gf__place { transition: none; } }
html.m-mobile .gf__box { height: auto; }
html.m-mobile .gf__row { min-height: var(--touch); }


/* ---- w-mail.css */
/* THE POST (panels/mail.ts, `.win.ml[data-win=mail]`, P23): the tabs and the letter count; the
   Inbox face (the page of letters and the pager in the list column; the open
   letter beside it) and the Write face (receiver, subject, body, Gold and postage, the eight
   attachment places); the foot's result line and the verbs of the face on show. Placed by gui/win.ts
   beside the Backpack's spot (a letter is filled by dragging out of the bag). 20-components.css draws every
   part; tokens only. Moved from 25-card.css. */

.wins > .win.ml > .win__body { display: flex; flex-direction: column; gap: .7em; overflow: hidden; }
/* DESKTOP: one height on both faces, ending above the hotbar's band (B192); each pane scrolls. */
html.m-desktop .wins > .win.ml { height: min(38em, calc(.88 * var(--frame-h) - 88px * var(--hud-z, 1))); }
.ml [hidden] { display: none !important; }

.ml__bar { display: flex; align-items: center; justify-content: space-between; gap: 1em; flex: none; }
.ml__bar .tabs { margin: 0; }
.ml__bar .band { min-width: 0; }

/* --- the inbox: the list column and the open letter */
.ml__inbox { display: grid; grid-template-columns: minmax(15em, 36%) minmax(0, 1fr); gap: .8em; flex: 1; min-height: 0; }
.ml__side { display: flex; flex-direction: column; gap: .5em; min-width: 0; min-height: 0; }
.ml__list { display: grid; align-content: start; gap: .6em; flex: 1; min-height: 0; padding: .15em; }
.ml__letters { display: grid; gap: .4em; }

.ml__row { min-height: 3.6em; padding: .45em .55em; border: 1px solid var(--field-border); border-radius: .4em;
  background: var(--paper); cursor: var(--cur-hand, pointer); }
.ml__row:hover { background: var(--paper-2); }
.ml__row.is-on { border-color: rgba(var(--brass-rgb), .55); background: var(--row-on), var(--paper); }
.ml__row .item-row__pic > img.g { width: 2em; height: 2em; object-fit: contain; }
.ml__row:not(.is-unread) .item-row__pic { opacity: .55; }
.ml__row.is-unread .item-row__name b { color: var(--ink-strong); }
/* One line each: the subject, then who and the short date (B192: the date wrapped to three lines). */
.ml__row .item-row__sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-sm); color: var(--ink-mute); }
.ml__row .item-row__end { display: grid; justify-items: end; gap: .2em; font-size: var(--fs-sm); }
.ml__goods { display: inline-flex; align-items: center; gap: .2em; font-variant-numeric: tabular-nums; }
.ml__goods > img.g { width: 1.3em; height: 1.3em; object-fit: contain; }
.ml .gui-empty { display: grid; justify-items: center; align-content: center; gap: .4em; padding: 1.4em 1em; text-align: center; }
.ml .gui-empty > img.g { width: 3em; height: 3em; object-fit: contain; }

.ml__pager { display: flex; align-items: center; justify-content: center; gap: .9em; flex: none; }
.ml__pager-at { min-width: 4em; text-align: center; color: var(--ink-mute); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

.ml__reading { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 1.2em 1.4em;
  border: 1px solid var(--field-border); border-radius: .45em; background: var(--paper); color: var(--ink); }
.ml__empty { border: 1px solid var(--field-border); border-radius: .45em; background: var(--paper); color: var(--ink-mute); }
.ml__back { display: none !important; }
.ml__subject { margin: 0; color: var(--ink-strong); font: var(--face-display-weight) var(--fs-xl)/1.15 var(--face-display); overflow-wrap: anywhere; }
.ml__sender { margin-top: .25em; color: var(--green); }
.ml__date, .ml__deadline { color: var(--ink-mute); font-size: var(--fs-sm); }
.ml__deadline { padding-bottom: .8em; border-bottom: 1px solid var(--hairline); }
.ml__body { padding: 1em 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.55; }
.ml__attachments { display: grid; gap: .5em; margin-top: auto; padding-top: 1em; }
.ml__attachment-items { display: flex; align-items: center; flex-wrap: wrap; gap: .7em; min-width: 0; }
.ml__att-items { --cols: 8; }

/* --- the writing face */
.ml__compose { display: grid; align-content: start; gap: .6em; flex: 1; min-height: 0; padding: 1em 1.2em;
  border: 1px solid var(--field-border); border-radius: .45em; background: var(--paper); }
.ml__compose .field--area textarea { min-height: 5em; }
.ml .band__label > img.coin { width: 1.1em; height: 1.1em; vertical-align: -.2em; margin-right: .3em; }
.ml .band__action { display: flex; align-items: center; gap: .8em; }
.ml__gold { width: 8em; min-width: 0; }
.ml__postage { display: inline-flex; align-items: center; gap: .4em; }
.ml__drop { display: grid; gap: .5em; padding: .6em; border: 1px dashed var(--tan-rim); border-radius: .5em; }
.ml__drop.is-over { border-color: var(--drop-on-rim); background: var(--drop-on-bg); }
.ml__att { --cols: 8; }
.ml__att .tile[data-att] { cursor: var(--cur-grab, grab); touch-action: none; }
/* An attachment no longer in the bag (or short of its amount): ringed in the refusal colour, its
   card says why (B190). */
.ml__att .tile.is-bad { outline: 2px solid var(--error); outline-offset: -2px; }

/* --- the foot: the result line, then the face's verbs */
.wins > .win.ml .win__foot { flex-wrap: wrap; gap: .5em; }
.ml__said { flex: 1 1 10em; min-width: 0; overflow-wrap: anywhere; }
.ml__group { display: flex; flex-wrap: wrap; gap: .5em; }

/* MOBILE MODE: half the sheet beside the bag (drag from one to the other); the list or the open
   letter, one at a time, with the letter's verbs only while it is open; the secondaries are their
   picture (the word on the card). */
html.m-mobile .wins > .win.ml { width: min(50em, calc(.5 * var(--frame-w) - 12px)); height: calc(var(--frame-h) - 32px); }
html.m-mobile .ml__bar .band { display: none; }
html.m-mobile .ml__inbox { grid-template-columns: minmax(0, 1fr); }
html.m-mobile .ml:not(.ml--reading) .ml__reading,
html.m-mobile .ml .ml__empty,
html.m-mobile .ml.ml--reading .ml__side,
html.m-mobile .ml:not(.ml--reading) .ml__group { display: none !important; }
html.m-mobile .ml.ml--reading .ml__back { display: inline-grid !important; align-self: flex-start; margin-bottom: .6em; }
html.m-mobile .ml__verb > span { display: none; }
html.m-mobile .ml__row { min-height: var(--touch); }
html.m-mobile .ml__att, html.m-mobile .ml__att-items { --cols: 4; }
html.m-mobile .ml__compose { padding: .7em; }

/* ---- w-map.css */
/* THE WORLD MAP (apps/game/client/world-map.ts, `.win.wm-win[data-win=map]`): the legend down the
   left (place names, a row per kind of pin with its count, you; each a picture and its word) and
   the plate beside it. Placed by gui/win.ts; this
   sheet lays it out. Moved from 25-card.css + 20-components.css (P19), tokens only.
   Everything on the plate stands at its corner and is moved by `translate` (world-map.ts), so a
   rule here centres it with `transform` and never sets left/top. */
.wm-win.win { width: max-content; }
.wm { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .5em; }

/* The legend: a column of rows as tall as the plate (height 0 + min-height 100% keeps it from
   stretching the window), scrolling when the kinds outgrow it. A row is its picture, its word and,
   for a kind of pin, its count; the lit one wears the selection. */
.wm__legend {
  display: flex; flex-direction: column; gap: .15em; min-width: 10em; max-width: 14em;
  height: 0; min-height: 100%; overflow-y: auto; overscroll-behavior: contain;
}
.wm__kinds { display: contents; }
.wm__kind {
  flex: none; display: flex; align-items: center; gap: .5em; width: 100%; padding: .25em .4em;
  border: 0; border-radius: .3em; background: none; color: var(--ink); font: inherit; text-align: left;
  cursor: var(--cur-hand, pointer);
}
.wm__kind:hover { background: var(--row-hover); }
.wm__kind.is-on { background: var(--row-on); color: var(--ink-strong); }
.wm__kind .ic { flex: none; width: 1.6em; height: 1.6em; }
.wm__label { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: var(--fs-sm); }
.wm__n {
  flex: none; min-width: 1.6em; padding: 0 .3em; border-radius: .6em;
  background: var(--count-bg); color: var(--ink-strong);
  font-size: var(--fs-xs); line-height: 1.4em; text-align: center; font-variant-numeric: tabular-nums;
}

.wm__well { padding: .3em; }
.wm__plate {
  position: relative; overflow: hidden; touch-action: none; cursor: var(--cur-hand, pointer);
  width: min(52em, calc((var(--frame-h) - 11em) * var(--wm-ratio, 1)), calc(var(--frame-w) - 19em));
  aspect-ratio: var(--wm-ar, 1); border-radius: .3em; background: var(--void);
}
.wm__ground { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.wm__layer { position: absolute; inset: 0; pointer-events: none; }
.wm .is-off { visibility: hidden; }

.wm__pin, .wm__name, .wm__mark, .wm__goal, .wm__you, .wm__coord, .wm__route { position: absolute; left: 0; top: 0; }

/* A pin: the painted badge centred on the point it names (data/art/ui/map), the head marker's own
   subject redrawn flat. Blue ways out, red doors, green counters - the badge's face says which. */
.wm__pin {
  width: 1.9em; height: 1.9em; padding: 0; border: 0; background: none; pointer-events: auto;
  transform: translate(-50%, -50%); transition: transform .12s, opacity .12s;
  cursor: var(--cur-hand, pointer);
}
.wm__pin--service { cursor: var(--cur-talk, pointer); }
.wm__pin--portal, .wm__pin--instance { cursor: var(--cur-portal, pointer); }
.wm__pin > img.g { display: block; width: 100%; height: 100%; object-fit: contain; }
.wm__pin:hover, .wm__pin.is-lit { z-index: 1; transform: translate(-50%, -50%) scale(1.25); }
/* A kind lit from the legend: the rest step back. */
.wm__pins.has-lit > .wm__pin:not(.is-lit) { opacity: .35; }

/* A named place: text on the ground, in the world layer's voice. */
.wm__name {
  transform: translate(-50%, -50%); white-space: nowrap;
  color: var(--world-text); font-size: var(--fs-sm); letter-spacing: .04em;
  text-shadow: 0 0 .2em rgba(var(--world-dark), .95), 0 .08em .12em rgba(var(--world-dark), .9);
}

/* Where a creature stands (a quest objective pressed), and a band still standing in a run. */
.wm__mark {
  width: .6em; height: .6em; transform: translate(-50%, -50%); border-radius: 50%;
  background: var(--ember-500); box-shadow: 0 0 0 2px var(--world-text), 0 1px 2px rgba(var(--world-dark), .55);
}
.wm__mark--band { width: .45em; height: .45em; background: var(--ember-400); box-shadow: 0 0 0 1.5px var(--world-text); }
/* A creature asked for: hovering a dot names it (data-tip-text). */
.wm__mark--ask { pointer-events: auto; cursor: var(--cur-hand, pointer); transition: transform .12s; }
.wm__mark--ask:hover { z-index: 1; transform: translate(-50%, -50%) scale(1.5); }

/* The walk under way: the server's route and where it ends. */
.wm__route { width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.wm__route.is-off { display: none; }
.wm__route polyline {
  fill: none; stroke: var(--world-text); stroke-width: 3px; stroke-dasharray: 6 5;
  stroke-linecap: round; stroke-linejoin: round;
}
.wm__goal {
  width: .7em; height: .7em; transform: translate(-50%, -50%); pointer-events: none; border-radius: 50%;
  background: var(--link-ink); box-shadow: 0 0 0 2px var(--world-text), 0 1px 2px rgba(var(--world-dark), .55);
}
/* You: the gold pin with a walker in its ring, standing on its point, over every badge and never a
   click target - the ground under it is. */
.wm__you { width: 1.7em; height: 2.6em; transform: translate(-50%, -100%); pointer-events: none; z-index: 2; }
.wm__you > img.g { display: block; width: 100%; height: 100%; object-fit: contain; }

/* The tile under the mouse, riding above and right of it; the answer's caption, top left. */
.wm__coord, .wm__ask {
  padding: .1em .45em; border-radius: .25em; pointer-events: none; white-space: nowrap;
  background: var(--count-bg); color: var(--ink-strong); font-size: var(--fs-sm);
}
.wm__coord { z-index: 3; transform: translate(12px, calc(-100% - 6px)); font-variant-numeric: tabular-nums; }
.wm__ask { position: absolute; left: .4em; top: .4em; z-index: 3; }
.wm__coord[hidden], .wm__ask[hidden], .wm__goal[hidden], .wm__you[hidden] { display: none; }

/* Mobile mode: the map takes the sheet. The legend stays the column down the left, narrower, the
   plate fills the rest at the sheet's height (zoom 1 covers it; a pinch out shows the whole map),
   and the badges grow to a thumb. */
html.m-mobile .wm-win.win { width: calc(var(--frame-w) - 16px); }
html.m-mobile .wm__legend { min-width: 8.5em; max-width: 11em; }
html.m-mobile .wm__kind { padding: .35em .4em; }
html.m-mobile .wm__plate { width: auto; aspect-ratio: auto; height: max(8em, calc(var(--frame-h) - 7.5em)); }
html.m-mobile .wm__pin { width: 2.4em; height: 2.4em; }

@media (prefers-reduced-motion: reduce) {
  .wm__pin { transition: none; }
}

/* ---- w-market.css */
/* THE MARKETPLACE (panels/market/*, `.win.market[data-win=market]`): the gui/ shell around four tabs.
   Placed by gui/win.ts; this sheet sizes it and lays out the two shelf panes (`.mks`): the Tamer
   Store (official.ts) and the Exchange (`.mks.mkl`, legacy.ts), the Lumi Exchange (`.mkx`, lumi.ts),
   My Transactions (`.mkm`, mine.ts) and the price chart (`.mkc`, book.ts) both book tabs share.
   20-components.css draws every part (tabs, well, item row, tile, chip, button, the "?"). The panes
   answer the window's own width (container `marketplace`), so a narrow window switches columns on a
   wide screen. Phones are landscape: the short height is the constraint, so every pane scrolls in
   its own columns and nothing stacks tall. */

/* A fixed height, so the lists scroll inside it and the purses stay in the foot. */
.wins > .win.market { height: min(55.333em, calc(var(--frame-h) - 32px)); }
html.m-mobile .wins > .win.market { width: min(var(--w), .8 * var(--frame-w)); height: calc(var(--frame-h) - 32px); }

.market { container: marketplace / inline-size; }
.market .win__foot { gap: .5em; }
.market__note { margin-left: auto; min-width: 0; overflow-wrap: anywhere; }

/* Every pane fills the body and scrolls in its own columns, on every screen: the old
   short-screen rule that made the whole body one scroller put its rows under the fold (B153). */
.wins > .win.market > .win__body { display: flex; flex-direction: column; gap: .6em; overflow: hidden; }
.mks { display: grid; grid-template-rows: auto minmax(0, 1fr); gap: .6em; flex: 1; min-height: 0; min-width: 0; }
.mks[hidden] { display: none; }

/* ---------------------------------------------------------------- the tools: search, sort, "?" */
.mks__tools { display: grid; grid-template-columns: minmax(0, 1fr) minmax(11em, 14em) auto; gap: .6em; align-items: center; min-width: 0; }
.mks__sort { display: flex; align-items: center; gap: .35em; min-width: 0; }
.mks__sort > .ic { flex: none; width: 1.2em; height: 1.2em; }
.mks__sort > .select { flex: 1; min-width: 0; }

/* ---------------------------------------------------------------- the shelf tree (shelves.ts)
   Calm (owner, 2026-10-10): no rule between rows, no counts, an empty
   shelf in muted ink, the pick lit with a brass edge, a class and a part indented on a guide line
   under their parent. */
.mks__grid { display: grid; grid-template-columns: 10.5em minmax(0, 1fr) minmax(18em, 24em); gap: .8em; min-height: 0; min-width: 0; }
.mks__cats, .mks__list, .mks__side { min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; }
.mks__cats { display: grid; align-content: start; gap: 1px; padding-right: .5em; border-right: 1px solid var(--hairline); }
.mks__cat {
  appearance: none; -webkit-appearance: none; margin: 0; font: inherit;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .35em;
  width: 100%; min-height: 1.95em; padding: .2em .5em; border: 0; border-radius: .35em;
  background: none; color: var(--ink); text-align: left; cursor: var(--cur-hand, pointer);
}
.mks__cat:hover { background: var(--row-hover); }
.mks__cat.is-on { background: var(--row-on); box-shadow: inset 2px 0 0 var(--brass-hi); }
.mks__cat.is-empty:not(.is-on) { color: var(--ink-mute); }
.mks__catname { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mks__cat[data-level="1"], .mks__cat[data-level="2"] {
  width: auto; border-radius: 0 .35em .35em 0; font-size: var(--fs-sm);
  box-shadow: inset 1px 0 0 var(--hairline);
}
.mks__cat[data-level="1"] { margin-left: .7em; }
.mks__cat[data-level="2"] { margin-left: 1.4em; }
.mks__cat[data-level].is-on:not([data-level="0"]) { box-shadow: inset 2px 0 0 var(--brass-hi); }
.mks__caret { width: .7em; height: .7em; flex: none; opacity: .55; transform: rotate(-90deg); transition: transform .15s; }
.mks__cat.is-open > .mks__caret { transform: none; }
.mks__caret[hidden] { display: none; }

/* ---------------------------------------------------------------- rows, the pick */
.mks__list { align-content: start; }
.mks__row { cursor: var(--cur-hand, pointer); }
.mks__row .item-row__end .chip { font-size: var(--fs-lg); }
.mks__sub { display: inline-flex; flex-wrap: wrap; gap: 0 .7em; }
.mks__row .item-row__sub { display: inline-flex; flex-wrap: wrap; gap: 0 .7em; }
.mks__none { display: grid; gap: .3em; justify-items: center; align-content: center; min-height: 12em; text-align: center; }
.mks__none[hidden] { display: none; }

.mks__side { display: flex; flex-direction: column; gap: .6em; }
.mks__side > [hidden] { display: none; }
/* The Exchange's pick (legacy.ts): tile, name, its facts, the verb. */
.mks__detail { display: grid; gap: .5em; align-content: start; }
.mks__detail > [hidden] { display: none; }
.mks__head { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .6em; align-items: center; }
.mks__head > div { display: grid; gap: .15em; min-width: 0; }
.mks__name { font-size: var(--fs-lg); overflow-wrap: anywhere; }
.mks__name--green { color: var(--rarity-green-text); }
.mks__name--blue { color: var(--rarity-blue-text); }
.mks__name--gold { color: var(--rarity-gold-text); }
.mks__name--yellow { color: var(--rarity-yellow-text); }
.mks__name--purple { color: var(--rarity-purple-text); }
.mks__head .mks__sub { color: var(--ink-mute); font-size: var(--fs-sm); }
/* The Tamer Store's pick (official.ts): the item's own card laid flat in the column, then the
   price and Buy pinned under it. */
.mks__pickme { margin: auto; text-align: center; }
.mks__card > .card { width: auto; padding: 0; background: none; box-shadow: none; }
.mks__card { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.mks__deal { flex: none; display: grid; gap: .5em; padding-top: .6em; border-top: 1px solid var(--hairline); }
.mks__deal > [hidden] { display: none; }
.mks__price { display: flex; align-items: baseline; gap: .6em; }
.mks__price .chip { font-size: var(--fs-xl); }
.mks__lot { color: var(--ink-mute); font-size: var(--fs-sm); }
/* A weekly-limited line with nothing left this week (official.ts): its tag greys out. */
.mks__row .tag.is-out { opacity: .5; }
.mks:not(.mkl) > .mks__grid > .mks__side { overflow: hidden; }

/* ---------------------------------------------------------------- narrow: the shelves become a strip
   In mobile mode and on a narrow window the shelf tree runs across the top as one line of chips
   (classes and parts follow Equipment in the same line), and the rows keep the full height beside
   the pick. */
@container marketplace (max-width: 62em) {
  .mks__grid { grid-template-columns: minmax(0, 1fr) minmax(15em, 19em); grid-template-rows: auto minmax(0, 1fr); gap: .5em; }
  .mks__cats { grid-column: 1 / -1; display: flex; gap: .3em; overflow-x: auto; overflow-y: hidden; padding: 0 0 .3em; border-right: 0; scrollbar-width: none; }
  .mks__cat, .mks__cat[data-level] { flex: none; width: auto; margin: 0; padding-inline: .7em; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--hairline); }
  .mks__cat.is-on, .mks__cat[data-level].is-on:not([data-level="0"]) { box-shadow: inset 0 0 0 1px var(--brass-hi); }
  .mks__cat { background: var(--field-face); }
  .mks__cat:hover { background: linear-gradient(var(--row-hover), var(--row-hover)), var(--field-face); }
  .mks__cat.is-on { background: linear-gradient(var(--row-on), var(--row-on)), var(--field-face); }
}
html.m-mobile .mks__grid { grid-template-columns: minmax(0, 1fr) minmax(13em, 16em); grid-template-rows: auto minmax(0, 1fr); gap: .5em; }
html.m-mobile .mks__cats { grid-column: 1 / -1; display: flex; gap: .3em; overflow-x: auto; overflow-y: hidden; padding: 0 0 .3em; border-right: 0; scrollbar-width: none; }
html.m-mobile .mks__cat, html.m-mobile .mks__cat[data-level] { flex: none; width: auto; margin: 0; min-height: var(--touch); padding-inline: .8em; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--hairline); }
html.m-mobile .mks__cat.is-on, html.m-mobile .mks__cat[data-level].is-on:not([data-level="0"]) { box-shadow: inset 0 0 0 1px var(--brass-hi); }
/* A chip has a face: the window's engraving ran straight through bare ones (owner, 2026-10-10). */
html.m-mobile .mks__cat { background: var(--field-face); }
html.m-mobile .mks__cat:hover { background: linear-gradient(var(--row-hover), var(--row-hover)), var(--field-face); }
html.m-mobile .mks__cat.is-on { background: linear-gradient(var(--row-on), var(--row-on)), var(--field-face); }
html.m-mobile .mks__tools { grid-template-columns: minmax(0, 1fr) minmax(9em, 11em) auto; gap: .4em; }
html.m-mobile .mks__row { min-height: var(--touch); }
@container marketplace (max-width: 34em) {
  .mks__grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; }
  .mks__side { max-height: 45%; }
}

/* ---------------------------------------------------------------- the Exchange (legacy.ts)
   The bar: Buy / Sell, then (buying) the finder, then the "?". Selling, the bag and the form take
   the grid's place. */
.mkl { grid-template-rows: auto minmax(0, 1fr); }
.mkl__tools { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .8em; align-items: center; min-width: 0; }
/* Buy / Sell: two equal halves of a fixed strip. The text tabs' own 7em floor and wide padding pushed
   Sell out of the strip and over the finder (owner, 2026-10-10). */
.mkl__sides { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 12em; }
.mkl__sides .tabs__tab, .mkx__sides .tabs__tab { min-width: 0; padding-inline: .4em; }
.mkl__find { display: grid; grid-template-columns: minmax(0, 1fr) minmax(11em, 14em); gap: .6em; align-items: center; min-width: 0; }
.mkl__find[hidden] { display: none; }
/* The Sell side's bag search: the field alone, no sort beside it. */
.mkl__find.mkl__find--bag, html.m-mobile .mkl__find.mkl__find--bag { grid-template-columns: minmax(0, 1fr); }
.mkl__tools > .ibtn { grid-column: 3; }
.mkl__row { cursor: var(--cur-hand, pointer); }
.mkl__row .item-row__sub, .mks__sub { display: inline-flex; flex-wrap: wrap; gap: 0 .7em; }
.mkl__seller { color: var(--ink-mute); }
.mkl__end { display: inline-flex; align-items: center; gap: .35em; }
.mkl__end .chip { font-size: var(--fs-lg); }
.mkl__lead > .ic { width: 1.1em; height: 1.1em; }
/* A timed lot's two prices, stacked at the row's end: the next bid, then the buy-out (a dash when none). */
.mkl__prices { display: grid; gap: .1em; justify-items: end; }
.mkl__price { display: inline-flex; align-items: center; gap: .35em; }
.mkl__price > small { color: var(--ink-mute); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; }
.mkl__end .mkl__price .chip { font-size: var(--fs-md); }
.mkl__facts .kv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3em .7em; }
.mkl__facts .kv > div { display: block; min-width: 0; }
.mkl__facts .kv__val { display: block; overflow-wrap: anywhere; }
.mkl__book, .mkl__gear { display: grid; gap: .5em; min-width: 0; }
.mkl__stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3em .7em; }
.mkl__stats > div { display: block; min-width: 0; }
.mkl__book > .tabs { display: flex; min-width: 0; }
.mkl__book > .tabs .tabs__tab { flex: 1; min-width: 0; padding-inline: .2em; }

.mkl__sell { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18em, 24em); gap: .8em; min-height: 0; min-width: 0; }
.mkl__stack { cursor: var(--cur-hand, pointer); }
.mkl__form { display: grid; gap: .5em; align-content: start; min-width: 0; }
.mkl__form > [hidden] { display: none; }
.mkl__qty { display: flex; align-items: center; gap: .5em; min-width: 0; }
.mkl__qty > .field { width: 6em; flex: none; }
.mkl__fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5em; min-width: 0; }
.mkl__fields > .field { min-width: 0; }
.mkl__hours { display: grid; gap: .3em; }

/* ---------------------------------------------------------------- the price chart (book.ts)
   Scale, line, axis: three rows of the well that holds it; a book with no line says why instead. */
.mkc { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: .3em; height: 100%; min-height: 9em; min-width: 0; }
.mkl__book .mkc { height: 10em; }
.mkc__scale, .mkc__axis { display: flex; justify-content: space-between; gap: .6em; color: var(--ink-mute); font-size: var(--fs-sm); }
.mkc__svg { width: 100%; height: 100%; min-height: 0; overflow: visible; }
.mkc__grid { stroke: var(--hairline); stroke-width: 1; stroke-dasharray: 3 5; }
.mkc__line { fill: none; stroke: var(--moss-400); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.mkc__area { fill: var(--moss-400); opacity: .13; stroke: none; }
.mkc__svg.is-down .mkc__line { stroke: var(--ember-400); }
.mkc__svg.is-down .mkc__area { fill: var(--ember-400); }
.mkc__none { grid-row: 1 / -1; place-self: center; text-align: center; }

@container marketplace (max-width: 62em) {
  .mkl__sell { grid-template-columns: minmax(0, 1fr) minmax(15em, 19em); }
}
html.m-mobile .mkl__sell { grid-template-columns: minmax(0, 1fr) minmax(13em, 16em); gap: .5em; }
html.m-mobile .mkl__tools { gap: .4em; }
html.m-mobile .mkl__sides { width: 9em; }
html.m-mobile .mkl__find { grid-template-columns: minmax(0, 1fr) minmax(8em, 10em); gap: .4em; }
html.m-mobile .mkl__row, html.m-mobile .mkl__stack { min-height: var(--touch); }
@container marketplace (max-width: 34em) {
  .mkl__tools { grid-template-columns: minmax(0, 1fr) auto; }
  .mkl__find { grid-column: 1 / -1; grid-row: 2; }
  .mkl__tools > .ibtn { grid-column: 2; grid-row: 1; }
  .mkl__sell { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; }
}

/* ---------------------------------------------------------------- the Lumi Exchange (lumi.ts)
   Buy / Sell, the rate and the "?" on one bar; then the deal (you give -> you get, amount and
   price, when it trades, the one button) beside Offers (who sells, who buys, the price line). Each
   column scrolls in itself, so the button stays in reach on a short screen. */
.mkx { display: grid; grid-template-rows: auto minmax(0, 1fr); gap: .6em; flex: 1; min-height: 0; min-width: 0; }
.mkx[hidden] { display: none; }
.mkx__bar { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .8em; align-items: center; min-width: 0; }
.mkx__sides { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.mkx__sides .tabs__tab { gap: .5em; }
.mkx__sides .coin { width: 1.5em; height: 1.5em; }
/* The form and the price history side by side on top, the order book under both, across the pane. */
.mkx__grid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(18em, 24em); grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas: "deal chart" "book book"; gap: .8em; min-height: 0; min-width: 0;
}
.mkx__deal { grid-area: deal; }
.mkx__chart { grid-area: chart; }
.mkx__board { grid-area: book; }
.mkx__deal, .mkx__board { min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; }

.mkx__deal { display: grid; gap: .75em; align-content: start; }
.mkx__deal > [hidden] { display: none; }
.mkx__rate { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: .4em; min-width: 0; font-size: var(--fs-lg); }
.mkx__eq { color: var(--ink-mute); }
.mkx__ratenote { color: var(--ink-mute); font-size: var(--fs-sm); margin-left: .3em; }
.mkx__swap { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: .6em; align-items: center; }
.mkx__leg { display: grid; gap: .2em; min-width: 0; padding: .55em .7em; border-radius: .45em; background: var(--field-face); box-shadow: inset 0 0 0 1px var(--hairline); }
.mkx__leg--get { box-shadow: inset 0 0 0 1px rgba(var(--brass-hi-rgb), .45); }
.mkx__amt .chip { font-size: var(--fs-xl); }
.mkx__arrow { width: 1.2em; height: 1.2em; opacity: .7; }
.mkx__inputs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6em; min-width: 0; }
.mkx__field { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .35em; align-items: end; min-width: 0; }
.mkx__whenrow { display: flex; align-items: center; justify-content: space-between; gap: .6em; min-height: 2em; }
.mkx__whenrow > .select[hidden] { display: none; }
.mkx__when { color: var(--ink-mute); }
.mkx__when.is-now { color: var(--green); }

.mkx__board { display: grid; grid-template-columns: minmax(0, 1fr); gap: .7em; align-content: start; overflow-x: hidden; }
/* Offers: buyers and sellers side by side, best price on top of each. A line is price | Lumi |
   orders over a bar as long as its share of its side, grown from the middle of the book outward. */
.mkx__depth { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.2em; min-width: 0; }
.mkx__col { display: grid; gap: .2em; align-content: start; min-width: 0; }
.mkx__head, .mkx__lvl { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 4em; gap: .4em; align-items: center; text-align: left; }
.mkx__head { padding: 0 .6em .25em; border-bottom: 1px solid var(--hairline); color: var(--ink-mute); font-size: var(--fs-md); }
.mkx__head > :not(:first-child), .mkx__lvl > :not(:first-child) { justify-self: end; }
.mkx__sidecap { font-size: var(--fs-md); }
.mkx__col[data-side="1"] .mkx__sidecap { color: var(--error); }
.mkx__col[data-side="0"] .mkx__sidecap { color: var(--green); }
.mkx__lvls { display: grid; gap: 1px; }
.mkx__lvl {
  appearance: none; -webkit-appearance: none; margin: 0; font: inherit; color: var(--ink); border: 0;
  width: 100%; min-width: 0; min-height: 0; height: auto; padding: .45em .6em; font-size: var(--fs-lg); border-radius: .3em; cursor: var(--cur-hand, pointer); font-variant-numeric: tabular-nums;
  --tone: var(--green);
  background: linear-gradient(to left, color-mix(in srgb, var(--tone) 22%, transparent) var(--depth, 0%), transparent var(--depth, 0%));
}
.mkx__lvls[data-side="1"] .mkx__lvl { --tone: var(--error); background: linear-gradient(to right, color-mix(in srgb, var(--tone) 22%, transparent) var(--depth, 0%), transparent var(--depth, 0%)); }
.mkx__lvl:hover { box-shadow: inset 0 0 0 1px var(--hairline); }
.mkx__head > span { display: inline-flex; align-items: center; gap: .25em; }
.mkx__head .coin { width: 1.2em; height: 1.2em; }
.mkx__p { color: var(--brass-hi); font-weight: 600; }
.mkx__lvl > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mkx__n { color: var(--ink-mute); }
.mkx__mid { display: flex; align-items: center; justify-content: space-between; gap: .5em; }
.mkx__per { display: inline-flex; align-items: center; gap: .35em; }
.mkx__ladder > .note-line[hidden] { display: none; }
.mkx__chart { display: grid; gap: .4em; align-content: start; min-width: 0; }
.mkx__chart > .tabs { display: flex; min-width: 0; }
.mkx__chart > .tabs .tabs__tab { flex: 1; min-width: 0; padding-inline: .2em; }
.mkx__chart .mkc { height: 9em; }

@container marketplace (max-width: 62em) {
  .mkx__grid { grid-template-columns: minmax(0, 1fr) minmax(16em, 20em); }
  .mkx__depth { gap: .8em; }
}
/* Mobile swaps the two (owner, 2026-10-14): the order book is the whole right column, top to
   bottom, where a thumb that just named a price reads who stands at it; the price history sits
   under the form only. The narrow column stacks Buying over Selling, each in its own half of the
   height and scrolling inside it, so both sides are always in sight. (A five-level reserve per
   side pushed Selling below the fold of a landscape phone: a fresh sell offer looked missing.) */
html.m-mobile .mkx__grid { grid-template-columns: minmax(0, 1fr) minmax(15em, 18em); grid-template-areas: "deal book" "chart book"; gap: .5em; }
html.m-mobile .mkx__depth { grid-template-columns: minmax(0, 1fr); gap: .6em; }
html.m-mobile .mkx__board { grid-template-rows: auto minmax(0, 1fr); align-content: stretch; overflow: hidden; }
html.m-mobile .mkx__depth { grid-template-rows: repeat(2, minmax(0, 1fr)); min-height: 0; }
html.m-mobile .mkx__col { grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; min-height: 0; }
html.m-mobile .mkx__ladder { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
/* The FORM IS NEVER CUT (owner, 2026-10-14: at Standard the Buy button was): its row takes all it
   needs first and the history takes what is left, shrinking its line to fit, at every size. The
   history is one band under the form: its title, last sale and ranges on the left, the line beside
   them. On a glass too short even for the form, the form scrolls. */
html.m-mobile .mkx__grid { grid-template-rows: minmax(0, max-content) minmax(0, 1fr); }
html.m-mobile .mkx__chart { grid-template-columns: minmax(12em, 16em) minmax(0, 1fr); column-gap: .8em; align-items: start; min-height: 0; overflow: hidden; }
html.m-mobile .mkx__chart > .well__title, html.m-mobile .mkx__chart > .mkx__mid, html.m-mobile .mkx__chart > .tabs { grid-column: 1; }
html.m-mobile .mkx__chart > .mkc { grid-column: 2; grid-row: 1 / span 3; }
html.m-mobile .mkx__deal { gap: .5em; }
html.m-mobile .mkx__leg { grid-auto-flow: column; justify-content: space-between; align-items: center; padding: .35em .55em; }
html.m-mobile .mkx__lvl { min-height: var(--touch); }
html.m-mobile .mkx__chart .mkc { height: auto; min-height: 0; align-self: stretch; }
@container marketplace (max-width: 34em) {
  .mkx__bar { grid-template-columns: minmax(0, 1fr) auto; }
  .mkx__rate { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
  .mkx__grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; grid-template-areas: "deal" "book" "chart"; overflow-y: auto; }
  .mkx__deal, .mkx__board { overflow: visible; }
}

/* ---------------------------------------------------------------- My Transactions (mine.ts)
   The filter, the mail and the "?" on one bar, a refusal line, then one scrolling column of lines:
   picture, name, where and when, the state, the money with its sign, the verb when it is open. */
.mkm { display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: .6em; flex: 1; min-height: 0; min-width: 0; }
.mkm[hidden], .mkm [hidden] { display: none; }
.mkm__tools { display: flex; align-items: center; gap: .5em; min-width: 0; }
.mkm__filter { min-width: 0; }
.mkm__tools > .btn { margin-left: auto; padding-inline: .5em; }
.mkm__mailic { width: 1.4em; height: 1.4em; }
.mkm__list { grid-row: 3; display: grid; gap: .6em; align-content: start; min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; }
.mkm__row.is-open { cursor: var(--cur-hand, pointer); }
.mkm__row .item-row__sub { display: inline-flex; flex-wrap: wrap; gap: 0 .7em; }
.mkm__row > .item-row__pic > .coin { width: 2em; height: 2em; }
.mkm__when { color: var(--ink-faint); }
.mkm__end { display: inline-flex; align-items: center; gap: .6em; }
.mkm__state { font-size: var(--fs-sm); color: var(--ink-mute); }
.mkm__state--open, .mkm__state--leading { color: var(--brass-hi); }
.mkm__state--done { color: var(--ink); }
.mkm__state--partial { color: var(--rarity-yellow-text); }
.mkm__money { display: inline-flex; align-items: center; gap: .1em; min-width: 5.5em; justify-content: flex-end; }
.mkm__money .chip { font-size: var(--fs-lg); }
.mkm__money.is-in { color: var(--green); }
.mkm__money.is-quiet { opacity: .6; }
.mkm__sign { font-variant-numeric: tabular-nums; }
.mkm__more { justify-self: center; }
.mkm__loading { justify-self: center; padding: 2em 0; }
.mkm__none { display: grid; gap: .4em; justify-items: center; text-align: center; min-height: 12em; align-content: center; }
.mkm__none > .g { width: 3em; height: 3em; }
html.m-mobile .mkm__row, html.m-mobile .mkm__end > .btn { min-height: var(--touch); }
@container marketplace (max-width: 48em) {
  .market .win__foot { flex-wrap: wrap; }
}
@media (prefers-reduced-motion: reduce) {
  .mks__caret { transition: none; }
}

/* ---------------------------------------------------------------- the Buy list's pager (legacy.ts)
   Twenty rows a page (owner, 2026-10-14), in the window's foot at the end of the purses' row so it
   is always in sight: the mail's arrows round "n / N". */
.mkl__pager { display: flex; align-items: center; gap: .6em; flex: none; } /* after the note, whose auto margin pushes both right */
.mkl__pager[hidden] { display: none; }
.mkl__pager-at { min-width: 4em; text-align: center; color: var(--ink-mute); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

/* The Exchange's shelf tree says how many listings stand on every line (owner, 2026-10-14). */
.mks__cats--count .mks__cat { grid-template-columns: minmax(0, 1fr) auto auto; }
.mks__catn { color: var(--ink-mute); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.mks__cat.is-on > .mks__catn { color: var(--ink); }

/* ---- w-partyask.css */
/* THE PARTY'S QUESTION (panels/party.ts): a team invitation or a duel challenge, `.win.pty-ask
   [data-win=partyask]`, a popup in the one window stack (placed centred by gui/win.ts). The
   inviter's face beside the sentence when this client has seen them; the countdown and how many
   more wait behind it on the left of the foot, Refuse and Agree on the right. */
.pty-ask__row { display: grid; grid-template-columns: auto 1fr; gap: .8em; align-items: center; min-width: 0; }
.pty-ask__face[hidden] { display: none; }
.pty-ask__who { min-width: 0; overflow-wrap: anywhere; }
.pty-ask .win__body { display: grid; gap: .7em; }
.pty-ask__ttl { margin-right: auto; font-variant-numeric: tabular-nums; }
.pty-ask__more[hidden] { display: none; }

/* ---- w-pets.css */
/* THE PET WINDOWS (pets.ts `.win.petw[data-win=pets]`, panels/pet-room.ts `.win.penw[data-win=petroom]`
   and the pen body `.pen` it shares with the Pet Dealer's first tab, panels/pet-sheet.ts
   `.win.psw[data-win=petsheet]`). Placed by gui/win.ts: Pet Slot docks right, the pen and the
   sheet stand beside it (B234). 20-components.css draws every part (row, tile, band, bar, stat, well);
   this sheet lays them out. */

/* ---------------------------------------------------------------- PET SLOT */
.petw__body { display: grid; gap: .6em; align-content: start; }
.petw .band .tabs { margin-left: auto; }
.petw__row { cursor: var(--cur-hand, pointer); min-height: 4.2em; }
.petw__row.is-shut { cursor: var(--cur-arrow, default); }
.petw__row.is-over { background: var(--drop-on-bg); box-shadow: inset 0 0 0 1px var(--drop-on-rim); border-radius: .35em; }
.petw__row.is-carried { opacity: .4; }
.petw__sub { display: grid; gap: .15em; }
.petw__line { display: flex; align-items: center; gap: .5em; }
.petw__lv { font-variant-numeric: tabular-nums; }
/* A vital: its painted mark, then the thin bar; the figures are on the pet's card. */
.petw__vital { display: grid; grid-template-columns: 1em minmax(0, 1fr); gap: .35em; align-items: center; }
.petw__vic { width: 1em; height: 1em; object-fit: contain; }
.petw__end { display: inline-flex; align-items: center; gap: .4em; }
/* Positions are icons only (RULES): the word is on each icon's card. */
.petw__pos .tabs__tab { padding: .15em .25em; min-width: 0; }
.petw__pos .pos-ic { width: 1.5em; height: 1.5em; object-fit: contain; }
.petw__pos .tabs__tab:not(.is-on) .pos-ic { opacity: .45; }

/* ---------------------------------------------------------------- THE PEN */
.pen { display: grid; gap: .6em; align-content: start; min-width: 0; }
.pen__grid { --cols: 6; justify-content: start; }
.pen__pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .6em; align-items: center; }
.pen__who { display: grid; gap: .1em; min-width: 0; }
.pen__who > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pen__verbs { display: inline-flex; gap: .35em; }
/* The pen's foot goes into its host's foot: the line takes the room, the two buttons stay whole. */
.pen__foot { display: contents; }
.pen__foot > .note-line { flex: 1; min-width: 0; }
.pen__acts { display: inline-flex; gap: .4em; margin-left: auto; flex: none; }

/* ---------------------------------------------------------------- PET INFORMATION */
.psw__body { display: grid; gap: .7em; align-content: start; }
.psw__grid { display: grid; grid-template-columns: 16em minmax(0, 1fr); gap: .9em; align-items: start; }
.psw__id { display: grid; gap: .5em; align-content: start; justify-items: center; min-width: 0; }
.psw__id > * { width: 100%; }
.psw__id > .portrait { width: 4.6em; } /* its own size back: `.psw__id > *` stretches, and `auto` left the absolutely placed picture a 0-wide well */
.psw__title { display: flex; align-items: center; gap: .4em; }
.psw__name { font-family: var(--face-ui); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.psw__mood { display: flex; align-items: center; gap: .35em; }
.psw__happy { display: inline-flex; gap: .1em; margin-left: auto; }
.psw__happy > .g { width: 1em; height: 1em; }
.psw__main { display: grid; gap: .6em; align-content: start; min-width: 0; }
.psw__page { display: grid; gap: .6em; align-content: start; min-width: 0; }
/* Stat rows: the painted icon (the word on its card), the value, the growth band, the score. */
.psw .stat, .psw .stats__head { grid-template-columns: 1.4em minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, .7fr); }
.psw .stats__head span:first-child { grid-column: auto; }
.psw__sic { width: 1.3em; height: 1.3em; object-fit: contain; }
.psw__val { min-width: 0; }
.psw__combat { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 1em; max-height: 8.5em; }
.psw__acts { display: inline-flex; align-items: center; gap: .4em; }
.psw__bandhead { display: flex; align-items: center; gap: .5em; }
.psw__bandhead > .pill { margin-left: auto; }
.psw .win__foot { flex-wrap: wrap; row-gap: .3em; }
.psw__tier { margin-left: auto; }

/* DESKTOP: the sheet ends above the hotbar's band, its body scrolling under the head. */
html.m-desktop .wins > .win.psw { max-height: calc(.92 * var(--frame-h) - 88px * var(--hud-z, 1)); }

/* MOBILE MODE: Pet Slot and the pen are two half sheets side by side, so a pet drags straight across;
   the sheet takes the width, its identity column narrower. Rows and buttons keep a finger's height. */
html.m-mobile .wins > .win.petw, html.m-mobile .wins > .win.penw { width: calc(.5 * var(--frame-w) - 12px); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .wins > .win.psw { width: min(calc(var(--frame-w) - 16px), 46em); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .petw__row { min-height: var(--touch); }
html.m-mobile .petw__pos .tabs__tab, html.m-mobile .pen__acts > .ibtn, html.m-mobile .pen__verbs > .ibtn { min-width: var(--touch); min-height: var(--touch); }
html.m-mobile .pen__grid { --cols: 4; }
/* A phone is ~390 css px tall: the identity column narrows, the portrait shrinks, and the stat rows
   drop the desktop's band height so Level, the three bars and the five attributes fit without
   scrolling. Rows stay whole; nothing is hidden. */
html.m-mobile .psw__grid { grid-template-columns: 10.5em minmax(0, 1fr); gap: .6em; }
html.m-mobile .psw__id { gap: .35em; }
html.m-mobile .psw__id > .portrait { font-size: 1.1em; }
html.m-mobile .psw__body, html.m-mobile .psw__main, html.m-mobile .psw__page { gap: .4em; }
html.m-mobile .psw__stats { gap: .25em; }
html.m-mobile .psw .stat { min-height: 2.1em; padding-block: .1em; border-radius: .5em; }
html.m-mobile .psw .stat .bar { height: 1.15em; }
html.m-mobile .psw__combat { max-height: 7em; }

/* ---- w-quest.css */
/* THE NPC CONVERSATION (dialog-panel.ts, `.win.dlg[data-win=dialog]`) AND THE QUEST LOG (quest.ts,
   `.win.ql[data-win=quests]`), and the quest sheet's links. Placed by gui/win.ts; this sheet lays
   them out. 20-components.css draws every part (well, item row, tile, kv, tabs, pin, the .qs sheet). */

/* ---------------------------------------------------------------- the links
   ONE GESTURE PER LINK (PLAN 1.4), and each kind looks like what it does before it is hovered: a
   name with the walking boots walks there and talks; a creature with the map mark opens the world
   map on it. A dotted rule at rest, solid on hover. A name somewhere else in the world is not a
   walk: plain, muted, its map on its card. */
.qlink {
  display: inline-flex; align-items: center; gap: .2em; vertical-align: bottom;
  color: var(--link-ink); cursor: var(--cur-hand, pointer);
  text-decoration: underline dotted; text-underline-offset: .18em; text-decoration-thickness: from-font;
}
.qlink:hover { text-decoration-style: solid; }
.qlink__ic { width: 1.15em; height: 1.15em; object-fit: contain; flex: none; }
.qlink--map { color: var(--orange); }
.qlink.is-dim { color: var(--ink-mute); text-decoration: none; cursor: var(--cur-arrow, default); }
.kv__npc.is-dim .pin { opacity: .45; }

/* The objective row: the sheet's own rules color it (20-components.css .qs); its picture is a painting. */
.qs .item-row__pic > .g { width: 1.3em; height: 1.3em; object-fit: contain; display: block; }

/* ---------------------------------------------------------------- the conversation */
.dlg__body { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .8em; align-items: start; }
.dlg__body.is-bare { grid-template-columns: minmax(0, 1fr); }
.dlg__side[hidden] { display: none; }
.dlg__bust { display: block; }
.dlg__main { display: grid; gap: .6em; min-width: 0; align-content: start; }
.dlg__main > [hidden] { display: none; }

/* The speech: a press turns the page while there is one; the chevron at its corner says so. */
.dlg__speech { position: relative; white-space: pre-line; }
.dlg__speech.is-more { padding-right: 2em; cursor: var(--cur-hand, pointer); }
.dlg__more { position: absolute; right: .55em; bottom: .55em; width: 1.1em; height: 1.1em; object-fit: contain; }
.dlg__more[hidden] { display: none; }

/* A choice row: the kind's painting, the words, the way on (or the ride's level). */
.dlg__rows .item-row, .dlg__qpick .item-row { cursor: var(--cur-hand, pointer); }
.dlg__ic { width: 1.6em; height: 1.6em; object-fit: contain; display: block; }
.dlg__go { width: 1em; height: 1em; object-fit: contain; opacity: .7; }
.dlg__rows .item-row.is-dim { opacity: .55; }
.dlg__note { white-space: pre-line; }

/* ---------------------------------------------------------------- the quest log */
.ql__body { display: grid; gap: .6em; align-content: start; }
/* Two columns that scroll on their own, as tall as the screen allows and no taller (B214: a fixed
   height ran off a phone). */
.ql__cols {
  display: grid; grid-template-columns: 20em minmax(0, 1fr); gap: .8em;
  height: min(30.5em, calc(var(--frame-h) - 12em));
}
.ql__titles { align-content: start; min-height: 0; overflow-y: auto; }
.ql__titles > .cap { padding: .5em .5em .15em; }
.ql__titles .item-row { cursor: var(--cur-hand, pointer); }
.ql__titles .item-row__pic > .pill { white-space: nowrap; }
.ql__titles .item-row__end > .tag[hidden] { display: none; }
.ql__detail { display: grid; gap: .55em; align-content: start; min-height: 0; overflow-y: auto; }
.ql__detail > [hidden], .ql__quest > [hidden] { display: none; }
.ql__quest { display: grid; gap: .55em; align-content: start; min-width: 0; }
.ql__empty { display: grid; justify-items: center; padding: 1em 0; }
.ql__mob { font-size: var(--fs-lg); }
.ql__wanted { display: grid; }
.ql__goto > .g { width: 1.3em; height: 1.3em; object-fit: contain; }
/* Why a verb is dark, said where a finger can read it. */
.ql__why { color: var(--ink-mute); font-size: var(--fs-sm); white-space: pre-line; }

/* The foot: four verbs, each its painting and its word (the word on its card in mobile mode). */
.ql .win__foot .btn { display: inline-flex; align-items: center; gap: .35em; }
.ql__vi { width: 1.25em; height: 1.25em; object-fit: contain; }
.ql .win__foot .btn[hidden] { display: none; }

/* A BUTTON THAT IS READY, LIT THE WAY A PREMIUM REWARD BOX IS. Quest's Complete Now wears it the
   moment the server says the quest would be accepted; it is dark the rest of the time and a dark
   button with a light running round it would be a lie.

   The light rides the button's own RIM layer - `background` on .btn is the rim under the face its
   ::before paints - so nothing new is stacked and only the 1px line round the face shows it. The angle is a
   registered custom property because a conic gradient cannot be rotated by `transform` without
   turning the element itself. Its inks are the --btn-charge-* tokens. */
@property --btn-orbit {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.btn--charged {
  background:
    conic-gradient(from var(--btn-orbit),
      var(--btn-charge-hot) 0 3%,
      var(--btn-charge) 9%,
      var(--btn-charge-dim) 18%,
      transparent 30% 50%,
      var(--btn-charge-soft) 55%,
      transparent 64% 100%),
    var(--btn-rim);
  animation: btn-charge 2.6s linear infinite;
}
@keyframes btn-charge { to { --btn-orbit: 360deg; } }
@media (prefers-reduced-motion: reduce) { .btn--charged { animation: none; } }

/* ---------------------------------------------------------------- mobile mode */
html.m-mobile .dlg__side { font-size: .75em; }
html.m-mobile .ql__cols { grid-template-columns: 13em minmax(0, 1fr); gap: .5em; height: calc(var(--frame-h) - 9em); }
html.m-mobile .ql__vl { display: none; }
html.m-mobile .ql .win__foot .btn { min-width: 2.6em; justify-content: center; }

/* ---- w-sanctum.css */
/* THE SKYWARD SANCTUM (panels/sanctum.ts `.win.snw[data-win=sanctum]`, vault note 263). Laid out
   after the owner's key art (2026-10-17) in the game's own look, Midnight Astrolabe, not its violet:
   a starlit title, the series in a column, and the picked series as an ASTROLABE - brass rings, the
   paw at the hub, the pets as medallions round the ring - over two brass-framed cards and an open
   tome. Starlight (--sky) is the glow; brass the metal. Paint only: CSS rings and gradients, the
   existing pet heads and paw painting; no new textures. Desktop: the column beside the stage.
   Mobile: the column becomes a strip; the astrolabe stands beside the cards. */

.wins > .win.snw > .win__body { padding: .6em .9em .8em; }
.snw [hidden] { display: none !important; }
/* The big title below says it; the head keeps only drag and close. */
.wins > .win.snw > .win__head > .win__title { opacity: 0; }

/* ---------------------------------------------------------------- the title */
.pc { display: grid; gap: .7em; }
.sn__hero { display: grid; justify-items: center; gap: .15em; text-align: center; padding-bottom: .5em; position: relative; }
.sn__hero::after {                                         /* the ornament rule under the tagline */
  content: ""; width: 60%; height: 1px; margin-top: .45em;
  background: linear-gradient(90deg, transparent, rgba(var(--brass-rgb), .7) 30%, var(--brass-hi) 50%, rgba(var(--brass-rgb), .7) 70%, transparent);
}
.sn__title {
  margin: 0; display: flex; align-items: center; gap: .55em;
  font: var(--face-display-weight) calc(var(--fs-title) * 1.55) / 1.1 var(--face-display); letter-spacing: .12em; text-transform: uppercase;
  background: linear-gradient(180deg, var(--brass-pale), var(--brass-hi) 45%, var(--brass) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 .5em rgba(var(--sky-rgb), .35)) drop-shadow(0 1px 0 rgba(0, 0, 0, .8));
}
/* A four-point star either side of the title. */
.sn__spark { width: .55em; height: .55em; background: var(--brass-hi); clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); filter: drop-shadow(0 0 .3em rgba(var(--sky-rgb), .9)); }
.sn__tagline { margin: 0; font-size: var(--fs-small); color: var(--sky); opacity: .85; letter-spacing: .03em; }

.sn__body { display: grid; grid-template-columns: 13em minmax(0, 1fr); gap: 1em; align-items: start; }

/* ---------------------------------------------------------------- the series column */
.sn__series {
  display: grid; gap: .4em; padding: .55em .45em; border-radius: .4em;
  background: rgba(var(--navy-0-rgb), .55); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .3);
}
.sn__series-title { margin: 0; text-align: center; font: var(--face-display-weight) var(--fs-small) / 1.2 var(--face-display); letter-spacing: .16em; text-transform: uppercase; color: var(--brass-hi); }
.sn__index { display: flex; flex-direction: column; gap: .1em; max-height: 31em; overflow-y: auto; scrollbar-color: var(--scroll-thumb) transparent; }
.sn__mark {
  position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .4em;
  width: 100%; min-height: 2.1em; padding: .25em .5em .25em .9em; text-align: left;
  border: 0; border-radius: .3em; background: transparent; color: var(--ink-mute);
  font: var(--face-weight) var(--fs-small) / 1.2 var(--face-ui); cursor: var(--cur-hand, pointer);
}
.sn__mark-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sn__mark-n { font-size: var(--fs-badge); font-variant-numeric: tabular-nums; color: var(--ink-faint); }
.sn__mark:hover { background: rgba(var(--sky-rgb), .07); color: var(--ink-strong); }
.sn__mark.is-on { background: linear-gradient(90deg, rgba(var(--brass-rgb), .22), rgba(var(--brass-rgb), .06)); color: var(--brass-pale); box-shadow: inset 2px 0 0 var(--brass-hi); }
.sn__mark.is-on .sn__mark-n, .sn__mark.is-done .sn__mark-n, .sn__mark.is-done .sn__mark-name { color: var(--brass-hi); }
.sn__mark.is-done::after { content: ""; position: absolute; right: .25em; top: .3em; width: .4em; height: .4em; background: var(--brass-hi); clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); }
.sn__mark-dot { position: absolute; left: .3em; top: 50%; width: .42em; height: .42em; margin-top: -.21em; border-radius: 50%; background: var(--moss-400); box-shadow: 0 0 .4em rgba(78, 145, 48, .8); }
/* A group heading in the column: Common Pets, then the Rare Collection in violet. */
.sn__group { display: flex; align-items: center; gap: .45em; margin: .5em .3em .15em; font: var(--face-display-weight) var(--fs-badge) / 1.2 var(--face-display); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.sn__group:first-child { margin-top: .1em; }
.sn__group::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(var(--brass-rgb), .4), transparent); }
.sn__group--rare { color: var(--world-q-purple); }
.sn__mark:focus-visible { outline: 2px solid var(--brass-hi); outline-offset: -2px; }

/* ---------------------------------------------------------------- the astrolabe */
.sn__stage { display: grid; gap: .9em; min-width: 0; }
.sn__orrery {
  --orb: 21em; --med: 3.7em; --r: calc(var(--orb) / 2 - var(--med) / 2 - .35em);
  position: relative; width: var(--orb); height: var(--orb); justify-self: center;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--sky-rgb), .18), rgba(var(--sky-rgb), .05) 38%, transparent 70%);
}
.sn__ring { position: absolute; border-radius: 50%; pointer-events: none; }
.sn__ring--outer { inset: 0; border: 2px solid rgba(var(--brass-rgb), .65); box-shadow: 0 0 1em rgba(var(--sky-rgb), .25), inset 0 0 1em rgba(var(--sky-rgb), .15); }
/* The mid ring wears the astrolabe's degree ticks. */
.sn__ring--mid {
  inset: 17%;
  background: repeating-conic-gradient(from 0deg, rgba(var(--brass-pale-rgb), .55) 0 .6deg, transparent .6deg 10deg);
  -webkit-mask: radial-gradient(circle, transparent calc(50% - .5em), #000 calc(50% - .5em) 50%, transparent 50%);
  mask: radial-gradient(circle, transparent calc(50% - .5em), #000 calc(50% - .5em) 50%, transparent 50%);
}
.sn__ring--mid::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1px solid rgba(var(--brass-rgb), .45); }
.sn__ring--inner { inset: 31%; border: 1px dashed rgba(var(--brass-pale-rgb), .45); }
/* The cross of the astrolabe: four faint rays from the hub, starlit at the tips. */
.sn__cross {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(90deg, transparent calc(50% - .5px), rgba(var(--sky-rgb), .35) calc(50% - .5px) calc(50% + .5px), transparent calc(50% + .5px)),
    linear-gradient(0deg, transparent calc(50% - .5px), rgba(var(--sky-rgb), .35) calc(50% - .5px) calc(50% + .5px), transparent calc(50% + .5px));
  -webkit-mask: radial-gradient(circle, #000 48%, transparent 50%);
  mask: radial-gradient(circle, #000 48%, transparent 50%);
}
.sn__orrery.is-done .sn__ring--outer { border-color: var(--brass-hi); box-shadow: 0 0 1.6em rgba(var(--brass-hi-rgb), .45), inset 0 0 1.2em rgba(var(--brass-hi-rgb), .25); }

/* The hub: the paw, the series and its count. */
.sn__hub {
  position: absolute; left: 50%; top: 50%; width: 7.4em; height: 7.4em; translate: -50% -50%;
  display: grid; place-content: center; justify-items: center; gap: .1em; text-align: center; border-radius: 50%;
  background: radial-gradient(circle at 50% 35%, var(--navy-3), var(--navy-0) 75%);
  box-shadow: 0 0 0 2px rgba(var(--brass-rgb), .7), 0 0 0 5px rgba(var(--navy-0-rgb), .8), 0 0 0 6px rgba(var(--brass-rgb), .35), 0 0 1.4em rgba(var(--sky-rgb), .35);
}
.sn__paw { width: 2.3em; height: 2.3em; object-fit: contain; filter: drop-shadow(0 0 .4em rgba(var(--sky-rgb), .8)); }
.sn__hub-name { max-width: 6.2em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-badge); color: var(--brass-pale); }
.sn__hub-count { font: var(--face-display-weight) var(--fs-heading) / 1 var(--face-display); color: var(--brass-hi); font-variant-numeric: tabular-nums; }

/* The medallions, round the ring at --a, the first at the top. */
.sn__meds { position: absolute; inset: 0; }
.sn__med {
  position: absolute; left: 50%; top: 50%; width: var(--med); height: var(--med); padding: 0;
  transform: translate(-50%, -50%) rotate(var(--a)) translateY(calc(-1 * var(--r))) rotate(calc(-1 * var(--a)));
  border-radius: 50%; border: 2px solid rgba(var(--brass-rgb), .7); cursor: var(--cur-hand, pointer);
  background: radial-gradient(circle at 50% 35%, var(--navy-3), var(--navy-0) 80%);
  box-shadow: 0 0 .7em rgba(var(--sky-rgb), .3), inset 0 0 .6em rgba(0, 0, 0, .6);
  display: grid; place-items: center; overflow: visible; transition: box-shadow .15s, border-color .15s;
}
.sn__med > .ico { width: 82%; height: 82%; border-radius: 50%; }
.sn__med.is-empty > .ico { filter: grayscale(1) brightness(.4); opacity: .85; }
.sn__med.is-empty { border-style: dashed; border-color: rgba(var(--brass-rgb), .45); box-shadow: inset 0 0 .6em rgba(0, 0, 0, .6); }
.sn__med.is-ready > .ico { filter: grayscale(.5) brightness(.75); opacity: 1; }
.sn__med-dot { display: none; position: absolute; right: .05em; top: .05em; width: .55em; height: .55em; border-radius: 50%; background: var(--moss-400); box-shadow: 0 0 .4em rgba(78, 145, 48, .9); }
.sn__med.is-ready .sn__med-dot { display: block; }
.sn__med[data-rarity] { border-style: solid; }
.sn__med[data-rarity="green"] { border-color: var(--world-q-green); box-shadow: 0 0 .9em rgba(134, 212, 95, .5); }
.sn__med[data-rarity="blue"] { border-color: var(--world-q-blue); box-shadow: 0 0 .9em rgba(99, 184, 255, .5); }
.sn__med[data-rarity="yellow"] { border-color: var(--world-q-yellow); box-shadow: 0 0 .9em rgba(255, 224, 102, .5); }
.sn__med[data-rarity="gold"] { border-color: var(--world-q-gold); box-shadow: 0 0 .9em rgba(255, 161, 74, .55); }
.sn__med[data-rarity="purple"] { border-color: var(--world-q-purple); box-shadow: 0 0 1em rgba(201, 139, 255, .6); }
.sn__med.is-on { outline: 2px solid var(--sky); outline-offset: 3px; }
.sn__med:hover { box-shadow: 0 0 1.1em rgba(var(--sky-rgb), .55); }
.sn__med:focus-visible { outline: 2px solid var(--brass-hi); outline-offset: 3px; }

/* ---------------------------------------------------------------- under the ring: set effect + deposit */
.sn__cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8em; align-items: start; }
.sn__card {
  position: relative; display: grid; align-content: start; gap: .45em; min-width: 0; padding: .7em .85em;
  border-radius: .35em; background: linear-gradient(160deg, rgba(var(--navy-3-rgb), .9), rgba(var(--navy-1-rgb), .95));
  box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .55), 0 .3em .9em rgba(0, 0, 0, .45);
}
/* Brass corner brackets, top-left and bottom-right, as the game's windows wear them. */
.sn__card::before, .sn__card::after { content: ""; position: absolute; width: .9em; height: .9em; pointer-events: none; }
.sn__card::before { left: -2px; top: -2px; border-left: 2px solid var(--brass-hi); border-top: 2px solid var(--brass-hi); }
.sn__card::after { right: -2px; bottom: -2px; border-right: 2px solid var(--brass-hi); border-bottom: 2px solid var(--brass-hi); }
.sn__card-title { margin: 0; display: flex; align-items: baseline; gap: .6em; padding-bottom: .35em; border-bottom: 1px solid rgba(var(--brass-rgb), .3); font: var(--face-display-weight) var(--fs-small) / 1.2 var(--face-display); letter-spacing: .08em; color: var(--brass-hi); }
.sn__card-state { margin-left: auto; font: var(--face-weight) var(--fs-badge) / 1.2 var(--face-ui); letter-spacing: 0; color: var(--ink-faint); }

/* The set effect: the series' bonus at each grade. Faint until the series is complete; then the
   grade it stands at is lit and the card glows. */
.sn__fx-lines { display: grid; gap: .2em; }
.sn__fx-line { display: grid; grid-template-columns: 5.5em minmax(0, 1fr); gap: .5em; align-items: baseline; padding: .2em .35em; border-radius: .25em; font-size: var(--fs-small); opacity: .65; }
.sn__fx-line b { color: var(--ink-strong); font-weight: 600; font-variant-numeric: tabular-nums; }
.sn__fx-line.is-on { opacity: 1; background: rgba(var(--brass-rgb), .16); box-shadow: inset 2px 0 0 var(--brass-hi); }
.sn__card--fx.is-on { box-shadow: inset 0 0 0 1px var(--brass-hi), 0 0 1.2em rgba(var(--brass-hi-rgb), .35); }
.sn__card--fx.is-on .sn__card-state { color: var(--moss-400); }
.sn__card--fx.is-on .sn__fx-line:not(.is-on) { opacity: .45; }
.sn__grade-ink--genius { color: var(--world-q-yellow); }
.sn__grade-ink--supreme { color: var(--world-q-gold); }
.sn__grade-ink--perfect { color: var(--world-q-purple); }

/* The deposit card: the picked slot's name with Take out beside it, your pets, the entry bar. */
.sn__pick-head { display: flex; align-items: center; gap: .6em; }
.sn__pick-head:has(> .sn__pick-name[hidden]) { display: none; }
.sn__pick-name { flex: 1; min-width: 0; font-size: var(--fs-heading); color: var(--ink-strong); }
.sn__rule { margin: 0; font-size: var(--fs-badge); font-style: italic; color: var(--ink-faint); }
.sn__pick-list { min-width: 0; }
.sn__cands { display: grid; gap: .35em; max-height: 8.5em; overflow-y: auto; }
.sn__cand { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: .3em .5em; }
.sn__cand > :last-child { grid-column: 1 / -1; justify-self: start; }   /* Deposit under the pet: a page is narrow */
.sn__cand-who { display: grid; gap: .05em; min-width: 0; }
.sn__cand-who b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sn__cand-who small { font-size: var(--fs-badge); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.sn__kept { display: grid; gap: .2em; }
.sn__cand.is-short > .tile, .sn__cand.is-short .sn__cand-who { opacity: .5; }
.sn__short { font-size: var(--fs-badge); color: var(--ink-faint); white-space: nowrap; }

/* ---------------------------------------------------------------- total bonus: the radar and the totals */
/* The column's first line: the total, set apart from the series by a rule under it. */
.sn__mark--total { margin-bottom: .3em; color: var(--brass-pale); box-shadow: inset 0 -1px 0 rgba(var(--brass-rgb), .3); }
/* The orrery stands where the astrolabe stands: faint dust, two brass orbits, the sun at the centre
   (the total, brighter with each complete series: --lit 0..1), and a planet per series, bigger the
   more pets the series holds. Unlit, a planet is slate; a complete series paints its planet in its
   grade's colour (the gradient is set by sanctum.ts) and glows. Rare planets wear a ring. */
.sn__overview { min-width: 0; }
/* On the Total Bonus page the stage is one column, the orrery across it, and the cards row under it
   holds the summary (narrow) beside the totals (wide). */
.sn__stage:has(> .sn__overview:not([hidden])) { grid-template-columns: minmax(0, 1fr); }
.sn__stage:has(> .sn__overview:not([hidden])) .sn__cards { grid-template-columns: 15em minmax(0, 1fr); align-items: stretch; }
.sn__sky { width: 100%; min-width: 0; }
.sn__sky-svg { display: block; width: 100%; height: auto; }
.sn__sky-dust { fill: rgba(var(--sky-rgb), .35); }
.sn__sky-ring { fill: none; stroke: rgba(var(--brass-rgb), .25); stroke-width: .9; stroke-dasharray: 2 4; }
/* The sun's glow and ink follow the grade most complete series stand at (data-grade on .sn__sun-g). */
.sn__sun-g { --sun-rgb: 255, 210, 110; --sun-ink: #3a2405; --sun-ink-soft: #5a3a0c; }
.sn__sun-g[data-grade="supreme"] { --sun-rgb: 255, 150, 70; --sun-ink: #3d1404; --sun-ink-soft: #5c2208; }
.sn__sun-g[data-grade="perfect"] { --sun-rgb: 188, 132, 255; --sun-ink: #24093f; --sun-ink-soft: #3a1766; }
.sn__sun { filter: drop-shadow(0 0 10px rgba(var(--sun-rgb), .7)); transition: filter .6s; }
.sn__sun-halo { --lit: 0; fill: rgba(var(--sun-rgb), calc(.06 + var(--lit) * .3)); filter: blur(6px); transition: fill .6s; }
.sn__sun-count { fill: var(--sun-ink); font: 700 17px var(--face-display); }
.sn__sun-label { fill: var(--sun-ink-soft); font: 700 6.5px var(--face-ui); letter-spacing: .04em; text-transform: uppercase; }
.sn__planet { cursor: var(--cur-hand, pointer); outline: none; --con: var(--sky); }
.sn__planet[data-grade="genius"] { --con: var(--world-q-yellow); }
.sn__planet[data-grade="supreme"] { --con: var(--world-q-gold); }
.sn__planet[data-grade="perfect"] { --con: var(--world-q-purple); }
.sn__planet-body { stroke: rgba(var(--sky-rgb), .3); stroke-width: .8; transition: filter .4s; }
.sn__planet-ring { fill: none; stroke: rgba(var(--sky-rgb), .35); stroke-width: 1.6; }
.sn__planet-name { fill: var(--ink-faint); font: 600 11px var(--face-ui); letter-spacing: .02em; transition: fill .4s; }
.sn__planet.is-lit .sn__planet-body { stroke: var(--con); filter: drop-shadow(0 0 7px var(--con)); }
.sn__planet.is-lit .sn__planet-ring { stroke: var(--con); }
.sn__planet.is-lit .sn__planet-name { fill: var(--con); }
.sn__planet:hover .sn__planet-name, .sn__planet:focus-visible .sn__planet-name { fill: var(--brass-pale); }
.sn__planet:hover .sn__planet-body { stroke: rgba(var(--sky-rgb), .7); }

/* The totals: a line per stat, its bar against the rim. */
.sn__tot-lines { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3em 1.2em; }
.sn__tot-line { display: grid; grid-template-columns: minmax(0, 8.5em) minmax(2em, 1fr) auto; align-items: center; gap: .55em; font-size: var(--fs-small); opacity: .6; }
.sn__tot-line.is-on { opacity: 1; }
.sn__tot-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-mute); }
.sn__tot-bar { display: block; height: .45em; border-radius: 999px; background: rgba(var(--navy-0-rgb), .9); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .3); overflow: hidden; }
.sn__tot-fill { display: block; height: 100%; transform-origin: 0 50%; background: linear-gradient(90deg, rgba(var(--sky-rgb), .5), var(--sky)); box-shadow: 0 0 .5em rgba(var(--sky-rgb), .6); transition: transform .3s ease-out; }
.sn__tot-val { color: var(--ink-strong); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.sn__tot-val small { color: var(--ink-faint); font-weight: 400; }

/* The summary: how many sets are complete, and at which grade. */
.sn__sum { display: grid; gap: .5em; }
.sn__sum-big { font: var(--face-display-weight) calc(var(--fs-title) * 1.4) / 1 var(--face-display); color: var(--brass-hi); }
.sn__sum-big small { font-size: var(--fs-heading); color: var(--ink-faint); }
.sn__sum-grades { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: .35em; }
.sn__sum-grade { display: grid; justify-items: center; gap: .1em; padding: .3em .2em; border-radius: .25em; background: rgba(0, 0, 0, .22); box-shadow: inset 0 0 0 1px rgba(var(--brass-rgb), .2); font-size: var(--fs-badge); }
.sn__sum-grade b { font-size: var(--fs-heading); color: var(--ink-strong); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- MOBILE */
:where(html.m-mobile) .wins > .win.snw > .win__body { padding: .4em .55em .6em; }
:where(html.m-mobile) .pc { gap: .4em; }
:where(html.m-mobile) .sn__hero { display: none; }                    /* the head says the title; the height is short */
:where(html.m-mobile) .wins > .win.snw > .win__head > .win__title { opacity: 1; }
:where(html.m-mobile) .sn__body { grid-template-columns: minmax(0, 1fr); gap: .5em; }
:where(html.m-mobile) .sn__series { padding: .3em; }
:where(html.m-mobile) .sn__series-title { display: none; }
:where(html.m-mobile) .sn__index { flex-direction: row; gap: .3em; max-height: none; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
:where(html.m-mobile) .sn__mark { flex: none; width: auto; min-height: var(--touch); max-width: 13em; padding: .2em .8em .2em 1em; }
:where(html.m-mobile) .sn__group { flex: none; margin: 0 .1em 0 .4em; align-self: center; }
:where(html.m-mobile) .sn__group::after { content: none; }
:where(html.m-mobile) .sn__stage { grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: .6em; }
:where(html.m-mobile) .sn__cards { grid-template-columns: minmax(0, 1fr); gap: .5em; }
:where(html.m-mobile) .sn__card { padding: .5em .7em; }
:where(html.m-mobile) .sn__orrery { --orb: 15.5em; --med: 2.9em; }
:where(html.m-mobile) .sn__tot-lines { grid-template-columns: minmax(0, 1fr); }
:where(html.m-mobile) .sn__tot-line { grid-template-columns: minmax(0, 7em) minmax(0, 1fr) auto; }
:where(html.m-mobile) .sn__hub { width: 5.6em; height: 5.6em; }
:where(html.m-mobile) .sn__paw { width: 1.7em; height: 1.7em; }
:where(html.m-mobile) .sn__cand .btn { min-height: var(--touch); }
:where(html.m-mobile) .sn__stage:has(> .sn__overview:not([hidden])) .sn__cards { grid-template-columns: 12.5em minmax(0, 1fr); }

/* ---- w-settings.css */
/* SETTINGS (panels/settings.ts, `.win.optw[data-win=settings]`, P31): the page strip over one body,
   a page of wells, each well a stack of lines (what it is, then its control at the end); sliders run
   the full width under their line. No fixed height: a page is as tall as its switches and the body
   scrolls inside the window (B258). Placed by gui/win.ts; moved from 25-card.css. Tokens only. */
.optw [hidden] { display: none !important; }
.wins > .win.optw > .win__body { display: grid; gap: .7em; align-content: start; min-width: 0; }

/* Five page names in one row at any size: the text strip's 7em floor would push the fifth out. */
.optw .win__body > .tabs--text { flex-wrap: wrap; }
.optw .win__body > .tabs--text .tabs__tab { min-width: 0; padding: 0 .9em; }

.optw__page { display: grid; gap: .7em; align-content: start; min-width: 0; }
.optw__sect { display: grid; grid-template-columns: minmax(0, 1fr); gap: .5em; }
.optw__line { display: flex; align-items: center; gap: .6em; min-width: 0; }
.optw__line > :first-child { flex: 1; min-width: 0; }
.optw__line > .tabs { flex: none; }
.optw__stack { display: grid; gap: .35em; min-width: 0; }
.optw__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.optw__num { flex: none; min-width: 3.2em; text-align: right; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.optw__range { width: 100%; margin: .2em 0 .4em; }
.optw__range:disabled { opacity: .5; cursor: var(--cur-arrow); }

/* The Auto Sell ladder: each band wears its quality's dot before its word, so White reads as a band
   and not as a pale label (B258). */
.optw__q { display: inline-block; width: .6em; height: .6em; margin-right: .35em; border-radius: 50%; background: var(--ink); vertical-align: .05em; }
.optw__q[data-q="green"] { background: var(--world-q-green); }
.optw__q[data-q="blue"] { background: var(--world-q-blue); }
.optw__q[data-q="yellow"] { background: var(--world-q-yellow); }
.optw__q[data-q="gold"] { background: var(--world-q-gold); }
.tabs__tab:disabled > .optw__q { opacity: .5; }

/* Controls: one line per action, its key caps at the end (one or two). */
.optw__keys { display: grid; gap: .35em; }
.optw__caps { display: inline-flex; gap: .35em; flex: none; }
.optw__cap { min-width: 3.2em; font-variant-numeric: tabular-nums; }
.optw__end { display: flex; justify-content: flex-end; }

/* The foot: the community's round painted buttons and the build, then Character selection flush
   right, level with the buttons' left inset (the crest sits below the foot's line, so no room
   is kept for it here). */
.wins > .win.optw > .win__foot { padding-right: 1.2em; }
.optw__links { display: inline-flex; align-items: center; gap: .4em; margin-right: auto; }
.optw__ver { margin-left: .5em; font-size: var(--fs-sm); color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap; }
.optw__social > img.g { width: 1.15em; height: 1.15em; object-fit: contain; }
body.play .optw__social { cursor: var(--cur-hand); }

html.m-mobile .optw__line { gap: .4em; }

/* ---- w-shop.css */
/* THE NPC COUNTER (panels/shop.ts, `.win.shop[data-win=shop]`, P20): the counter's band, an event's
   window, its shelves, the stock in two columns with the pager, the barter well, the buyback well, and
   the pick (tile, name, price, the verb, a refusal under it). Placed by gui/win.ts; 20-components.css draws
   every part (band, tabs, rows, tile, chip, well, button). Moved from 25-card.css, tokens only. Also the
   Repair Equipment button's painting. */

.wins > .win.shop > .win__body { display: flex; flex-direction: column; gap: .6em; }
.shop__event { display: grid; gap: .2em; }
.shop__event[hidden], .shop__tabs[hidden], .shop__stock[hidden], .shop__barter[hidden], .shop .gui-empty[hidden] { display: none; }
.shop__left { margin-left: .6em; color: var(--ink-mute); font-variant-numeric: tabular-nums; }

.shop__lists { display: grid; gap: .7em; min-width: 0; }
.shop__stock { display: grid; gap: .6em; min-width: 0; }
.shop__stock > .tabs { justify-self: start; }

/* Two columns, filled down the left then down the right: a page is six and six, a barter list half
   and half (`--rows`, shop.ts). */
.shop__cols { display: grid; grid-auto-flow: column; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(var(--rows, 6), auto); gap: 0 1.2em; align-content: start; min-width: 0; }
.shop__row { cursor: var(--cur-hand, pointer); }
/* A long name takes a second line rather than an ellipsis (B128). */
.shop__row .item-row__name b { white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow-wrap: anywhere; }
.shop__row .item-row__end .chip { font-size: var(--fs-md); }

/* What the purse or the bag cannot cover: the number in red, never the row greyed (B128). */
.shop .chip.is-short .chip__num, .shop .swap-cost__one > b.is-short { color: var(--error); }

.shop__barter { display: grid; gap: .4em; }
.shop__barhead { display: flex; justify-content: flex-end; }
.shop__holds { display: flex; flex-wrap: wrap; gap: .3em .8em; }
.shop__holds > .swap-cost__one > .ico, .swap-cost .swap-cost__one > .ico { --ico-box: 1.5em; }
.swap-cost { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .2em .5em; }

.shop .gui-empty { display: grid; justify-items: center; gap: .3em; padding: 1.4em 1em; text-align: center; }
.shop .gui-empty > img.g { width: 3em; height: 3em; object-fit: contain; }

/* The pick: tile, name and price, then the verb with its refusal under it. */
.shop__pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .4em .8em; align-items: center; }
.shop__pick[hidden], .shop__pick [hidden] { display: none; }
.shop__pick > .note-line--mute { grid-column: 1 / -1; }
.shop__pickinfo { display: grid; gap: .15em; min-width: 0; }
.shop__name { font-size: var(--fs-lg); overflow-wrap: anywhere; }
.shop__name--green { color: var(--rarity-green-text); }
.shop__name--blue { color: var(--rarity-blue-text); }
.shop__name--gold { color: var(--rarity-gold-text); }
.shop__name--yellow { color: var(--rarity-yellow-text); }
.shop__name--purple { color: var(--rarity-purple-text); }
.shop__sub { color: var(--ink-mute); font-size: var(--fs-sm); }
.shop__price .chip { font-size: var(--fs-lg); }
.shop__act { display: grid; gap: .3em; justify-items: end; max-width: 16em; }
.shop__act > .note-line { text-align: right; }

.wins > .win.shop .win__foot { gap: .6em; }
.shop__tokens { display: inline-flex; gap: .4em; }
.shop__tokens:empty { display: none; }
.shop__token > .ico { --ico-box: 1.5em; }
.shop__token[hidden], .wins > .win.shop .win__foot > .chip[hidden], .wins > .win.shop .win__foot > .btn[hidden] { display: none; }
.shop__note { margin-left: auto; min-width: 0; overflow-wrap: anywhere; }

/* A painted verb on a button (Buy, Repair Equipment: the old bench's foot wears it too). */
.btn:has(> .btn__ic) { grid-auto-flow: column; gap: .4em; }
.btn > img.g.btn__ic { width: 1.3em; height: 1.3em; object-fit: contain; flex: none; }
.btn > img.g.btn__ic[hidden] { display: none; }

/* Mobile mode: the window takes the sheet's width; the rows keep a thumb's height. */
html.m-mobile .wins > .win.shop { width: min(var(--w), .8 * var(--frame-w)); }
html.m-mobile .shop__row { min-height: var(--touch); }

/* ---- w-skills.css */
/* THE SKILL WINDOW (skillpanel.ts, `.win.spw[data-win=skills]`): Skills, two columns of rows, and
   Talents, the class's two doctrines side by side as three-lane trees. Placed by gui/win.ts; this
   sheet lays it out. 20-components.css draws every part (tabs, item row, tile, skill node, band, pill). */
.spw__body { display: grid; gap: .7em; align-content: start; }
.spw__list { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 1.2em; align-content: start; }
.spw__list[hidden], .spw__tree[hidden], .spw__foot[hidden] { display: none; }

/* The talents page is the wider of the two; it follows the tab. */
.wins > .win.spw[data-tab="belief"] { width: 52em; }
.spw__tree { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .8em; align-items: start; }
.spw__mod { display: grid; align-content: start; gap: .6em; min-width: 0; }
.spw__lanes { justify-self: center; }
/* The + is there only on a node a point can go into now (skillpanel.ts READY). */
.spw .skill > [data-add][hidden] { display: none; }
/* A refused point, said under the node that was pressed. */
.spw .skill__why { max-width: 8em; font-size: var(--fs-xs); text-align: center; }
.spw .skill__why[hidden] { display: none; }

/* The foot: the page strip, or the points in hand and spent of earned. */
.spw__foot { display: flex; align-items: center; gap: .6em; width: 100%; }
.spw__pts { display: inline-flex; align-items: center; gap: .3em; }
.spw__pts > .ic { width: 1.2em; height: 1.2em; }
.spw__pool { margin-left: auto; }

/* While a skill is carried out of it the window gets out of the way: see-through, and the pointer
   passes to the hotbar it may be covering. */
.wins > .win.spw.is-lending { opacity: .22; pointer-events: none; transition: opacity .12s; }
@media (prefers-reduced-motion: reduce) {
  .wins > .win.spw.is-lending { transition: none; }
}

/* DESKTOP: the window ends above the hotbar's band (as the Character window), the body scrolling. */
html.m-desktop .wins > .win.spw { max-height: calc(.92 * var(--frame-h) - 88px * var(--hud-z, 1)); }

/* ---- w-smartbuff.css */
/* SMART BUFF (panels/smart-buff.ts, `.win.sbw[data-win=smartBuff]`, owner 2026-10-14): the skill's
   name, how the list is read, the list in order with its up / down / remove, the people who can be
   added as small buttons, Clear and Save in the foot. 20-components.css draws the parts. */
.sbw .win__body { display: grid; gap: .55em; align-content: start; }
.sbw__skill { font-size: var(--fs-lg); color: var(--brass-hi); }
.sbw__list[hidden] { display: none; }
.sbw__n { min-width: 1.4em; text-align: center; color: var(--brass-hi); font-weight: 600; }
.sbw__acts { display: inline-flex; gap: .25em; }
.sbw__adds { display: flex; flex-wrap: wrap; gap: .4em; }
.sbw__adds:empty { display: none; }
.sbw .note-line[hidden] { display: none; }
.sbw__gap { flex: 1; }

/* ---- w-social.css */
/* THE SOCIAL WINDOW (panels/social.ts, P24): `.win.so[data-win=social]`, centred. The tab strip
   (Friends with its count and the waiting requests' mark, Blocked with its count, Daily), one page
   at a time in a scroll of its own (friends with requests on top, blocked names, the day's
   allowances in two columns), the foot's line, Messages and the page's Add / Block. 20-components.css
   draws every part; tokens only. Moved from 25-card.css (`.so`, `.so-day`). */

.wins > .win.so > .win__body { display: flex; flex-direction: column; gap: .7em; min-height: 0; }
.so .gui-empty[hidden], .so__pane[hidden], .so-day__sect[hidden], .so__wait[hidden], .so .note-line[hidden] { display: none; }

/* The strip: each tab its word, its figure, and (Friends) the requests' mark. */
.so__tabx { display: inline-flex; align-items: center; gap: .45em; }
.so__n { color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.so__n:empty { display: none; }
/* The pill sits on the lit tab, whose words carry a shadow: the figure does not. */
.so__wait { min-width: 1.8em; text-shadow: none; }

/* A page keeps one height on a desk, so switching tabs does not make the window jump. */
.so__pane { height: 18em; min-width: 0; }
.so__pane.so-day { height: min(30em, .6 * var(--frame-h)); display: grid; gap: .6em; align-content: start; }

/* A row: the head in its class ring (B186: one size for requests and friends, and inside the row's
   height). The ring hangs a little outside the well so the class still reads around the face; the
   margin is that overhang, so it never reaches the name. */
.so__row { min-height: 3.4em; }
.so .so__face { font-size: .58em; --sigil-in: -24%; margin: 1.1em; }
.so .so__face.is-dim > .head { filter: grayscale(1); opacity: .45; }
.so__row[data-friend], .so__row[data-blocked] { cursor: var(--cur-hand, pointer); }
.so__row--ask .item-row__end { gap: .4em; }
.so__row--ask .ibtn > img.g { width: 1.3em; height: 1.3em; object-fit: contain; }
.so__hint { color: var(--ink-mute); }

/* A friend's guild on the row's right: the name over its rank, right-aligned; a long name is cut,
   and "No guild" is quiet. */
.so__guild { display: grid; justify-items: end; gap: .1em; max-width: 12em; min-width: 0; text-align: right; }
.so__guild-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--brass-pale); font-weight: 600; }
.so__guild-name.is-none { color: var(--ink-mute); font-weight: 400; }
.so__guild-rank { color: var(--ink-mute); font-size: var(--fs-sm); white-space: nowrap; }
.so__guild-rank[hidden] { display: none; }

/* Presence: a dot in front of the words, both ways (grey away, green here). */
.so__dot { display: inline-block; width: .5em; height: .5em; margin-right: .4em; border-radius: 50%; background: var(--ink-mute); }
.so__dot.is-on { background: var(--green); }

/* An empty page: no box, the picture centred in the page on a soft brass glow, a friendly title
   and one quiet line under it. */
.so .gui-empty {
  display: grid; justify-items: center; align-content: center; gap: .45em; min-height: 100%; box-sizing: border-box;
  padding: 1.4em 1em; text-align: center; background: none; border: 0; box-shadow: none;
}
.so .gui-empty > img.g {
  width: 4.4em; height: 4.4em; object-fit: contain; margin-bottom: .35em;
  filter: drop-shadow(0 0 1.1em rgba(var(--brass-hi-rgb), .28));
}
.so .gui-empty > b { font-family: var(--face-display); font-size: var(--fs-heading); color: var(--brass-pale); letter-spacing: .02em; }
.so .gui-empty > .note-line { max-width: 24em; margin: 0; }

/* Daily: one allowance a row. The end column is three fixed tracks - the base "used / ceiling",
   the Ticket's share, then the status chip - so every row's figures and chips stand in the same
   columns down the list. No picture column, so the name takes the shrinking track and a long zone
   name wraps under itself rather than running into the figures. */
.so-day__cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(25em, 1fr)); gap: .6em 1.4em; align-items: start; }
.so-day__col, .so-day__sect { display: grid; gap: .4em; align-content: start; min-width: 0; }
.so-day .item-row { min-height: 2.4em; grid-template-columns: minmax(0, 1fr) auto; }
.so-day .item-row__name b { white-space: normal; overflow-wrap: anywhere; }
.so-day .item-row__end { display: grid; grid-template-columns: 3.4em 4.6em 6.6em; gap: .5em; align-items: center; }
/* The Base / Tamer Ticket heads, on the rows' own grid so each word stands over its column. */
.so-day .so-day__head { min-height: 0; padding-block: 0; border: 0; background: none; box-shadow: none; font-size: var(--fs-sm); color: var(--ink-mute); }
.so-day__n { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.so-day__tk--none { color: var(--ink-mute); }
/* No live Ticket: what it would add, dimmed behind the lock - a promise, not a count. */
.so-day__tk--locked { color: var(--ink-mute); opacity: .75; }
.so-day__lock { width: .9em; height: .9em; object-fit: contain; vertical-align: -.1em; }
.so-day__lv { color: var(--ink-mute); }
.so-day__lv--under { color: var(--error); }
.so-day__st { justify-self: start; padding: .1em .65em; border-radius: 1em; font-size: var(--fs-sm); font-weight: 600; line-height: 1.5; white-space: nowrap; }
.so-day__st--full { color: var(--error); background: color-mix(in srgb, var(--error) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--error) 45%, transparent); }
.so-day__st--near { color: var(--tan-text); background: var(--tan); box-shadow: inset 0 0 0 1px var(--tan-rim); }
.so-day__st--open { color: var(--green); background: color-mix(in srgb, var(--green) 12%, transparent); }

/* The foot: the line on the left, the verbs on the right. */
.wins > .win.so .win__foot { gap: .5em; }
.so__said { margin-right: auto; min-width: 0; overflow-wrap: anywhere; }
.so .win__foot .btn[hidden] { display: none; }
/* Messages carries the HUD's red dot (40-hud.css .hud-dot) while a message waits unread. */
.so__unread { z-index: 2; right: -3px; top: -3px; }

/* Mobile mode: the whole sheet; a page fills what is left of it and scrolls (no fixed height), rows
   keep a thumb's height. */
html.m-mobile .wins > .win.so { width: min(var(--w), .8 * var(--frame-w)); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .so__pane, html.m-mobile .so__pane.so-day { height: auto; flex: 1 1 auto; min-height: 0; }
html.m-mobile .so__row { min-height: var(--touch); }
html.m-mobile .so__guild { max-width: 8em; }
html.m-mobile .so-day__cols { grid-template-columns: minmax(0, 1fr); }

/* ---- w-stall.css */
/* THE PERSONAL STALL (panels/stall.ts, P22): your own counter `.win.stall--mine[data-win=stall]`
   (racks used and open/closed, the sign, the counter in two columns of five with the pager, the
   refusal, the trade records; the foot's purse, Stop and Start) and somebody else's
   `.win.stall--theirs[data-win=stallView]` (whose and how many, the lines, the pick with Buy; the
   foot's purse and receipt). Placed by gui/win.ts: yours beside the Backpack's spot (a line is racked
   by dragging out of the bag), theirs centred. 20-components.css draws every part; tokens only. Moved from
   25-card.css. */

.wins > .win.stall > .win__body { display: flex; flex-direction: column; gap: .6em; }
.stall .note-line[hidden], .stall .gui-empty[hidden], .stall__cols[hidden], .stall__pick[hidden], .stall__pick [hidden] { display: none; }

/* Two columns, filled down the left then down the right: a page is five and five. */
.stall__cols { display: grid; grid-auto-flow: column; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(5, auto); gap: 0 1.2em; align-content: start; min-width: 0; }
.stall__row .item-row__name b { white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow-wrap: anywhere; }
.stall__row .item-row__end .chip { font-size: var(--fs-md); }
.stall__theirs .stall__row { cursor: var(--cur-hand, pointer); }
.stall__mine .stall__row[data-line] { cursor: var(--cur-grab, grab); touch-action: none; }
.stall__counter { min-width: 0; }

.stall__records { max-height: 7em; }
.stall__sale { display: flex; align-items: center; gap: .5em; }
.stall__sale > span:first-child { min-width: 0; overflow-wrap: anywhere; }

/* The pick: tile, name and price, then Attributes (a pet) and Buy with the refusal under them. */
.stall__pick { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .4em .8em; align-items: center; }
.stall__pick > .note-line--mute { grid-column: 1 / -1; }
.stall__pickinfo { display: grid; gap: .15em; min-width: 0; }
.stall__name { font-size: var(--fs-lg); overflow-wrap: anywhere; }
.stall__name--green { color: var(--rarity-green-text); }
.stall__name--blue { color: var(--rarity-blue-text); }
.stall__name--gold { color: var(--rarity-gold-text); }
.stall__name--yellow { color: var(--rarity-yellow-text); }
.stall__name--purple { color: var(--rarity-purple-text); }
.stall__pickinfo > .chip { font-size: var(--fs-lg); justify-self: start; }
.stall .chip.is-short .chip__num { color: var(--error); }
.stall__act { display: grid; gap: .3em; justify-items: end; max-width: 16em; }
.stall__act > .note-line { text-align: right; }
.stall__btns { display: flex; gap: .5em; align-items: center; }

.stall .gui-empty { display: grid; justify-items: center; gap: .3em; padding: 1.4em 1em; text-align: center; }
.stall .gui-empty > img.g { width: 3em; height: 3em; object-fit: contain; }

.wins > .win.stall .win__foot { gap: .6em; }
.stall__gap { margin-left: auto; }
.stall__note { margin-left: auto; min-width: 0; overflow-wrap: anywhere; }

/* Mobile mode: yours takes half the sheet beside the bag (drag from one to the other) and stacks its
   columns; theirs takes the sheet. Rows keep a thumb's height. */
html.m-mobile .wins > .win.stall--mine { width: min(40em, calc(.5 * var(--frame-w) - 12px)); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .stall--mine .stall__cols { grid-auto-flow: row; grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
html.m-mobile .wins > .win.stall--theirs { width: min(var(--w), .8 * var(--frame-w)); }
html.m-mobile .stall__row { min-height: var(--touch); }

/* ---- w-ticket.css */
/* TAMER TICKET (panels/tamer-ticket.ts, `.win.tpw[data-win=ticket]`, P30): the state band and the
   time left, a quiet line, the day's sections (caption + rows: name, "used / cap", and where the
   Ticket pays the sparkle and "base > cap"), the places, the "would lock" line, and Buy in the foot.
   The body scrolls inside the window (no fixed-height list cutting the last rows, B247). The welcome
   sheet's list (.tpw__welcome) is an ask.ts question's body. Placed by gui/win.ts; moved from
   25-card.css. Tokens only. */
.tpw [hidden] { display: none !important; }
.wins > .win.tpw > .win__body { display: grid; gap: .7em; align-content: start; min-width: 0; }

.tpw .band.is-on .band__label { color: var(--gold-bright); }
.tpw .band__num { font-variant-numeric: tabular-nums; }
.tpw__sect { display: grid; gap: .3em; min-width: 0; }
.tpw__row { display: flex; align-items: baseline; gap: .6em; }
.tpw__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tpw__num { flex: none; font-variant-numeric: tabular-nums; }
.tpw__paid { flex: none; display: inline-flex; align-items: center; gap: .25em; color: var(--blue); font-variant-numeric: tabular-nums; }
.tpw__paid > img.g { width: 1em; height: 1em; object-fit: contain; }
.tpw__paid > .tpw__arrow { width: .8em; height: .8em; }
.tpw .btn__ic { width: 1.2em; height: 1.2em; object-fit: contain; }

.tpw__welcome { display: grid; gap: .7em; }
.tpw__perks { list-style: none; margin: 0; padding: 0; display: grid; gap: .35em; }
.tpw__perks > li { display: flex; gap: .5em; align-items: baseline; }
.tpw__perks > li > img.g { flex: none; width: 1em; height: 1em; object-fit: contain; transform: translateY(.15em); }

/* ---- w-trade.css */
/* THE TRADE WINDOW (panels/trade.ts, `.win.trd[data-win=trade]`, P21): who you trade with, the two
   offer columns (who and whether they agreed, their Gold, twenty places), Pick from bag, the refusal
   line; the foot's Cancel and Confirm. Placed by gui/win.ts beside the Backpack's spot (never over
   it: an offer is filled by dragging out of the bag). 20-components.css draws every part; tokens only.
   Also the receipt and the picker (gui/ask.ts questions). Moved from 25-card.css. */

.wins > .win.trd > .win__body { display: grid; gap: .7em; align-content: start; }
.trd__cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8em; align-items: start; min-width: 0; }
.trd__col { display: grid; gap: .5em; min-width: 0; container-type: inline-size; }
.trd__colhead { display: flex; align-items: center; justify-content: space-between; gap: .5em; min-width: 0; }
.trd__colhead > .cap { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Five to a row; a cell shrinks with its column (a phone's half sheet) instead of hanging off it. */
.trd__grid {
  --cols: 5;
  --tile: min(calc(10em / 3), calc((100cqw - 4 * .4em) / 5));
  justify-content: center;
}
.trd__grid > .tile--drop { background: var(--tile-empty-bg); }
.trd__grid > .tile--drop.is-over { border-color: var(--drop-on-rim); background: var(--drop-on-bg); }
.trd__mine .tile[data-line] { cursor: var(--cur-grab, grab); touch-action: none; }

.trd__gold { min-width: 0; width: 7.5em; }
.trd .band__action { display: flex; align-items: center; gap: .4em; }
.trd .band__label > img.coin { width: 1.1em; height: 1.1em; vertical-align: -.2em; margin-right: .3em; }

.trd__pick { display: flex; justify-content: flex-end; }
.trd .note-line[hidden] { display: none; }
.wins > .win.trd .win__foot { justify-content: flex-end; gap: .6em; }
.wins > .win.trd .win__foot .btn > .ic { width: 1.1em; height: 1.1em; }
.wins > .win.trd .win__foot .btn:has(> .ic) { grid-auto-flow: column; gap: .4em; }

/* The receipt and the picker. */
.trd__done { display: grid; gap: .7em; }
.trd__got, .trd__picks { --cols: 5; justify-content: center; }
.trd__picks { --cols: 6; max-height: 22em; overflow: auto; overscroll-behavior: contain; }
.trd__picks > .tile { cursor: var(--cur-hand, pointer); }

html.m-mobile .wins > .win.trd { width: min(46em, calc(.5 * var(--frame-w) - 12px)); max-height: calc(var(--frame-h) - 32px); }
html.m-mobile .trd__cols { grid-template-columns: minmax(0, 1fr); }
html.m-mobile .trd__gold { width: 5.5em; }

/* ---- w-workshop.css */
/* THE BLACKSMITH (panels/workshop/*, `.win.wsw[data-win=workshop]`, P28): the service strip (painting
   and word; the word on its card in mobile mode), one page per service in a body of one fixed size
   (no jump on a tab press or a drop, B122), and the foot: the purse, the page's own line, cost and
   commit, then Repair. 20-components.css draws every part (tabs, tile, rows, pill, chip, drop, note line);
   this sheet lays them out. Tokens only. */

.wsw [hidden] { display: none !important; }
.wins > .win.wsw > .win__body { display: flex; flex-direction: column; gap: .6em; min-height: 0; overflow: hidden; }
html.m-desktop .wins > .win.wsw { height: min(40em, calc(.92 * var(--frame-h) - 88px * var(--hud-z, 1))); }

/* --- the strip: each service's painting and its word */
.wsw__tabs { flex: none; }
.wsw__tabs .tabs__tab { min-width: 0; padding-inline: .5em; }
.wsw__tab { display: inline-flex; align-items: center; gap: .4em; min-width: 0; }
.wsw__tab > .ic { width: 1.5em; height: 1.5em; object-fit: contain; flex: none; }
.wsw__word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the pages: one scrolls at a time inside the body's fixed height */
.wsw__pages { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.wsw__page { display: grid; gap: .7em; align-content: start; min-width: 0; padding: .1em; }

/* --- the piece well: a dashed drop while empty; once loaded, the tile and its figures */
.wsw__input { display: grid; gap: .5em; }
.wsw__piece { display: flex; align-items: center; gap: .8em; min-height: 4.4em; padding: .5em .7em; border-radius: .5em;
  border: 1px solid var(--hairline); text-align: start; }
.wsw__piece.drop { display: flex; justify-items: start; text-align: start; }
.wsw__piece.is-over:not(.drop) { outline: 1px dashed var(--drop-on-rim); outline-offset: -.15em; }
.wsw__piece > .tile { flex: none; }
.wsw__copy { display: grid; gap: .3em; min-width: 0; }
.wsw__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wsw__figs { display: flex; flex-wrap: wrap; align-items: center; gap: .3em .7em; font-size: var(--fs-sm); }
.wsw__mark { display: inline-flex; align-items: center; gap: .25em; font-variant-numeric: tabular-nums; }
.wsw__mark > .g { width: 1.2em; height: 1.2em; object-fit: contain; }
.wsw__hint { color: var(--ink-mute); }
.wsw__hint.note-line--error { color: var(--error); }

/* --- what the service does to it: Lv 34 > Lv 35, the quality and the warning */
.wsw__change { display: flex; flex-wrap: wrap; align-items: center; gap: .5em; }
.wsw__change > .note-line { flex: 1; min-width: 12em; }
.wsw__to { width: 1em; height: 1em; }
.wsw__change > [hidden] { display: none; }

/* --- the material table: tile, name, have / need (short in red); a gain in green */
.wsw__quote { display: grid; gap: .3em; }
.wsw__fig { font-variant-numeric: tabular-nums; color: var(--ink); }
.wsw__fig.is-short { color: var(--error); }
.wsw__gain:not([class*="item-row--"]) .item-row__name b, .wsw__gain .wsw__fig { color: var(--green); } /* a tiered gain keeps its tier ink */

/* --- Craft: the five filters, the families (the list scrolls; the table stays under it) */
.wsw__filters { display: flex; flex-wrap: wrap; gap: .5em; }
.wsw__filters .select { min-width: 8.5em; }
.wsw__list { display: grid; gap: .4em; max-height: 15em; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.wsw__recipes .item-row { cursor: var(--cur-hand, pointer); }

/* --- a row's end with more than one part (a stepper and a clear, a fee and a button) */
.wsw__end { display: inline-flex; align-items: center; gap: .5em; }
.wsw__end .btn__ic { width: 1.2em; height: 1.2em; object-fit: contain; }

/* --- Sockets: one row of six marks (a stone, an open socket, a padlock); a set stone is pressed */
.wsw__sockets { display: grid; gap: .5em; }
.wsw__holes { display: grid; grid-template-columns: repeat(6, max-content); gap: .4em; justify-content: start; }
.wsw__holes .wsw__stone { cursor: var(--cur-hand, pointer); }
.wsw__sockets.is-over .wsw__holes .tile--empty { outline: 1px dashed var(--drop-on-rim); outline-offset: -.15em; }

/* --- Up-star: the Memory Gem row is a toggle; the odds bar */
.wsw__toggle { cursor: var(--cur-hand, pointer); }
.tile > img.wsw__plus { inset: 0; margin: auto; width: 45%; height: 45%; opacity: .8; }
.wsw__toggle:hover .wsw__plus { opacity: 1; }
.wsw__odds { width: min(100%, 22em); }

/* --- the foot: the purse, the page's line + cost + commit, Repair */
.wsw .win__foot { gap: .6em; }
.wsw__foot { display: flex; align-items: center; gap: .6em; flex: 1; min-width: 0; }
.wsw__foot > .note-line { flex: 1; min-width: 0; }
.wsw__foot > .btn { flex: none; }
.wsw__foot .btn__ic { width: 1.3em; height: 1.3em; object-fit: contain; }

/* --- mobile: half the sheet (the bag beside it, so a piece drags across), the strip pictures only */
html.m-mobile .wins > .win.wsw { width: min(48em, calc(.5 * var(--frame-w) - 12px)); height: calc(var(--frame-h) - 32px); }
html.m-mobile .wsw__word { display: none; }
html.m-mobile .wsw__tabs .tabs__tab { min-height: var(--touch); }
html.m-mobile .wsw__foot > .btn, html.m-mobile .wsw__recipes .item-row, html.m-mobile .wsw__toggle { min-height: var(--touch); }
html.m-mobile .wsw .win__foot, html.m-mobile .wsw__foot { flex-wrap: wrap; }
html.m-mobile .wsw__foot > .note-line { flex-basis: 100%; order: -1; }
html.m-mobile .wsw__filters .select { min-width: 0; flex: 1 1 7em; }
