/* WHERE A WINDOW SITS, AND NOTHING ELSE.
   This is the placement half of the old public/panels.css, carried across so the HUD design
   system can own everything a window LOOKS like. .win is position:relative and the design
   system positions nothing at all, so without this sheet every one of the game's windows lands
   in the document flow, stacked down the top of <body>.

   What this file deliberately does not own: any face, rim, shadow, border, colour, font,
   padding or gap inside a window; any component; any state. If a rule here says how something
   is drawn rather than where it is, it is in the wrong file - it belongs in
   apps/game/public/hud-ui/design.css.

   It reads no --gs-* token from any other sheet. The old sheet read 52 of them and would have
   collapsed silently the day play.html stopped loading gs.css: a token that resolves to nothing
   inside a `width` takes the whole declaration with it. Every one is a literal here, or a token
   this file declares itself. The z-index literals are the shared Z table (window 20, dialog 80,
   card 90, scrim 100, menu 120); --gs-hud-scale keeps its old name because apps/game/client/ui.ts
   reads and writes that exact property on <html>, but its only declaration is now the one below.

   Loaded LAST, after design.css. Dimensions are in em against the 15px window size, so a
   placement scales with the window it places. */

/* The always-on HUD is NOT placed here. apps/game/public/hud.css owns every .hud-* rule, its offsets
   included, because the world layer is its own language and not this design system's: no box,
   no plate, no well anywhere on screen while you play. A parchment window is the right answer
   for a thing you open and close and the wrong answer for a thing that is always there covering
   the game. This sheet places WINDOWS. A HUD cluster offset appearing below is a bug. */

/* The HUD scale. setHudScale in apps/game/client/ui.ts writes this onto <html> and reads it back, so
   the default has to be a real declaration somewhere and this sheet is the last one standing. */
:root { --gs-hud-scale: 1.15; }
/* The height of the hotbar, which is what the conversation stands on: 34px of slot (hud.css
   --hud-slot) at the HUD's own 1.15 zoom, rounded up to a whole pixel. */
:root { --hud-bar-h: 40px; }

/* ------------------------------------------------------------------ GLOBAL BEHAVIOUR */
/* Three things the old Stonebevel sheet was quietly carrying for the whole document, none of
   which the design system restates. A drag is this game's main gesture, and without the first
   rule every drag across a panel paints a text selection instead. The second keeps native
   scrollbars and the surviving form controls dark on a machine set to a light theme. The third
   is the colour behind the renderer canvas, which is what a player sees while the map streams.
   Box-sizing is NOT here: design.css already sets border-box on `*`. */
* { user-select: none; -webkit-user-select: none; }
input, textarea, [contenteditable] { user-select: text; -webkit-user-select: text; }
html { color-scheme: dark; }
html, body { background: #2c2c2f; }

/* The page frame. apps/game/client/ui.ts used to write these three declarations into a <style> tag at
   mount, which put a panel file in charge of the document and read --gs-void out of the sheet
   that is being retired - and a token that resolves to nothing takes its whole declaration with
   it. The colour is the one above; only the frame is left to say. */
body { margin: 0; overflow: hidden; }
canvas { display: block; }

/* The world's announcement ladder: centred under the top edge, lines stacking downward, and
   never a click target - it prints over the world while the player is still walking through it.
   Above the window stack and below the scrim, so a modal covers it. */
.tl-cast {
  position: fixed;
  left: 50%;
  top: 5.6em;
  transform: translateX(-50%);
  z-index: 94;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .333em;
  width: max-content;
  max-width: min(41.333em, calc(100vw - 2.133em));
  pointer-events: none;
}

/* The proclamation, across the upper third: the one band the world says something in. Edge to
   edge, because the wash has no edge of its own and a band inset from the sides would read as a
   strip laid over the game. Never a click target - the player is still walking under it - and one
   layer above the ladder, because your own arrival outranks somebody else's good luck, and under
   the scrim, so a question you have to answer still covers it. */
.tl-procl {
  position: fixed;
  left: 0;
  right: 0;
  top: 22%;
  z-index: 95;
  pointer-events: none;
}

/* A PHONE shrinks both by its own height, the factor every window already takes (orientation.ts
   writes --win-k = innerHeight / 640, never above 1). Both sit straight on body at the page's 15px,
   so on a 400px-tall screen a broadcast and your own notice were desktop-sized over half the world.
   Floored at 11px so a line stays readable; the big broadcast stays 1.4x of it (design.css). Your
   own notice also sets tighter: 2.1em with wide tracking ran a long sentence edge to edge. */
@media (hover: none) and (pointer: coarse) {
  .tl-cast, .tl-procl { font-size: max(11px, calc(15px * var(--win-k, 1))); }
  .tl-cast { top: 4.2em; }
  /* Four broadcast lines reach a third of a 400px screen, so your own notice stands below them
     rather than printing over the ladder. */
  .tl-procl { top: 34%; }
  .tl-procl .procl__text { font-size: 1.6em; letter-spacing: .08em; }
}

/* The host for a card that follows the pointer: a hover card, a dragged item's ghost. Above every
   window, below the scrim, and never a hit target of its own. */
.card--float { position: fixed; z-index: 90; pointer-events: none; }
/* ...except while a modal is up. The scrim takes the pointer from everything under it, so the only
   thing a player can hover then is the dialog itself - a prize in the gacha window, a stack in a
   result - and its card belongs over it, not behind (reported 2026-09-26). */
body:has(> .scrim) .card--float, body:has(> .scrim) .sp-card { z-index: 110; }

/* The NPC conversation stands on the hotbar, centred, and it is wide: the quest it offers is
   drawn INSIDE it now (apps/game/client/quest.ts), so the body carries a portrait, a speech well and a
   sheet side by side. Centred in the screen it covered the body it was talking to; over the bar it
   covers the floor nobody is looking at. --hud-bar-h is the hotbar's own height, declared here
   because this sheet reads no token it does not own - see the file header.

   The NPC conversation and the small service window it opens. Neither is in the window stack -
   the conversation closes itself and takes the service box with it - so nobody else places them,
   and they were the last two windows placing themselves from a <style> tag apps/game/client/dialog-panel.ts
   wrote at mount. The two layers are relative to each other and to the stack (window 20, dialog
   80): the conversation sits just under an ordinary window, and a question raised over the
   service box comes up over it rather than behind it. */
.dlg { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 44em; z-index: 19; }
.dlg-svc { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 23em; z-index: 81; }

/* ------------------------------------------------------------------ THE WINDOWS */
/* One line each: where it opens and how wide. The widths are not decoration, they are the shape
   of the content - a ten-column bag, a doll frame, a bracket. Where the design system draws the
   window, its width IS the width: apps/game/public/hud-ui/design.css draws one, and that is
   the number here. The rest still carry the measurement panels.css made against Stonebevel, and
   the wave that ports each of those re-measures it.

   That distinction is not pedantry. The old widths were taken against a 44px slot and a denser
   face; a `.tile` is 3.33em, so a ten-column bag needs 36.933em of grid plus 2.8em of padding and
   the old 39.333em put the last two columns outside the frame - `.win` has no overflow clip, so
   they hang in the air over the world. A window that is too narrow for the design system's own
   components is a visible break, not a tight fit, and one that is too WIDE leaves the grid
   floating in a band of dead paper, which is the same break read the other way. Both are why
   these two widths are re-derived whenever --tile moves.

   Two of these are DERIVED, not authored: `.warehouse` and `.ps` sit beside the window they trade
   with, so their `right` is that window's width plus two gutters. Change a bag width and its
   neighbour's offset moves with it.

   `max-width` keeps a window inside a laptop viewport; the mobile block at the bottom takes over
   from all of it on a phone. */

/* Bag and warehouse, right-hand side, side by side so an item drags straight across. */
/* 46em, not the 41.333em the grid alone asks for: the foot (two purses, Split, Sort, Auto Sell)
   is what sets this window's width, and a window sized to its grid would clip its own buttons. */
.bag { position: fixed; right: .933em; top: 12%; width: 46em; }
/* A warehouse page is 200 cells laid out 20 x 10 (user, 2026-09-24: one warehouse per account,
   five pages of 200, every cell on screen with no scrolling). Twenty columns wide. It still
   opens to the left of the bag, and on a screen too narrow for both it slides right until its own
   left edge is on screen rather than hanging off it. */
.warehouse {
  position: fixed;
  /* THE CELL FOLLOWS THE SCREEN'S HEIGHT, never above the design's 40px (`--tile`). Ten rows at
     full size need about 51em of window, which a desktop has and a phone held sideways does not:
     at its 9px window size that is 460px on a 390px screen, and the bottom rows and the footer
     went off the edge. So a short screen gets smaller cells: the window's chrome (title, page
     tabs, slot band, foot and padding - measured at about 14.1em, so 14.5em leaves a hair of air)
     comes off the height, and so do the 32px the touch rule keeps clear; ten rows share the rest.
     On any screen tall enough this is the design's own 40px and changes nothing. */
  --tile: min(calc(10em / 3), calc((100dvh - 32px - 14.5em) / 10 - .4em));
  /* Twenty cells, nineteen gaps, and the window's own padding and frame (2.93em). At the full
     cell this is the 77.2em the window has always been. */
  width: calc(20 * var(--tile) + 19 * .4em + 2.93em);
  top: max(.5em, min(12%, calc(100dvh - 16px - 14.5em - 10 * (var(--tile) + .4em))));
  right: max(.933em, min(47.867em, calc(100vw - 20 * var(--tile) - 19 * .4em - 2.93em - .933em)));
}
/* A PHONE KEEPS THE CELL AND SCROLLS THE PAGE. Two hundred cells squeezed onto a 360-430px screen
   come out at 16-23px - a finger's worth of three of them - so a touch screen keeps the design's
   cell, the same size the bag's are, and the page scrolls under its own tabs instead. The window
   is then twenty full cells wide (about 694px at a phone's 9px window size), which the narrowest
   phone held sideways still fits. The drag is a press-and-hold (touchdrag.ts), so a swipe over
   the grid is free to be the scroll. */
@media (hover: none) and (pointer: coarse) {
  /* Plus room for the scrollbar the page now has: without it the bar took its width out of the
     grid and the twentieth column slid under it, with a sideways scrollbar to reach it. */
  .warehouse { --tile: calc(10em / 3); width: calc(20 * var(--tile) + 19 * .4em + 2.93em + .9em); }
  .warehouse .spread {
    max-height: calc(100dvh - 32px - 14.5em);
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-gutter: stable;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
  }
}
.bag .tiles,
.warehouse .tiles { justify-content: center; }
/* The page strip of the Backpack and the warehouse: one tab per page, each an equal share of the
   window. The text-tab minimum and padding are sized for two or three tabs, and five of them ran
   past the warehouse's edge; the Backpack wears the same strip so the two windows read as one
   system (user, 2026-09-24). */
.bag .tabs--text .tabs__tab,
.warehouse .tabs--text .tabs__tab { min-width: 0; padding: 0 .6em; white-space: nowrap; }

/* The character sheet, left, clear of the bag. The ceiling is the viewport minus the 8% it opens
   below and a gutter under it: the doll, the equipment columns, the attribute rows and the combat
   table together stand about 51em, which fits a 900px screen and runs off the bottom of the 813px
   one a laptop actually has. See THE VIEWPORT CEILING below for why the body is the scroller. */
.cs {
  position: fixed; left: .933em; top: 8%;
  width: 56em; max-width: calc(100vw - 1.867em);
  max-height: calc(92dvh - 1.6em);
}
.cs .stat__key { display: inline-flex; align-items: center; gap: .2em; }
.cs [data-attr-help] { width: 1em; height: 1em; min-width: 1em; padding: 0; }

/* Everything an NPC raises opens in the viewport centre. */
.ws { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 58em; max-width: calc(100vw - 1.867em); }
.ql { position: fixed; left: 50%; top: 8%; transform: translateX(-50%); width: 60em; max-width: calc(100vw - 1.867em); }

/* The pet roster and its detail sheet, in the same band as the bag they trade with. The sheet
   and the pen both grow with the player's pets - the sheet's Stats page runs to a combat table,
   and the pen draws nine travelling rows plus eight penned ones in one list - so both carry a
   ceiling. The pen is centred, so its ceiling is the whole viewport less a gutter each side;
   the sheet opens at 14%, so its ceiling is what is left under that. */
.pt { position: fixed; right: .933em; top: 14%; width: 46em; }
/* Reordering the panel by dragging a row. The row being carried stays behind as a dashed hole in
   its place, the row under the cursor lights tan as a drop slot does, and the ghost under the
   cursor is a copy of the whole row on the window's own surface (pets.ts sets the surface). */
.pt .item-row.is-carried { opacity: .35; outline: 1px dashed var(--tan-rim); outline-offset: -1px; border-radius: .35em; }
.pt .item-row.is-over { background: rgba(var(--tan-rgb), .14); box-shadow: inset 0 0 0 1px var(--tan); border-radius: .35em; }
.pt .pt__ghost { position: fixed; left: -200vw; top: 0; border: 1px solid var(--tan); border-radius: .35em; box-shadow: 0 .4em 1.2em rgba(0, 0, 0, .35); pointer-events: none; }
.ps {
  position: fixed; right: 47.867em; top: 14%;
  width: 50em; max-width: calc(100vw - 1.867em);
  max-height: calc(86dvh - 1.6em);
}
/* The pen's own window: one collection now, the travelling list being the HUD's Pet Slot window,
   which opens beside it at the right edge. Narrow enough that the two do not overlap on a 1366px
   screen. */
.pr {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 34em; max-width: calc(100vw - 1.867em);
  max-height: calc(100dvh - 3.2em);
}

/* THE VIEWPORT CEILING, AND WHAT SCROLLS UNDER IT.
   A `.win` is a flex column of head, body and foot, and only the body is `flex: 1`. Capping the
   window alone would therefore squeeze the body's own height while its content kept its size and
   hung out of the frame - the design system sets no overflow on `.win__body`, because most
   windows are measured to fit. So the three that are not say it here, once: the body is the
   scroller, which keeps the title and the buttons in the frame while the list moves.

   `min-height: 0` is not optional. A flex child's default minimum is its content, so without it
   the body refuses to shrink below its rows and the cap above does nothing at all. */
.cs > .win__body, .ps > .win__body, .pr > .win__body { min-height: 0; overflow-y: auto; }

/* Marketplace lists scroll inside the fixed window on a wide screen. On a small screen the body
   becomes the sole scroller; min-height:0 lets either layout stay inside the viewport. */
.market > .win__body { min-height: 0; }

/* The dealer, one width for three of its tabs. */
.pd { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 50em; max-width: calc(100vw - 1.867em); }
/* And wider for the fourth. The fusion diagram is 22em of its own before the rules, the rate and
   the stone get a column - the same split the v0.10 panel made when it took 760px for fusion and
   660px for everything else. The class is set by the panel when the tab changes, not by a
   selector: the tab lives in the body and a stylesheet cannot see which one is up. */
.pd.pd--fusion { width: 66em; }

/* The growth table scrolls inside itself rather than growing past the viewport. */
.pd-growth {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 34em; max-width: calc(100vw - 1.6em); max-height: calc(100dvh - 1.6em); overflow: auto;
}

/* Trade and commerce. */
.shop { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 38em; max-width: calc(100vw - 1.867em); }
/* The barter list scrolls inside itself only while it shares the window with a stock list; a pure
   barter counter (the event exchange) is that list and nothing else, so the window holds all of it. */
.shop__barter { max-height: 13em; }
.shop--barter-only .shop__barter { max-height: none; }
/* The Marketplace uses the lab's list plus inspection layout, sized by its own container so a
   resized game window switches columns even when the device viewport is still wide. */
.market {
  position: fixed; left: 50%; top: 3%; transform: translateX(-50%);
  width: 80em; max-width: calc(100vw - 1.867em);
  height: min(55.333em, calc(100vh - 1.867em)); max-height: min(55.333em, calc(100vh - 1.867em));
  container: marketplace / inline-size;
}
.market > .win__body { display: grid; gap: .7em; min-width: 0; grid-template-rows: auto minmax(0, 1fr); }
.market > .win__body > .tabs { border-color: #574331; border-radius: .35em; background: #17130f; }
.market > .win__body > .tabs .tabs__tab { border-radius: 0; }
.market > .win__body > .tabs .tabs__tab.is-on { padding-inline: 1.4em; background: linear-gradient(#94704e, #755339); color: #fff3df; box-shadow: inset 0 -2px #e2b677; text-shadow: none; }
.market__browse { display: flex; flex-direction: column; gap: .7em; min-height: 0; min-width: 0; }
.market__sides-bar { display: flex; align-items: center; justify-content: space-between; gap: 1em; }
.market [data-sides] { width: 15em; flex: none; padding: .25em; border-color: #574331; background: #17130f; }
.market [data-sides] .tabs__tab { padding-inline: .6em; height: 2.35em; }
.market [data-sides] .tabs__tab.is-on { padding-inline: .6em; background: #b98c63; box-shadow: none; color: #1c140f; text-shadow: none; }
.market__sides-note { color: var(--ink-mute); font-size: var(--fs-small); }
.market__tools { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(11em, 13em); gap: .6em; align-items: center; min-width: 0; }
.market__tools [data-sort] { min-width: 0; }
.market__grid { display: grid; flex: 1; grid-template-columns: 10em minmax(0, 1fr) minmax(25em, 30em); gap: .8em; min-height: 0; min-width: 0; }
.market [data-categories-panel] { display: grid; gap: .4em; grid-template-rows: auto minmax(0, 1fr); min-height: 0; min-width: 0; padding: .2em .8em .2em 0; border-right: 1px solid #574331; border-radius: 0; background: transparent; }
.market [data-categories-panel] > .scroll { min-height: 10em; }
.market [data-categories] .item-row { min-height: 2.5em; padding: .35em .5em; border: 0; border-radius: .3em; background: transparent; color: #d6b18c; }
.market [data-categories] .item-row:hover { background: #382b21; }
.market [data-categories] .item-row.is-on { background: #453527; color: #fff1dc; box-shadow: inset 2px 0 #e3bb7d; }
.market [data-categories] .item-row__end { color: #baa58b; }
/* Equipment > class > part: the classes and parts hang indented under Equipment, the open line's
   caret turned down. In the narrow layout they move to their own strips (below). */
.market__caret { width: .7em; height: .7em; display: grid; place-items: center; }
.market__caret::before { content: ""; border-left: .38em solid #9c8466; border-block: .26em solid transparent; transition: transform .15s; }
.market .is-open > .market__caret::before { transform: rotate(90deg); border-left-color: #e3bb7d; }
.market [data-categories] .market__cat-l1 { min-height: 2.2em; padding-left: 1.3em; }
.market [data-categories] .market__cat-l2 { min-height: 2em; padding-left: 2.6em; font-size: .94em; }
.market [data-categories] .market__cat-l1.is-open:not(.is-on) { color: #f0d9b8; }
.market__substrip { display: none; }
.market__results { display: grid; gap: .45em; grid-template-rows: auto auto minmax(0, 1fr); min-height: 0; min-width: 0; }
.market__results > div:first-child { flex-wrap: wrap; }
.market__results [data-result-title] { font-size: 1.15em; }
.market__column-head { display: flex; justify-content: space-between; gap: 1em; padding: .15em .6em .5em; border-bottom: 1px solid #574331; color: #b9a38a; font-size: .73em; letter-spacing: .08em; text-transform: uppercase; }
.market__results .item-row { min-height: 4.1em; margin-top: .15em; padding: .45em .6em; border: 1px solid transparent; border-bottom-color: #463629; border-radius: .3em; }
.market__results .item-row:hover { background: #392d22; }
.market__results .item-row.is-on { background: linear-gradient(90deg, #4b3928, #34271f); border-color: #a07a4c; box-shadow: inset 3px 0 #e4b978; }
.market__results .item-row__end { color: #efbd77; font-size: 1.1em; font-weight: 700; }
.market__inspection { display: flex; flex-direction: column; gap: .6em; min-height: 0; min-width: 0; overflow-y: auto; }
.market [data-detail], .market [data-book-panel], .market [data-gear-panel] { background: #30251e; border: 1px solid #67503b; border-radius: .4em; }
.market [data-detail] { display: grid; gap: .5em; align-self: start; min-width: 0; width: 100%; }
.market__detail-head { display: grid; gap: .15em; padding-bottom: .5em; border-bottom: 1px solid var(--rule); }
.market__detail-head b { font-size: 1.1em; overflow-wrap: anywhere; }
.market [data-detail] .kv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3em .7em; }
.market [data-detail] .kv > div { display: block; padding: 0; border: 0; min-width: 0; }
.market [data-detail] .kv__val { display: block; margin-top: .1em; overflow-wrap: anywhere; }
.market [data-detail] [data-buy]:not(:disabled) { min-height: 2.5em; background: #bb8d61; color: #1b130c; }
.market [data-book-panel], .market [data-gear-panel] { display: grid; gap: .5em; min-width: 0; }
.market__history-head { display: grid; gap: .35em; }
.market__history-head > b { color: #f1d6ab; }
.market__history-stats { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: .3em .7em; color: #bca58d; font-size: .82em; }
.market__history-stats > span { display: inline-flex; align-items: center; gap: .25em; }
.market__history-stats b { color: #f1d6ab; }
.market__history-stats .coin { width: 1.2em; height: 1.2em; }
.market__ranges { display: flex; min-width: 0; }
.market__ranges .tabs__tab { flex: 1; min-width: 0; padding-inline: .2em; }
.market__chart { position: relative; display: grid; gap: .3em; grid-template-rows: auto minmax(0, 1fr) auto; height: 10em; min-width: 0; border: 1px solid #493727; background: #231b15; }
.market__chart:has(> .note-line)::before { content: ""; position: absolute; inset: .7em; opacity: .45; background: repeating-linear-gradient(to bottom, transparent 0, transparent 1.8em, #70543b 1.86em, transparent 1.92em); border-left: 1px solid #70543b; border-bottom: 1px solid #70543b; pointer-events: none; }
.market__chart > .note-line { place-self: center; text-align: center; }
.market__sell { display: grid; flex: 1; grid-template-columns: minmax(0, 1fr) 23em; grid-template-rows: minmax(0, 1fr) auto; gap: .8em; min-height: 0; min-width: 0; }
.market__sell > [data-book-panel] { grid-column: 1 / -1; }
.market [data-sell-bag] { display: grid; gap: .4em; grid-template-rows: auto auto minmax(0, 1fr); min-height: 0; min-width: 0; }
.market__fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5em; min-width: 0; }
.market__exchange { display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: .7em; align-content: start; min-height: 0; min-width: 0; }
.market__exchange-intro, .market__market-info, .market__order { display: grid; gap: .6em; align-content: start; min-width: 0; }
.market__lumi-sides { gap: .6em; }
.market__lumi-sides .tabs__tab { display: flex; align-items: center; justify-content: flex-start; gap: .7em; min-height: 3.8em; padding-inline: 1em; border: 1px solid #594332; border-radius: .35em; background: #2b211a; }
.market__lumi-sides .tabs__tab.is-on { padding-inline: 1em; border-color: #dbaf74; background: #483425; box-shadow: inset 3px 0 #e3bb7d; color: #fff1dc; text-shadow: none; }
.market__lumi-sides .coin { width: 2em; height: 2em; }
.market__exchange-columns { display: grid; grid-template-columns: minmax(0, 1fr) 19em; gap: .8em; min-width: 0; }
.market__lumi-heading { display: flex; align-items: center; gap: .5em; min-height: 1.7em; }
.market__lumi-heading b { font-size: 1.1em; }
.market__order, .market__depth .well, .market [data-lumi-chart] { padding: .7em .8em; border: 1px solid #594332; border-radius: .4em; background: #30251e; }
.market__depth .well__title { display: flex; align-items: center; gap: .4em; margin-bottom: .35em; }
.market__lumi-kind { display: flex; align-items: center; justify-content: space-between; gap: .4em; min-width: 0; }
.market__lumi-field { position: relative; min-width: 0; }
.market__lumi-field > .market__help { position: absolute; right: .2em; top: .1em; }
.market__lumi-field-head { display: flex; align-items: center; gap: .5em; }
.market__help { display: inline-grid; place-items: center; flex: none; width: 1.65em; height: 1.65em; min-height: 0; padding: 0; border: 1px solid #98734e; border-radius: 50%; background: #211a15; color: #e8c698; font: 700 .85em/1 sans-serif; cursor: var(--cur-hand, pointer); }
.market__help:hover, .market__help[aria-expanded="true"] { background: #b58a5b; border-color: #ebc995; color: #21150d; }
.market__help:focus-visible { outline: 2px solid #ebc995; outline-offset: 2px; }
.market__order .kv dt .market__help { margin-left: .5em; vertical-align: middle; }
.market__help-tip { position: fixed; z-index: 300; width: min(19em, calc(100vw - 16px)); padding: .7em .85em; border: 1px solid #cba16b; border-radius: .4em; background: #211912; box-shadow: 0 .6em 1.5em #000b; color: #f4e4ce; font-size: 13px; line-height: 1.35; pointer-events: none; }
.market__help-tip b, .market__help-tip span { display: block; }
.market__help-tip b { color: #f3cb91; font-size: 1.08em; margin-bottom: .25em; }
.market__depth { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8em; min-width: 0; }
.market [data-pane][hidden], .market [data-sell-side][hidden], .market [data-book-panel][hidden], .market [data-gear-panel][hidden], .market [data-detail-head][hidden], .market [data-ranges][hidden] { display: none; }

@container marketplace (max-width: 62em) {
  .market__grid { grid-template-columns: minmax(0, 1fr) minmax(21em, 27em); grid-template-rows: auto minmax(0, 1fr); }
  .market [data-categories-panel] { grid-column: 1 / -1; grid-template-rows: auto auto; padding: 0 0 .5em; border-right: 0; border-bottom: 1px solid #574331; }
  .market [data-categories-panel] > .scroll { min-height: 0; overflow-x: auto; overflow-y: hidden; }
  .market [data-categories] { display: flex; gap: .3em; width: max-content; }
  .market [data-categories] > .item-row { flex: none; width: auto; min-width: 9em; }
  .market [data-categories] > .market__cat-l1, .market [data-categories] > .market__cat-l2 { display: none; }
  .market [data-categories-panel] { grid-template-rows: auto auto auto auto; }
  .market__substrip:not([hidden]) { display: block; min-height: 0; overflow-x: auto; overflow-y: hidden; }
  .market__substrip > .rows { display: flex; gap: .3em; width: max-content; }
  .market__substrip .item-row { flex: none; width: auto; min-width: 7em; min-height: 2.2em; padding: .3em .5em; border: 0; border-radius: .3em; background: transparent; color: #d6b18c; }
  .market__substrip .item-row:hover { background: #382b21; }
  .market__substrip .item-row.is-on { background: #453527; color: #fff1dc; box-shadow: inset 0 -2px #e3bb7d; }
  .market__substrip .item-row__end { color: #baa58b; }
  .market__sides-note { display: none; }
  .market__sell { grid-template-columns: minmax(0, 1fr) minmax(17em, 23em); }
  .market__exchange-columns { grid-template-columns: minmax(0, 1fr) minmax(16em, 19em); }
  .market__depth { grid-template-columns: minmax(0, 1fr); }
}

@container marketplace (max-width: 48em) {
  .market__tools { grid-template-columns: minmax(0, 1fr) minmax(10em, 12em); }
  .market__tools > .cap { display: none; }
  .market__grid, .market__sell, .market__exchange-columns { grid-template-columns: minmax(0, 1fr); }
  .market__grid { grid-template-rows: auto auto auto; }
  .market__sell { grid-template-rows: auto; }
  .market__exchange { grid-template-rows: auto; }
  .market > .win__body { display: block; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
  .market > .win__body > .tabs { position: sticky; top: 0; z-index: 3; flex-wrap: wrap; background: var(--win-face); }
  .market > .win__body > .tabs .tabs__tab { min-width: 8em; min-height: 44px; height: auto; white-space: normal; }
  .market__browse { display: block; }
  .market__browse > :not([hidden]) + :not([hidden]) { margin-top: .7em; }
  .market__results > .scroll, .market [data-sell-bag] > .scroll,
  .market__exchange, .market [data-pane="mine"] { min-height: 0; overflow: visible; }
  .market [data-sell-bag] { grid-template-rows: auto auto auto; }
  .market__inspection { overflow: visible; }
  .market__depth { grid-template-columns: minmax(0, 1fr); }
  .market .win__foot { flex-wrap: wrap; }
  .market .win__foot [data-note] { min-width: 0; overflow-wrap: anywhere; }
}

@container marketplace (max-width: 30em) {
  .market__tools, .market__fields { grid-template-columns: minmax(0, 1fr); }
  .market__tools > .cap { display: block; }
  .market__exchange-intro .band { flex-wrap: wrap; }
  .market__exchange-intro .band__action { display: grid; grid-template-columns: minmax(0, 1fr) auto; width: 100%; margin-left: 0; }
}
@media (max-width: 80em) {
  /* The desktop window manager stores a pixel left position; recenter when the viewport shrinks. */
  .market {
    left: 50% !important;
    right: auto !important;
    transform: translateX(-50%) !important;
    width: min(80em, calc(100vw - 16px)) !important;
  }
}
@media (max-width: 48em) {
  .market {
    inset: 8px 8px auto 8px !important;
    transform: none !important;
    width: calc(100vw - 16px) !important;
    max-width: none;
    height: calc(100dvh - 16px) !important;
    max-height: calc(100dvh - 16px);
  }
}
.gth { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: 30em; max-width: calc(100vw - 1.867em); }
.cmp { position: fixed; left: 50%; top: 14%; transform: translateX(-50%); width: 30em; max-width: calc(100vw - 1.867em); }
.ml { position: fixed; left: 50%; top: 4%; transform: translateX(-50%);
  width: 62em; max-width: calc(100vw - 1.867em); height: min(53em, 92dvh); }
.ml .win__body { display: flex; flex-direction: column; min-height: 0; gap: .7em; }
.ml__bar { display: flex; align-items: center; justify-content: space-between; gap: 1em; flex: none; }
.ml .win__body > .ml__bar .tabs { margin: 0; }
.ml__bar .band { min-width: 0; }
.ml__inbox { display: grid; grid-template-columns: minmax(17em, 34%) minmax(0, 1fr);
  gap: .8em; min-height: 0; flex: 1; }
.ml__side { display: flex; flex-direction: column; gap: .55em; min-width: 0; min-height: 0; }
.ml__list { min-height: 0; flex: 1; }
/* Prev / next arrows round "Page n / N" under the letters. The sprite has one chevron, pointing
   down, so each arrow turns it, as the season road does. `:not([hidden])` because a bare
   `display` would show the pager again on a one-page box. */
.ml__pager:not([hidden]) { display: flex; align-items: center; justify-content: center; gap: .9em; flex: none; }
.ml__pager-at { min-width: 6em; text-align: center; color: var(--ink-mute); font-size: var(--fs-small); font-variant-numeric: tabular-nums; }
.ml__pager [data-step="-1"] .ic { transform: rotate(90deg); }
.ml__pager [data-step="1"] .ic { transform: rotate(-90deg); }
.ml__letters { display: grid; gap: .45em; padding: .15em; }

/* Social > Daily: one allowance a row. The end column is two fixed tracks - "used / ceiling"
   right-aligned in tabular figures, then the status chip - so every row's figures and chips stand
   in the same two columns down the list. The chip is one shape in three colours, all read off
   tokens, so the dark tone (html[data-tone="dark"]) needs no rules of its own. */
.so-day .item-row { min-height: 2.4em; grid-template-columns: minmax(0, 1fr) auto; }
/* No picture column on these rows, so the name takes the shrinking track; a long zone name wraps
   under itself rather than running into the figures on a phone. */
.so-day .item-row__name { min-width: 0; }
.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-small); color: var(--ink-mute); }
.so-day .so-day__head .so-day__n { white-space: nowrap; }
.so-day__tk--none { color: var(--ink-mute); }
/* No live Ticket: what it would add, dimmed behind the sprite's lock - a promise, not a count. */
.so-day__tk--locked { color: var(--ink-mute); opacity: .75; }
.so-day__tk--locked .ic { width: .9em; height: .9em; vertical-align: -.1em; }
.so-day__lv { color: var(--ink-mute); }
.so-day__lv--under { color: var(--error); }
.so-day__n { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.so-day__st { justify-self: start; padding: .1em .65em; border-radius: 1em;
  font-size: var(--fs-small); font-weight: 700; 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); }
.ml__letters .item-row { min-height: 4.3em; border: 1px solid var(--field-border);
  border-radius: .4em; background: var(--paper); padding: .55em; }
.ml__letters .item-row:hover { background: var(--paper-2); }
.ml__letters .item-row.is-on { border-color: var(--tan-rim);
  background: color-mix(in srgb, var(--tan) 16%, var(--paper)); }
.ml__letters .item-row > .ic { width: 2.3em; height: 2.3em; padding: .45em;
  border: 1px solid var(--tan-rim); border-radius: 50%; color: var(--tan); }
.ml__letters .item-row__name { gap: .25em; }
.ml__letters .item-row__sub { white-space: normal; }
.ml__letters .item-row__end { font-size: var(--fs-small); }
.ml__reading { display: flex; flex-direction: column; min-width: 0; min-height: 0;
  padding: 1.45em 1.7em; border: 1px solid var(--field-border); border-radius: .45em;
  background: var(--paper); color: var(--ink); }
.ml__empty { display: grid; place-content: center; justify-items: center; gap: .6em;
  border: 1px solid var(--field-border); border-radius: .45em; color: var(--ink-mute);
  background: var(--paper); text-align: center; }
.ml__empty .ic { width: 2.4em; height: 2.4em; }
.ml__subject { margin: 0; color: var(--ink-strong); font: var(--face-display-weight) 2em/1.1 var(--face-display); }
.ml__sender { margin-top: .25em; color: var(--green); font-weight: 700; }
.ml__date, .ml__deadline { color: var(--ink-mute); font-size: var(--fs-small); }
.ml__deadline { padding-bottom: .8em; border-bottom: 1px solid var(--hairline); }
.ml__body { padding: 1.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__attachment-items .tiles { flex: 1; }
.ml__gift .tiles { grid-template-columns: repeat(4, max-content); }
.ml__back { display: none; }
.ml__compose { display: grid; gap: .7em; min-height: 0; padding: 1.25em 1.5em;
  border: 1px solid var(--field-border); border-radius: .45em; background: var(--paper); }
.ml .win__foot { flex-wrap: wrap; }
.ml .win__foot .note-line { min-width: 0; }
@media (max-width: 48em) {
  .ml.win { height: calc(100dvh - 2em); overflow: hidden; }
  .ml.win > .win__body { overflow: hidden; }
  .ml__bar .band { display: none; }
  .ml__inbox { grid-template-columns: minmax(0, 1fr); }
  .ml__reading { display: none; }
  .ml__empty { display: none; }
  .ml.ml--reading .ml__side { display: none; }
  .ml.ml--reading .ml__reading { display: flex; }
  .ml:not(.ml--reading) .win__foot [data-money],
  .ml:not(.ml--reading) .win__foot [data-goods],
  .ml:not(.ml--reading) .win__foot [data-del],
  .ml:not(.ml--reading) .win__foot [data-all] { display: none; }
  .ml__back { display: inline-flex; align-items: center; gap: .3em; align-self: flex-start;
    margin: 0 0 .7em; padding: .2em; border: 0; background: transparent; color: var(--tan);
    font: inherit; cursor: var(--cur-hand, pointer); }
  .ml__back .ic { width: 1.3em; height: 1.3em; transform: rotate(90deg); }
  .ml__attachment-items .tiles, .ml__compose .tiles { grid-template-columns: repeat(4, max-content); }
  .ml .win__foot { gap: .3em; }
}
/* The next level gift under the letters (mail.ts): a promise, so it must not read as an attachment.
   A dashed gold rim on a faint gold wash, its own sparkle head, and the tiles dimmed - nothing in
   it is owned yet. The margin keeps it off the letter's Take buttons in the foot. */
.ml__gift { display: grid; gap: .45em; margin-top: .3em; padding: .6em .75em .7em;
  border: 1px dashed var(--rarity-gold); border-radius: 8px;
  background: color-mix(in srgb, var(--rarity-gold) 9%, transparent); }
.ml__gift[hidden] { display: none; }
.ml__gift-head { display: flex; align-items: center; gap: .4em; font-size: var(--fs-small); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--rarity-gold-text); }
.ml__gift-head .ic { width: 1.1em; height: 1.1em; fill: currentColor; }
.ml__gift-row { display: flex; align-items: center; gap: 1em; flex-wrap: wrap; min-width: 0; }
.ml__gift-text { display: grid; gap: .1em; min-width: 0; }
.ml__gift-text span { font-size: var(--fs-small); color: var(--ink-mute); }
.ml__gift .tile { opacity: .72; filter: saturate(.75); }
.auc { position: fixed; left: 50%; top: 9%; transform: translateX(-50%); width: 50em; max-width: calc(100vw - 1.867em); }
.stall { position: fixed; left: 50%; top: 9%; transform: translateX(-50%); width: 44em; max-width: calc(100vw - 1.867em); }
/* The trade window stands just LEFT OF THE BAG, the way the warehouse does, never over it: a trade
   is filled by dragging out of the bag, and centred it covered the bag's left half on a laptop and
   the whole of "your offer" on a phone held sideways - the one place the drag has to land
   (2026-09-24). On a screen too narrow for both it slides right until its own left edge is on
   screen. */
.trd {
  position: fixed; top: 10%; width: 46em; max-width: calc(100vw - 1.867em);
  right: max(.933em, min(47.867em, calc(100vw - 46em - .933em)));
}
.ie { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 42em; max-width: calc(100vw - 1.867em); }
/* The door's four facts sit beside a plate in half the window's width, and one of them is a boss
   name. A kv row is a label and a value pushed to opposite ends of a flex line, and a flex line
   does not wrap: the pair was wider than the column, so the column was wider than the window and
   the values were painted outside the frame. Wrapping lets the value fall under its own label
   instead, which is the one way out of this that keeps both words whole. */
.ie .kv > div { flex-wrap: wrap; }
/* And the reason the whole body hung outside the frame, facts included: a grid column is as wide
   as its widest item's MIN-content, and one item here is a six-tab strip of nowrap tabs with a 7em
   floor each - some 60em of unbreakable row. The window was 34em; everything in it was painted to
   the strip's width. The column is pinned to the window now, and the strip wraps to a second line
   instead of pushing. */
.ie .win__body { grid-template-columns: minmax(0, 1fr); }
.ie .tabs--block { flex-wrap: wrap; }
.ie .tabs--text .tabs__tab { min-width: 5.6em; padding: 0 1.1em; }
/* The reward list reads as a shelf, not a scroll: two goods a line, so a materials path of twenty
   entries is half as tall and the window stops running off the screen. Every path's list, not just
   this one. The hairline is dropped from the last PAIR, since two rows now end the list. */
.ie [data-loot] .rows { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: .9em; }
.ie [data-loot] .rows > .item-row:nth-last-child(-n+2):nth-child(odd),
.ie [data-loot] .rows > .item-row:nth-last-child(-n+2):nth-child(odd) + .item-row { border-bottom: 0; }

/* The world map and the social boards. The world map is as wide as its content: the legend and
   the plate, which sizes itself to the screen (world-map.ts setMap), so it stands near the top. */
.wm { position: fixed; left: 50%; top: 3%; transform: translateX(-50%); width: max-content; max-width: calc(100vw - 1.867em); }
/* Where a creature stands, one dot a band, answered by the server when a quest objective is
   pressed (2026-09-13). A ring of light around the ink so the dot reads on sand, water, grass and
   forest alike - the plate is a painting and the marks have to survive all of it. */
.wm-mark {
  position: absolute; width: .6em; height: .6em; transform: translate(-50%, -50%);
  border-radius: 50%; background: #bb6e66;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, .9), 0 1px 2px rgba(60, 25, 12, .55);
}
/* A band still standing in a dungeon run: smaller, and the minimap's red for what fights you. */
.wm-mark--band { width: .45em; height: .45em; background: #d4553a; box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .85), 0 1px 2px rgba(60, 25, 12, .55); }
.so { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: 54em; max-width: calc(100vw - 1.867em); }
/* Direct Message. The one window a player keeps open WHILE playing, so it stands out of the way
   in the bottom-right corner rather than over the middle of the world - and it stands directly
   over its own folded heads, so putting a conversation away and taking it back out is one place
   on screen rather than two.

   What it clears is the bottom-right corner the HUD owns: 10px of edge, the 44px menu row, and
   the 53px row of folded heads over it, plus 8px of air. Those are apps/game/public/hud.css's own pixels
   (--hud-edge, --hud-dm-row, and .talk-head at 3.4em of the HUD's 14px), restated here as
   literals rather than read across, which is this sheet's rule and the reason it survives
   hud.css not loading. They are scaled by the same --gs-hud-scale the HUD is. */
.dm {
  position: fixed; right: 1.2em;
  bottom: calc(115px * var(--gs-hud-scale, 1));
  width: 44em; max-width: calc(100vw - 1.867em);
}
/* Two rows of menu tiles at this width, so the corner is 41px deeper. Same breakpoint hud.css
   uses for .hud-menu, and it has to be restated for the same reason the numbers above are. */
@media (min-width: 761px) and (max-width: 1300px), (hover: none) and (pointer: coarse) and (min-width: 761px) {
  .dm { bottom: calc(156px * var(--gs-hud-scale, 1)); }
}
.gu { position: fixed; left: 50%; top: 6%; transform: translateX(-50%); width: 58em; max-width: calc(100vw - 1.867em); }
.gf { position: fixed; left: 50%; top: 14%; transform: translateX(-50%); width: 28em; max-width: calc(100vw - 1.867em); }

/* Nothing in the party rail itself is placed: the gear, both menus and every frame are rows of
   the rail's own grid, so a menu opens under the thing it belongs to with no measured offset.
   The one question the party asks is placed, over every window, at the dialog layer. */
.pty-ask { position: fixed; left: 50%; top: 22%; transform: translateX(-50%); width: 26em; z-index: 80; }

/* Seven day tiles to a row with no names under them want no more than this; a phone takes the
   whole width instead (below), where seven 30px tiles still fit a 360px screen. */
.sgn { position: fixed; left: 50%; top: 6%; transform: translateX(-50%); width: 36em; max-width: calc(100vw - 1.867em); }

/* The gacha window (panels/gacha.ts). The pity is a blue energy bar (owner, 2026-09-26): one step a
   pull, full on the pull that guarantees the grand prize, with a notch where soft pity starts and a
   glow once the count has reached it - the bar says "close" before the number does. */
.gch { position: fixed; left: 50%; top: 6%; transform: translateX(-50%); width: 34em; max-width: calc(100vw - 1.867em); }
.gch__banner { display: block; width: 100%; aspect-ratio: 1600 / 512; object-fit: cover; border-radius: .5em;
  box-shadow: 0 0 0 1px var(--win-rim), 0 .3em .8em rgba(0, 0, 0, .35); }
.gch__head { display: flex; align-items: center; justify-content: space-between; gap: .6em; }
.gch__head > span { display: flex; align-items: center; gap: .4em; }
.gch__show { display: flex; flex-wrap: wrap; gap: .4em; align-items: center; }
.gch__pity { display: grid; gap: .3em; }
.gch__pity[hidden] { display: none; }
.gch__pity-row { display: flex; justify-content: space-between; }
.gch__pity-bar { position: relative; }
.gch__pity-bar .bar { height: .9em; }
.gch__soft { position: absolute; top: -.15em; bottom: -.15em; width: 2px; border-radius: 1px; background: #f4e3a6;
  box-shadow: 0 0 4px rgba(255, 214, 110, .9); pointer-events: none; }
.gch__soft[hidden] { display: none; }
.gch__pity-bar .bar.is-hot { box-shadow: 0 0 .5em rgba(96, 170, 255, .75); animation: gch-hot 1.4s ease-in-out infinite; }
@keyframes gch-hot { 50% { box-shadow: 0 0 1em rgba(120, 190, 255, 1); } }
.gch__pull { display: flex; gap: .6em; justify-content: center; }
.gch__odds { max-height: 14em; display: grid; gap: .2em; }
.gch__row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .5em; font-size: .9em; }
.gch__row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gch__row-pct { font-variant-numeric: tabular-nums; color: var(--ink-mute); }
.gch__won { display: grid; grid-template-columns: repeat(auto-fill, minmax(4.4em, 1fr)); gap: .6em; }
.gch__won-one { display: grid; justify-items: center; gap: .25em; text-align: center; font-size: .85em; }
/* The pull animation's canvas (gacha-fx.ts): over every window, under the prize dialog it opens. */
.gch-fx { position: fixed; inset: 0; z-index: 200; width: 100vw; height: 100vh; cursor: pointer; }
.gch__won-one.is-grand .tile { box-shadow: 0 0 .6em 2px rgba(255, 206, 90, .9); }
@media (prefers-reduced-motion: reduce) { .gch__pity-bar .bar.is-hot { animation: none; } }
/* Profile and activity. */
.pf {
  position: fixed; left: 50%; top: 4%; transform: translateX(-50%);
  width: 42em; max-width: calc(100vw - 1.867em);
}
.act { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: 34em; max-width: calc(100vw - 1.867em); }
/* The Tamer Ticket window had no rule at all and so sat in the page flow under the game, not over
   it. The activity window's size and place: both are one column of short rows. */
.tp { position: fixed; left: 50%; top: 10%; transform: translateX(-50%); width: 34em; max-width: calc(100vw - 1.867em); }
/* The Battle Pass. Wider than the activity window and shorter than the sign-in's calendar: it
   carries a row of purchase buttons, five mission lines and a hundred rungs in a scroller, and
   the rung is a three-column grid - a level, its Free reward and its Premium one - which reads as
   a ladder only while the two columns stay narrow enough to be one glance apart. Without a rule
   here it fell back to the unplaced default and lay across the whole bottom of the screen with
   its rungs a hand's width apart. */
.bps { position: fixed; left: 50%; top: 6%; transform: translateX(-50%); width: 70em; max-width: calc(100vw - 1.867em); }

/* THE BOARD AND THE GRAND PRIZE (owner, 2026-09-24, after a reference season pass). The window is
   two columns: the road on the left, the level-100 costume standing on the right at full height,
   worn by the player's own figure. Under both, one foot with the pass and "Claim all". */
.bps__layout { display: grid; grid-template-columns: minmax(0, 1fr) 14em; gap: .8em; align-items: stretch; }
.bps__main { display: grid; gap: .7em; align-content: start; min-width: 0; }

/* The head: medallion, meter, and the two things a player does from here. The buttons sit on the
   BAR's line, which is what they act on. */
.bps__head { display: flex; align-items: center; gap: .9em; flex-wrap: wrap; }
.bps__medal {
  flex: none; display: grid; justify-items: center; gap: .1em; min-width: 3.8em;
  padding: .35em .55em; border-radius: .5em;
  background: rgba(var(--tan-rgb), .18); box-shadow: inset 0 0 0 1px var(--tan-rim);
}
.bps__medal b { font-size: 2.3em; line-height: 1; color: var(--bar-exp); }
.bps__meter { flex: 1; min-width: 14em; display: grid; gap: .35em; }
.bps__actions { flex: none; display: flex; gap: .4em; align-self: end; }

/* The board: the track cards at the left, OUTSIDE the scroller so they stay while the road slides.
   Every row height is fixed here, because the cards cannot measure the rows they name. */
.bps__board { display: grid; grid-template-columns: 6.4em minmax(0, 1fr); gap: .4em; }
.bps__side { display: grid; grid-template-rows: 1.9em 6.6em 6.6em; gap: .4em; }
.bps__track {
  display: grid; justify-items: center; align-content: center; gap: .2em;
  border-radius: .5em; text-align: center; font-size: var(--fs-small); letter-spacing: .06em;
  background: var(--grey-pill); box-shadow: inset 0 0 0 1px var(--line);
}
.bps__track .ic { width: 1.6em; height: 1.6em; color: var(--ink-soft, var(--ink)); }
/* The premium card wears the track's gold, the same gold its rungs' light runs in. */
.bps__track--prem {
  background: linear-gradient(160deg, rgba(236, 196, 90, .35), rgba(201, 162, 39, .12));
  box-shadow: inset 0 0 0 1px rgba(201, 162, 39, .7);
}
.bps__track--prem .ic { color: #b8891c; }
/* The browser's own bar is hidden: the drawn one under the road replaces it (.bps__nav). */
.bps__road { max-width: 100%; overflow-y: hidden; scrollbar-width: none; }
.bps__road::-webkit-scrollbar { display: none; }

/* The drawn scrollbar: arrow, track with a draggable thumb, arrow. The chevron is the sprite's one
   downward chevron turned to point along the road. */
.bps__nav { display: flex; align-items: center; gap: .45em; }
.bps__nav [data-step="-1"] .ic { transform: rotate(90deg); }
.bps__nav [data-step="1"] .ic { transform: rotate(-90deg); }
.bps__bar {
  position: relative; flex: 1; min-width: 0; height: .7em; border-radius: 1em; cursor: pointer;
  background: rgba(var(--tan-rgb), .18); box-shadow: inset 0 0 0 1px var(--line);
  touch-action: none;
}
.bps__thumb {
  position: absolute; left: 0; top: 0; bottom: 0; border-radius: 1em; cursor: grab;
  background: linear-gradient(180deg, #e8c25a, #c9962a); box-shadow: 0 0 0 1px rgba(150, 105, 20, .55);
}
.bps__thumb:active { cursor: grabbing; }
.bps__here { display: flex; justify-content: center; }
.bps__levels { height: 1.9em; align-items: center; justify-items: center; }
.bps__row { height: 6.6em; }

/* The level numbers across the top. Reached ones take the tan, the one you stand on is ringed. */
.bps__lv {
  font-size: var(--fs-small); font-weight: 700; color: var(--ink-mute, var(--ink)); opacity: .7;
  padding: .15em .45em; border-radius: 1em; white-space: nowrap;
}
.bps__lv.is-reached { opacity: 1; color: var(--ink); }
.bps__lv.is-big { color: #a07a14; opacity: 1; }
.bps__lv.is-here { background: var(--tan); color: var(--win-face); opacity: 1; }

/* A rung is its picture. Gold motes rise through it when it can be taken; it dims when it cannot,
   with a corner mark for taken (check) or not bought (padlock). */
.bps__rung {
  position: relative; display: grid; place-items: center; padding: .3em; min-width: 0;
  border-radius: .45em;
}
.bps__art { display: flex; gap: .15em; justify-content: center; }
.bps__rung.is-big { box-shadow: inset 0 0 0 1px var(--bar-exp); }
.bps__rung.is-locked { opacity: .7; }
.bps__rung.is-taken .bps__art { opacity: .55; }
.bps__rung.is-can { cursor: pointer; }
.bps__rung.is-can:hover { filter: brightness(1.08); }
.bps__rung.is-can:focus-visible { outline: 2px solid #e0b64a; outline-offset: 1px; }

/* THE MOTES (owner, 2026-09-24: "border hijau ganti particle emas"). Ten points of gold light
   rise from the foot of a claimable rung and fade before its top, each on its own lane, size,
   speed and delay so they never move as one. A deep amber core under a pale centre, so a mote
   reads on the cream paper as well as on the dark tone. Transform and opacity only - the two
   things a compositor animates without a repaint, which matters on a board that can light forty. */
.bps__spark { position: absolute; inset: 0; z-index: 1; overflow: hidden; border-radius: inherit; pointer-events: none; }
.bps__spark i {
  position: absolute; bottom: -.4em; left: var(--x); width: var(--s); height: var(--s); border-radius: 50%;
  background: radial-gradient(circle, #fffbe0 0 20%, #f5c542 45%, #d9960f 62%, rgba(200, 130, 10, 0) 74%);
  box-shadow: 0 0 .45em .08em rgba(232, 168, 30, .8);
  opacity: 0; animation: bps-mote var(--d) ease-in var(--w) infinite;
}
.bps__spark i:nth-child(1)  { --x: 10%; --s: .5em;  --d: 2.6s; --w: 0s;    --dx: .3em; }
.bps__spark i:nth-child(2)  { --x: 28%; --s: .36em; --d: 2.1s; --w: -.9s;  --dx: -.25em; }
.bps__spark i:nth-child(3)  { --x: 45%; --s: .58em; --d: 3s;   --w: -1.7s; --dx: .2em; }
.bps__spark i:nth-child(4)  { --x: 62%; --s: .4em;  --d: 2.3s; --w: -.4s;  --dx: -.3em; }
.bps__spark i:nth-child(5)  { --x: 80%; --s: .48em; --d: 2.8s; --w: -2.2s; --dx: .25em; }
.bps__spark i:nth-child(6)  { --x: 20%; --s: .3em;  --d: 1.9s; --w: -1.3s; --dx: .15em; }
.bps__spark i:nth-child(7)  { --x: 72%; --s: .34em; --d: 2.4s; --w: -.2s;  --dx: -.15em; }
.bps__spark i:nth-child(8)  { --x: 54%; --s: .3em;  --d: 2s;   --w: -1.1s; --dx: -.2em; }
.bps__spark i:nth-child(9)  { --x: 36%; --s: .44em; --d: 2.7s; --w: -2.5s; --dx: .1em; }
.bps__spark i:nth-child(10) { --x: 88%; --s: .32em; --d: 2.2s; --w: -1.6s; --dx: -.25em; }
@keyframes bps-mote {
  0%   { transform: translate(0, 0) scale(.5); opacity: 0; }
  18%  { opacity: 1; }
  65%  { opacity: .9; }
  100% { transform: translate(var(--dx), -6.2em) scale(1); opacity: 0; }
}
/* Still, the motes stay as scattered points of light, so a claimable rung is still told apart. */
@media (prefers-reduced-motion: reduce) {
  .bps__spark i { animation: none; opacity: .85; bottom: auto; top: 30%; }
  .bps__spark i:nth-child(odd) { top: 62%; }
}
.bps__mark {
  position: absolute; right: .2em; bottom: .2em; display: grid; place-items: center;
  width: 1.25em; height: 1.25em; border-radius: 50%;
  background: var(--win-face); box-shadow: 0 0 0 1px var(--line);
}
.bps__mark .ic { width: .8em; height: .8em; }
.bps__mark--done { background: var(--green); box-shadow: none; }
.bps__mark--done .ic { color: #fff; }
.bps__stop { align-self: center; border-top: 1px dashed var(--line); }

/* The grand prize: a tall card, the level that pays it over its head like a tag, the figure in
   the costume standing on a soft light, and its name under its feet. */
.bps__prize {
  position: relative; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr auto auto; justify-items: center;
  gap: .5em; padding: .8em .7em .7em; border-radius: .6em; min-height: 0;
  background:
    radial-gradient(ellipse 70% 38% at 50% 78%, rgba(236, 196, 90, .38), transparent 70%),
    linear-gradient(180deg, rgba(var(--tan-rgb), .10), rgba(var(--tan-rgb), .24));
  box-shadow: inset 0 0 0 1px rgba(201, 162, 39, .6);
}
.bps__prize[hidden] { display: none; }
.bps__prize-when {
  justify-self: stretch; text-align: center; font-weight: 700; font-size: var(--fs-small);
  padding: .3em .6em; border-radius: 1em;
  background: linear-gradient(90deg, rgba(125, 190, 170, .35), rgba(125, 190, 170, .15));
  box-shadow: inset 0 0 0 1px rgba(80, 150, 130, .45);
}
.bps__prize-stage { display: grid; place-items: center; min-height: 0; }
.bps__prize-stage .doll-box { --doll-w: 7.6em; }
.bps__prize-stage .doll > .tile { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.bps__prize-name { display: grid; justify-items: center; gap: .1em; text-align: center; }
.bps__prize-name strong { font-size: var(--fs-heading); }
.bps__prize [data-prize-act] { justify-self: stretch; text-align: center; }
/* The pair of drawings to choose between (her look, his look). The card is 14em wide and two text
   tabs at their 7em minimum stood wider than it, pushing the tabs and the level tag over its right
   edge; here they split the card between them. */
.bps__prize-pick { display: flex; margin-bottom: .5em; }
.bps__prize-pick .tabs__tab { flex: 1 1 0; min-width: 0; padding: 0 .5em; }
.bps__prize-pick .tabs__tab.is-on { padding: 0 .85em; }

/* The foot: the season's clock on the left, the pass and "Claim all" on the right. */
.bps__foot { display: flex; align-items: center; gap: .6em; flex-wrap: wrap; }
.bps__foot [data-foot-note] { flex: 1; min-width: 10em; }
.bps__foot-btns { display: flex; gap: .5em; flex-wrap: wrap; justify-content: flex-end; }

/* A narrow window stacks the prize over the road, sideways: figure left, words right. */
@media (max-width: 760px) {
  .bps__layout { grid-template-columns: minmax(0, 1fr); }
  .bps__prize { order: -1; grid-template-columns: auto 1fr; grid-template-rows: auto auto auto; justify-items: start; }
  .bps__prize-when { grid-column: 1 / -1; }
  .bps__prize-stage { grid-row: 2 / 4; }
  .bps__prize-stage .doll-box { --doll-w: 4.6em; }
  .bps__prize-name { justify-items: start; text-align: left; align-self: end; }
}

/* THE PREMIUM TRACK'S OWN LIGHT (owner, 2026-09-22; gold 09-23; per-box orbit 09-23).

   The light is on the REWARD BOXES now, not on the row. Two earlier tries died the same way: an
   accent drawn on the row is covered by the rungs, and on a bought pass every rung wears the green
   "claimable" outline and its own face, so the mark for the premium track vanished exactly when
   that track is in use. A box carries its own light and nothing can sit in front of it.

   The mechanic is a conic gradient spun by `transform` - the one property a compositor animates
   without repainting - on a square wider than the box, clipped to the box's own rounded corners.
   The face is painted by a second pseudo-element inset over it, so all that survives is a band of
   light running the perimeter. Both pseudos are at z-index -1: that paints them over the element's
   own background and UNDER its contents, which is what keeps the tile, the count and the button
   readable without giving any of them a stacking context of their own. */
.bps [data-prem] > .well {
  position: relative;
  /* ISOLATION IS LOAD-BEARING. A z-index of -1 is only "behind my contents" when the element owns a
     stacking context; without one the pseudo joins the panel's context and paints behind the
     panel's own face, which is how the first attempt at this rendered nothing at all. */
  isolation: isolate;
  overflow: hidden;                 /* the spinning square is clipped to the rounded box */
  background: transparent;          /* the face moves to ::after, or it would hide the light */
}
.bps [data-prem] > .well::before {
  content: ""; position: absolute; z-index: -1;
  left: 50%; top: 50%; width: 150%; aspect-ratio: 1;
  /* One bright head with a tail behind it, and a second dimmer one opposite, so the box never
     looks unlit on the side the light has left. */
  background: conic-gradient(from 0deg,
    rgba(255, 244, 200, 1) 0 3%,
    rgba(236, 196, 90, .75) 8%,
    rgba(201, 162, 39, .28) 16%,
    transparent 28% 47%,
    rgba(214, 172, 60, .45) 52%,
    transparent 60% 100%);
  animation: bps-orbit 3s linear infinite;
}
/* The face. Inset far enough to clear the green outline a claimable rung wears (2px, offset -1),
   so the two signals stack instead of fighting: green says take me, gold says premium. */
.bps [data-prem] > .well::after {
  content: ""; position: absolute; z-index: -1; inset: 3px;
  border-radius: .35em; background: var(--grey-pill);
  /* A thin steady rim under the moving light: a premium box reads as premium even in the instant
     the head of the light is on its far side. */
  box-shadow: 0 0 0 1px rgba(201, 162, 39, .55);
}
@keyframes bps-orbit {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}
/* The rail that names the track keeps the ink every other rail wears: the light is the signal. */
@media (prefers-reduced-motion: reduce) {
  .bps [data-prem] > .well::before { animation: none; }
}
/* Centred on both axes and clipped to the viewport: this window grows a section at a time, and a
   fixed `top` ran each new one further down the screen until the last was off the bottom. */
.opt {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 44em; max-width: calc(100vw - 1.867em); max-height: min(46em, calc(100vh - 3.733em));
}

/* The skill panel and the skill card that follows the pointer out of it. The talents page is a
   three-lane tree and wants more room than the skill list; it follows the tab, so the phone block
   below can take both widths off with one rule. */
.sp { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: 44em; }
.sp[data-tab="belief"] { width: 52em; }
/* While a skill is being dragged out of it the window gets out of the way: see-through, and the
   pointer passes to the hotbar it may be covering (skillpanel.ts is-lending). */
.sp.is-lending { opacity: .22; pointer-events: none; transition: opacity .12s; }

/* EVERY SCREEN FITS ITS WINDOWS (owner, 2026-09-26). The widths above are a laptop's (up to 72em,
   1008px at 14px) and nothing capped a window's height anywhere but on a touch screen, so a small
   laptop (1366x768, 1280x720) or a narrow browser window ran panels off the right and the bottom
   with their buttons. No window is wider than the screen, and on a short or narrow screen none is
   taller than it either: the BODY scrolls, so the head stays in reach to drag by, and panel.ts
   fitOnScreen puts a window that opens past an edge back inside. The marketplace keeps its own
   inner scrollers. The lobby's windows are pages, not floating panels, and are left to shell.css. */
.win:not(.shell .win) { max-width: calc(100vw - 16px); }
@media (max-height: 900px), (max-width: 1366px) {
  .win:not(.shell .win) { max-height: calc(100dvh - 32px); }
  .win:not(.shell .win) > .win__body { min-height: 0; overflow-y: auto; }
  .market > .win__body { overflow-y: visible; }
}

/* A PHONE HELD SIDEWAYS keeps the desktop positions above, and has 400px of height to put them in.
   No window may be taller than the screen less a gutter each end, and its body is the scroller so
   the head stays in reach to drag it by (panel.ts keepOnScreen puts it back inside on open). The
   marketplace keeps its own inner scrollers, as above. */
@media (hover: none) and (pointer: coarse) and (min-width: 761px) {
  .win { max-height: calc(100dvh - 32px); }
  .win > .win__body { min-height: 0; overflow-y: auto; }
  .market > .win__body { overflow-y: visible; }
  /* One scroller per window on a touch screen: a list scrolling inside a body that scrolls too is
     a list a thumb drags the wrong one of. The shop body is the scroller. */
  .shop__barter { max-height: none; }
  /* The gacha window the same way: its odds list is the body's to scroll, and the machine's name
     line goes - the tab above the banner already says which machine this is, and a sideways phone
     has no height to say it twice. The purse stays: it is what a pull is paid from. */
  .gch__odds { max-height: none; }
  .gch__head { justify-content: flex-end; }
  .gch__head > strong { display: none; }
  /* A floating card - a tooltip, the menu a body or a row opens - sets no size and so read at the
     page's 15px, a third bigger than the window it came out of. It reads at the window size. */
  .card { font-size: var(--win-size); max-height: calc(100dvh - 16px); overflow: hidden; }
  /* A worn piece's card was still two thirds of the screen at that size: a star row set in fixed
     px by gs.css (16px a star, 34px tall), a 19px line per stat and six socket lines. The stars
     come down to the card's own type, the stat and socket lines lose most of their air, and the
     unpunched sockets say it once instead of up to six times. `[data-stars]` so this outranks
     gs.css's tier rules, which name up to three classes. */
  .card .gs-tooltip__stars[data-stars] { min-height: 0; }
  .card .gs-tooltip__stars[data-stars] i { font-size: 10px; letter-spacing: 1px; padding-block: 1px; }
  .card .gs-tooltip__stars[data-stars] .gs-star { margin-right: 1px; }
  /* The two-row rating that used to live here is gs.css's now: the card is 17em on every screen,
     so past ten stars it never fitted one row anywhere. Only the type size below is about phones. */
  .card .kv > div { padding-block: .15em; }
  .card .item-row { padding-block: .15em; }
  .card .item-row--empty ~ .item-row--empty { display: none; }}

/* ================================================================= MOBILE SHEETS
   Desktop windows keep their authored positions above 760px. On a narrow screen every substantial
   window becomes one viewport-safe sheet: no panel may depend on a desktop left/right offset, and
   tall content scrolls inside the sheet instead of disappearing below the gesture bar. A phone held
   sideways is laid out wide (orientation.ts) and keeps the desktop positions, scaled with the page. */
@media (max-width: 760px) {
  :root { --place-gutter: .4em; }

  .bag, .warehouse, .cs, .ws, .ql, .pt, .ps, .pr, .pd, .shop, .market, .gch,
  .gth, .cmp, .ml, .auc, .stall, .trd, .ie, .wm, .so, .gu, .dm, .pf, .gf, .tp,
  /* `.pd.pd--fusion` is two classes against this list's one, so the fusion tab's 58em would win
     here and pin a phone. Said again at equal specificity rather than by weakening the desktop rule. */
  .pd.pd--fusion { width: auto; }
  .sp, .sp[data-tab="belief"] { width: auto; }
  .pty-ask, .sgn, .act, .bps, .opt, .dlg, .dlg-svc {
    left: max(var(--place-gutter), env(safe-area-inset-left));
    right: max(var(--place-gutter), env(safe-area-inset-right));
    top: max(var(--place-gutter), env(safe-area-inset-top));
    bottom: auto;
    width: auto;
    min-width: 0;
    max-width: none;
    max-height: calc(100dvh - max(.8em, env(safe-area-inset-top)) - max(.8em, env(safe-area-inset-bottom)));
    height: auto;
    transform: none;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* Panels commonly opened together share one sheet position. The newest one already wins through
     the panel stack, so keeping the desktop side-by-side offsets would only clip both. */
  .warehouse, .ps { right: max(var(--place-gutter), env(safe-area-inset-right)); }

  /* The guild panel is the tallest of them and starts higher up the screen for it. */
  .gu { top: 2%; max-height: 96vh; overflow-y: auto; }
  .market { max-height: calc(100dvh - .8em); }

  /* The party rail cannot hold five frames on a phone: past 30dvh the list scrolls inside itself
     rather than pushing chat off the bottom. Found by its hook and not by a class, because the
     rail is the design system's `.rail` now and that name is also the player's own block. */
  [data-party-rail] { max-height: 30dvh; overflow-x: hidden; overflow-y: auto; }
}

/* In a viewport too narrow or short for nested columns, the Marketplace body is the one vertical
   scroller. The title and balances stay in the window while every pane remains reachable. */
@media (max-width: 760px), (max-height: 560px) {
  .market { overflow: hidden; }
  .market > .win__body { display: block; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scroll-padding-top: 6em; }
  .market > .win__body > .tabs { position: sticky; top: 0; z-index: 3; flex-wrap: wrap; background: var(--win-face); }
  .market > .win__body > .tabs .tabs__tab { min-width: 8em; min-height: 44px; height: auto; padding-block: .35em; white-space: normal; text-align: center; }
  .market__browse { display: block; }
  .market__browse > :not([hidden]) + :not([hidden]) { margin-top: .7em; }
  .market__grid, .market__sell { min-height: 0; }
  .market__sell { grid-template-rows: auto; }
  .market__exchange { grid-template-rows: auto; }
  .market__results > .scroll, .market [data-sell-bag] > .scroll,
  .market__exchange, .market [data-pane="mine"] { min-height: 0; overflow: visible; }
  .market__inspection { overflow: visible; }
  .market [data-sell-bag] { grid-template-rows: auto auto auto; }
  .market .win__foot { flex-wrap: wrap; padding-top: .35em; }
  .market .win__foot [data-note] { min-width: 0; overflow-wrap: anywhere; }
  .market .item-row, .market .btn, .market .select__trigger { min-height: 44px; }
  .market [data-chart] > div, .market [data-lumi-chart] > div { flex-wrap: wrap; }
}

@media (max-width: 760px) {
  .market {
    height: calc(100dvh - max(.8em, env(safe-area-inset-top)) - max(.8em, env(safe-area-inset-bottom)));
    max-height: calc(100dvh - max(.8em, env(safe-area-inset-top)) - max(.8em, env(safe-area-inset-bottom)));
  }
}

@media (max-width: 420px) {
  .market__tools, .market__fields { grid-template-columns: minmax(0, 1fr); }
  .market__tools > .cap { display: block; }
}


/* --- the price line ---------------------------------------------------------------------------
   THE ONE THING IN THE GAME THE DESIGN SYSTEM DOES NOT DRAW. The Marketplace's chart is an inline
   SVG the panel builds itself (marketplace.ts drawChart): a line, a soft fill under it and a
   dashed grid. These five rules used to live in public/panels.css and were the last thing the
   market still read out of the old stone sheet; they are restated here against the design's own
   hues - the sage and rose the buttons wear - so the sheet could go.

   The scale and the date row are NOT positioned here: the chart's well is a three-row grid and
   they are the first and last row of it. The SVG only has to fill the middle one. */
.market-chart { width: 100%; height: 100%; min-height: 0; overflow: visible; }
.market-chart__grid { stroke: var(--rule, #d8c6b2); stroke-width: 1; stroke-dasharray: 3 5; opacity: .55; }
.market-chart__line { fill: none; stroke: #7d946e; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.market-chart__area { fill: #7d946e; opacity: .13; stroke: none; }
.market-chart.is-down .market-chart__line { stroke: #bb6e66; }
.market-chart.is-down .market-chart__area { fill: #bb6e66; }

/* 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 the hexagon clips it for free. The angle is a
   registered custom property because a conic gradient cannot be rotated by `transform` without
   turning the element itself. Lives here because it is one
   button's motion, not a system token. */
@property --btn-orbit {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.btn--charged {
  background:
    conic-gradient(from var(--btn-orbit),
      rgba(255, 244, 200, 1) 0 3%,
      rgba(236, 196, 90, .85) 9%,
      rgba(201, 162, 39, .25) 18%,
      transparent 30% 50%,
      rgba(214, 172, 60, .5) 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; } }
/* The world map's pointer readout: the tile under the cursor, riding just above and right of it.
   It takes no clicks, so the walk the click asks for still lands on the plate. */
.wm-coord {
  position: absolute; z-index: 3; pointer-events: none;
  transform: translate(12px, calc(-100% - 6px));
  padding: .1em .45em; border-radius: .25em;
  background: rgba(18, 13, 9, .85); color: #f3e3c3;
  font-size: .85em; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.wm-coord[hidden] { display: none; }
