/* bryce.today — /game/ additions only.
   Loaded after /styles.css, which already provides the desk, the book, the
   parchment page, the ribbon tabs, buttons, fields, notes, the seal, the
   panel and every mobile-first layout rule. Nothing from there is repeated
   here; this file is only what the game pages need on top.

   Mobile-first like the shared sheet: no fixed widths, no hover-only
   affordances, 44px (2.75rem) minimum targets via --tap, and the one
   min-width query below only adds breathing room. */

/* ---- page frame ---------------------------------------------------------- */
/* Reserve the scrollbar's gutter even while nothing scrolls, so hopping
   between a short section (World) and a tall one (Account) never shifts the
   whole page sideways when the bar pops in. The reserved strip belongs to
   <html>, which body's desk texture does not reach — so html wears the same
   woodgrain (both tile from the viewport origin, so the seam is invisible)
   and the desk runs edge to edge, scrollbar or not. */
html {
  scrollbar-gutter: stable;
  background-color: var(--desk-base);
  background-image: var(--desk);
}

/* The wash behind duty cards, the duty page and inventory rows. 0.45 is the
   shipped default (20 points more opaque than the original 0.25); the
   Settings slider overrides it through this custom property, and
   /game/prepaint.js restores the saved value before first paint. */
:root { --panel-alpha: 0.45; }

/* The Energy resource's fixed amber — see .bar-energy below. */
:root { --energy: #cc8a1e; }

/* Health's fixed oxblood — same rule as Energy: its own constant, never the
   accent, so no cover/page/accent choice can recolour the resource. The
   heart icon (icon-health.svg) carries the same value. */
:root { --health: #a33b2e; }
/* The ink a warning speaks in. Light papers borrow the health red — the
   established "this hurt" colour — but that red sits at 2.4:1 against the
   dark papers, so there it lifts to a tint that clears 4.5:1. Its own token
   rather than --health itself, which is a bar fill and must not move. */
:root { --warn-ink: #a33b2e; }
html.mode-dark { --warn-ink: #e8867a; }

/* On game pages the AXIS washes follow the opacity slider too — the
   coloured Mastery cells, tinted duty cards and ledger entries. theme.css
   fixes these at 0.10 for the rest of the site; 0.25 × the 0.45 default
   lands on that same ~0.11, so the shipped look is unchanged, but the
   slider now moves these panels with everything else. Dark mode's brighter
   --axis-*-rgb values flow through automatically. The neutral wash (multi-
   stat cards) scales the same way. */
:root {
  --axis-force-wash:         rgba(var(--axis-force-rgb), calc(var(--panel-alpha) * 0.25));
  --axis-understanding-wash: rgba(var(--axis-understanding-rgb), calc(var(--panel-alpha) * 0.25));
  --axis-endurance-wash:     rgba(var(--axis-endurance-rgb), calc(var(--panel-alpha) * 0.25));
  --axis-precision-wash:     rgba(var(--axis-precision-rgb), calc(var(--panel-alpha) * 0.25));
  --neutral-wash: rgba(0, 0, 0, calc(var(--panel-alpha) * 0.1));
}

/* The book and the site chrome cap themselves at 62rem; <main> has to agree
   so the status line above the book lines up with its left edge. */
.shell {
  width: 100%;
  max-width: 62rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

/* The signed-in player's name under the mark, wearing the mark's own dress:
   same display face, same tracking, a size down, gold like ".today". Empty
   until the session is known. */
.mark-stack { display: flex; flex-direction: column; }
.mark-player {
  font-family: var(--display);
  font-size: 0.66rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold-lit);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65);
}

/* The header's right side: the settings gear beside the logout button. The
   shared .site-head is space-between, so the pair has to travel as one flex
   item to stay together on the right. */
.head-actions {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}
/* "Log out" broke over two lines on a narrow phone and made the header a row
   taller than it needed to be. That was cheap when the header scrolled away;
   it is not now that the shell holds it on screen for good, so the words stay
   on one line and the header keeps the height of a single row. */
.head-actions > * { white-space: nowrap; }
/* On a phone the row is tight, and flex shrank the icon buttons to nothing
   — the gear and the bell were there, 19px wide, drawing nothing. The
   glyph buttons keep their size; the search field is what gives. */
.head-actions .ghost { flex: 0 0 auto; }
.head-actions .head-search { flex: 1 1 9rem; min-width: 3rem; }

/* The header player search: a slim field left of the gear, its results
   dropping as a parchment list anchored to the field. High z-index so
   the list clears the sticky bars strip (z 5). */
.head-search { position: relative; margin-right: 0.3rem; }
#head-search-input {
  font: inherit; font-size: 0.8rem; color: var(--parchment);
  width: 100%; max-width: 9rem; padding: 0.25rem 0.5rem;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 2px;
}
#head-search-input::placeholder { color: rgba(255, 255, 255, 0.55); }
#head-search-input:focus { outline: none; border-color: var(--gold); }
/* The desk is dark in BOTH modes, but dark mode turns --parchment
   near-black — the same trap styles.css pins the ghost buttons out of.
   Pin the typed text to the lit-parchment constant likewise. */
html.mode-dark #head-search-input { color: #f2e8d2; }
@media (max-width: 30rem) { #head-search-input { max-width: 6.5rem; } }
.head-search-results {
  position: absolute; right: 0; top: calc(100% + 0.25rem); z-index: 30;
  list-style: none; margin: 0; padding: 0.2rem; min-width: 100%;
  background: var(--parchment); border: 1px solid var(--rule-strong);
  border-radius: 2px; box-shadow: 0 0.35rem 0.9rem rgba(0, 0, 0, 0.4);
}
.head-search-hit {
  font: inherit; font-size: 0.85rem; color: var(--ink);
  display: block; width: 100%; text-align: left;
  padding: 0.3rem 0.6rem; background: transparent; border: 0;
  cursor: pointer; white-space: nowrap;
}
.head-search-hit:hover, .head-search-hit:focus-visible {
  background: rgba(0, 0, 0, 0.07);
}

/* No-JS note and the app-level status line both sit on the desk rather than
   on parchment, so they take the desk's light-on-dark treatment. */
.desk-note {
  margin: 0;
  min-height: 0;          /* shared .note reserves 1.5em; on the desk that
                             would leave a permanent gap above the book */
  color: var(--parchment);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}

.noscript-note {
  width: 100%;
  max-width: 62rem;
  margin: 0;
  padding: 0.75rem 1rem;
  border: 1px solid var(--gold);
  border-radius: 0.25rem;
  background: rgba(0, 0, 0, 0.3);
  color: var(--parchment);
  font-size: 0.95rem;
}

/* ---- reveal gate -------------------------------------------------------- */
/* app.html keeps its content out of sight until GET /me resolves, so a signed
   -out visitor never sees a flash of the shell before the redirect. game.js
   adds .ready on success AND on the retry path, so the retry panel is always
   reachable — the page must never end up blank. Only the app page is gated;
   index.html and 404.html paint immediately. */
body[data-page='app'] .shell {
  opacity: 0;
  transition: opacity 0.18s ease;
}
body[data-page='app'].ready .shell { opacity: 1; }

/* ---- login mode ribbons ------------------------------------------------- */
/* The two login modes are plain buttons carrying aria-pressed, not ARIA tabs
   (no arrow-key tab semantics are implied). The shared sheet only paints the
   active state for aria-current / aria-selected, so the pressed state is
   mapped onto the same look here. */
.tab[aria-pressed='true'] {
  background: var(--parchment);
  color: var(--ink);
  box-shadow: 0 -2px 0 var(--gold) inset;
  font-weight: 600;
}

/* ---- title line --------------------------------------------------------- */
/* Heading on the left, wax seal on the right; wraps rather than shrinking. */
.title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
}
.title-row h1 { margin: 0; }

/* ---- identity block ----------------------------------------------------- */
/* Seal plus the "Signed in as" lines. Stacks on a narrow phone. */
.identity {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
}
.identity > div { flex: 1 1 14rem; min-width: 0; }
.identity p:last-child { margin-bottom: 0; }
/* The seal carries the account's first initial; Cinzel has caps only. */
.identity .seal { text-transform: uppercase; }

/* Names come from the API. Long ones break instead of pushing the page wide. */
.name { overflow-wrap: anywhere; }

/* ---- Admin badge -------------------------------------------------------- */
.stamp {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--gold);
  border-radius: 0.15rem;
  background: rgba(156, 124, 44, 0.14);
  color: var(--ink-soft);
  font-family: var(--display);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: 0.15em;
}

/* ---- account record ----------------------------------------------------- */
/* Label / value rows on ruled parchment. Two columns once there is room, one
   column on a phone so a long value never forces a sideways scroll. */
.record { margin: 0; }
.record > div {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
}
.record > div:last-child { border-bottom: 0; }
.record dt {
  font-family: var(--display);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.record dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* ---- inline command name ------------------------------------------------ */
.cmd {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  padding: 0.1em 0.35em;
  border: 1px solid var(--rule-strong);
  border-radius: 0.15rem;
  background: rgba(255, 255, 255, 0.4);
  white-space: nowrap;
}

/* ---- form footers ------------------------------------------------------- */
/* Submit button beside its "Saved" confirmation; wraps on a narrow screen so
   the button keeps its full tap target. */
.form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

/* A hairline before a trailing action (the second Log out button). */
.rule-off {
  margin: 1.75rem 0 1.25rem;
  border: 0;
  border-top: 1px solid var(--rule);
}

.stack-gap { margin-top: 1.5rem; }

/* ---- retry panel -------------------------------------------------------- */
/* Shown when /me fails for any reason other than a definite 401/403. */
.retry-actions { margin-top: 1.25rem; }

@media (min-width: 34rem) {
  /* Enough width for the label column to sit beside its value. */
  .record > div { grid-template-columns: 9rem minmax(0, 1fr); align-items: baseline; }
}

@media (min-width: 64rem) {
  /* Matches the shared sheet's wide-screen active ribbon, which keys on
     aria-current / aria-selected only. Same breakpoint, same values — moved
     with the sidebar from 48rem to 64rem so the pair stays in step. */
  .tab[aria-pressed='true'] {
    box-shadow: 0.15rem 0.15rem 0.4rem rgba(0, 0, 0, 0.3), -3px 0 0 var(--gold) inset;
    transform: translateX(0.25rem);
  }
}

/* ---- settings: cover and desk pickers ------------------------------------
   The chips (.option-row/.option), the swatch and every sw-* colour class
   moved to /styles.css: the homepage gear panel shows the same pickers
   now, so they are shared chrome. Only game-specific settings widgets
   (avatar seals, the sliders) keep rules in this file. */

/* ---- mastery -------------------------------------------------------------
   Colour here is ALWAYS the axis (--axis-force etc from /theme.css), never the
   school, and it never travels without .cell-mark: under deuteranopia Force
   and Endurance are the same colour, so the glyph is what actually carries
   which axis a cell belongs to. Do not "simplify" the mark away. */

/* The bars ride above everything and stay put. A parchment plate rather than
   leather, so the fills keep the contrast they were picked for — woad on dark
   leather would go muddy. */
.mbars {
  position: sticky;
  top: 0.4rem;   /* a sliver of desk stays visible above it when stuck */
  z-index: 5;
  background: var(--parchment);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  padding: 0.4rem 0.6rem;
  box-shadow: 0 0.35rem 0.9rem rgba(0, 0, 0, 0.4);
}

/* All three across at EVERY width, including the narrowest phone: stacked
   bars cost extra vertical space, and this thing is pinned to the top of the
   screen on every page — height is the whole budget. minmax(0, 1fr) matters:
   a bare 1fr bottoms out at min-content, and the nowrap name + number would
   prop a column open and shove Health past the plate's edge. */
.bars {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem;
}
.bar { min-width: 0; }

/* Sized to be read at a glance, not squinted at: these two numbers are the
   game's whole dashboard, and the strip is on screen on every page. */
.bar-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.35rem; }
/* In a pinch the NAME gives way (ellipsis), never the number — the count is
   the reading, the label is decor once the icon and colour carry identity. */
.bar-name {
  display: inline-flex; align-items: center; gap: 0.3em;
  font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.08em; font-size: 0.82rem; color: var(--ink);
  white-space: nowrap; min-width: 0;
}
.bar-name > span:last-child {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.bar-num {
  font-size: 0.95rem; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  flex-shrink: 0;
}
.bar-track {
  position: relative;   /* the countdown overlays the fill */
  height: 1.5rem; margin-top: 0.25rem; border-radius: 2px;
  background: var(--parchment-shade); border: 1px solid var(--rule);
  overflow: hidden;
}
/* The time-to-full, printed inside the track: white with a black outline
   (four offset shadows + a soft halo), so it reads over the gold fill, the
   woad fill, and the bare shade alike — no per-bar text colour to fall out
   of step with the fills. */
.bar-time {
  position: absolute; inset: 0; padding-left: 0.45rem;
  display: flex; align-items: center; justify-content: flex-start;
  font-size: 1rem; font-weight: 600; color: #fff;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  text-shadow:
    -1px -1px 0 rgba(0, 0, 0, 0.85), 1px -1px 0 rgba(0, 0, 0, 0.85),
    -1px  1px 0 rgba(0, 0, 0, 0.85), 1px  1px 0 rgba(0, 0, 0, 0.85),
     0 0 3px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}
.bar-fill { display: block; height: 100%; width: 0; transition: width 0.4s linear; }
/* Energy is The World's bar, so it takes The World's own axis-free identity:
   gold, the material accent. Study is the scholarly one — woad. */
/* Energy is HARD amber — its own constant, deliberately not the accent, so
   no cover/page/accent choice can ever recolour the resource. The bolt icon
   (icon-energy.svg) carries the same value. */
.bar-energy .bar-fill { background: var(--energy); }
.bar-study .bar-fill { background: var(--axis-understanding); }
.bar-health .bar-fill { background: var(--health); }

/* Phones: the count moves INTO the track, printed where the countdown was,
   and the countdown goes — two numbers can't share a phone-width column, and
   how-much beats how-long at a glance. Same white-with-outline treatment as
   .bar-time so it reads over any fill. Scoped to .bars: the fight plates
   reuse .bar-num/.bar-track and keep their own layout. */
@media (max-width: 480px) {
  .bars .bar { position: relative; }
  .bars .bar-time { display: none; }
  .bars .bar-num {
    position: absolute; left: 0.45rem; bottom: 0; z-index: 1;
    height: 1.5rem; display: flex; align-items: center;
    pointer-events: none;
    color: #fff;
    text-shadow:
      -1px -1px 0 rgba(0, 0, 0, 0.85), 1px -1px 0 rgba(0, 0, 0, 0.85),
      -1px  1px 0 rgba(0, 0, 0, 0.85), 1px  1px 0 rgba(0, 0, 0, 0.85),
       0 0 3px rgba(0, 0, 0, 0.55);
  }
  /* A tap flips one bar to its countdown (game.js toggles .show-time);
     the whole bar is the target, comfortably past 44px wide. */
  .bars .bar { -webkit-tap-highlight-color: transparent; }
  .bars .bar.show-time .bar-time { display: flex; }
  .bars .bar.show-time .bar-num { display: none; }
}

/* Very narrow phones: three tracks now share the strip, so the labels and
   numbers tighten rather than wrap and double the strip's height. */
@media (max-width: 22rem) {
  .bars { gap: 0.4rem; }
  .bar-name { font-size: 0.68rem; letter-spacing: 0.03em; }
  .bar-num { font-size: 0.8rem; }
  .bar-time { font-size: 0.85rem; padding-left: 0.3rem; }
}

.grid-head { margin-top: 1.4rem; }
/* A head with a figure at its far end — "Moves      3 / 4 equipped". */
.grid-head-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.grid-head-sub { font-size: 0.78rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.grid-head h3 {
  font-family: var(--display); text-transform: uppercase; letter-spacing: 0.09em;
  font-size: 0.82rem; color: var(--ink-soft); margin: 0 0 0.2rem;
}

/* Mastery: only the trainable attributes, grouped by which bar pays. */
.trainable { margin-top: 0.7rem; }
.train-group + .train-group { margin-top: 1.1rem; }
.train-group-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem;
  border-bottom: 1px solid var(--rule); padding-bottom: 0.35rem; margin-bottom: 0.55rem;
}
.train-group-name {
  font-family: var(--display); text-transform: uppercase; letter-spacing: 0.07em;
  font-size: 0.82rem;
}
.train-group-bar {
  display: inline-flex; align-items: center; gap: 0.3em;
  font-size: 0.75rem; color: var(--ink-faint); font-style: italic;
}
/* Now that the nav is inline until 64rem, a phone has the whole width — so
   the four attributes go 2x2 rather than a single tall stack, and all four
   sit on one row as soon as there is room. */
.train-row { display: grid; gap: 0.5rem; grid-template-columns: 1fr 1fr; }
@media (max-width: 22rem) { .train-row { grid-template-columns: 1fr; } }
@media (min-width: 44rem) { .train-row { grid-template-columns: repeat(4, 1fr); } }
.train-row .cell { margin-bottom: 0; }

/* Account: the record is the octagon (styled with the rest of the figure,
   further down); this is the one line that outlives it — all sixteen, added
   up, under the two halves the figure shows. */
.grand-total {
  margin: 1rem 0 0; padding-top: 0.6rem; border-top: 1px solid var(--rule);
  font-family: var(--display); text-transform: uppercase; letter-spacing: 0.08em;
  font-size: 0.8rem; color: var(--ink-soft);
  display: flex; justify-content: space-between;
}
.grand-total span { font-family: var(--body); font-size: 1.1rem; color: var(--ink); letter-spacing: 0; }

.cell {
  border: 1px solid var(--rule); border-left: 3px solid currentColor;
  border-radius: 2px; padding: 0.45rem 0.55rem; margin-bottom: 0.5rem;
}
.cell.axis-force { color: var(--axis-force); background: var(--axis-force-wash); }
.cell.axis-understanding { color: var(--axis-understanding); background: var(--axis-understanding-wash); }
.cell.axis-endurance { color: var(--axis-endurance); background: var(--axis-endurance-wash); }
.cell.axis-precision { color: var(--axis-precision); background: var(--axis-precision-wash); }

.cell-top { display: flex; align-items: baseline; gap: 0.4rem; }
.cell-mark { font-size: 0.8rem; line-height: 1; }
.cell-name { font-weight: 600; font-size: 1rem; }
.cell-pts { margin-left: auto; color: var(--ink); font-variant-numeric: tabular-nums; font-size: 0.9rem; }
/* Experience toward the next level: a thin track under the job line, the
   fill in the axis colour, the numbers printed on it. At the top it fills
   and says so. */
.cell-xp {
  position: relative; height: 1rem; margin: 0.3rem 0 0.1rem;
  border: 1px solid var(--rule); border-radius: 2px; overflow: hidden;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.cell-xp-fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--ink-faint); opacity: 0.45; }
.axis-force .cell-xp-fill         { background: var(--axis-force); }
.axis-understanding .cell-xp-fill { background: var(--axis-understanding); }
.axis-endurance .cell-xp-fill     { background: var(--axis-endurance); }
.axis-precision .cell-xp-fill     { background: var(--axis-precision); }
.cell-xp.is-max .cell-xp-fill { opacity: 0.7; }
.cell-xp-text {
  position: relative; display: block; text-align: center;
  font-size: 0.66rem; line-height: 1rem; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.cell-axis {
  margin: 0.1rem 0 0.4rem; font-size: 0.72rem; font-style: italic;
  color: var(--ink-faint); text-transform: lowercase;
}
.cell-train {
  font-family: var(--body); font-size: 0.82rem; cursor: pointer;
  background: transparent; color: currentColor;
  border: 1px solid currentColor; border-radius: 2px;
  padding: 0.25rem 0.55rem; width: 100%;
}
.cell-train:hover:not(:disabled) { background: var(--parchment); }
.cell-train:disabled { cursor: default; opacity: 0.45; }
.cell-train .bicon { margin-left: 0.3em; }

/* ---- resource icons -------------------------------------------------------
   Bolt = Energy, flame = Resolve, printed beside every bar name and every
   cost so which resource pays is visible without reading. The SVGs live in
   /game/ (img-src 'self'); their colours are baked in to match the bar
   fills — gold and woad. */
.bicon {
  display: inline-block; width: 1.05em; height: 1.05em;
  vertical-align: -0.15em; flex: none;
  background-position: center; background-repeat: no-repeat;
  background-size: contain;
}
.bicon-energy { background-image: url('/game/icon-energy.svg'); }
.bicon-study  { background-image: url('/game/icon-resolve.svg'); }
.bicon-health { background-image: url('/game/icon-health.svg'); }
.cell.is-locked { color: var(--ink-faint); background: transparent; }


/* ---- phone: one row that scrolls, along the bottom edge ------------------ */
/* Eight ribbons and then ≡, all the same width, on a strip you swipe. This
   was five equal columns for a long time, and the fifth slot was the whole
   problem: everything past it went behind More and was invisible until you
   went looking. A strip is not rationed by width, so the bar can hold what it
   ought to hold and stay one row tall.

   The strip used to stick to the TOP, half a finger under the mastery bars,
   and the two of them fought: both pinned at the top of the viewport, the
   ribbons winning on z-index and painting straight over the bar names. Two
   hundred pixels of chrome claimed for one hundred pixels of screen, and the
   dashboard lost. So the ribbons went to the bottom, where the thumb already
   is — the bars keep the top to themselves, and the nav is nearer the hand
   than it ever was up there.

   Source order is the bar order. Unlike the top strip it can no longer sit
   bare on the desk: the page scrolls UNDER it now, so it needs a floor.
   Leather, the same frame the book wears, so the bar reads as the desk's
   near edge rather than a slab dropped on top.
   Scoped to #menu, NOT .spread .tabs: the login page also has .tabs inside a
   .spread — its two-button Log in / Create account switch — and it must not
   become a scrolling strip. */
@media (max-width: 63.999rem) {
  /* The bar's own height, published to the page so that everything else
     pinned to the bottom edge — the chat dock, the impersonation banner, the
     task toast — stands ON the bar instead of underneath it.

     Derived, not measured: a ribbon at the tap target, the plate's padding
     above and below it, and the hairline. Hand-typing "3.3rem" here works
     right up until someone changes the padding, and then everything standing
     on the bar sinks half into it with nothing to say why. The home-indicator
     inset is 0 on a viewport that is not cover-fit — which this one is not —
     but it rides inside the same sum, so if that ever changes the bar and its
     tenants move together. */
  :root {
    --botnav-pad-b: max(0.15rem, env(safe-area-inset-bottom, 0px));
    --botnav-h: calc(var(--tap) + 0.35rem + var(--botnav-pad-b) + 1px);
  }

  #menu {
    display: block;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    /* One rung above the chat dock's 30, and the reason is the drawer. The
       bar and the dock never actually touch — the dock stands ON the bar —
       but the More drawer opens upward into the very corner the dock tabs
       occupy, and the dock comes later in the markup. Left level, the dock
       would paint over a drawer the player had just that moment opened. */
    z-index: 31;
    padding: 0.35rem 0 var(--botnav-pad-b);
    /* The leather frame, lit and shaded exactly as .book's is. Opaque on
       purpose: the page runs beneath the bar and must not read through it. */
    background: var(--leather);
    background-image: linear-gradient(rgba(255, 255, 255, 0.07), rgba(0, 0, 0, 0.22));
    border-top: 1px solid var(--ink);
    /* The lift is cast upward — the bar is above the page, not below it. */
    box-shadow: 0 -0.35rem 0.9rem rgba(0, 0, 0, 0.45);
  }

  #menu .bar-scroll {
    display: flex;
    gap: 0.2rem;
    padding: 0 0.1rem;
    overflow-x: auto;
    /* pan-x, not auto: a vertical swipe that starts on a ribbon belongs to
       the page, and only the horizontal component steers the strip. */
    touch-action: pan-x;
    /* Reaching the last ribbon must not hand the overscroll up to the page. */
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    /* No snapping: the strip stops where the thumb stopped it, and since every
       ribbon is the same width a strip parked mid-ribbon reads as deliberate
       rather than adrift. */
    scroll-behavior: auto;
    /* A drag across ribbons is a gesture, not a text selection, and a tap
       should not flash a blue box over the plate. */
    user-select: none;
    -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
    /* The scrollbar is desk furniture on a phone, and it was eating a row of
       pixels under the bar for a control no thumb uses. The edges say "there
       is more this way" instead — see .at-start / .at-end below. */
    scrollbar-width: none;
  }
  #menu .bar-scroll::-webkit-scrollbar { display: none; }

  /* Whichever side still has ribbons behind it fades into the leather. The
     classes come from game.js, because which edge should fade depends on
     where the strip is parked. */
  #menu .bar-scroll {
    --strip-mask: linear-gradient(to right,
      transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
    -webkit-mask-image: var(--strip-mask);
    mask-image: var(--strip-mask);
  }
  #menu .bar-scroll.at-start {
    --strip-mask: linear-gradient(to right,
      #000 0, #000 calc(100% - 1.5rem), transparent 100%);
  }
  #menu .bar-scroll.at-end {
    --strip-mask: linear-gradient(to right, transparent 0, #000 1.5rem, #000 100%);
  }
  /* Both ends at once means it all fits — nothing to promise, no fade. */
  #menu .bar-scroll.at-start.at-end { --strip-mask: none; }

  /* The fixed width is the point: a ribbon holds its size and its place
     whether its label is "World" or "Inventory", so the bar is learned by
     position instead of read every time. */
  #menu .bar-scroll .tab {
    flex: 0 0 5.5rem;
    min-height: var(--tap);
    justify-content: center;
    text-align: center;
    padding: 0.5rem 0.15rem;
    font-size: 0.6rem;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
  .tab-more .more-glyph { font-size: 0.95rem; letter-spacing: 0; line-height: 1; }

  /* Only styled when open, so the plain [hidden] attribute still hides it —
     setting `display` unconditionally here would override [hidden] and leave
     the menu permanently on screen.
     It opens UPWARD now. A drawer that dropped from a bar on the bottom edge
     would open straight off the screen, so the anchor moves from the bar's
     underside to its top: bottom:100% instead of top:100%. The hard offset
     shadow stays down-and-right, the way every plate on this desk casts. */
  .menu-more:not([hidden]) {
    position: absolute;
    bottom: 100%;
    right: 0.1rem;
    z-index: 20;
    display: flex;
    flex-direction: column;
    min-width: 9rem;
    /* Taller than the screen is the one way this drawer can lose an entry,
       so it scrolls before it reaches the mastery bars up top. */
    max-height: calc(100vh - var(--botnav-h) - 7rem);
    overflow-y: auto;
    background: #cbbfa3;
    border: 1px solid var(--ink);
    box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35);
  }
  #menu .menu-more .tab {
    width: 100%;
    justify-content: flex-start;
    text-align: left;
    border-radius: 0;
    background: transparent;
    background-image: none;
    box-shadow: none;
    border-bottom: 1px solid rgba(42, 31, 22, 0.28);
    padding: 0.5rem 0.7rem;
    font-size: 0.68rem;
  }
  .menu-more .tab:last-child { border-bottom: 0; }
}

/* ---- phone: everything else that lives on the bottom edge ----------------
   The nav bar is fixed, so it is out of the page's flow and the page knows
   nothing about it — left alone, the last line of every section and the
   footer would end up underneath the ribbons. Three things have to give it
   room, and they are all here rather than scattered by feature, because the
   thing they share is the bar, not what they do.

   Anything already pinned to the bottom simply stands one bar higher. The
   dock in particular gains from it: its tabs are drawn with a flat bottom
   and rounded top precisely so they can rest ON an edge, and now there is a
   real edge under them instead of the screen's. */
@media (max-width: 63.999rem) {
  /* THE DOCUMENT DOES NOT SCROLL. The shell does.

     This is the whole answer to a bar on the bottom edge that would not sit
     still. Chrome on Android keeps its address bar along the bottom and
     retracts it as you scroll — but only ever in reply to the DOCUMENT
     scrolling. Scroll something inside the document and the toolbar has no
     opinion at all. So the page stops being the thing that scrolls: the body
     is exactly one viewport tall and holds its content, and the shell in the
     middle of it takes the overflow.

     The toolbar never leaves, so the bottom edge of the viewport is a single
     line rather than two a toolbar apart, and everything pinned to it — the
     ribbons, the dock standing on them — is simply still. It costs the strip
     the toolbar used to hand back on the way down, and it costs the site
     header its exit; both were paid deliberately, for a bar that does not
     move while you read.

     dvh rather than svh: whatever state the toolbar happens to be in as the
     page paints, the body matches it exactly and no strip of desk is left
     over at the bottom. With nothing to retract for, it then stays there. */
  body[data-page='app'] {
    height: 100vh;          /* fallback first */
    height: 100dvh;
    overflow: hidden;
    /* The bar's clearance moves to the scroller, which is what runs under it
       now — the body no longer has a bottom for anything to hide at. */
    padding-bottom: 0;
  }
  body[data-page='app'] .shell {
    flex: 1 1 auto;
    min-height: 0;          /* or a flex item refuses to shrink and scroll */
    overflow-y: auto;
    /* Naming the x axis matters: leaving it `visible` makes the browser
       compute it to `auto` the moment y is not visible, and a stray wide
       row would hand the reader a sideways scrollbar it never had before. */
    overflow-x: hidden;
    /* Reaching the end of the shell must not hand the scroll up to a document
       that cannot take it — and would wake the toolbar if it could. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding-bottom: calc(var(--botnav-h) + var(--pad));
  }
  /* Nothing in a scrolling column gets squeezed: it keeps its own height and
     the container carries the overflow. Without this the shell is a flex box
     with a fixed height and too much in it, so it goes looking for something
     to compress — and a flex item's automatic minimum size is its content,
     EXCEPT for a scroll container, whose minimum is nought. The ticker is the
     one child with overflow:hidden, which made it the only thing that could
     give, so it gave all of it: the town crier's plate flattened to ten
     pixels while the bars and the book stood their ground. */
  body[data-page='app'] .shell > * { flex-shrink: 0; }
  /* One line of copyright, and the bar is standing on it. It has the whole
     desktop and every other page to be read on. */
  body[data-page='app'] > footer { display: none; }

  /* The dock and the impersonation banner stand on the bar too — but not
     from here. Their own rules, further down this file, read --botnav-h with
     a zero fallback: an override written up here loses to them on source
     order alone, and for a day the dock sat under the bar with nothing to
     say why. Anything new that pins to the bottom edge follows suit. */
}

/* On the wide rail the More ribbon STAYS, at the bottom of the list, and
   its menu opens accordion-style inside the rail — so the Forum (and
   Account, Admin, Settings) are reachable from a dropdown there rather
   than crowding the rail as permanent items. Same button, same open/close
   manners as the phone. */
@media (min-width: 64rem) {
  .tab-more { justify-content: flex-start; }
  .more-glyph { margin-right: 0.45em; }
  .menu-more:not([hidden]) { display: block; }
  .menu-more .tab { padding-left: 1.9rem; font-size: 0.66rem; }
  /* The strip dissolves and its ribbons rejoin the rail's own column, exactly
     as #menu-more does above — otherwise the rail would be a single flex row
     stuffed into an 11.5rem column. There is nothing to scroll here and no
     width to fix, so the phone rules stop at 63.999rem and this undoes the
     one thing that would outlive them: the extra box in the middle. */
  #menu .bar-scroll { display: contents; }
}
/* The word appears only where there is room for words. */
.more-label { display: none; }
@media (min-width: 64rem) { .more-label { display: inline; } }
/* Rail-only ribbons (Home): a phone lands on Home by default and has no
   slot to spare; the rail has room for the word. */
.tab-desktop { display: none; }
@media (min-width: 64rem) { .tab-desktop { display: block; } }

/* ---- phone: the menu leaves the page entirely -----------------------------
   These pages used to keep the wide layout's side panel on a narrow 7.5rem
   rail all the way down to the smallest phone. That rail cost the page a
   seventh of its width at exactly the size where width is scarcest, which is
   why the mastery grid could never open up on a phone.

   The rail went, and the ribbons spent a while as a strip along the top —
   which cost the page height instead of width, and cost it right where the
   mastery bars were already standing. Now they are lifted out of the flow
   altogether, into the fixed bar on the bottom edge, and the page owes them
   nothing but the padding underneath it. The side panel returns at 64rem in
   /styles.css, where there is room for both. */

/* ---- purse ----------------------------------------------------------------
   A slim line under the two bars, inside the sticky strip so the coin count
   is on every page. Height up there is the whole budget: one short row, no
   wrapping. */
.purse {
  display: flex; justify-content: space-between; align-items: center;
  margin: 0.3rem 0 0; padding-top: 0.3rem; border-top: 1px solid var(--rule);
}
/* Status marks in the label's old seat: only ACTIVE states show, each a
   16px inline stroke icon with its words in the title/aria-label. The
   min-height keeps the row from breathing when the icons come and go. */
.status-icons {
  display: inline-flex; align-items: center; gap: 0.45rem;
  min-height: 1.1rem;
}
.status-icon { display: inline-flex; align-items: center; gap: 0.2rem; }
/* The countdown beside each mark; tabular digits so the strip does not
   wobble as seconds tick. */
.status-time {
  font-size: 0.78rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.status-infirmary { color: var(--health); }
.status-fight { color: var(--ink-soft); }
.status-streak { color: var(--ink-soft); }
.purse-amt {
  font-size: 0.9rem; font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The town crier: its own slim plate just under the sticky bars strip —
   NOT sticky; it scrolls away with the page. The line slides right to
   left; duration comes from game.js (--ticker-s, scaled to the line's
   length); padding-left 100% starts the line off the right edge and
   translateX(-100%) carries it fully out the left. */
.ticker {
  overflow: hidden;
  margin-top: 0.4rem;
  padding: 0.25rem 0.6rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
}
.ticker-text {
  display: inline-block; white-space: nowrap;
  /* Overflow clips at the plate's PADDING edge, but a plain 100% only
     reaches the content edge — the extra 0.6rem (the plate's own side
     padding) parks the line fully off-frame between passes. */
  padding-left: calc(100% + 0.6rem);
  /* One pass, not infinite: game.js restarts it after a 2s rest, so the
     line catches its breath off the right edge between readings. */
  animation: ticker-slide var(--ticker-s, 20s) linear;
  font-size: 0.85rem; color: var(--ink-soft);
}
@keyframes ticker-slide { to { transform: translateX(-100%); } }
/* The crier's pen on Home: the line box takes what the row has left,
   the Post plate keeps its own width. */
.crier-row input { flex: 1 1 12rem; min-width: 0; min-height: var(--tap); }
/* Respect a reader who asked for stillness: park the line instead. */
@media (prefers-reduced-motion: reduce) {
  .ticker-text { animation: none; padding-left: 0; }
}
.purse-amt span + span { margin-left: 0.35em; }
/* Denomination colours are decorative — the g/s/c letters carry the meaning. */
.coin-g { color: var(--gold); font-weight: 600; }
.coin-s { color: #6f6a5f; }
.coin-c { color: #8a5a3c; }

/* ---- duties ---------------------------------------------------------------
   The board: one card per duty The World offers, then a single seek-work
   button — WHICH duty you draw is the server's roll. */
/* The boards are grids of compact square TILES, grouped by colour (game.js
   sorts: neutral first, then the axis order). Each tile is a real <button>:
   clicking navigates to that duty's own page (#duty/<id>), where the
   attempt — and the category, goods hint and prose — all live. The board's
   job is one glance: mark, icon, name. */
.duty-board {
  margin-top: 0.7rem;
  display: flex; flex-direction: column; gap: 0.5rem;
}
/* One line per rung (game.js groups them by unlock floor, early on top),
   and every line the same five columns: the neutral all-rounders first,
   then one column per stat in the axis order the bars use — so a stat's
   ladder stands in one column under its base duty, and a rung with a gap
   leaves the gap. Tiles fill their column; the square comes from the
   aspect ratio, not a fixed width. */
.duty-row {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0.5rem;
}
.duty-row .duty-tile { width: auto; }
.duty-col-multi         { grid-column: 1; }
.duty-col-force         { grid-column: 2; }
.duty-col-understanding { grid-column: 3; }
.duty-col-endurance     { grid-column: 4; }
.duty-col-precision     { grid-column: 5; }
/* Five across on a phone is a tight fit: the square is let go so a long
   name can take a third line, and the type steps down a notch. */
@media (max-width: 40rem) {
  .duty-row { gap: 0.3rem; }
  .duty-row .duty-tile { aspect-ratio: auto; min-height: 5rem; padding: 1.15rem 0.15rem 0.3rem; }
  .duty-row .tile-name { font-size: 0.62rem; }
  .duty-row .tile-lock { font-size: 0.56rem; }
  /* Four marks and the info dot share a 66px top edge: tighten both. */
  .duty-row .tile-marks { font-size: 0.56rem; letter-spacing: 0.02em; right: 0.2rem; }
  .duty-row .tile-info { width: 0.7rem; height: 0.7rem; font-size: 0.5rem; left: 0.2rem; }
}
/* Past the horizon: a rung the server would not name. A question mark and
   nothing else, the colour kept so the ladder it belongs to still reads —
   and nothing to press. */
.duty-tile.is-mystery { opacity: 0.45; cursor: default; }
.tile-mystery {
  font-family: var(--display); font-size: 1.5rem; font-weight: 700;
  line-height: 1; color: var(--ink-faint);
  width: 32px; height: 32px; display: inline-flex;
  align-items: center; justify-content: center;
}
.duty-tile {
  aspect-ratio: 1; width: 5.4rem; min-height: var(--tap);
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 0.25rem; position: relative;
  font: inherit; color: inherit; cursor: pointer; text-align: center;
  border: 1px solid var(--rule); border-top: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.3rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.duty-tile:hover { border-color: var(--rule-strong); }
/* The tint classes are shared with the duty page, whose generic rules paint
   a LEFT edge; on a tile the colour belongs to the TOP edge, so put the
   side borders back on the plain rule explicitly. */
.duty-tile.duty-axis-force,
.duty-tile.duty-axis-understanding,
.duty-tile.duty-axis-endurance,
.duty-tile.duty-axis-precision,
.duty-tile.duty-axis-multi { border-left: 1px solid var(--rule); }
.duty-tile.duty-axis-force         { border-top-color: var(--axis-force); }
.duty-tile.duty-axis-understanding { border-top-color: var(--axis-understanding); }
.duty-tile.duty-axis-endurance     { border-top-color: var(--axis-endurance); }
.duty-tile.duty-axis-precision     { border-top-color: var(--axis-precision); }
.duty-tile.duty-axis-multi         { border-top-color: var(--ink-soft); }
.tile-marks {
  position: absolute; top: 0.18rem; right: 0.35rem;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.12em;
}
/* The info dot: a thin ring in the corner opposite the axis marks, so the
   two bits of tile furniture balance rather than crowd. Faint by default
   and inking in on hover — present, but never competing with the name. */
.tile-info {
  position: absolute; top: 0.18rem; left: 0.3rem;
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--ink-faint); color: var(--ink-faint);
  font-size: 0.58rem; font-weight: 700; font-style: italic;
  line-height: 1; letter-spacing: 0;
}
.duty-tile:hover .tile-info { border-color: var(--ink-soft); color: var(--ink-soft); }
.tile-info:hover { border-color: var(--ink); color: var(--ink); }
/* The goods panel the dot opens: WHAT the work can hand over, never how
   much or how often. Hangs below the tile on a parchment plate, and the
   hovered tile is lifted out of the flow's paint order so the panel is not
   covered by the tile to its right. Click-through, because the tile
   underneath is the thing you actually want to press. */
.duty-tile:hover { z-index: 5; }
.tile-drops {
  position: absolute; top: calc(100% + 0.3rem); left: -0.25rem;
  z-index: 40; min-width: 9rem; max-width: 13rem;
  display: none; flex-direction: column; gap: 0.22rem;
  padding: 0.45rem 0.6rem; text-align: left;
  border: 1px solid var(--rule-strong); border-radius: 3px;
  background: var(--parchment);
  box-shadow: 0 2px 9px rgba(0, 0, 0, 0.18);
  font-size: 0.72rem; line-height: 1.3; font-weight: 400;
  pointer-events: none;
}
.tile-info:hover ~ .tile-drops { display: flex; }
.tile-drops-head {
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-faint);
  padding-bottom: 0.15rem; border-bottom: 1px solid var(--rule);
  margin-bottom: 0.1rem;
}
.tile-drops-none { color: var(--ink-soft); font-style: italic; }
.tile-drop { display: flex; align-items: center; gap: 0.35rem; }
.tile-drop-name { font-weight: 600; }
.tile-icon { display: inline-flex; }
.tile-name { font-weight: 600; font-size: 0.72rem; line-height: 1.15; }
/* A veteran duty still out of reach. It sits on the board wearing the price
   of entry rather than being kept off it — hiding them made the threshold
   unlearnable, which was worse than spoiling it. Flat, dimmed, and dashed
   along the top edge, so one glance sorts the work you can take from the
   work you are training toward. The dash and the dimming both say it: the
   state is never carried by colour alone. */
.duty-tile.is-locked {
  cursor: default;
  opacity: 0.6;
  background: transparent;
  border-top-style: dashed;
}
.duty-tile.is-locked:hover { border-color: var(--rule); }
/* The art goes grey and small — the counter needs the room more than the
   icon does, and a colourless tile reads as shut. */
.duty-tile.is-locked .tile-icon img { width: 24px; height: 24px; }
.duty-tile.is-locked .tile-icon { filter: grayscale(1); }
/* The ring around a locked icon: a radial gauge of the stat against the
   gate, --p percent full, in the tile's axis colour on the plain rule. The
   disc inside is the tile's own wash, so the icon sits on paper. */
.tile-ring {
  --p: 0; --ring-ink: var(--ink-soft);
  position: relative; width: 40px; height: 40px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: conic-gradient(var(--ring-ink) calc(var(--p) * 1%), var(--rule) 0);
}
.tile-ring::before {
  content: ''; position: absolute; inset: 3px; border-radius: 50%;
  background: var(--parchment);
}
.tile-ring .tile-icon { position: relative; }
.duty-axis-force .tile-ring         { --ring-ink: var(--axis-force); }
.duty-axis-understanding .tile-ring { --ring-ink: var(--axis-understanding); }
.duty-axis-endurance .tile-ring     { --ring-ink: var(--axis-endurance); }
.duty-axis-precision .tile-ring     { --ring-ink: var(--axis-precision); }
/* Where you stand against the gate: "234 / 500". Tabular, so a row of them
   lines up down the board. */
.tile-lock {
  font-size: 0.62rem; font-weight: 600; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
}
/* The card wears the colour of the ONE stat the duty looks for — the axis
   system's colours, never new ones. The stat's name and mark are printed in
   .duty-stat, so colour is never the only encoding. */
.duty-axis-force         { border-left-color: var(--axis-force);         background: var(--axis-force-wash); }
.duty-axis-understanding { border-left-color: var(--axis-understanding); background: var(--axis-understanding-wash); }
.duty-axis-endurance     { border-left-color: var(--axis-endurance);     background: var(--axis-endurance-wash); }
.duty-axis-precision     { border-left-color: var(--axis-precision);     background: var(--axis-precision-wash); }
/* Multi-stat duties are NEUTRAL: no single axis may claim the card. Every
   gating stat's mark still prints, each in its own axis colour. */
.duty-axis-multi         { border-left-color: var(--ink-soft); background: var(--neutral-wash, rgba(0, 0, 0, 0.045)); }
.duty-stat { display: inline-flex; align-items: baseline; gap: 0.3em; font-weight: 600; }
.duty-axis-force .duty-stat         { color: var(--axis-force); }
.duty-axis-understanding .duty-stat { color: var(--axis-understanding); }
.duty-axis-endurance .duty-stat     { color: var(--axis-endurance); }
.duty-axis-precision .duty-stat     { color: var(--axis-precision); }
.duty-stat .mark-force,         .tile-marks .mark-force,         .unlock-name .mark-force         { color: var(--axis-force); }
.duty-stat .mark-understanding, .tile-marks .mark-understanding, .unlock-name .mark-understanding { color: var(--axis-understanding); }
.duty-stat .mark-endurance,     .tile-marks .mark-endurance,     .unlock-name .mark-endurance     { color: var(--axis-endurance); }
.duty-stat .mark-precision,     .tile-marks .mark-precision,     .unlock-name .mark-precision     { color: var(--axis-precision); }
.duty-drops { font-style: italic; color: var(--ink-faint); }
/* An axis as a tag — mark plus a word — wherever a stat's colour and job
   travel together: the gear strip, the stage ladders, the moves book. */
.axis-tag { display: inline-flex; align-items: baseline; gap: 0.3em; font-size: 0.82rem; font-weight: 600; }
.axis-tag.axis-force         { color: var(--axis-force); }
.axis-tag.axis-understanding { color: var(--axis-understanding); }
.axis-tag.axis-endurance     { color: var(--axis-endurance); }
.axis-tag.axis-precision     { color: var(--axis-precision); }
.axis-tag.is-zero { opacity: 0.5; font-weight: 400; }
/* What the body wears, summed by colour, under the doll. */
.gear-strip {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 0.3rem 0.9rem;
  margin-top: 0.5rem; font-variant-numeric: tabular-nums;
  /* No stacking of its own: the sockets are the tap targets, and if the
     strip ever brushes one, the socket must win the finger. */
}
/* The move a cell unlocks, under the axis line: the price until earned. */
.cell-move { font-size: 0.74rem; color: var(--ink-faint); margin: 0.1rem 0 0; font-style: italic; }
/* ---- the move picker -------------------------------------------------------
   Four slots and a card (game.js "the move picker"), the same at every
   width: a phone stacks them, the desktop breakpoint sets them side by
   side. */
.moves-book { display: grid; gap: 0.6rem; grid-template-columns: minmax(0, 1fr); align-items: start; }
.move-slots { display: flex; flex-direction: column; gap: 0.45rem; min-width: 0; }

/* Every button in the picker is a parchment plate (the fill and the lift
   come from the "raised buttons" family further down); this is the edge. */
.slot-arrow, .slot-face, .move-chip, .mcard-act {
  border: 1px solid var(--rule-strong); border-radius: 3px;
}
/* A slot: previous, the face, next. The face is a button too — it opens
   the search — and wears its move's stat as a left rule, like a cell. */
.slot-row { display: grid; grid-template-columns: var(--tap) minmax(0, 1fr) var(--tap); gap: 0.35rem; }
.slot-arrow { min-height: var(--tap); font: inherit; font-size: 1.1rem; font-weight: 600; color: var(--ink); cursor: pointer; }
.slot-arrow:disabled { opacity: 0.4; cursor: default; }
.slot-face {
  min-height: 3.2rem; padding: 0.35rem 0.6rem; min-width: 0;
  display: flex; flex-direction: column; justify-content: center; gap: 0.05rem;
  font: inherit; text-align: left; color: var(--ink); cursor: pointer;
  border-left-width: 3px;
}
.slot-face.axis-force { border-left-color: var(--axis-force); }
.slot-face.axis-understanding { border-left-color: var(--axis-understanding); }
.slot-face.axis-endurance { border-left-color: var(--axis-endurance); }
.slot-face.axis-precision { border-left-color: var(--axis-precision); }
.slot-face.is-empty { color: var(--ink-faint); font-style: italic; }
.slot-name { display: flex; align-items: baseline; gap: 0.35rem; font-weight: 600; font-size: 0.98rem; }
.slot-find { margin-left: auto; font-size: 0.75rem; font-weight: 400; font-style: normal; color: var(--ink-faint); }
.slot-hint { font-size: 0.8rem; color: var(--ink-soft); }
.slot-face .move-req { margin-left: 0; font-size: 0.8rem; }
.slot-search {
  grid-column: 1 / -1; display: flex; flex-direction: column; gap: 0.4rem;
  padding: 0.5rem; background: var(--parchment-shade);
  border: 1px solid var(--rule); border-radius: 3px;
}
.slot-search input {
  min-height: var(--tap); width: 100%; box-sizing: border-box; padding: 0 0.6rem;
  font: inherit; color: var(--ink); background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 3px;
}
.slot-results { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.slot-none { margin: 0; font-size: 0.9rem; color: var(--ink-soft); }
.move-chip {
  min-height: var(--tap); padding: 0 0.7rem; display: inline-flex; align-items: center; gap: 0.35rem;
  font: inherit; font-size: 0.95rem; font-weight: 600; color: var(--ink); cursor: pointer;
}
/* The mark wears its stat's pigment wherever the picker prints it — a bare
   .cell-mark only inherits, and a plate's ink is black. */
.slot-face.axis-force .cell-mark, .move-chip.axis-force .cell-mark { color: var(--axis-force); }
.slot-face.axis-understanding .cell-mark, .move-chip.axis-understanding .cell-mark { color: var(--axis-understanding); }
.slot-face.axis-endurance .cell-mark, .move-chip.axis-endurance .cell-mark { color: var(--axis-endurance); }
.slot-face.axis-precision .cell-mark, .move-chip.axis-precision .cell-mark { color: var(--axis-precision); }
.move-chip > span { white-space: nowrap; }
.move-chip small, .move-chip .move-req { margin-left: 0; font-weight: 400; font-size: 0.78rem; color: var(--ink-soft); }
.move-chip .move-req.is-unmet { color: var(--axis-force); }

/* Wide enough, the card stands beside the slots instead of under them. */
@media (min-width: 64rem) {
  .moves-book { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

/* The card. Square and double-ruled where the rest of the page is soft —
   it is a read-out, not a plate — with the bar in the stat's pigment. */
.mcard {
  border: 2px solid var(--ink); background: var(--parchment-shade); min-width: 0;
  box-shadow: inset 0 0 0 2px var(--parchment), inset 0 0 0 3px var(--rule-strong);
}
.mcard-bar { display: flex; align-items: center; gap: 0.45rem; margin: 3px 3px 0; padding: 0.4rem 0.65rem; color: var(--parchment); }
.mcard.axis-force .mcard-bar { background: var(--axis-force); }
.mcard.axis-understanding .mcard-bar { background: var(--axis-understanding); }
.mcard.axis-endurance .mcard-bar { background: var(--axis-endurance); }
.mcard.axis-precision .mcard-bar { background: var(--axis-precision); }
.mcard-name { font-family: var(--display); font-weight: 700; letter-spacing: 0.05em; font-size: 0.98rem; }
.mcard-kind {
  margin-left: auto; padding: 0.05rem 0.4rem; border: 1px solid currentColor; white-space: nowrap;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
}
.mcard-body { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.55rem 0.7rem 0.7rem; }
.mcard-stats { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.3rem 0.6rem; align-items: center; margin: 0; }
.mcard-stats dt { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft); }
.mcard-stats dd { margin: 0; text-align: right; font-weight: 600; font-size: 0.95rem; font-variant-numeric: tabular-nums; }
.mcard-meter { display: flex; gap: 2px; height: 0.55rem; }
.mcard-meter i { flex: 1; background: var(--parchment-deep); border: 1px solid var(--rule); }
.mcard.axis-force .mcard-meter i.is-on { background: var(--axis-force); border-color: var(--axis-force); }
.mcard.axis-understanding .mcard-meter i.is-on { background: var(--axis-understanding); border-color: var(--axis-understanding); }
.mcard.axis-endurance .mcard-meter i.is-on { background: var(--axis-endurance); border-color: var(--axis-endurance); }
.mcard.axis-precision .mcard-meter i.is-on { background: var(--axis-precision); border-color: var(--axis-precision); }
.mcard-tags { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.7rem; font-size: 0.85rem; }
.mcard-tags .move-req { margin-left: 0; font-size: 0.85rem; }
.mcard-text {
  margin: 0; padding: 0.45rem 0.55rem; min-height: 2.7em; font-size: 0.95rem;
  background: var(--parchment-deep); border: 1px solid var(--rule-strong);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
}
.mcard-act { min-height: var(--tap); width: 100%; font: inherit; font-weight: 600; color: var(--ink); cursor: pointer; }
.mcard-act:disabled { opacity: 0.55; cursor: default; box-shadow: none; }
/* The wage every duty pays whether or not it drops goods: what a success
   teaches. Italic like the goods hint — both are standing promises, not
   numbers that change between attempts. */
.duty-trains { font-style: italic; color: var(--ink-faint); }
.duty-head { display: flex; align-items: baseline; gap: 0.4rem; }
.duty-name { font-weight: 600; }
.duty-cat {
  font-size: 0.72rem; font-style: italic; color: var(--ink-faint);
  text-transform: lowercase;
}
.duty-meta {
  display: flex; gap: 0.75rem; font-size: 0.78rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.duty-act { margin-top: 0.4rem; }

/* ---- the single-duty page (#duty/<id>) ------------------------------------
   The frame is generic; the DECORATION hooks are per duty. game.js stamps
   the built container with duty-decor-<id> (dots as dashes) plus the axis
   class, so a duty's page is styled from here alone, e.g.:

     .duty-decor-world-road_salvage  { ... }
     .duty-decor-world-quarry_labor  { ... }
     .duty-decor-world-moss_tending  { ... }
*/
.duty-view-back { margin: 0 0 0.8rem; }
.duty-view {
  border: 1px solid var(--rule); border-left: 4px solid var(--rule-strong);
  border-radius: 2px;
  padding: 0.9rem 1rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.duty-view-top {
  display: flex; align-items: baseline; gap: 0.75rem;
  font-size: 0.85rem;
}
.duty-view-flavor { margin: 0.6rem 0; font-size: 1.02rem; }
.duty-view .duty-meta { margin-bottom: 0.5rem; }

/* The ledger. Colour + the printed word Success/Failure — never colour
   alone, same rule as the axis marks. */
.duty-log { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.duty-entry {
  /* relative: a discovery's spark shower positions off the entry's centre */
  position: relative;
  border-left: 3px solid var(--rule-strong); border-radius: 2px;
  padding: 0.45rem 0.6rem; margin-bottom: 0.5rem;
}
.duty-entry-success { border-left-color: var(--axis-precision); background: var(--axis-precision-wash); }
.duty-entry-failure { border-left-color: var(--axis-force); background: var(--axis-force-wash); }
.duty-entry-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.duty-entry-name { font-weight: 600; }
.duty-entry-verdict {
  font-family: var(--display); font-size: 0.62rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-soft);
}
.duty-entry-coin {
  color: var(--gold); font-weight: 600; font-variant-numeric: tabular-nums;
}
.duty-entry-loot {
  display: inline-flex; align-items: center; gap: 0.25rem; align-self: center;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
/* Goods the telling itself promised, beside the table's own drop. The
   quiet gold marks it as kept words rather than luck. */
.duty-entry-promised { color: var(--gold); }
/* The points the attempt taught, beside the coin and the goods. The mark
   inside carries the axis colour, so the chip itself stays plain ink —
   the same rule the grids follow. */
.duty-entry-train {
  display: inline-flex; align-items: center; gap: 0.25rem; align-self: center;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.duty-entry-text { margin: 0.25rem 0 0; font-size: 0.88rem; }
/* When it happened: the clock rides the entry head's right edge; the
   fuller telling ("14:32 — 2h ago") is its own faint line. */
.duty-entry-when {
  margin-left: auto; order: 99; align-self: center;
  font-size: 0.72rem; color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
.duty-entry-when-line {
  color: var(--ink-soft); font-size: 0.78rem; font-variant-numeric: tabular-nums;
}

/* The Fights page's call-out search: the trade bench's lookup clothes,
   plus a Fight press riding each hit. */
.fight-search {
  display: flex; align-items: center; gap: 0.5rem; margin: 0.6rem 0 0.8rem;
}
.fight-hit { display: flex; align-items: center; gap: 0.4rem; padding-right: 0.3rem; }
.fight-hit .trade-to-hit { flex: 1; }
.fight-hit-go { font-size: 0.8rem; padding: 0.2rem 0.7rem; min-height: 2.2rem; }

/* A unique outcome: the golden band under the ordinary telling. */
.duty-entry-discovery {
  margin-top: 0.5rem; padding: 0.5rem 0.6rem;
  border: 1px solid var(--gold); border-left: 3px solid var(--gold);
  border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.duty-discovery-head {
  margin: 0; font-family: var(--display); font-size: 0.68rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--gold);
}
.duty-discovery-note {
  margin: 0.35rem 0 0.2rem; font-size: 0.8rem; font-style: italic;
  color: var(--ink-soft);
}

/* A mishap: the discovery band's twin on the other side of the ledger,
   wearing the failure colour. The note under it says what it cost. */
.duty-entry-mishap {
  margin-top: 0.5rem; padding: 0.5rem 0.6rem;
  border: 1px solid var(--axis-force); border-left: 3px solid var(--axis-force);
  border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.duty-mishap-head {
  margin: 0; font-family: var(--display); font-size: 0.68rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--axis-force);
}
.duty-mishap-note {
  margin: 0.35rem 0 0; font-size: 0.8rem; font-style: italic;
  color: var(--ink-soft);
}

/* ---- inventory ------------------------------------------------------------
   One flat list: a search box and the kind filters and sort switch on top, a
   row per item, and a click opens the row's action strip. Names and kinds
   come from /game/api/items, quantities from the state snapshot. */
.inv-search {
  font: inherit; font-size: 1rem; /* 16px: no iOS focus zoom */
  color: var(--ink); display: block;
  width: 100%; max-width: 18rem; min-height: var(--tap);
  margin: 0.7rem 0 0.5rem; padding: 0.3rem 0.5rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
html.mode-dark .inv-search { background: rgba(255, 255, 255, 0.07); }
/* The browser's own clear button comes in its blue; draw the cross out of
   two gradients instead so it wears the page's ink. No asset, no data URI,
   nothing for the CSP to refuse. */
.inv-search::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: 0.8rem; height: 0.8rem; cursor: pointer; opacity: 0.55;
  background:
    linear-gradient(45deg, transparent 43%, var(--ink-soft) 43%,
      var(--ink-soft) 57%, transparent 57%),
    linear-gradient(-45deg, transparent 43%, var(--ink-soft) 43%,
      var(--ink-soft) 57%, transparent 57%);
}
.inv-search::-webkit-search-cancel-button:hover { opacity: 1; }
.inv-filters {
  display: flex; flex-wrap: wrap; gap: 0.35rem 1.2rem; margin-top: 0.7rem;
}
.inv-filter-group { display: inline-flex; flex-wrap: wrap; gap: 0.35rem; }
.inv-chip {
  font: inherit; font-size: 0.8rem; cursor: pointer;
  background: transparent; color: var(--ink-soft);
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 0.2rem 0.55rem;
}
.inv-chip:hover { border-color: var(--rule-strong); }
.inv-chip[aria-pressed="true"] {
  background: var(--parchment); color: var(--ink);
  border-color: var(--rule-strong); font-weight: 600;
}

.inv-list { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.inv-item {
  border: 1px solid var(--rule); border-radius: 2px;
  margin-bottom: 0.35rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.inv-item.is-open { border-color: var(--rule-strong); }
.inv-row {
  display: flex; align-items: baseline; gap: 0.6rem; width: 100%;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  background: transparent; border: 0;
  padding: 0.45rem 0.6rem; min-height: var(--tap);
  /* Held rows are a gesture (game.js onLongPress), so the phone must not
     answer the hold with its own furniture: no double-tap zoom wait, no
     iOS callout, no selection magnifier over the name. */
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
}
/* The fill that says "keep holding" — it arrives over the length of the
   press, so letting go early visibly abandons it. Touch feedback is the
   whole of it on iOS, which ignores navigator.vibrate. */
.inv-row.is-holding {
  background: var(--neutral-wash);
  transition: background 460ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .inv-row.is-holding { transition: none; }
}
.inv-row .inv-name { font-weight: 600; flex: 1; }
.inv-row .inv-kind {
  font-size: 0.72rem; font-style: italic;
  color: var(--ink-faint); text-transform: lowercase;
}
.inv-row .inv-qty {
  font-variant-numeric: tabular-nums; color: var(--ink-soft);
  white-space: nowrap;
}
.inv-row .inv-equipped {
  font-size: 0.66rem; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--ink-soft);
  border: 1px solid var(--rule-strong); border-radius: 2px;
  padding: 0.06rem 0.32rem; white-space: nowrap;
}
/* ---- the figure ------------------------------------------------------------
   The person-shaped half of the Inventory: a silhouette with equipment
   sockets pinned to it. The hand socket is live — a dragged weapon drops
   onto it, and clicking it unequips. The dim sockets at head, chest and
   feet are decoration until their slots exist server-side. */
.inv-layout {
  display: flex; flex-wrap: wrap; gap: 0.9rem 1.4rem;
  align-items: flex-start; margin-top: 0.7rem;
}
.inv-main { flex: 1 1 260px; min-width: 0; }
.inv-main .inv-search { margin-top: 0; }
.inv-main .inv-filters { margin-top: 0; }
.doll {
  position: relative; flex: 0 0 auto; width: 208px; margin: 0 auto;
  border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
  padding: 0.6rem;
}
.doll-figure {
  display: block; width: 118px; margin: 0 auto;
  color: var(--ink-faint); opacity: 0.35;
}
.doll-figure circle, .doll-figure rect, .doll-figure path { fill: currentColor; }
.doll-socket {
  position: absolute; width: var(--tap); height: var(--tap);
  border: 1px dashed var(--rule-strong); border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  text-align: center; overflow: hidden;
}
/* Every socket, live: prints the worn piece's name on two short lines, or
   the slot's own name in ghost italics when empty. Wide enough for a short
   item name; sized to stay a comfortable tap. */
.doll-sock {
  width: 56px; height: var(--tap);
  font: inherit; font-size: 0.62rem; line-height: 1.15; font-weight: 600;
  color: var(--ink-soft); background: transparent; cursor: pointer;
  padding: 0.15rem 0.2rem;
}
.doll-sock .doll-socket-empty {
  font-weight: 400; font-style: italic; color: var(--ink-faint);
}
.doll-sock.is-filled { border-style: solid; color: var(--ink); }
/* Positions: armor runs down the figure's centre line; the hand sockets
   flank it (weapon right, gloves left); jewelry takes the upper corners. */
.doll-s-head { top: 6px; left: 50%; transform: translateX(-50%); }
.doll-s-chest { top: 78px; left: 50%; transform: translateX(-50%); }
.doll-s-legs { top: 132px; left: 50%; transform: translateX(-50%); }
/* Top-anchored like every other socket: the gear strip below grows the
   doll's box, and a bottom anchor would ride the feet off the figure and
   under the strip. 195px is where bottom:6px sat before the strip came. */
.doll-s-feet { top: 195px; left: 50%; transform: translateX(-50%); }
.doll-s-weapon { right: 2px; top: 96px; }
.doll-s-hands { left: 2px; top: 96px; }
.doll-s-necklace { right: 2px; top: 12px; }
.doll-s-ring1 { left: 2px; top: 12px; }
.doll-s-ring2 { left: 2px; top: 54px; }
/* While gear is airborne the sockets beckon; the piece's own socket lights
   on hover (game.js adds is-over to the catalog's slot for the drag). */
html.is-dragging-equip .doll-sock {
  border-style: solid; border-color: var(--rule-strong);
}
.doll-sock.is-over {
  background: var(--parchment);
  box-shadow: 0 0 0 2px var(--rule-strong);
}
.inv-row[draggable="true"] { cursor: grab; }

/* ---- the item sheet -------------------------------------------------------
   A worn piece, examined: raised over the page like the claim modal, but
   politely dismissible — backdrop, Escape or Close all put it away. */
.item-modal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.55); padding: 1rem;
}
.item-sheet {
  width: min(22rem, 100%);
  background: var(--parchment); color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: 3px;
  box-shadow: 0 0.6rem 2rem rgba(0, 0, 0, 0.5);
  padding: 0.9rem 1rem;
}
.item-sheet-head { display: flex; align-items: center; gap: 0.7rem; }
.item-sheet-names { min-width: 0; }
.item-sheet-name { margin: 0; font-weight: 600; font-size: 1.05rem; }
.item-sheet-kind { margin: 0.1rem 0 0; font-size: 0.78rem; color: var(--ink-soft); }
.item-sheet-flavor {
  margin: 0.6rem 0 0; font-size: 0.85rem; font-style: italic;
  color: var(--ink-soft);
}
.item-sheet-stats {
  list-style: none; margin: 0.6rem 0 0; padding: 0.45rem 0.6rem;
  border: 1px solid var(--rule); border-radius: 2px;
}
.item-sheet-stat {
  display: flex; align-items: center; gap: 0.4rem; font-size: 0.92rem;
}
/* A gear stat wears its axis — colour AND mark — with the job as its
   label; the value keeps the green "more is good" ink below. */
.item-sheet-stat.axis-force .item-stat-label,         .item-sheet-stat.axis-force .cell-mark         { color: var(--axis-force); }
.item-sheet-stat.axis-understanding .item-stat-label, .item-sheet-stat.axis-understanding .cell-mark { color: var(--axis-understanding); }
.item-sheet-stat.axis-endurance .item-stat-label,     .item-sheet-stat.axis-endurance .cell-mark     { color: var(--axis-endurance); }
.item-sheet-stat.axis-precision .item-stat-label,     .item-sheet-stat.axis-precision .cell-mark     { color: var(--axis-precision); }
.item-sheet-equip { font-size: 0.85rem; color: var(--ink-soft); margin: 0.5rem 0 0; }
.item-stat-value {
  margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums;
  /* A PROPER green — the success token (23,54,32) is a wash base and
     reads as mud when used as ink. Brighter still on a dark page. */
  color: #2fae52;
}
html.mode-dark .item-stat-value { color: #52d47a; }
.item-sheet-acts {
  display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.8rem;
}

/* ---- enchanting -----------------------------------------------------------
   The hexagon bench: the centre slot holds the piece, six sockets ring it
   at 60° steps. Sockets dress like the doll's: dashed when empty, solid
   when set, names in rarity tints. The spark burst on a successful working
   flies from the centre on per-particle --dx/--dy vectors. */
.hex-bench {
  position: relative; width: 17rem; height: 17rem; margin: 1rem auto 0.4rem;
}
.hex-slot {
  position: absolute; width: 4.4rem; height: 4.4rem;
  transform: translate(-50%, -50%);
  border: 2px dashed var(--rule-strong); border-radius: 50%;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
  display: flex; align-items: center; justify-content: center;
  text-align: center; overflow: hidden; padding: 0.25rem;
  font: inherit; font-size: 0.62rem; line-height: 1.15; font-weight: 600;
  color: var(--ink-soft); cursor: pointer;
}
.hex-slot.is-filled { border-style: solid; color: var(--ink); }
.hex-slot-empty { font-size: 1.4rem; color: var(--ink-faint); }
.hex-center {
  border-radius: 10px; border-color: var(--gold); cursor: default;
  left: 50%; top: 50%;
}
.hex-s0 { left: 50%; top: 11%; }
.hex-s1 { left: 84%; top: 30.5%; }
.hex-s2 { left: 84%; top: 69.5%; }
.hex-s3 { left: 50%; top: 89%; }
.hex-s4 { left: 16%; top: 69.5%; }
.hex-s5 { left: 16%; top: 30.5%; }
.hex-act-row { justify-content: center; }
/* The centre with nothing on it yet: a quiet instruction, not a name. */
.hex-center-empty { font-size: 0.62rem; font-weight: 400; color: var(--ink-faint); }
.hex-center .px-icon { margin: 0 auto 0.15rem; }
.hex-center.is-filled { flex-direction: column; }
/* The Workshop's bench sits in its own wash panel above the recipes, so
   the sockets read as a place and the cards as the book beside it. */
.bench-panel {
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 0.4rem 0.8rem 0.6rem; margin-top: 0.2rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1) * 0.6));
}
.bench-panel .hex-bench { margin-top: 0.5rem; }
.bench-note { text-align: center; min-height: 1.2em; margin: 0.35rem 0 0; }
.ench-spark {
  position: absolute; left: 50%; top: 50%; z-index: 4;
  width: 0.45rem; height: 0.45rem; border-radius: 50%;
  pointer-events: none; opacity: 0;
  animation: ench-fly 1.1s ease-out forwards;
}
.ench-spark-0 { background: var(--gold); }
.ench-spark-1 { background: var(--axis-force); }
.ench-spark-2 { background: var(--axis-precision); }
.ench-spark-3 { background: var(--axis-understanding); }
.ench-spark-4 { background: var(--axis-endurance); }
@keyframes ench-fly {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(1.2); }
  10% { opacity: 1; }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.3);
  }
}

/* ---- combat juice (everyone) ----------------------------------------------
   The blows themselves are drawn on a canvas laid over the scene
   (game.js fightFx); this is the chrome around them. The landed number
   floats off the point of impact: big blows swell red, floor-scrapes
   shrink faint. It lives on a layer of its own above the canvas — a
   zero-size anchor at the impact point centres it — because a flinching
   plate is a transform, and would stack anything inside it under the
   layer. The plate flinches under any hit and takes a tint in the move's
   colour; a big blow moves the whole scene. */
.fight-fx { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
.fight-fx-floats { position: absolute; inset: 0; z-index: 6; pointer-events: none; overflow: hidden; }
.hit-anchor {
  position: absolute; width: 0; height: 0;
  display: flex; justify-content: center; align-items: flex-end;
}
.hit-float {
  position: relative; z-index: 6;
  font-weight: 700; font-size: 1.35rem; color: var(--ink);
  pointer-events: none; opacity: 0; white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  animation: hit-rise 1.4s ease-out forwards;
}
/* The admin's per-blow arithmetic still floats inside the plate. */
.fight-plate > .hit-float { position: absolute; top: 34%; }
.fight-plate.is-flashed::after {
  content: ''; position: absolute; inset: 0; border-radius: 2px; z-index: 1;
  background: var(--fx-tint, var(--health)); pointer-events: none;
  animation: plate-flash 0.24s ease-out forwards;
}
@keyframes plate-flash {
  0% { opacity: 0.26; }
  100% { opacity: 0; }
}
#fight-scene.is-quaking { animation: scene-quake 0.32s ease-out; }
@keyframes scene-quake {
  0% { transform: translate(0, 0); }
  20% { transform: translate(-3px, 2px); }
  40% { transform: translate(3px, -1px); }
  60% { transform: translate(-2px, -2px); }
  80% { transform: translate(1px, 1px); }
  100% { transform: translate(0, 0); }
}
.hit-float.hit-big { font-size: 1.7rem; color: var(--health); }
.hit-float.hit-weak { font-size: 1rem; color: var(--ink-faint); }
@keyframes hit-rise {
  0% { opacity: 0; transform: translateY(0.5rem) scale(0.8); }
  12% { opacity: 1; transform: translateY(0) scale(1.08); }
  25% { transform: translateY(-0.3rem) scale(1); }
  100% { opacity: 0; transform: translateY(-2.6rem) scale(0.95); }
}
.fight-plate.is-struck { animation: plate-flinch 0.4s ease-out; }
@keyframes plate-flinch {
  0% { transform: translateX(0); box-shadow: inset 0 0 0 2px var(--health); }
  25% { transform: translateX(-4px); }
  50% { transform: translateX(3px); box-shadow: inset 0 0 0 2px var(--health); }
  75% { transform: translateX(-2px); }
  100% { transform: translateX(0); box-shadow: none; }
}

/* Reduced motion: no canvas is mounted at all (game.js), and the chrome
   around it holds still — the number and the bar still tell the story. */
@media (prefers-reduced-motion: reduce) {
  .fight-fx { display: none; }
  .fight-plate.is-struck, #fight-scene.is-quaking { animation: none; }
  .fight-plate.is-flashed::after { display: none; }
  .hit-float { animation-duration: 0.6s; }
}

/* ---- admin damage tutorial ------------------------------------------------
   Four numbers per blow, one per step of the reckoning, floating up over
   the struck plate in the four axis colors; the panel below repeats the
   steps in words, same colors. Served to admin sessions only. */
#fight-scene { position: relative; }
.dmg-float {
  position: absolute; top: 55%; z-index: 6;
  font-weight: 700; font-size: 0.95rem; white-space: nowrap;
  pointer-events: none; opacity: 0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  animation: dmg-rise 1.7s ease-out forwards;
}
@keyframes dmg-rise {
  0% { opacity: 0; transform: translateY(0.6rem); }
  12% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-3rem); }
}
.dmg-step-1 { color: var(--axis-force); }
.dmg-step-2 { color: var(--axis-precision); }
.dmg-step-3 { color: var(--axis-understanding); }
.dmg-step-4 { color: var(--axis-endurance); }
.fight-math {
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 0.55rem 0.8rem; font-size: 0.85rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.fight-math-head { margin: 0 0 0.25rem; font-weight: 600; }
.fight-math-step { margin: 0.15rem 0; }

/* ---- fight recap ----------------------------------------------------------
   The ledger's picture view: both fighters as dolls, worn gear beneath,
   verdict and tallies in plain words. */
.fight-recap { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.5rem; }
.fight-recap-side {
  flex: 1 1 12rem; text-align: center;
  padding: 0.6rem; border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.fight-recap-name {
  margin: 0 0 0.3rem; display: flex; justify-content: center;
  align-items: baseline; gap: 0.5rem; font-weight: 600;
}
.fight-recap-verdict {
  font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.1em; font-size: 0.7rem; color: var(--ink-soft);
}
.recap-doll { margin: 0 auto; }
.fight-recap-worn {
  list-style: none; margin: 0.35rem 0 0; padding: 0; font-size: 0.8rem;
}
.fight-recap-stats { margin: 0.35rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }

/* ---- stall dropdowns ------------------------------------------------------
   A counter's family header: same row chrome as everything else, the
   chevron carrying the open/shut state (aria-expanded speaks it too). The
   folded listings indent a step so the family reads as one shelf. */
.stall-group { font-weight: 600; }
.stall-group-mark { width: 1.1em; text-align: center; color: var(--ink-soft); }
.stall-group-list { margin: 0.15rem 0 0.5rem 1.25rem; }

/* ---- item rarity ----------------------------------------------------------
   The ladder's tints, junk (dark grey) to legendary (orange). Common wears
   no class — plain ink IS the common color. Values tuned per theme so grey
   stays legible on parchment and on dark leather alike. Names only ever
   carry the tint; meaning always rides the word (the name itself), never
   colour alone. */
.rarity-junk { color: #6d6961; }
.rarity-uncommon { color: #3d7a35; }
.rarity-rare { color: #2b62a8; }
.rarity-epic { color: #7442a8; }
.rarity-legendary { color: #b05f10; }
html.mode-dark .rarity-junk { color: #8f8a80; }
html.mode-dark .rarity-uncommon { color: #7fb573; }
html.mode-dark .rarity-rare { color: #7aa7e0; }
html.mode-dark .rarity-epic { color: #b48ae0; }
html.mode-dark .rarity-legendary { color: #e0954a; }

/* ---- impersonation banner --------------------------------------------------
   Pinned bottom-left, clear of the chat dock on the right: while the
   session wears another player's account this must be unmissable, and the
   way out one press. */
.imp-banner {
  position: fixed; left: 0.5rem; z-index: 40;
  /* On the phone the bar owns the bottom edge; --botnav-h is unset elsewhere. */
  bottom: calc(var(--botnav-h, 0px) + 0.5rem);
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.45rem 0.7rem;
  background: var(--parchment); color: var(--ink);
  border: 2px solid var(--gold); border-radius: 4px;
  box-shadow: 0 0.35rem 0.9rem rgba(0, 0, 0, 0.4);
  font-size: 0.85rem; font-weight: 600;
}

.inv-actions {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  padding: 0 0.6rem 0.55rem;
}
/* The weapon's one-line dossier under its verbs — full row of the strip. */
.inv-equip-note {
  flex-basis: 100%; margin: 0.1rem 0 0;
  font-size: 0.78rem; font-style: italic; color: var(--ink-faint);
}
.inv-act {
  font: inherit; font-size: 0.82rem; cursor: pointer;
  background: transparent; color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: 2px;
  padding: 0.25rem 0.7rem; min-height: var(--tap);
}
.inv-act:hover { background: var(--parchment); }

/* The charts' Open link is an anchor wearing the plate, so it needs the
   plate's own geometry — a link does not centre its text in a box for free. */
#ch-open {
  display: inline-flex; align-items: center; text-decoration: none;
}
#ch-open[hidden] { display: none; }

/* ---- admin: duty outcomes --------------------------------------------------
   Two levels of dropdown (duty, telling), so the indent has to do the
   explaining: each level steps in and takes a rule down its left edge.
   Admin chrome is plainer than the player's pages by standing order, but
   the tap targets stay honest. */
.oc-panel { padding: 0.3rem 0 0.5rem 0.8rem; }
.oc-section { margin-top: 0.6rem; }
.oc-sub-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.oc-sub {
  margin: 0.2rem 0; font-family: var(--display); font-size: 0.7rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-soft);
}
.oc-add { font-size: 0.75rem; }
/* The warning count, wherever it bubbles up to. Never colour alone — the
   number is the message, and the title spells it out. Its own class, well
   away from the findings' is-warn: one shared name and the badge's fill
   lands under a line of text. */
.oc-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.2rem; padding: 0 0.3rem; border-radius: 0.6rem;
  background: var(--warn-ink); color: #fff;
  font-size: 0.72rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
html.mode-dark .oc-count { color: #2a201c; } /* the lifted ink wants dark type */
.oc-variant { border-left: 2px solid var(--rule); margin: 0.25rem 0 0.25rem 0.4rem; }
.oc-variant.is-warned { border-left-color: var(--warn-ink); }
/* The row holds a whole telling, so it wraps and the marks stay with the
   first line rather than floating beside a paragraph. */
.oc-variant-row {
  padding-left: 0.5rem; gap: 0.5rem;
  align-items: baseline; text-align: left; line-height: 1.35;
  padding-top: 0.35rem; padding-bottom: 0.35rem;
}
.oc-num {
  font-variant-numeric: tabular-nums; color: var(--ink-faint); font-size: 0.8rem;
}
.oc-snip { flex: 1 1 14rem; min-width: 0; font-size: 0.85rem; overflow-wrap: anywhere; }
.oc-promise-tag {
  font-size: 0.72rem; font-style: italic; color: var(--gold);
  white-space: nowrap;
}
.oc-findings { padding: 0 0 0.2rem 1.4rem; }
.oc-finding { margin: 0.1rem 0; font-size: 0.78rem; }
.oc-finding.is-warn { color: var(--warn-ink); font-weight: 600; }
.oc-finding.is-note { color: var(--ink-faint); font-style: italic; }
.oc-editor { padding: 0.3rem 0 0.5rem 1.4rem; }
.oc-text {
  width: 100%; font: inherit; font-size: 0.9rem; color: var(--ink);
  background: rgba(255, 255, 255, 0.42);
  border: 1px solid var(--rule-strong); border-radius: 0.2rem;
  padding: 0.4rem 0.5rem; resize: vertical;
}
html.mode-dark .oc-text { background: rgba(255, 255, 255, 0.07); }
.oc-line {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
  margin-top: 0.4rem;
}
.oc-label {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); min-width: 4.5rem;
}
.oc-dash { color: var(--ink-faint); }
.oc-line input[type='number'] { width: 5rem; }
.oc-table { margin: 0.2rem 0 0.5rem; }
.oc-table-list { list-style: none; margin: 0.2rem 0 0; padding: 0; }
.oc-table-row {
  display: flex; gap: 0.6rem; font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}
.oc-table-name { flex: 1; }
.oc-table-pct { color: var(--ink-soft); }
.oc-acts { margin-top: 0.7rem; }
.oc-hint { font-size: 0.75rem; margin: 0.3rem 0 0; }

/* ---- admin: icon pairing ---------------------------------------------------
   Rows reuse the inventory plate; the expanded picker is a searchable grid
   of every sheet icon, scrolling inside its own box so a long sheet never
   takes the page with it. */
.ai-current { width: 36px; display: inline-flex; justify-content: center; }
/* Sliced sheet icons everywhere they appear: integer-scaled pixels, never
   smoothed into mush. */
.px-icon { image-rendering: pixelated; display: block; }
/* The icon slot on inventory and market rows: fixed width whether or not
   the item has art, so every name starts on the same line. */
.inv-icon {
  flex: 0 0 24px; width: 24px; height: 24px; align-self: center;
  display: inline-flex; align-items: center; justify-content: center;
}
.duty-icon { align-self: center; display: inline-flex; }
/* ---- the recipe book -------------------------------------------------------
   Cards, two-up where there is room: the MADE thing at the head — its art
   and its name in its rarity's ink — and under a rule the makings, one
   per line: art, name, and a held-over-needed tally at the line's end.
   A card whose makings are all in hand carries a Ready tag; a making the
   pantry is short of prints faint so the missing piece is the one the
   eye finds. The wash and rule are the market tiles' own. */
.recipe-list {
  list-style: none; margin: 0.4rem 0 0; padding: 0;
  display: grid; gap: 0.55rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  /* Each card keeps its own height: a shut card beside an open one must
     not be stretched to the row, or shutting it changes nothing. */
  align-items: start;
}
.recipe-list-one { grid-template-columns: minmax(0, 20rem); margin-top: 0.35rem; }
/* The enchanting book's categories: one tile per prefix, four across,
   shut by default — the name, its gift, a pip per working (lit when
   found) and the count. An open tile takes the whole row and shows its
   found workings' cards beneath the head. */
.recipe-groups {
  list-style: none; margin: 0.5rem 0 0; padding: 0;
  display: grid; gap: 0.5rem; grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: stretch;
}
@media (max-width: 47.99rem) { .recipe-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.recipe-group {
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
  padding: 0 0.55rem 0.3rem; min-width: 0; display: flex; flex-direction: column;
}
.recipe-group.is-open { grid-column: 1 / -1; padding-bottom: 0.6rem; animation: group-lift 220ms ease-out; }
@keyframes group-lift {
  from { opacity: 0.4; transform: translateY(0.5rem); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .recipe-group.is-open { animation: none; } }
.recipe-group.is-unfound { border-left-color: var(--rule); }
.recipe-group-head {
  display: flex; flex-direction: column; align-items: stretch; gap: 0.2rem; flex: 1;
  width: 100%; min-height: var(--tap); padding: 0.45rem 0 0.3rem; position: relative;
  border: 0; background: none; font: inherit; text-align: left; color: inherit; cursor: pointer;
}
/* The chevron sits in the tile's top corner and turns when it opens. */
.recipe-group-head::after {
  content: '\25BE'; position: absolute; right: 0; top: 0.4rem;
  font-size: 1.1rem; line-height: 1; color: var(--ink-soft);
  transform: rotate(-90deg); transition: transform 160ms ease-out;
}
.recipe-group.is-open .recipe-group-head::after { transform: none; }
.recipe-group-head:focus-visible { outline: 2px solid var(--ink-soft); outline-offset: -2px; border-radius: 2px; }
/* The head reads the same shut or open: the name on its own line with
   the chevron at its end, the gift under it, then the meter and count. */
.recipe-group-title { margin: 0; font-size: 0.84rem; color: var(--ink-soft); line-height: 1.3; flex: 1; }
.recipe-group-title strong {
  display: block; padding-right: 1.3rem; color: var(--ink);
  font-family: var(--display); letter-spacing: 0.06em; font-size: 1rem; line-height: 1.3;
}
.recipe-group-tally { margin: 0.1rem 0 0; display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.74rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
/* One segment per working, on one line whatever the count: the row
   divides itself, so fifteen fit where one does. */
.recipe-pips { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; width: 100%; height: 0.5rem; }
.recipe-group:not(.is-open) .recipe-list, .recipe-group:not(.is-open) .recipe-group-none { display: none; }
@media (prefers-reduced-motion: reduce) { .recipe-group-head::after { transition: none; } }
.recipe-pip { display: block; border-radius: 1px; border: 1px solid var(--rule-strong); background: transparent; min-width: 0; }
.recipe-pip.is-found { background: var(--ink-soft); border-color: var(--ink-soft); }
.recipe-group.is-unfound .recipe-group-title strong { color: var(--ink-soft); }
.recipe-group-none { margin: 0.1rem 0 0.3rem; font-size: 0.88rem; }
.recipe-group .recipe-list { margin-top: 0.2rem; }
.recipe-card {
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.45rem 0.6rem 0.4rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.recipe-card.is-ready { border-left-color: var(--ink-soft); }
.recipe-head {
  display: flex; align-items: center; gap: 0.5rem; min-height: var(--tap);
  padding-bottom: 0.3rem; border-bottom: 1px solid var(--rule);
  border-radius: 0.2rem;
}
.recipe-head .recipe-item-name { font-weight: 600; font-size: 1rem; line-height: 1.2; }
/* Two lines' worth of head whether the name takes one or two, so a row of
   shut cards lines up. */
.recipe-head { min-height: 3rem; }
.recipe-item { display: inline-flex; align-items: center; gap: 0.45rem; min-width: 0; }
.recipe-item-name { overflow-wrap: anywhere; }
/* Items still waiting on art keep their column: a blank the icon's size. */
.recipe-noart { display: inline-block; width: 24px; height: 24px; }
.recipe-head .recipe-noart { width: 32px; height: 32px; }
.recipe-qty { font-size: 0.85rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.recipe-ready {
  margin-left: auto; font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.08em; font-size: 0.68rem; color: var(--ink-soft);
  border: 1px solid var(--rule-strong); border-radius: 2px; padding: 0.1rem 0.4rem;
}
/* What a working lends, under the head of an enchanting card — the
   prefix in the made thing's ink, each gift in its axis colour. */
.recipe-lends { margin: 0.35rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }
.recipe-lends-prefix { font-weight: 600; color: var(--ink); }
.recipe-lend { white-space: nowrap; font-weight: 600; font-variant-numeric: tabular-nums; }
.recipe-lend .cell-mark { font-size: 0.7em; }
.recipe-lend.mark-force         { color: var(--axis-force); }
.recipe-lend.mark-understanding { color: var(--axis-understanding); }
.recipe-lend.mark-endurance     { color: var(--axis-endurance); }
.recipe-lend.mark-precision     { color: var(--axis-precision); }
/* "or Iron Sword, Hunting Bow": the other pieces a working fits, on a
   line of its own under the centre's name so the name never folds. */
.recipe-making { flex-wrap: wrap; }
.recipe-also {
  flex-basis: 100%; order: 9; margin-top: -0.35rem; padding: 0 0 0.35rem calc(24px + 0.45rem);
  font-size: 0.78rem; color: var(--ink-faint);
}
/* The plate that takes a ready card to the bench: the inventory's own
   raised button, pulled to the head's end. */
.recipe-bench { margin-left: auto; font-size: 0.82rem; padding: 0 0.6rem; white-space: nowrap; }
/* The fold chevron at the head's end: a bare glyph on a full tap target,
   turned on its side when the card is shut. Shut, the makings are gone
   and the head loses its rule, so the card is one line. */
.recipe-fold {
  flex: none; margin-left: auto; width: var(--tap); min-height: var(--tap);
  padding: 0; border: 0; background: none; color: var(--ink-soft);
  font: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.recipe-head > .recipe-bench + .recipe-fold, .recipe-head > .recipe-ready + .recipe-fold { margin-left: 0; }
.recipe-fold::before {
  content: '\25BE'; font-size: 1.1rem; line-height: 1;
  transition: transform 160ms ease-out;
}
.recipe-card.is-folded .recipe-fold::before { transform: rotate(-90deg); }
.recipe-fold:hover::before, .recipe-fold:focus-visible::before { color: var(--ink); }
.recipe-fold:focus-visible { outline: 2px solid var(--ink-soft); outline-offset: -4px; border-radius: 4px; }
.recipe-card.is-folded .recipe-makings, .recipe-card.is-folded .recipe-lends { display: none; }
.recipe-card.is-folded .recipe-head { border-bottom: 0; padding-bottom: 0; }
@media (prefers-reduced-motion: reduce) { .recipe-fold::before { transition: none; } }
.recipe-makings { list-style: none; margin: 0.15rem 0 0; padding: 0; }
.recipe-making {
  display: flex; align-items: center; gap: 0.45rem;
  min-height: var(--tap); padding: 0 0.15rem; border-radius: 0.2rem;
  font-size: 0.92rem;
}
.recipe-making + .recipe-making { border-top: 1px dotted var(--rule); }
.recipe-making.is-short .recipe-item-name,
.recipe-making.is-short .recipe-held { color: var(--ink-faint); }
.recipe-role {
  font-family: var(--display); text-transform: uppercase; letter-spacing: 0.08em;
  font-size: 0.62rem; color: var(--ink-faint);
  border: 1px solid var(--rule); border-radius: 2px; padding: 0.05rem 0.3rem;
}
.recipe-tally {
  margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums;
  white-space: nowrap; font-size: 0.92rem;
}
.recipe-slash { color: var(--ink-faint); font-weight: 400; margin: 0 0.1rem; }
.recipe-making.is-met .recipe-held { color: var(--ink); }
/* Held rows and heads are a gesture (game.js onLongPress), so the phone
   must not answer the hold with its own furniture first — a long press
   on a bare <img> otherwise summons "save image" and the page never hears
   the gesture at all. Same guard the inventory rows carry. */
.is-holdable {
  cursor: pointer;
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
}
/* The fill that says "keep holding", arriving over the length of the hold —
   on iOS it is the ONLY feedback, since Safari ignores the buzz. */
.recipe-card .is-holding {
  background: var(--neutral-wash);
  transition: background 460ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .recipe-card .is-holding { transition: none; }
}
.stall-tile-head { display: flex; align-items: center; gap: 0.45rem; }
.ai-none { color: var(--ink-faint); }
.ai-picker { padding: 0 0.6rem 0.55rem; }
.ai-search {
  font: inherit; font-size: 0.85rem; color: var(--ink);
  width: 100%; max-width: 18rem; margin-bottom: 0.45rem;
  padding: 0.35rem 0.5rem; background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.ai-grid {
  display: grid; gap: 0.25rem;
  grid-template-columns: repeat(auto-fill, minmax(var(--tap), 1fr));
  max-height: 16rem; overflow-y: auto; scrollbar-gutter: stable;
}
.ai-cell {
  display: flex; align-items: center; justify-content: center;
  min-height: var(--tap); cursor: pointer;
  font: inherit; font-size: 0.72rem; color: var(--ink);
  border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.ai-cell:hover { border-color: var(--rule-strong); }
.ai-cell.is-current { border-color: var(--ink); box-shadow: 0 0 0 1px var(--ink); }
.ai-cell:disabled { opacity: 0.5; cursor: default; }
.ai-default { grid-column: span 2; }

/* ---- market ---------------------------------------------------------------
   The directory: one tile per stall, on the duty-card recipe — a wash
   panel with a strong left rule — sized to tile two-up where there is
   room. A tile is a real button; clicking routes to the stall's counter. */
.market-tiles {
  display: grid; gap: 0.5rem;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  margin-top: 0.7rem;
}
.stall-tile {
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.55rem 0.65rem; min-height: var(--tap);
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.stall-tile:hover { border-color: var(--rule-strong); }
.stall-name { display: block; font-weight: 600; font-size: 0.95rem; }
.stall-blurb { display: block; margin-top: 0.2rem; font-size: 0.78rem; color: var(--ink-soft); }
.stall-count { display: block; margin-top: 0.3rem; font-size: 0.72rem; font-style: italic; color: var(--ink-faint); }

/* The stall's counter.
   Stall rows wear the inventory plate but nothing expands: the flavor sits
   under the name and the Buy plates ride at the row's end, one per count.
   The row wraps on a narrow page so the plates land on their own line
   rather than crushing the name. */
.market-row { cursor: default; align-items: center; flex-wrap: wrap; }
.market-blurb {
  display: block; font-weight: 400; font-size: 0.78rem;
  color: var(--ink-faint); max-width: 34em;
}
.market-price { font-weight: 600; }
.market-buys { display: flex; gap: 0.4rem; }

/* ---- dark mode ------------------------------------------------------------
   The mode-dark surface tokens (dark parchment, pale inks, the dark
   papers and woods, the accents) moved to /styles.css so the homepage
   reads the same dark surfaces from one copy. What stays here is
   game-only: the panel-wash maths and the fixups below. */
html.mode-dark {
  /* The panel wash is white; at full strength it would grey the dark page
     out, so dark mode damps the slider's value instead of fighting it. */
  --panel-dim: 0.16;
  --neutral-wash: rgba(255, 255, 255, calc(var(--panel-alpha) * 0.13));
}
/* White-based fills that read wrong on a dark page. */
html.mode-dark .cmd { background: rgba(255, 255, 255, 0.08); }
html.mode-dark .coin-s { color: #a9a294; }
html.mode-dark .coin-c { color: #c08a5e; }

/* ---- dark mode: the section rail and phone menu ---------------------------
   The rail panel is hardcoded light khaki (#cbbfa3) in styles.css (desktop)
   and in the phone overflow above, with dark-brown hairlines and a milky
   gradient on the bar tabs — all assume a lit page. In dark mode the panel
   flips to a dark board with pale hairlines. Declared last, inside the same
   breakpoints, so these win the cascade. The active ribbon keeps the shared
   inverted treatment: var(--ink) on var(--parchment) swaps by itself. */
@media (min-width: 64rem) {
  html.mode-dark .tabs {
    background: #2b2620;
    border-color: var(--rule-strong);
  }
  html.mode-dark .tab { border-bottom-color: rgba(231, 221, 199, 0.2); }
  html.mode-dark .tab:hover,
  html.mode-dark .tab:focus-visible { background: rgba(231, 221, 199, 0.12); }
}
@media (max-width: 63.999rem) {
  html.mode-dark .tab {
    background-image: linear-gradient(rgba(255, 255, 255, 0.07), rgba(0, 0, 0, 0.25));
  }
  html.mode-dark .menu-more:not([hidden]) {
    background: #2b2620;
    border-color: var(--rule-strong);
  }
  html.mode-dark #menu .menu-more .tab {
    border-bottom-color: rgba(231, 221, 199, 0.2);
  }
}


/* ---- raised buttons --------------------------------------------------------
   Buttons inside a wash panel are little parchment PLATES: the page surface
   plus the same soft emboss the ribbon tabs wear, and a thin lift shadow.
   Solid, so they read as raised at any slider setting; on the surface
   variables, so they flip correctly with dark mode and the dark papers —
   and never borrow the accent. */
/* The inventory doll's weapon socket stays OFF this list: every socket on
   the figure dresses alike (transparent, dashed-to-solid). The fight
   scene's doll keeps the plate — its weapon prints a name, and the name
   needs the ground. */
.cell-train,
.inv-act,
.slot-arrow, .slot-face, .move-chip, .mcard-act,
.info-dot-mark,
.fight-doll .doll-weapon.is-filled,
.duty-view :where(.btn) {
  background-color: var(--parchment);
  background-image: linear-gradient(rgba(255, 255, 255, 0.35), rgba(0, 0, 0, 0.07));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
/* The milky top-light reads wrong on a dark page — same swap as the tabs. */
html.mode-dark :where(.cell-train, .inv-act, .slot-arrow, .slot-face, .move-chip, .mcard-act, .info-dot-mark, .fight-doll .doll-weapon.is-filled, .duty-view .btn) {
  background-image: linear-gradient(rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.28));
}


/* ---- mastery page rework ---------------------------------------------------
   Size and accessibility, to the same bar the strip was raised to: 44px
   train targets and every number and state label sized to be read rather
   than squinted at. Overrides declared last, so they win their earlier
   same-specificity bases. */
/* Screen-reader-only text (the "points" after each bare number). */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Size upgrades. */
.train-group-name { font-size: 0.92rem; }
.train-group-bar { font-size: 0.85rem; color: var(--ink-soft); }
.cell-mark { font-size: 0.95rem; }
.cell-pts { font-size: 1.05rem; font-weight: 600; }
.cell-axis { font-size: 0.78rem; }
.cell-train { font-size: 0.95rem; padding: 0.4rem 0.55rem; min-height: var(--tap); }
.grid-head h3 { font-size: 0.9rem; }
.duty-entry-text { font-size: 0.92rem; }
.duty-entry-verdict { font-size: 0.7rem; }

/* ---- tighter crown ---------------------------------------------------------
   The shared sheet pads the top of the page and the header↔content gap with
   --pad (up to 2rem each) — a third of a phone screen before anything
   useful. Game pages pull both in; the header row itself keeps its tap
   height. */
body {
  padding-top: 0.5rem;
  row-gap: 0.75rem;
}

/* An item's real verb stands out from the not-yet placeholders. */
.inv-act-real { font-weight: 600; }

/* A cooling verb: greyed plate, with the wait printed beside the icon. */
.inv-act:disabled { cursor: default; opacity: 0.5; }
.inv-act-cd { font-weight: 400; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* ---- inventory: the Actions dropdown and the crafting bench ----------------
   Verbs live under one Actions button per row (same open/close manners as
   the overflow menu); the bench under the list holds staged ingredients and
   whatever recipes they satisfy. */
.inv-dd { position: relative; display: inline-block; }
.inv-dd-menu {
  position: absolute; top: 100%; left: 0; z-index: 15;
  min-width: 12rem;
  display: flex; flex-direction: column;
  background: var(--parchment);
  border: 1px solid var(--rule-strong);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35);
}
.inv-dd-item {
  font: inherit; font-size: 0.9rem; cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: 0.3em;
  background: transparent; color: var(--ink); border: 0;
  border-bottom: 1px solid var(--rule);
  padding: 0.5rem 0.7rem; min-height: var(--tap);
}
.inv-dd-item:last-child { border-bottom: 0; }
.inv-dd-item:hover:not(:disabled),
.inv-dd-item:focus-visible { background: var(--parchment-shade); }
.inv-dd-item:disabled { cursor: default; opacity: 0.5; }

/* The crafting bench: the hexagon's sockets laid in a centred row
   (position: relative hosts the spark burst on a landed working). */
.craft-bench { margin-top: 0.8rem; position: relative; }
.craft-slots { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; }
/* relative (not the hexagon's absolute) so each socket can host its own
   rising fill; overflow clips the fill to the circle. */
.craft-slot { position: relative; transform: none; overflow: hidden; }
.craft-slot:disabled { cursor: default; }
/* The working's rise: a green veil climbing the socket floor-to-brim for
   exactly the time still owed (game.js sets the starting height and the
   remaining duration; the keyframe only names the destination). */
.craft-fill {
  position: absolute; left: 0; right: 0; bottom: 0; height: 0;
  background: rgba(var(--state-success-rgb), 0.4);
  pointer-events: none;
  animation-timing-function: linear; animation-fill-mode: forwards;
}
@keyframes craft-fill { to { height: 100%; } }
.craft-act-row { margin-top: 0.5rem; align-items: center; justify-content: center; gap: 0.6rem; }
.craft-timer { color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* ---- the victor's claim, modal --------------------------------------------
   Winning GRABS the screen: a dimmed backdrop and one parchment card, no
   dismiss and no escape hatch — Walk Away IS the polite refusal, so a
   choice is always one press away. */
.claim-modal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.55); padding: 1rem;
}
.claim-modal[hidden] { display: none; }
.claim-box {
  background: var(--parchment); border: 1px solid var(--rule-strong);
  border-radius: 2px; box-shadow: 0 0.6rem 1.6rem rgba(0, 0, 0, 0.5);
  padding: 1rem 1.2rem; max-width: 24rem; width: 100%;
}
.claim-lead { margin: 0.2rem 0 0.8rem; }
.claim-opt {
  font: inherit; color: var(--ink); cursor: pointer;
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem;
  width: 100%; text-align: left; min-height: var(--tap);
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
  border: 1px solid var(--rule-strong); border-radius: 2px;
  padding: 0.5rem 0.7rem; margin-bottom: 0.45rem;
}
.claim-opt:hover, .claim-opt:focus-visible { border-color: var(--gold); }
.claim-opt:disabled { cursor: default; opacity: 0.6; }
.claim-opt-label { font-weight: 700; }
.claim-opt-hint { font-size: 0.85rem; color: var(--ink-soft); }

/* ---- trades ---------------------------------------------------------------
   The compose bench: the To line (with the header search's fold-down
   lookup), then two wash panels — hand over / ask for — each a copper
   line, plate rows for the staged goods, and a fold-out icon picker on
   the admin pairing grid's recipe. The open lists sit on duty-card
   panels with a You get / You give ledger. Text fields sit on parchment
   so they flip with dark mode by themselves. */
.trade-to-row {
  display: flex; align-items: center; gap: 0.5rem; margin-top: 0.6rem;
}
.trade-to-wrap { position: relative; display: inline-block; }
.trade-to {
  font: inherit; font-size: 0.95rem; color: var(--ink);
  width: 13rem; min-height: var(--tap); padding: 0.3rem 0.6rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.trade-to:focus { outline: none; border-color: var(--gold); }
.trade-to-hits {
  position: absolute; left: 0; top: calc(100% + 0.25rem); z-index: 30;
  list-style: none; margin: 0; padding: 0.2rem; min-width: 100%;
  background: var(--parchment); border: 1px solid var(--rule-strong);
  border-radius: 2px; box-shadow: 0 0.35rem 0.9rem rgba(0, 0, 0, 0.4);
}
.trade-to-hit {
  font: inherit; font-size: 0.9rem; color: var(--ink);
  display: block; width: 100%; text-align: left;
  min-height: 2.2rem; padding: 0.3rem 0.6rem;
  background: transparent; border: 0; cursor: pointer; white-space: nowrap;
}
.trade-to-hit:hover, .trade-to-hit:focus-visible { background: rgba(0, 0, 0, 0.07); }

.trade-pair { display: grid; gap: 0.9rem; margin-top: 0.6rem; }
@media (min-width: 44rem) { .trade-pair { grid-template-columns: 1fr 1fr; } }
.trade-side {
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.6rem 0.65rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.trade-side-title { margin: 0; font-weight: 600; font-size: 0.95rem; }
.trade-side-hint {
  margin: 0.1rem 0 0.5rem; font-size: 0.78rem; color: var(--ink-faint);
}
.trade-copper-row { display: flex; align-items: center; gap: 0.5rem; }
.trade-copper {
  font: inherit; font-size: 0.95rem; color: var(--ink);
  font-variant-numeric: tabular-nums;
  width: 7.5rem; min-height: var(--tap); padding: 0.3rem 0.5rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.trade-coin-live {
  font-size: 0.85rem; color: var(--ink-soft); font-variant-numeric: tabular-nums;
}
.trade-lines { display: grid; gap: 0.4rem; margin: 0.6rem 0; }
.trade-lines-empty {
  margin: 0; font-size: 0.82rem; font-style: italic; color: var(--ink-faint);
}
.trade-line {
  display: flex; align-items: center; gap: 0.45rem;
  min-height: var(--tap); padding: 0.25rem 0.45rem;
  border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.trade-line-name { font-size: 0.9rem; min-width: 0; overflow-wrap: anywhere; }
.trade-stepper {
  display: inline-flex; align-items: center; gap: 0.25rem; margin-left: auto;
}
/* The − / + plates: small but honest targets, the raised treatment. Held
   rather than pressed, they repeat (game.js holdRepeat) — so they get the
   same no-callout, no-selection treatment the inventory rows do. */
.trade-step {
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
  font: inherit; font-size: 1.05rem; line-height: 1; cursor: pointer;
  min-width: 2.2rem; min-height: 2.2rem; color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: 2px;
  background-color: var(--parchment);
  background-image: linear-gradient(rgba(255, 255, 255, 0.35), rgba(0, 0, 0, 0.07));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
html.mode-dark .trade-step {
  background-image: linear-gradient(rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.28));
}
.trade-step:disabled { opacity: 0.45; cursor: default; }
.trade-qty {
  font: inherit; font-size: 0.9rem; color: var(--ink);
  font-variant-numeric: tabular-nums; text-align: center;
  width: 3.4rem; min-height: 2.2rem; padding: 0.15rem 0.3rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.trade-rm {
  font: inherit; font-size: 1rem; line-height: 1; cursor: pointer;
  min-width: 2.2rem; min-height: 2.2rem;
  background: transparent; color: var(--ink-soft);
  border: 1px solid var(--rule); border-radius: 2px;
}
.trade-rm:hover { color: var(--ink); border-color: var(--rule-strong); }
.trade-add { font-size: 0.9rem; }
.trade-picker { margin-top: 0.5rem; }
.trade-pick-search {
  font: inherit; font-size: 0.9rem; color: var(--ink);
  width: 100%; max-width: 16rem; min-height: var(--tap);
  margin-bottom: 0.45rem; padding: 0.3rem 0.5rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.trade-pick-grid {
  display: grid; gap: 0.25rem;
  grid-template-columns: repeat(auto-fill, minmax(var(--tap), 1fr));
  max-height: 13rem; overflow-y: auto; scrollbar-gutter: stable;
}
.trade-pick-cell {
  position: relative; display: flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0.1rem; cursor: pointer;
  font: inherit; font-size: 0.68rem; color: var(--ink);
  border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.trade-pick-cell:hover:not(:disabled) { border-color: var(--rule-strong); }
.trade-pick-cell:disabled { opacity: 0.4; cursor: default; }
.trade-pick-name { overflow: hidden; text-align: center; line-height: 1.15; }
.trade-pick-qty {
  position: absolute; right: 1px; bottom: 0;
  font-size: 0.62rem; color: var(--ink-soft); font-variant-numeric: tabular-nums;
  background: var(--parchment); padding: 0 0.15rem; border-radius: 2px;
}
.trade-pick-none { grid-column: 1 / -1; margin: 0; }
.trade-offer-row { display: flex; justify-content: center; margin-top: 0.7rem; }
.trade-offer-go { font-size: 0.95rem; padding: 0.4rem 1.4rem; }

/* The open lists: one duty-card panel per offer, the ledger inside. */
.trade-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.trade-row {
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.55rem 0.65rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.trade-head {
  margin: 0 0 0.35rem; display: flex; align-items: baseline;
  gap: 0.4rem; flex-wrap: wrap;
}
.trade-expiry {
  margin-left: auto; font-size: 0.75rem; font-style: italic; color: var(--ink-faint);
}
.trade-swap {
  display: grid; gap: 0.3rem 0.6rem;
  grid-template-columns: max-content 1fr; align-items: center;
}
.trade-dirlab { font-size: 0.8rem; color: var(--ink-soft); }
.trade-chips { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; }
.trade-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  border: 1px solid var(--rule-strong); border-radius: 2px;
  padding: 0.2rem 0.45rem; font-size: 0.85rem; font-variant-numeric: tabular-nums;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.trade-acts { display: flex; gap: 0.5rem; margin-top: 0.55rem; flex-wrap: wrap; align-items: center; }

/* ---- the auction house ----------------------------------------------------
   The block dresses in the trade bench's clothes; only the duration
   plates and the bid field are its own. */
.auc-tabs { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.6rem 0 0.7rem; }
.auc-tab { font-size: 0.9rem; }
.auc-tab.is-active {
  border-color: var(--ink); box-shadow: 0 0 0 1px var(--ink);
  font-weight: 600;
}
.auc-add-list { display: grid; gap: 0.4rem; }
.auc-add-row .inv-qty { margin-left: auto; }
/* The unfolded listing form: a card that names its goods, a label/control
   ledger, the deed bottom-right. */
.auc-form {
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.7rem 0.8rem; margin: -0.1rem 0 0.4rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.auc-form-head {
  display: flex; align-items: center; gap: 0.45rem; margin-bottom: 0.65rem;
}
.auc-form-name { font-weight: 600; }
.auc-form-head .inv-qty { margin-left: auto; }
.auc-form-grid {
  display: grid; grid-template-columns: max-content 1fr;
  gap: 0.6rem 0.9rem; align-items: center;
}
.auc-form-label { font-size: 0.88rem; color: var(--ink-soft); }
.auc-form-cell {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  min-width: 0;
}
.auc-form-hint { font-size: 0.75rem; font-style: italic; color: var(--ink-faint); }
.auc-reckon { font-size: 0.85rem; font-style: normal; color: var(--ink-soft); }
.auc-form-still { font-size: 0.92rem; font-weight: 600; }
.auc-form-acts {
  display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.8rem;
}
@media (max-width: 30rem) {
  .auc-form-grid { grid-template-columns: 1fr; gap: 0.25rem; }
  .auc-form-label { margin-top: 0.4rem; }
}
.auc-buy-qty { width: 4rem; min-height: var(--tap); }

/* Browse and My listings: one line per stand, the inventory's manner. */
.auc-lines { display: grid; gap: 0.4rem; }
.auc-line { flex-wrap: wrap; }
.auc-line-name { flex: 1 1 8rem; }
.auc-line-seller { font-size: 0.85rem; }
.auc-line-price {
  font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.auc-line-earned {
  font-size: 0.8rem; color: var(--ink-soft); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.auc-line-clock { margin-left: 0; }
.auc-line-yours {
  font-size: 0.75rem; font-style: italic; color: var(--ink-faint);
  margin-left: auto;
}
.auc-line-buy {
  display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto;
}

/* ---- social: chat, friends, search -----------------------------------------
   One round plate — the bubble — sits in the bottom-right corner and opens
   the chat sheet above itself. Its badge sums everything waiting: letters,
   friend requests, or a bare mark for fresh words in general. Inside the
   sheet the conversations run as chips along the top (General, Friends, and
   every open private chat); the window under them is the one that gets
   rebuilt. The log is a fixed window that scrolls itself; the row buttons
   are the same plates as everywhere else. Message bodies wrap hard — a
   300-char word must not widen the page. */
.chat-dock {
  position: fixed; right: clamp(0.6rem, 3vw, 1.5rem); z-index: 30;
  /* Stands on the phone's bottom bar; --botnav-h is unset on the wide rail. */
  bottom: calc(var(--botnav-h, 0px) + 0.7rem);
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem;
}
/* The bubble: a parchment plate like every other button, only round, and
   lifted a little higher because it floats over the page rather than
   sitting in one. */
.chat-bubble {
  position: relative;
  width: 3.4rem; height: 3.4rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: 1.5rem; line-height: 1; cursor: pointer;
  color: var(--ink);
  background-color: var(--parchment);
  background-image: linear-gradient(rgba(255, 255, 255, 0.35), rgba(0, 0, 0, 0.07));
  border: 1px solid var(--rule-strong);
  box-shadow: 0 0.25rem 0.7rem rgba(0, 0, 0, 0.35);
}
html.mode-dark .chat-bubble {
  background-image: linear-gradient(rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.28));
}
.chat-bubble:hover, .chat-bubble:focus-visible { border-color: var(--gold); }
.chat-bubble[aria-expanded="true"] { border-color: var(--gold); }
.chat-bubble svg { display: block; }
/* The badge rides the bubble's shoulder, ringed in parchment so it reads
   as sitting ON the plate rather than printed across its edge. */
.chat-bubble .dock-badge {
  position: absolute; top: -0.25rem; right: -0.25rem;
  border: 2px solid var(--parchment);
}

/* The chips along the top of the sheet: the inventory filters' shape, at
   the tap height, in one row that scrolls sideways once the private chats
   outgrow it. */
.chat-dock-tabs {
  display: flex; gap: 0.3rem; flex: none; align-items: center;
  overflow-x: auto; scrollbar-width: none;
  padding-bottom: 0.4rem; margin-bottom: 0.4rem;
  border-bottom: 1px solid var(--rule);
}
.chat-dock-tabs::-webkit-scrollbar { display: none; }
.dock-tab {
  display: inline-flex; align-items: center; gap: 0.35em; flex: none;
  font: inherit; font-size: 0.8rem; cursor: pointer;
  min-height: var(--tap); padding: 0.2rem 0.65rem;
  background: transparent; color: var(--ink-soft);
  border: 1px solid var(--rule); border-radius: 2px;
}
.dock-tab:hover { border-color: var(--rule-strong); }
.dock-tab[aria-pressed="true"] {
  background: var(--parchment); color: var(--ink);
  border-color: var(--rule-strong); font-weight: 600;
}
.dock-tab svg { display: block; }
.dock-badge {
  min-width: 1.3em; text-align: center;
  font-size: 0.7rem; font-weight: 600; color: #fff;
  background: var(--axis-force); border-radius: 0.65em;
  padding: 0.05em 0.35em;
}

/* The sheet that opens above the bubble. Its height is capped by the
   viewport minus the bar it stands on and the bubble under it, so on a
   short phone it never runs up under the site header. */
.chat-panel {
  width: min(22rem, calc(100vw - 1.2rem));
  height: min(28rem, calc(100vh - var(--botnav-h, 0px) - 6rem));
  height: min(28rem, calc(100dvh - var(--botnav-h, 0px) - 6rem));
  display: flex; flex-direction: column;
  background: var(--parchment);
  border: 1px solid var(--rule-strong);
  border-radius: 0.5rem;
  box-shadow: 0 0.35rem 1rem rgba(0, 0, 0, 0.4);
  padding: 0.5rem 0.6rem 0.4rem;
}
.chat-panel[hidden] { display: none; }
.chat-win { flex: 1; display: flex; flex-direction: column; min-height: 0; }
@media (max-width: 480px) {
  .chat-panel { width: calc(100vw - 1.2rem); }
}
.chat-panel-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin-bottom: 0.35rem;
}
.chat-panel-title {
  font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.1em; font-size: 0.72rem; color: var(--ink-soft);
  overflow-wrap: anywhere;
}
.chat-panel-close {
  font: inherit; font-size: 1.1rem; line-height: 1; cursor: pointer;
  background: transparent; color: var(--ink-soft); border: 0;
  padding: 0.2rem 0.5rem;
}
.chat-panel-close:hover { color: var(--ink); }
.chat-panel .chat-log { flex: 1; height: auto; min-height: 0; margin-top: 0; }
.chat-panel-body { flex: 1; overflow-y: auto; min-height: 0; }
.chat-panel-note { margin: 0.2rem 0 0; min-height: 1.2em; font-size: 0.8rem; }
/* Words arrived while you were reading further up: the log holds your place
   and this plate offers the way back down. Its own row under the log rather
   than a floating overlay — nothing to mis-position over a scrolling box. */
.chat-jump {
  align-self: center; margin-top: 0.3rem;
  font-size: 0.76rem; padding: 0.15rem 0.7rem;
}
.chat-jump[hidden] { display: none; }
.friend-sub {
  margin: 0.8rem 0 0.2rem;
  font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.1em; font-size: 0.7rem; color: var(--ink-soft);
}

/* Names are links wherever they appear. */
.name-link {
  font: inherit; font-weight: 600; cursor: pointer;
  background: transparent; border: 0; padding: 0;
  color: inherit; text-decoration: underline;
  text-decoration-color: var(--rule-strong); text-underline-offset: 0.15em;
}
.name-link:hover, .name-link:focus-visible { text-decoration-color: var(--gold); }
.chat-log {
  list-style: none; margin: 0.6rem 0 0; padding: 0.5rem 0.6rem;
  height: 18rem; overflow-y: auto;
  border: 1px solid var(--rule-strong); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.chat-msg {
  margin: 0 0 0.35rem; font-size: 0.95rem; line-height: 1.35;
  overflow-wrap: anywhere;
}
.chat-from { font-weight: 600; margin-right: 0.45em; }
.chat-msg.is-mine .chat-from { color: var(--gold); }
/* The stamp, in print beside every message. */
.chat-time {
  margin-left: 0.45em; font-size: 0.68rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.chat-form { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.chat-form input {
  flex: 1; min-width: 0;
  font-family: var(--body); font-size: 1rem; /* 16px: no iOS focus zoom */
  min-height: var(--tap); color: var(--ink);
  background: rgba(255, 255, 255, 0.42);
  border: 1px solid var(--rule-strong); border-radius: 0.2rem;
  padding: 0.45rem 0.7rem;
}
html.mode-dark .chat-form input { background: rgba(255, 255, 255, 0.07); }

.friend-list { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.friend-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--rule);
}
.friend-row:last-child { border-bottom: 0; }
.friend-name { flex: 1; font-weight: 600; min-width: 8rem; overflow-wrap: anywhere; }
.friend-state {
  font-size: 0.78rem; font-style: italic; color: var(--ink-faint);
}
.friend-row-request { background: var(--axis-endurance-wash); padding: 0.4rem 0.5rem; border-radius: 2px; }

/* ---- college ---------------------------------------------------------------
   The prospectus is a stack of the auction form's cards; only the spacing
   and the line naming what a course teaches are the College's own. */
.college-list { display: grid; gap: 0.6rem; margin-top: 0.7rem; }
.college-running { border-left-color: var(--gold); }
.college-reward { margin: 0.35rem 0 0; font-weight: 600; }

/* ---- the Book of Merits ------------------------------------------------------
   Borrowed clothes again: the College's cards in the College's grid. What is
   the book's own is the mark beside every merit number, the gold edge an
   earned card wears, and the quiet an unearned one keeps. Two columns once
   there is room; the phone reads it as one list. */
.merit-list { display: grid; gap: 0.6rem; margin-top: 0.5rem; }
@media (min-width: 40rem) { .merit-list { grid-template-columns: 1fr 1fr; } }
.merit-group { margin: 1.1rem 0 0; font-size: 1rem; }
.merit-card { opacity: 0.72; }
.merit-card.is-earned { opacity: 1; border-left-color: var(--gold); }
.merit-when { margin: 0.25rem 0 0; font-size: 0.9rem; }
.merit-points { display: inline-flex; align-items: baseline; gap: 0.25rem; white-space: nowrap;
  font-weight: 600; font-variant-numeric: tabular-nums; }
.merit-mark { color: var(--gold); }
.merit-purse { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.7rem;
  margin: 0.6rem 0 0; font-size: 1rem; }
.merit-purse-label { font-weight: 600; }
.merit-stall-head { margin: 1.6rem 0 0; font-size: 1.05rem; }
.profile-merits { margin: 0.3rem 0 0; }

/* ---- guilds ----------------------------------------------------------------
   The tag is a small stamp worn beside names wherever they print; the rest
   of the block dresses in borrowed clothes — the market's tiles, the friends
   list, the auction form, the claim modal. Only the fields are its own. */
.guild-tag {
  display: inline-block; padding: 0.05rem 0.4rem;
  border: 1px solid var(--rule-strong); border-radius: 0.15rem;
  font-family: var(--display); font-size: 0.68rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-soft); vertical-align: 0.1em;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
html.mode-dark .guild-tag { background: rgba(255, 255, 255, 0.08); }
.dock-tab .guild-tag { font-size: 0.6rem; padding: 0 0.3rem; }
.guild-head {
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
  margin-top: 0.6rem;
}
.guild-name { margin: 0; font-size: 1.15rem; }
.guild-desc { max-width: 40em; margin: 0.4rem 0 0; overflow-wrap: anywhere; }
.guild-describe { margin-top: 0.4rem; }
/* Fields in the trade bench's copper field's clothes: parchment, tap-high. */
.guild-input {
  font: inherit; font-size: 1rem; /* 16px: no iOS focus zoom */
  color: var(--ink); width: 100%; max-width: 22rem;
  min-height: var(--tap); padding: 0.3rem 0.5rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 2px;
}
.guild-textarea { resize: vertical; max-width: 34rem; }
.guild-tag-input { max-width: 6rem; text-transform: uppercase; letter-spacing: 0.1em; }
/* The officers' invite line: a name and the plate, on one row. */
.guild-invite-row { margin-top: 0.4rem; }
.guild-invite-input { max-width: 14rem; }
html.mode-dark .guild-input { background: rgba(255, 255, 255, 0.07); }
.profile-guild {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
  margin: 0.3rem 0 0;
}

/* ---- profiles --------------------------------------------------------------
   The profile page: the wax-seal avatar writ large, the record of mastery,
   and the social actions. The Settings picker reuses .option chips with a
   seal inside. */
.profile-seal { font-size: 1.6rem; }
/* The age counter: the profile's one glanceable stat, sat on the identity
   row's far edge (and dropping under the portrait when the row wraps). The
   number is the whole point — big, semibold, tabular, so two profiles
   compare down the column — with the label in the small display caps every
   other stat label in the game wears. */
.profile-age {
  text-align: center; line-height: 1;
  padding: 0.4rem 0.9rem;
  border-left: 1px solid var(--rule);
}
.identity > .profile-age { flex: 0 0 auto; }
.profile-age-num {
  display: block;
  font-family: var(--display);
  font-size: 2.4rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.profile-age-label {
  display: block; margin-top: 0.25rem;
  font-family: var(--display); font-size: 0.66rem;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-faint);
}
/* Wrapped onto its own line, the rule reads as a stray mark — the row
   above it does the separating instead. */
@media (max-width: 30rem) {
  .profile-age { border-left: 0; border-top: 1px solid var(--rule); width: 100%; }
}
.profile-bio { margin: 0.4rem 0 0; overflow-wrap: anywhere; }
.profile-actions {
  display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem;
}
.avatar-pick { font-size: 1.05rem; }


/* ---- the Home map -----------------------------------------------------------
   The town and the five roads out of it. Scenery is inked in the page's own
   colours: the river and bay in woad, the wood in verdigris, hills and roads
   in soft ink, the frame in gold. The frame is filled with parchment so the
   ruled page does not show through the chart. Everything sized off the
   400×300 viewBox; the SVG scales to its column. */
.map-spread { display: grid; grid-template-columns: 1fr; gap: 1.25rem; margin-top: 0.5rem; }
@media (min-width: 48rem) {
  .map-spread { grid-template-columns: minmax(0, 3fr) minmax(16rem, 2fr); align-items: start; }
}
.chart svg { display: block; width: 100%; height: auto; }
.map-frame { fill: var(--parchment); stroke: var(--gold); stroke-width: 1.5; }
.map-frame-in { fill: none; stroke: var(--gold); stroke-width: 0.5; opacity: 0.7; }
.map-river { fill: none; stroke: var(--axis-understanding); stroke-width: 2.4; stroke-linecap: round; opacity: 0.85; }
.map-river-bank { fill: none; stroke: var(--axis-understanding-wash); stroke-width: 9; stroke-linecap: round; }
.map-sea { fill: var(--axis-understanding-wash); }
.map-wave { fill: none; stroke: var(--axis-understanding); stroke-width: 0.7; opacity: 0.55; }
.map-hill { fill: none; stroke: var(--ink-soft); stroke-width: 0.9; stroke-linejoin: round; }
.map-tree { fill: none; stroke: var(--axis-precision); stroke-width: 0.9; }
.map-road { fill: none; stroke: var(--ink-soft); stroke-width: 1.1; stroke-dasharray: 3 3; opacity: 0.8; }
.map-road.is-closed { opacity: 0.45; }
.map-road.is-lit { stroke: var(--gold); stroke-width: 2; stroke-dasharray: 5 4; opacity: 1; animation: map-march 1.4s linear infinite; }
.map-road.is-closed.is-lit { animation: none; opacity: 0.9; }
@keyframes map-march { to { stroke-dashoffset: -18; } }
@media (prefers-reduced-motion: reduce) { .map-road.is-lit { animation: none; } }
.map-label { font-family: var(--display); font-size: 8.5px; letter-spacing: 0.08em; fill: var(--ink); text-transform: uppercase; }
.map-region { font-family: var(--body); font-style: italic; font-size: 8px; letter-spacing: 0.2em; fill: var(--ink-faint); }
.map-rose { fill: none; stroke: var(--ink-soft); stroke-width: 0.8; }
.map-rose-n { fill: var(--ink); }
.map-rose-t { font-family: var(--display); font-size: 7px; fill: var(--ink-soft); }
.map-here-tag { font-family: var(--body); font-style: italic; font-size: 7.5px; letter-spacing: 0.1em; fill: var(--ink-soft); }

/* Pins: a 44px hit circle round a small inked mark. A closed pin is not a
   control — no hand, no hover, an inked X in the cover's leather through
   it and a line through its name. The town's diamond flies a pennant. */
.map-pin { cursor: pointer; }
.map-pin.is-closed { cursor: default; }
.map-hit { fill: transparent; }
.map-halo { fill: none; stroke: transparent; }
.map-dot { fill: var(--parchment); stroke: var(--ink); stroke-width: 1.4; transition: fill 0.12s ease; }
.map-core { fill: var(--ink); }
.map-pin:not(.is-closed):hover .map-dot, .map-pin:not(.is-closed):focus-visible .map-dot { fill: var(--gold-lit); }
.map-pin:focus-visible { outline: none; }
.map-pin:focus-visible .map-dot, .map-pin:focus-visible .map-town { stroke: var(--gold); stroke-width: 2.2; }
.map-pin.is-picked .map-halo { stroke: var(--gold); opacity: 0.8; }
.map-pin.is-picked .map-dot { fill: var(--gold-lit); }
.map-pin.is-closed .map-dot { stroke: var(--ink-soft); }
.map-pin.is-closed .map-core { fill: var(--ink-soft); }
.map-pin.is-closed .map-label { text-decoration: line-through; fill: var(--ink-soft); }
.map-strike { fill: none; stroke: var(--leather); stroke-width: 2.2; stroke-linecap: round; opacity: 0.9; }
.map-town { fill: var(--leather); stroke: var(--gold-lit); stroke-width: 1; transition: fill 0.12s ease; }
.map-pole { fill: none; stroke: var(--ink); stroke-width: 1; stroke-linecap: round; }
.map-pennant { fill: var(--gold); stroke: var(--ink); stroke-width: 0.6; stroke-linejoin: round; }
.map-home:hover .map-town, .map-home:focus-visible .map-town { fill: var(--gold); }
.map-home.is-picked .map-pennant { fill: var(--gold-lit); }
/* The leather is too dark to read as a strike on a dark page. */
html.mode-dark .map-strike { stroke: #c9706f; }

/* The route card: a wash panel beside the chart. On a phone it stacks
   under the chart, so it is kept short: tighter padding and gaps, the
   Town's list in one line per entry, no plate of its own until a road
   opens (the disabled "Roads closed" plate said nothing the aside did not). */
.map-route {
  border: 1px solid var(--rule); border-radius: 0.2rem;
  padding: 0.8rem 0.9rem 0.9rem;
  display: flex; flex-direction: column; gap: 0.55rem;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.map-route h3 { margin: 0; font-size: 1.1rem; letter-spacing: 0.1em; text-transform: none; }
.map-route p { margin: 0; max-width: 34em; }
.map-eyebrow { font-family: var(--display); font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-soft); }
.map-note { font-style: italic; }
.map-here { margin: 0; padding: 0.6rem 0; list-style: none; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.98rem; }
.map-here b { font-family: var(--display); font-weight: 600; font-size: 0.78rem; letter-spacing: 0.08em; }
.map-fare { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin: 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); padding: 0.6rem 0; }
.map-fare div { display: flex; flex-direction: column; gap: 0.1rem; }
.map-fare dt { font-family: var(--display); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); }
.map-fare dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; font-size: 1.05rem; display: inline-flex; align-items: center; gap: 0.3rem; }
/* An hourglass: two triangles of ink, point to point. */
.glass { display: inline-block; width: 0.7em; height: 0.85em; background: currentColor; clip-path: polygon(0 0, 100% 0, 50% 50%, 100% 100%, 0 100%, 50% 50%); opacity: 0.85; }
.map-go { min-width: 9rem; }
.map-go[disabled] { display: none; }
@media (max-width: 47.99rem) {
  .map-spread { gap: 0.75rem; }
  .map-route { padding: 0.65rem 0.8rem 0.75rem; gap: 0.4rem; }
  .map-route p, .map-here { font-size: 0.95rem; }
  /* The Town's list and the closing note both repeat what is already on
     the page — the blurb names the four places, the aside says the roads
     are shut — and the phone has no height to say things twice. */
  .map-here, .map-note { display: none; }
}

/* ---- the bell ---------------------------------------------------------------
   Notices, behind one press in the header. The count sits on the bell's
   shoulder in the cover's leather; hidden at nought. */
.bell { position: relative; }
.bell-count {
  position: absolute; top: 0.35rem; right: 0.1rem;
  min-width: 1.1rem; height: 1.1rem; padding: 0 0.25rem; border-radius: 0.55rem;
  background: var(--leather); color: var(--parchment);
  font-family: var(--body); font-size: 0.7rem; font-weight: 600; line-height: 1.1rem; text-align: center;
  font-variant-numeric: tabular-nums; text-shadow: none;
  box-shadow: 0 0 0 1px var(--gold-lit);
}

/* ---- notices ------------------------------------------------------------------
   The morning post (its own page now, off the More menu and the bell): one card per thing worth acting on, colour-striped by
   what it concerns (amber Energy, woad Resolve, gold letters, green
   callers), whole card clickable. The words carry the meaning; the stripe
   is just the sorting. */
/* Home's quick links: anchors wearing the parchment plate, one to a cell,
   so the front door is a row of doors and not a list of words. */
.quick-links {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.5rem; margin-top: 0.6rem;
}
.quick-links .inv-act {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; font-size: 0.95rem; font-weight: 600;
}
.notice-list { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.notice {
  margin-bottom: 0.45rem; display: flex; align-items: stretch; gap: 0.3rem;
  transition: transform 0.25s ease, opacity 0.25s ease;
}
/* The put-away swipe: off to the left and gone; the repaint that follows
   the server write removes the row for real. */
.notice.is-dismissing { transform: translateX(-110%); opacity: 0; pointer-events: none; }
.notice .notice-body { flex: 1; min-width: 0; }
/* The put-away button on dismissable notices: quiet until hovered, but a
   full-height 44px-wide target. */
.notice-x {
  flex: none; width: 2.4rem; border: 1px solid var(--rule); border-radius: 2px;
  background: transparent; color: var(--ink-faint);
  font: inherit; font-size: 1.1rem; line-height: 1; cursor: pointer;
}
.notice-x:hover, .notice-x:focus-visible {
  color: var(--ink); border-color: var(--rule-strong);
}
/* The dismissal that is a verb (Decline) — a word, sized to itself. */
.notice-x-word { width: auto; padding: 0 0.7rem; font-size: 0.9rem; }
.notice-body {
  display: flex; align-items: baseline; gap: 0.5em; width: 100%;
  font: inherit; font-size: 0.98rem; text-align: left; cursor: pointer;
  color: var(--ink);
  border: 1px solid var(--rule); border-left: 3px solid var(--rule-strong);
  border-radius: 2px; padding: 0.55rem 0.7rem; min-height: var(--tap);
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.notice-body:hover, .notice-body:focus-visible { border-color: var(--rule-strong); }
.notice-energy .notice-body { border-left-color: var(--energy); }
.notice-study  .notice-body { border-left-color: var(--axis-understanding); }
.notice-infirmary .notice-body { border-left-color: var(--health); }
.notice-fight .notice-body { border-left-color: var(--health); }
.notice-letter .notice-body { border-left-color: var(--gold); }
.notice-friend .notice-body { border-left-color: var(--axis-precision); }
.notice-sale .notice-body { border-left-color: var(--gold); }

/* ---- the fight scene ------------------------------------------------------
   Two combatant plates (the opponent's first — they stand across from you),
   the blow-by-blow, then your moves. HP tracks reuse .bar-track/.bar-fill,
   so the width transition that animates the chrome bars animates the fight
   too. Move buttons are .inv-act, so the parchment-plate treatment and its
   dark-mode twin apply without new selectors. All sizes in rem. */
.fight-scene { display: grid; gap: 0.9rem; margin-top: 0.5rem; }
/* Two plates face each other at every width — the versus screen. The
   attacker stands on the LEFT (game.js orders them). */
.fight-plates { display: grid; gap: 0.9rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fight-plate {
  position: relative; /* the floating hit numbers land inside */
  display: flex; flex-direction: column; gap: 0.45rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--rule); border-left: 4px solid var(--rule-strong);
  border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.fight-plate.is-mine { border-left-color: var(--gold); }
.fight-plate-info { width: 100%; min-width: 0; }
.fight-plate .fight-doll-box { margin: 0 auto; }
.fight-plate-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem;
}
.fight-hp { height: 1.25rem; margin-top: 0.3rem; }
.fight-hp .bar-fill { background: var(--health); }
/* A combatant's doll on their fight plate: the MAIN EVENT — same
   silhouette and socket dress as the inventory doll, read-only, big.
   Gear flanks it as ICONS ringed in their rarity colour: jewelry left,
   armor right, the weapon in the hand socket. The name is the tooltip
   (and the aria-label), never the picture — two dolls abreast on a phone
   left no room for words. The socket offset tracks the figure size (the
   hand sits ~42% down the rendered figure). */
.fight-doll-box { flex: none; text-align: center; }
.fight-doll-row { display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
.fight-doll-row .doll-worn {
  flex: 0 1 auto; margin: 0; min-width: 2rem;
  display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
}
.doll-gear {
  display: flex; align-items: center; justify-content: center;
  width: 2.15rem; height: 2.15rem;
  border: 2px solid currentColor; border-radius: 3px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
/* Common wears no rarity class, so its ring is the rule colour; a rarity
   class sets `color`, and the ring follows it. */
.doll-gear:not([class*="rarity-"]) { color: var(--rule-strong); }
.doll-gear .px-icon { width: 28px; height: 28px; }
/* No icon to show: the word, small, as it was. */
.doll-gear-word {
  width: auto; height: auto; min-height: 2.15rem; padding: 0 0.3rem;
  font-size: 0.66rem; line-height: 1.15; font-weight: 600;
  border-style: dashed;
}
.fight-doll { width: 10.5rem; padding: 0.5rem; margin: 0 auto; flex: none; }
.fight-doll .doll-figure { width: 104px; }
.fight-doll .doll-weapon {
  right: 2px; top: 84px; width: 2.6rem; height: 2.6rem; min-height: 0;
  cursor: default; font-size: 0.7rem; padding: 0;
}
.fight-doll .doll-weapon.is-filled { border-width: 2px; border-color: currentColor; }
.fight-doll .doll-weapon.is-filled:not([class*="rarity-"]) { color: var(--rule-strong); }
.fight-doll .doll-weapon .px-icon { width: 32px; height: 32px; }
.doll-worn {
  list-style: none; margin: 0.25rem 0 0; padding: 0;
  font-size: 0.78rem; line-height: 1.4;
}
/* Phones: still side by side, scaled to fit two abreast. Icons shrink a
   notch and the ring thins; they stay legible where words did not. */
@media (max-width: 480px) {
  .fight-plates { gap: 0.5rem; }
  .fight-plate { padding: 0.5rem 0.55rem; }
  /* Two dolls abreast leave no room for a column either side, so the
     gear becomes rows: jewelry above the figure, armor beneath it. */
  .fight-doll-row { gap: 0.25rem; flex-wrap: wrap; }
  .fight-doll-row .doll-worn {
    flex: 1 0 100%; flex-direction: row; flex-wrap: wrap; justify-content: center;
    min-width: 0; gap: 0.2rem;
  }
  .fight-doll-row .doll-worn-left { margin-bottom: 0.25rem; }
  .fight-doll-row .doll-worn-right { margin-top: 0.25rem; }
  .fight-doll-row .doll-worn:empty { display: none; }
  .fight-doll { width: 6.5rem; padding: 0.3rem; }
  .fight-doll .doll-figure { width: 72px; }
  .fight-doll .doll-weapon { top: 56px; right: 0; width: 2rem; height: 2rem; font-size: 0.6rem; }
  .fight-doll .doll-weapon .px-icon { width: 24px; height: 24px; }
  .doll-gear { width: 1.7rem; height: 1.7rem; border-width: 1.5px; }
  .doll-gear .px-icon { width: 20px; height: 20px; }
  .doll-gear-word { font-size: 0.6rem; min-height: 1.7rem; }
}
.fight-lines {
  list-style: none; margin: 0; padding: 0.6rem 0.9rem;
  max-height: 14rem; overflow-y: auto; scrollbar-gutter: stable;
  display: flex; flex-direction: column; gap: 0.35rem;
  border: 1px solid var(--rule); border-radius: 2px;
  background: rgba(255, 255, 255, calc(var(--panel-alpha) * var(--panel-dim, 1)));
}
.fight-line { font-size: 0.95rem; }
.fight-line-dmg { font-weight: 600; font-variant-numeric: tabular-nums; }
/* The verdict prints its word large — never colour alone. */
.fight-verdict {
  font-family: var(--display); text-transform: uppercase;
  letter-spacing: 0.12em; font-weight: 600; margin-top: 0.25rem;
}
.fight-moves { display: flex; flex-direction: column; gap: 0.6rem; }
/* The verb row at the root of the fight menu: Attack folds the move
   plates out beneath it, Bag waits on in-fight items, Flee stands at the
   far end so a panicked thumb never grazes it reaching for Attack. */
.fight-verbs { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.fight-verbs .inv-act { flex: 1 1 auto; min-width: 6.5rem; }
.fight-verbs .fight-flee { margin-left: auto; }
.fight-moves:not(.is-open) .move-plates { display: none; }
/* The move plates: every earned move as a parchment plate (the raised
   .inv-act-real fill), name on top, what it does beneath. The axis mark
   wears its colour; the plate itself never borrows an accent. */
.move-plates {
  display: grid; gap: 0.35rem;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
.move-plate {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem;
  text-align: left; padding: 0.35rem 0.6rem;
}
.move-plate-head { display: flex; align-items: baseline; gap: 0.35em; font-weight: 600; font-size: 0.95rem; width: 100%; flex-wrap: wrap; }
/* The key a weapon move asks for — "requires sword" — sits at the end of
   the name row, plain ink when the hand is right and red when it is not.
   Force's red is the game's red; the plate itself never tints. */
.move-req { margin-left: auto; font-size: 0.7rem; font-weight: 500; color: var(--ink-soft); white-space: nowrap; }
.move-req.is-unmet { color: var(--axis-force); }
.move-plate.is-unmet .move-plate-name { color: var(--ink-soft); }
.move-plate-head .mark-force         { color: var(--axis-force); }
.move-plate-head .mark-understanding { color: var(--axis-understanding); }
.move-plate-head .mark-endurance     { color: var(--axis-endurance); }
.move-plate-head .mark-precision     { color: var(--axis-precision); }
.move-plate-hint { font-size: 0.74rem; color: var(--ink-soft); font-weight: 400; }
.fight-line-miss { font-style: italic; color: var(--ink-faint); }
.fight-line-stage { color: var(--ink-soft); font-size: 0.9rem; }
.hit-float.hit-heal { font-size: 1.2rem; color: var(--axis-precision); }
/* The stage ladder under a plate's health: only shifted stats print. */
.fight-stages { display: flex; flex-wrap: wrap; gap: 0.3rem 0.6rem; min-height: 1.1rem; margin-top: 0.25rem; }
.fight-stages .axis-tag.is-up::after { content: ' ↑'; }
.fight-stages .axis-tag.is-down::after { content: ' ↓'; }
.fight-wait { color: var(--ink-soft); font-style: italic; }
/* Phones: the strike buttons hold still — the log moves BELOW them, so a
   growing blow-by-blow never shoves the buttons mid-tap. (The log keeps
   itself scrolled to the newest line, so reading order survives.) Eight
   plates stack a screen tall in one column, so the grid goes two-up with
   the hint a touch smaller — the whole menu fits under the thumb. */
@media (max-width: 480px) {
  #fight-scene { display: flex; flex-direction: column; gap: 0.9rem; }
  #fight-scene .fight-lines { order: 1; }
  .move-plates { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.3rem; }
  .move-plate { padding: 0.3rem 0.5rem; min-width: 0; }
  .move-plate-head { font-size: 0.9rem; gap: 0.3em; }
  .move-plate-hint { font-size: 0.7rem; line-height: 1.25; }
  /* The requires line takes a row of its own under the name, so a long
     name and a short one read the same. */
  .move-req { font-size: 0.66rem; flex-basis: 100%; margin-left: 0; }
}
.notice-craft  .notice-body { border-left-color: var(--axis-endurance); }

/* The live player search box in the friends panel. */
.friend-search {
  width: 100%;
  font-family: var(--body); font-size: 1rem; /* 16px: no iOS focus zoom */
  min-height: var(--tap); color: var(--ink);
  background: rgba(255, 255, 255, 0.42);
  border: 1px solid var(--rule-strong); border-radius: 0.2rem;
  padding: 0.45rem 0.7rem;
}
html.mode-dark .friend-search { background: rgba(255, 255, 255, 0.07); }

/* Density inside the dock panel: it is a narrow window, not a page — the
   plates and rows go compact, names stay left, one line per row. */
.chat-panel .friend-row {
  padding: 0.25rem 0; gap: 0.45rem; align-items: center; flex-wrap: nowrap;
}
.chat-panel .friend-name {
  min-width: 0; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: left;
}
.chat-panel .inv-act {
  min-height: 2rem; padding: 0.2rem 0.55rem; font-size: 0.78rem;
  flex: none;
}
.chat-panel .friend-state { flex: none; font-size: 0.72rem; }
.chat-panel .friend-search { min-height: 2.4rem; }
.chat-panel .friend-sub { margin-top: 0.6rem; }
.chat-panel .friend-sub:first-child { margin-top: 0; }

/* Uploaded profile pictures: always displayed in a 3:4 portrait frame,
   whatever arrived — object-fit makes the ratio law. */
.avatar-img {
  width: 9rem; height: 12rem; object-fit: cover;
  border: 1px solid var(--rule-strong); border-radius: 0.25rem;
  background: var(--parchment-shade);
}
.avatar-preview { width: 4.5rem; height: 6rem; }

/* ---- popped-out chat (chat.html) -------------------------------------------
   The whole window is one conversation: a parchment panel filling the
   viewport, log stretched, composer at the bottom. */
body[data-page='chat'] { padding: 0.5rem; }
body[data-page='chat'] .pop-shell {
  width: 100%; max-width: 30rem; flex: 1; min-height: 0;
  display: flex; flex-direction: column; gap: 0.35rem;
  background: var(--parchment);
  border: 1px solid var(--rule-strong); border-radius: 0.3rem;
  padding: 0.5rem 0.6rem;
}
body[data-page='chat'] .chat-log { flex: 1; height: auto; min-height: 0; margin-top: 0; }

/* The window buttons sit as a tight group on the panel's right shoulder. */
.chat-panel-btns { display: inline-flex; align-items: center; }

/* The ruled writing lines read as clutter now that the pages hold cards,
   seals and portraits rather than ledger entries — on game pages the paper
   keeps its mottling and loses the rules entirely. (The shared sheet still
   rules the homepage and forum, which are prose.) */
.page.ruled {
  background-image:
    radial-gradient(60% 45% at 18% 12%, rgba(198, 168, 112, 0.16), transparent 70%);
}

/* ---- admin tools ----------------------------------------------------------
   Admin-only chrome: the drop-table editor's grid rows, and the two-column
   economy readout. Deliberately plainer than player-facing pages. */
.lt-editor { padding: 0.5rem 0.7rem; }
.lt-row {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) 5rem 3.5rem 3.5rem 4.5rem 4rem 2rem;
  gap: 0.4rem; align-items: center; margin-bottom: 0.3rem;
}
.lt-head { font-weight: 600; font-size: 0.85em; color: var(--ink-faint); }
/* Board seams in the drop-table list — one heading per school that has a
   board, so the eye knows where one ends and the next begins. */
.lt-group {
  list-style: none;
  margin: 0.9rem 0 0.25rem; padding: 0;
  font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-faint);
}
#lt-list .lt-group:first-child { margin-top: 0.2rem; }
.lt-row input, .lt-row select { min-width: 0; padding: 0.25rem 0.35rem; }
.lt-pct { font-variant-numeric: tabular-nums; text-align: right; }
.lt-x { min-height: 0; padding: 0.1rem 0.4rem; cursor: pointer; }
.econ-cols {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 0.8rem;
}
#pi-panel .record { margin: 0.6rem 0; }

/* ---- the stat octagon ------------------------------------------------------
   Eight live attributes on one frame: The World across the four southern
   vertices in axis order, the battle stats mirrored across the four
   northern ones, so each axis stands directly above its own twin. Colour
   is the axis here as everywhere, and it ships with the axis mark in the
   label, so the halves stay readable without it.

   Two pages draw it. The Admin panel points it at whoever was looked up;
   the Account page points it at you.

   Everything paints through the theme tokens, which is what carries the
   figure into dark mode: the axis inks, the gold of the reading, and the
   parchment halo behind the ring numbers all flip on their own. */
.oct { margin: 0.9rem 0 0.5rem; }
.oct-svg {
  display: block;
  width: 100%; max-width: 34rem; height: auto;
  /* The wheel is a zoom over this box, not a scroll. */
  touch-action: none;
}
.oct-half { stroke: none; }
.oct-half-north { fill: var(--gold); fill-opacity: 0.09; }
.oct-half-south { fill: var(--ink); fill-opacity: 0.06; }
.oct-ring { fill: none; stroke: var(--rule); stroke-width: 1; }
.oct-rim { fill: none; stroke: var(--rule-strong); stroke-width: 1.4; }
.oct-split { stroke: var(--rule-strong); stroke-width: 1; stroke-dasharray: 3 5; }
.oct-spoke { stroke-width: 1; opacity: 0.4; }
.oct-spoke.axis-force { stroke: var(--axis-force); }
.oct-spoke.axis-understanding { stroke: var(--axis-understanding); }
.oct-spoke.axis-endurance { stroke: var(--axis-endurance); }
.oct-spoke.axis-precision { stroke: var(--axis-precision); }
/* The reading itself wears the house metal — it belongs to no single axis,
   and gold is the one accent that holds on every paper, light and dark. */
.oct-shape {
  fill: var(--gold); fill-opacity: 0.16;
  stroke: var(--gold); stroke-width: 2;
  stroke-linejoin: round;
}
.oct-v.axis-force .oct-dot, .oct-v.axis-force .oct-over { fill: var(--axis-force); }
.oct-v.axis-understanding .oct-dot, .oct-v.axis-understanding .oct-over { fill: var(--axis-understanding); }
.oct-v.axis-endurance .oct-dot, .oct-v.axis-endurance .oct-over { fill: var(--axis-endurance); }
.oct-v.axis-precision .oct-dot, .oct-v.axis-precision .oct-over { fill: var(--axis-precision); }
.oct-v.axis-force .oct-name, .oct-v.axis-force .oct-halo { color: var(--axis-force); }
.oct-v.axis-understanding .oct-name, .oct-v.axis-understanding .oct-halo { color: var(--axis-understanding); }
.oct-v.axis-endurance .oct-name, .oct-v.axis-endurance .oct-halo { color: var(--axis-endurance); }
.oct-v.axis-precision .oct-name, .oct-v.axis-precision .oct-halo { color: var(--axis-precision); }
.oct-dot { stroke: var(--parchment); stroke-width: 1.2; }
.oct-halo { fill: none; stroke: currentColor; stroke-width: 2; opacity: 0; }
.oct-v.is-hot .oct-halo { opacity: 0.6; }
.oct-name {
  fill: currentColor;
  font-size: 12.5px; font-weight: 600;
}
.oct-val {
  fill: var(--ink);
  font-size: 12px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* A stat past the rim keeps its dot ON the rim, so the number beside it is
   the only honest reading — say so by weight, and by the chevron pointing
   out of the frame that game.js draws alongside. */
.oct-val-over { fill: var(--gold); font-style: italic; }
.oct-cap {
  fill: var(--ink-soft);
  font-family: var(--display);
  font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
}
/* Ring numbers sit due north, in the gap between two vertices where no
   spoke and no reading can reach them. The parchment stroke under the fill
   keeps them legible where the shape passes behind. */
.oct-ring-label {
  fill: var(--ink-faint); font-size: 9.5px;
  font-variant-numeric: tabular-nums;
  stroke: var(--parchment); stroke-width: 3px; stroke-linejoin: round;
  paint-order: stroke fill;
}
/* A whole 45-degree wedge is the hover target: at low readings every dot
   piles into the middle, and a wedge stays hittable regardless. */
.oct-hit { fill: transparent; pointer-events: all; }
.oct-read {
  margin: 0.15rem 0 0.5rem;
  min-height: 1.25rem;
  font-size: 0.85rem; color: var(--ink-soft);
}
.oct-bar { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.oct-scale {
  font-size: 0.8rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 480px) {
  .lt-row { grid-template-columns: 1fr 4rem 3rem 3rem 3.5rem 3.5rem 2rem; }
}
.mk-row {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) 6rem 6rem 2rem;
  gap: 0.4rem; align-items: center; margin-bottom: 0.3rem;
}
.mk-row input, .mk-row select { min-width: 0; padding: 0.25rem 0.35rem; }

/* ---- section cards --------------------------------------------------------
   One parchment card under a section's page head, the first time it is
   opened: what the place is and one small task worth a silver. Inline, not
   a veil — the section stays usable underneath, and Got it puts it away.
   The toast is the silver landing: one small line, bottom-centre, gone in
   a few seconds. The task tick is the one new colour here. */
.sec-card {
  margin: 0 0 1rem;
  padding: 0.8rem 1rem;
  background: var(--parchment); color: var(--ink);
  border: 1px solid var(--gold); border-radius: 4px;
  box-shadow: 0 2px 8px rgba(20, 14, 8, 0.18);
  --task-done: #4d7a3a;
}
html.mode-dark .sec-card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); }
html.mode-dark .sec-card, html.mode-dark .toasts, html.mode-dark .task-list { --task-done: #8fc26f; }
.sec-card-eyebrow {
  margin: 0 0 0.2rem;
  font-family: var(--display); font-size: 0.72rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--gold);
}
.sec-card-title {
  margin: 0 0 0.4rem;
  font-family: var(--display); font-weight: 600; font-size: 1.15rem;
  text-wrap: balance;
}
.sec-card-text { margin: 0 0 0.4rem; font-size: 0.95rem; }
.sec-card-task {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0.6rem 0; font-weight: 600; font-size: 0.95rem;
}
.sec-card-tick {
  flex: 0 0 auto; width: 1.4rem; text-align: center;
  color: var(--ink-faint);
}
.sec-card-task.is-done .sec-card-tick,
.task-list .is-done .sec-card-tick,
.toast-tick { color: var(--task-done); }
.sec-card-task.is-done .sec-card-task-text {
  text-decoration: line-through; color: var(--ink-soft); font-weight: 400;
}
.sec-card-reward {
  margin-left: auto; white-space: nowrap;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.sec-card-actions {
  display: flex; gap: 0.6rem; justify-content: flex-end; align-items: center;
  flex-wrap: wrap;
}
.sec-card .inv-act, .sec-card .btn { min-height: var(--tap); }

/* The silver landing. pointer-events: none — nothing here is a control,
   and a toast must never stand between a thumb and the page. */
.toasts {
  position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%);
  z-index: 1190;
  display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
  width: calc(100% - 2rem); max-width: 24rem;
  pointer-events: none;
  --task-done: #4d7a3a;
}
.toast {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.55rem 0.9rem;
  font-size: 0.92rem;
  background: var(--parchment); color: var(--ink);
  border: 1px solid var(--gold); border-radius: 4px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  animation: toast-in 200ms ease-out;
}
.toast.is-leaving { opacity: 0; transition: opacity 300ms ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }
/* Below the rail breakpoint the nav bar owns the bottom edge; the toast
   stands on it, and on a phone clears the chat bubble too. */
@media (max-width: 63.999rem) { .toasts { bottom: calc(var(--botnav-h) + 1rem); } }
@media (max-width: 480px) { .toasts { bottom: calc(var(--botnav-h) + 4.4rem); } }

/* Settings: the tasks and their ticks. */
.task-list {
  list-style: none; margin: 0.5rem 0 0; padding: 0;
  --task-done: #4d7a3a;
}
.task-list li {
  display: flex; align-items: center; gap: 0.5rem;
  min-height: var(--tap); padding: 0.15rem 0;
  border-top: 1px solid var(--rule);
  font-size: 0.92rem;
}
.task-list li:first-child { border-top: 0; }
.task-list-title { font-weight: 600; flex: 0 0 auto; }
.task-list-text { color: var(--ink-soft); }
.task-list .is-done .task-list-text { text-decoration: line-through; }

/* ---- the fanfare ----------------------------------------------------------
   The house flourish for a once-ever moment — a veteran duty landing
   mid-session, a first injury — as a full-screen beat: dimmed desk, a
   parchment card, and (motion permitting) a canvas burst behind it. Gold
   by default; .unlock-veil-hurt is the bad-news tone. Click anywhere
   dismisses. */
.unlock-veil {
  position: fixed; inset: 0; z-index: 1300;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 14, 8, 0.45);
  animation: unlock-in 220ms ease-out;
  cursor: pointer;
}
.unlock-veil.is-leaving { opacity: 0; transition: opacity 240ms ease; }
.unlock-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}
.unlock-card {
  position: relative; z-index: 1;
  max-width: min(26rem, calc(100% - 2rem));
  background: var(--parchment); color: var(--ink);
  border: 1px solid var(--gold); border-radius: 4px;
  padding: 1.1rem 1.8rem 1rem; text-align: center;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  animation: unlock-pop 420ms cubic-bezier(0.2, 1.6, 0.4, 1);
}
.unlock-eyebrow {
  margin: 0 0 0.35rem;
  font-family: var(--display); font-size: 0.78rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--gold);
}
.unlock-name {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  font-family: var(--display); font-weight: 600; font-size: 1.35rem;
  text-wrap: balance;
}
.unlock-icon { display: inline-flex; }
.unlock-mark { font-size: 1.05rem; }
.unlock-sub {
  margin: 0.4rem 0 0;
  font-style: italic; font-size: 0.9rem; color: var(--ink-soft);
}
/* A first that HURTS wears the failure colour instead of the gold: same
   card, same burst, different news. */
.unlock-veil-hurt .unlock-card { border-color: var(--axis-force); }
.unlock-veil-hurt .unlock-eyebrow { color: var(--axis-force); }
@keyframes unlock-in { from { opacity: 0; } }
@keyframes unlock-pop {
  0% { transform: scale(0.6); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .unlock-veil, .unlock-card { animation: none; }
}

/* ---- the fishing rig (bait duties) ----------------------------------------
   The water is a little stage: a pond-dark pool, a bobber riding it, and
   the '!' that breaks the surface for under a second. The whole pool is
   the strike surface — one big honest tap, no hunting for a button
   mid-bite. The pool keeps its own water colours in both modes (a pond is
   not parchment); everything AROUND it follows the page. The bobber is
   decor, so its art lives here and not in game.js. */
.fish-rig { margin-top: 0.6rem; }
.fish-gear {
  display: flex; align-items: center; gap: 0.4rem;
  margin: 0 0 0.5rem; font-size: 0.85rem;
}
.fish-gear img { image-rendering: pixelated; }
.fish-gear-missing { color: var(--warn-ink); }
.fish-baits { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 0.6rem; }
.fish-bait { display: inline-flex; align-items: center; gap: 0.35rem; }
.fish-bait img { image-rendering: pixelated; }
.fish-bait[aria-pressed='true'] {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink), 0 1px 2px rgba(0, 0, 0, 0.18);
}
.fish-bait:disabled { opacity: 0.45; cursor: default; }
.fish-bait-qty { font-weight: 600; font-variant-numeric: tabular-nums; }
.fish-water {
  position: relative; display: block; width: 100%; height: 7.5rem;
  margin: 0 0 0.4rem; padding: 0;
  border: 1px solid var(--rule-strong); border-radius: 4px;
  cursor: pointer;
  background:
    radial-gradient(ellipse at 30% 22%, rgba(255, 255, 255, 0.16), transparent 55%),
    linear-gradient(#2a5d74, #143646);
}
html.mode-dark .fish-water {
  background:
    radial-gradient(ellipse at 30% 22%, rgba(255, 255, 255, 0.08), transparent 55%),
    linear-gradient(#1c4152, #0c2430);
}
.fish-water.is-reeling { opacity: 0.85; cursor: wait; }
.fish-bobber {
  position: absolute; left: 50%; top: 55%;
  width: 32px; height: 32px; margin: -16px 0 0 -16px;
  background: url('/game/icons/fishing22.png') center / contain no-repeat;
  image-rendering: pixelated;
  animation: fish-bob 2.6s ease-in-out infinite;
}
.fish-water.is-waiting .fish-bobber { animation: fish-drift 3.4s ease-in-out infinite; }
.fish-water.is-bite .fish-bobber { animation: fish-dunk 0.3s ease-in both; }
/* The mark: hidden at scale(0) until the bite pops it. Yellow on pond-dark
   reads in both modes; this is stage light, not the accent. */
.fish-mark {
  position: absolute; left: 50%; top: 16%;
  transform: translateX(-50%) scale(0);
  font-size: 2.4rem; font-weight: 700; color: #ffd23e;
  text-shadow: 0 2px 0 rgba(20, 14, 8, 0.7);
  pointer-events: none;
}
.fish-water.is-bite .fish-mark { animation: fish-mark-pop 0.16s ease-out both; }
.fish-note { margin: 0 0 0.4rem; font-size: 0.9rem; min-height: 1.3em; }
@keyframes fish-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(3px); }
}
@keyframes fish-drift {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50% { transform: translateY(4px) rotate(4deg); }
}
@keyframes fish-dunk { to { transform: translateY(11px); } }
@keyframes fish-mark-pop { to { transform: translateX(-50%) scale(1); } }
@media (prefers-reduced-motion: reduce) {
  .fish-bobber { animation: none !important; }
  /* The '!' must still SHOW — it is information, not garnish. */
  .fish-water.is-bite .fish-mark { animation: none; transform: translateX(-50%) scale(1); }
}

/* ---- the stat tutorial -----------------------------------------------------
   Sits under the training grid and explains what the numbers buy. The four
   axis lines are always shown; the detail is a plate away. Colour is the
   axis colour and nothing else — the panel never borrows the accent. */
/* ---- the info dot ----------------------------------------------------------
   'How stats work' sits behind this, tucked under the section's gold rule and
   over to the right where it is out of the way of the thing you came to do.

   Two boxes, not one: the BUTTON is the full 44px tap target because a thumb
   needs it, and the MARK inside is the 26px circle the eye wants. Making the
   button itself small enough to look right would have made it too small to
   hit; making the circle big enough to hit would have made it a bauble. */
.sec-info {
  display: grid;
  justify-items: end;
  /* Pulls the dot up under the rule — .page-head leaves 1.25rem below itself,
     which is the right gap before a heading and too much before a control. */
  margin-top: -0.85rem;
}
.sec-info > .statlab { justify-self: stretch; margin-top: 0.3rem; }
/* Shut, the dot is one small mark alone on a line, and leaving a full row of
   nothing under the rule for it reads as a gap rather than as breathing room.
   The page closes up behind it — the heading below is left-aligned and the dot
   is hard right, so they share the band without ever meeting. Opened, the
   margin goes and the panel gets its own room. */
.sec-info:has(> .statlab[hidden]) { margin-bottom: -1rem; }

.info-dot {
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  padding: 0; border: 0; background: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.info-dot-mark {
  position: relative;      /* the pulse ring hangs off this */
  width: 1.6rem; height: 1.6rem;
  display: grid; place-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: 50%;
  /* --display is Cinzel, an inscriptional Roman with no true lowercase — it
     draws an 'i' as a capital, which is precisely not what an info button is.
     The body face has real minuscules, so the mark keeps the lower-case i
     everyone already reads as "information", upright rather than italic. */
  font-family: var(--body); font-style: normal;
  font-weight: 600;
  font-size: 1.15rem; line-height: 1;
  /* An 'i' has no descender, but the line box still reserves room for one, so
     centring the BOX leaves the letter sitting high in the circle. This pads
     it back down — but stops a pixel SHORT of centring the ink, on purpose.
     The tittle is small and detached, so the eye weighs the heavy stem under
     it and reads a perfectly ink-centred i as sitting low. Measured: 0.32em
     lands the ink box dead centre and looks a shade heavy; this sits one
     device pixel above it, which is where it stops looking like anything. */
  padding-top: 0.2em;
  color: var(--ink);
}
.info-dot:hover .info-dot-mark,
.info-dot:focus-visible .info-dot-mark { border-color: var(--gold); }
/* Open reads as pressed IN — the plate's lift becomes a well. */
.info-dot[aria-expanded='true'] .info-dot-mark {
  border-color: var(--gold);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.25);
}

/* Never yet approached: one slow ring, going out. Drawn as a scaling border
   rather than an expanding box-shadow because --gold is the player's ACCENT
   and arrives as a hex — there is no alpha to fade in a shadow colour without
   hardcoding a value that would be wrong the moment someone picks steel.
   A ring can fade with plain opacity instead, and transform and opacity are
   the two things that animate without touching layout. */
.info-dot.is-new .info-dot-mark { border-color: var(--gold); }
.info-dot.is-new .info-dot-mark::after {
  content: '';
  position: absolute; inset: -3px;
  border: 2px solid var(--gold);
  border-radius: 50%;
  animation: info-pulse 2.6s ease-out infinite;
  pointer-events: none;
}
@keyframes info-pulse {
  0%   { transform: scale(1);    opacity: 0.7; }
  70%  { transform: scale(1.6);  opacity: 0; }
  100% { transform: scale(1.6);  opacity: 0; }
}
/* Stillness asked for: the ring stays, it just stops breathing. */
@media (prefers-reduced-motion: reduce) {
  .info-dot.is-new .info-dot-mark::after { animation: none; opacity: 0.55; }
}

.statlab {
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 0.7rem 0.8rem;
  display: grid;
  gap: 1rem;
  justify-items: start;
}

/* ---- how stats work: the two figures ---------------------------------------
   The panel is drawings now, not paragraphs — see paintStatLab in game.js for
   why. Everything here is sized in the SVG's own user units, so one number
   holds at every width: the figure scales, the proportions do not move. The
   cap keeps it from swelling to the width of the book on a desk monitor while
   still filling a phone. */
.statlab-fig { margin: 0; width: 100%; }
.statlab-svg {
  display: block;
  width: 100%;
  max-width: 22rem;
  height: auto;
}
/* Column headings: the quietest thing in the frame, because the axis names
   are what the eye is here for. */
.sl-head {
  font-family: var(--display);
  font-size: 8px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  fill: var(--ink-faint);
}
/* fill, not color: inside the SVG namespace `color` paints nothing. The axis
   colour rides the name and its mark, exactly as it does in the grid — and
   the mark is what carries the axis under a colour-blind reading, which is
   why it is drawn beside every name rather than instead of one. */
.sl-name { font-size: 12px; font-weight: 600; }
.sl-mark { font-size: 8px; }
.sl-role {
  font-size: 8.5px;
  fill: var(--ink-faint);
}
.sl-arrow { fill: none; stroke: var(--rule-strong); stroke-width: 1.3; }
.sl-tip { fill: var(--rule-strong); }
.sl-ax-force { fill: var(--axis-force); }
.sl-ax-understanding { fill: var(--axis-understanding); }
.sl-ax-endurance { fill: var(--axis-endurance); }
.sl-ax-precision { fill: var(--axis-precision); }
.statlab-cap {
  margin: 0.4rem 0 0;
  font-size: 0.82rem;
  color: var(--ink-soft);
  max-width: 46ch;
}


/* ---- worn points -----------------------------------------------------------
   What a piece grants, shown before it is taken up: in the Inventory strip
   above the verbs, and on a market listing under its blurb. Colour is the
   axis colour and the mark rides with it, same contract as the grid. */
.stat-chips {
  display: flex; flex-wrap: wrap; gap: 0.3rem 0.7rem;
  padding: 0.15rem 0.6rem 0.4rem;
}
/* On a market listing the chips sit inside the name block, which already
   carries the row's padding — so they take none of their own. */
.market-row .stat-chips {
  padding: 0.25rem 0 0;
  font-weight: 400;
}
.stat-chip {
  font-size: 0.78rem;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Health is not an axis, so its chip wears plain ink and its bar's icon
   rather than borrowing one of the four colours. */
.stat-chip-health {
  display: inline-flex; align-items: baseline; gap: 0.3em;
  font-weight: 600; color: var(--ink-soft);
}


/* ---- admin: the trade floor ------------------------------------------------
   A read-only list, so it stays plainer than a player page — but the stale
   tag has to catch the eye, because catching it is the panel's whole job. */
.trade-admin-row { align-items: baseline; flex-wrap: wrap; }
.trade-id {
  font-weight: 600; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; min-width: 3ch;
}
.trade-parties { display: block; font-weight: 600; }
.trade-goods {
  display: block; font-weight: 400; font-size: 0.82rem;
  color: var(--ink-faint); max-width: 60em;
}
/* Warning ink — the same --warn-ink every other warning in the game uses,
   which already flips for the dark papers, so this needs no twin rule. Not
   an axis colour and not the accent. */
.trade-stale {
  font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; padding: 0.1rem 0.4rem; border-radius: 2px;
  color: var(--warn-ink); border: 1px solid var(--warn-ink);
}

/* The training dummy's page: one labelled select per choice. The selects
   wear the raised-button plate (.inv-act) like every other control on a
   wash panel, and keep the 44px target. */
.dummy-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; margin: 0.35rem 0;
}
.dummy-row-label { font-size: 0.95rem; }
/* One width for every select, so the rows line up whatever is chosen. */
.dummy-select { min-height: var(--tap); width: 60%; font: inherit; }

/* The Pin button in a page head: the same plate as any other button, kept
   small in type but still a full tap target. */
.pin-btn { min-height: var(--tap); padding: 0 0.8rem; font-size: 0.9rem; }
