/* Conquer — the mode screens.
   styles.css holds the shell and the shared theme (tokens, buttons, panels,
   the topbar, the board). This file is everything that belongs to one screen:
   the expedition map, the skin shop, settings, the online mock-up, and the
   Learn illustrations. Split from styles.css because that file had grown past
   the point where a change to the shop meant scrolling through the board. */

/* --- Reduced motion, chosen in Settings -----------------------------------
   In addition to the OS-level media query in styles.css, never instead of it:
   someone who set it at the OS level must keep it whatever this says. */
body.reduce-motion #bg-fx {
  display: none;
}

body.reduce-motion .screen {
  animation: none;
}

/* --- Notices --------------------------------------------------------------
   Used wherever the app has to say plainly that something is not real yet. */
.notice {
  margin: var(--s2) 0 0;
  padding: var(--s2) var(--s3);
  border-left: 3px solid var(--edge-bright);
  background: rgb(var(--surface-rgb) / 0.55);
  color: var(--ink-dim);
  font-size: var(--t-sm);
  line-height: 1.55;
  text-align: left;
}

.notice strong {
  color: var(--ink);
}

/* Louder, for "this is a mock-up and does nothing". */
.notice-warn {
  border-left-color: var(--accent);
}

.notice-warn strong {
  color: var(--accent);
}

/* --- Online --------------------------------------------------------------- */

/* The two text fields on the Online screen: a name, and an invite code. */
.text-input {
  width: 100%;
  box-sizing: border-box;
  padding: var(--s2) var(--s3);
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  background: var(--bg-deep);
  border: 1px solid var(--edge-bright);
  border-radius: var(--radius);
}

.text-input:focus {
  outline: 2px solid var(--select);
  outline-offset: 1px;
}

.join-row {
  display: flex;
  gap: var(--s2);
}

.join-row .text-input {
  flex: 1 1 auto;
  min-width: 0;
}

/* The code is read aloud and typed back, so it gets the same monospace and
   spacing as the code it will be compared against. */
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

#online-rejoin,
#online-rematch-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}

#online-rejoin.hidden,
#online-rematch-note.hidden {
  display: none;
}

.room-headline {
  color: var(--ink);
}

.room-code {
  margin-top: var(--s3);
  text-align: center;
}

.room-share {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2);
}

.room-versus {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  margin-top: var(--s3);
  font-size: var(--t-lg);
  font-weight: 600;
  color: var(--ink);
}

.room-versus.hidden {
  display: none;
}

.room-vs {
  font-size: var(--t-sm);
  font-weight: 400;
  color: var(--ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.room-summary {
  margin: var(--s3) 0 0;
  padding: 0;
  list-style: none;
  color: var(--ink-dim);
  font-size: var(--t-sm);
  line-height: 1.7;
  text-align: center;
}

.room-summary.hidden {
  display: none;
}

.room .menu-list {
  margin-top: var(--s4);
}

/* The room is laid out down the middle; a left-aligned paragraph in it reads as
   something that wandered in from another screen. */
.room .hint {
  text-align: center;
}

.session-id {
  padding: var(--s3);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-align: center;
  color: var(--select);
  background: var(--bg-deep);
  border: 1px solid var(--edge-bright);
  margin-bottom: var(--s2);
}

.lobby-status {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  background: rgb(var(--surface-rgb) / 0.55);
  border: 1px solid var(--edge);
  color: var(--ink-dim);
  font-size: var(--t-sm);
}

/* Amber while waiting for the other player, green once both are here, and
   dark once the room is over for good. Green is only ever shown when it is
   true: a light that says "connected" over an empty room is how somebody waits
   for an opponent who is not coming. */
.lobby-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
  animation: pulse 1.8s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 1;
  }
}

.lobby-dot.is-live {
  background: var(--good);
  box-shadow: 0 0 10px var(--good);
  animation: none;
}

.lobby-dot.is-off {
  background: var(--ink-dim);
  box-shadow: none;
  animation: none;
}

body.reduce-motion .lobby-dot {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .lobby-dot {
    animation: none;
  }
}

/* The tutorial's "tap here" arrow, on the same pulse. A blink is a strong cue
   and the app spends it sparingly -- this is the second and last place that
   animates without stopping, and both of them mean "waiting for you".

   Switched off it still reads: the arrow stays drawn and fully opaque, so
   nothing is lost by turning motion off, only the nudge. */
#board-marks.is-blinking {
  animation: pulse 1.4s ease-in-out infinite;
}

body.reduce-motion #board-marks.is-blinking {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  #board-marks.is-blinking {
    animation: none;
  }
}

/* --- Settings controls ---------------------------------------------------- */

.field {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--s1) var(--s2);
  margin-bottom: var(--s2);
}

.field-label {
  font-size: var(--t-sm);
  color: var(--ink-dim);
}

.field-value {
  font-family: var(--display);
  font-size: var(--t-sm);
  color: var(--select);
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  text-align: right;
}

.field input[type="range"] {
  grid-column: 1 / -1;
  width: 100%;
  height: var(--tap);
  accent-color: var(--select);
  background: transparent;
  cursor: pointer;
  /* Browsers give a range input a default 2px side margin, which pushed it
     past its container and put a scrollbar along the bottom of the screen. */
  margin: 0;
}

.field input[type="range"]:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The whole row is the target, not just the checkbox — a 16px box is well
   under the 44px this app holds everything else to. */
.switch {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0 var(--s2);
  cursor: pointer;
  font-size: var(--t-md);
}

.switch input {
  width: 1.15rem;
  height: 1.15rem;
  accent-color: var(--select);
  cursor: inherit;
  flex: 0 0 auto;
}

.switch em {
  color: var(--ink-dim);
  font-style: normal;
  font-size: var(--t-sm);
}

.switch.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --- Skin shop ------------------------------------------------------------ */

.shop-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  padding-bottom: var(--s2);
  margin-bottom: var(--s3);
  border-bottom: 1px solid var(--edge);
}

.shop-header .btn {
  min-height: var(--tap);
}

.skin-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s2);
  padding-bottom: var(--s3);
}

.skin-card {
  display: flex;
  flex-direction: column;
  /* A grid item's default `min-width: auto` is its content's intrinsic width,
     and the preview canvas is laid out at the board's full logical size.
     Without this the two columns refuse to shrink and the cards are clipped. */
  min-width: 0;
  gap: var(--s1);
  padding: var(--s2);
  background: linear-gradient(180deg, var(--bg-panel), rgb(var(--panel-lo-rgb)));
  border: 1px solid var(--edge);
  clip-path: polygon(
    var(--chamfer-sm) 0,
    100% 0,
    100% calc(100% - var(--chamfer-sm)),
    calc(100% - var(--chamfer-sm)) 100%,
    0 100%,
    0 var(--chamfer-sm)
  );
}

.skin-card.is-equipped {
  border-color: var(--select);
  box-shadow: 0 0 18px rgb(var(--select-rgb) / 0.2);
}

/* The preview is a real board drawn at its own logical size; CSS is what fits
   it to the card, exactly as .board-viewport does in play. */
.skin-preview {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--edge);
}

.skin-name {
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.skin-blurb {
  font-size: var(--t-sm);
  color: var(--ink-dim);
  line-height: 1.4;
  flex: 1 1 auto;
}

/* `.btn-small` is 36px, which is right for the dev review stepper and the
   puzzle controls but not for the main action on a card. Buy/Equip is what a
   player comes to this screen to press, so it gets a full tap target. */
.skin-action {
  width: 100%;
  min-height: var(--tap);
  justify-content: center;
}

/* Same reasoning for the session controls on the online screen. */
#screen-online .option-row .btn {
  min-height: var(--tap);
}

/* The price on a Buy button. Set apart by weight rather than by colour: it is
   money now, and money is not a game token with a currency colour. */
.skin-action .price {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.pack-list {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin-bottom: var(--s3);
}

.pack {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}

/* These are disabled but are meant to be *read* — they say what a pack would
   cost once purchasing exists. The default 0.4 disabled opacity makes the
   prices hard to make out, so they are dimmed less and given a plain
   not-a-button look instead. */
.pack:disabled {
  opacity: 0.75;
  background: rgb(var(--panel-lo-rgb) / 0.9);
  border-style: dashed;
}

.pack-price {
  color: var(--accent);
  font-weight: 700;
}

/* --- Expedition map ------------------------------------------------------- */

#screen-expedition {
  padding: 0;
  justify-content: stretch;
  align-items: stretch;
}

/* The map is taller than any phone by design, so this is the one screen whose
   content legitimately scrolls — inside itself, never the shell. */
.map-scroll {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
}

.map {
  position: relative;
  width: 100%;
  /* Height is set from JS: it depends on the level count. */
  background:
    radial-gradient(ellipse 80% 30% at 50% 100%, rgb(var(--select-rgb) / 0.09), transparent 70%),
    radial-gradient(ellipse 80% 30% at 50% 0%, rgb(var(--accent-rgb) / 0.07), transparent 70%);
}

.map-track {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.map-track-line {
  fill: none;
  stroke: var(--edge-bright);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 3 3;
  /* The viewBox is stretched to the map's width, so without this the dashes
     would be squashed into ovals. */
  vector-effect: non-scaling-stroke;
  opacity: 0.75;
}

.zone-banner {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--s1) 0;
  text-align: center;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgb(var(--accent-rgb) / 0.12), transparent);
  border-top: 1px solid rgb(var(--accent-rgb) / 0.35);
}

.zone-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
}

.zone-blurb {
  font-size: var(--t-sm);
  color: var(--ink-dim);
}

.map-node {
  position: absolute;
  transform: translate(-50%, 50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 62px;
  height: 62px;
  padding: var(--s1);
  font-family: var(--display);
  color: var(--ink);
  background: linear-gradient(180deg, var(--bg-raised), var(--bg-panel));
  border: 2px solid var(--edge-bright);
  border-radius: 50%;
  cursor: pointer;
  transition:
    transform 0.14s ease,
    box-shadow 0.14s ease;
}

.map-node:hover:not(:disabled) {
  transform: translate(-50%, 50%) scale(1.08);
}

.map-node:focus-visible {
  outline: 2px solid var(--select);
  outline-offset: 3px;
}

.node-number {
  font-size: var(--t-md);
  font-weight: 700;
  line-height: 1;
}

/* Open: cyan, the app's "you can act here" colour. */
.map-node.is-open {
  border-color: var(--select);
  box-shadow:
    0 0 18px rgb(var(--select-rgb) / 0.35),
    inset 0 0 14px rgb(var(--select-rgb) / 0.12);
}

/* Complete: yellow, matching the ticks it has earned. */
.map-node.is-complete {
  border-color: var(--accent);
  box-shadow: 0 0 16px rgb(var(--accent-rgb) / 0.3);
}

.map-node.is-locked {
  color: var(--ink-dim);
  border-color: var(--edge);
  background: rgb(var(--panel-lo-rgb) / 0.85);
  cursor: not-allowed;
  opacity: 0.75;
}

.node-ticks {
  display: flex;
  gap: 3px;
}

/* A small diamond rather than a check mark: at 7px a tick glyph is mush,
   and a filled diamond still reads as "done" at a glance. */
.node-ticks .tick {
  width: 7px;
  height: 7px;
  border: 1px solid var(--edge-bright);
  background: transparent;
  transform: rotate(45deg);
}

.node-ticks .tick.is-won {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 6px var(--accent);
}

.map-node.is-locked .tick {
  border-color: var(--edge);
}

body.reduce-motion .map-node {
  transition: none;
}

/* --- Level detail panel --------------------------------------------------- */

.size-rows {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin-bottom: var(--s3);
}

.size-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  width: 100%;
}

.size-state {
  font-size: var(--t-sm);
  color: var(--select);
}

.size-row.is-won .size-state {
  color: var(--accent);
}

.size-row.is-won .size-state::before {
  content: "\25C8 ";
}

/* What the win was worth, on the game-over panel. */
.reward-line {
  margin: 0 0 var(--s3);
  color: var(--accent);
  font-family: var(--display);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1.5;
}

/* --- Account (mock-up) ---------------------------------------------------- */

/* The card lines up with the buttons under it: the same width every button
   list in the app uses, and the same gap. It used to run 34px wider than the
   buttons and sit flush against them. */
#screen-account .account-card {
  box-sizing: border-box;
  width: min(100%, 380px);
  margin-bottom: var(--s3);
}

.account-card {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3);
  background: linear-gradient(180deg, rgb(var(--surface-rgb) / 0.8), rgb(var(--panel-rgb) / 0.6));
  border: 1px solid var(--edge);
  clip-path: polygon(
    var(--chamfer-sm) 0,
    100% 0,
    100% calc(100% - var(--chamfer-sm)),
    calc(100% - var(--chamfer-sm)) 100%,
    0 100%,
    0 var(--chamfer-sm)
  );
}

.account-avatar {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--t-lg);
  color: var(--bg-deep);
  background: linear-gradient(180deg, var(--select), color-mix(in srgb, var(--select) 78%, black));
  border-radius: 50%;
}

.account-lines {
  min-width: 0;
}

.account-name {
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: 0.04em;
}

.account-sub {
  font-size: var(--t-sm);
  color: var(--ink-dim);
}

/* --- Challenge: the boss roster ------------------------------------------- */

.boss-list {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding-bottom: var(--s3);
}

.boss-card {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: 66px;
  padding: var(--s2) var(--s3);
  text-align: left;
  text-transform: none;
  border-left: 3px solid var(--p1);
}

.boss-card:hover:not(:disabled) {
  border-left-color: var(--select);
}

/* No boss card is ever locked now — every one is fightable from the first
   launch, and what is gated is the settings on its own setup screen. The rules
   that dimmed a locked card went with the expedition that earned them; the map
   node still has its own, above. */


/* A letter in a ring, rather than artwork: a new boss needs no asset, the same
   way the shop's previews are real boards rather than pictures of boards. */
.boss-sigil {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex: 0 0 auto;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--t-lg);
  color: var(--p1);
  background: rgb(var(--p1-rgb) / 0.12);
  border: 2px solid var(--p1);
  border-radius: 50%;
  box-shadow: 0 0 14px rgb(var(--p1-rgb) / 0.35);
}

.boss-lines {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.boss-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--t-md);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}

.boss-epithet {
  font-family: var(--body);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
}

/* Beaten. Pushed to the far right by the name block taking the slack, so five
   cards line their ticks up with each other rather than each one sitting
   wherever its epithet ended. Sized against the sigil on the other side, so the
   card reads as a row with a mark at each end.

   `--accent`, the interface blue, rather than `--select`: the teal is what the
   app uses for the thing you are on, and this is a mark on a thing you are
   not. */
.boss-tick {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: 1.5rem;
  line-height: 1;
  font-weight: 700;
  color: var(--accent);
}

/* The chosen boss, at the top of its setup screen. */
.boss-hero {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s2);
}

.boss-hero .boss-sigil {
  width: 54px;
  height: 54px;
  font-size: var(--t-xl);
}

/* --- Boss nodes on the expedition map ------------------------------------- */

.map-node.is-boss {
  width: 78px;
  height: 78px;
  border-width: 3px;
  border-color: var(--p1);
  box-shadow:
    0 0 22px rgb(var(--p1-rgb) / 0.45),
    inset 0 0 16px rgb(var(--p1-rgb) / 0.15);
}

.map-node.is-boss .node-number {
  font-size: var(--t-lg);
}

/* A locked boss stays dimmed like any other locked node — being a boss is not
   a reason to advertise a level the player cannot reach yet. */
.map-node.is-boss.is-locked {
  border-color: var(--edge);
  box-shadow: none;
}

.node-boss-name {
  position: absolute;
  top: 100%;
  margin-top: 4px;
  font-family: var(--display);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--p1);
  white-space: nowrap;
  text-shadow: 0 2px 8px rgb(var(--shadow-rgb) / 0.9);
  pointer-events: none;
}

.map-node.is-locked .node-boss-name {
  color: var(--ink-dim);
}

/* --- Theme picker (Developer Mode) ---------------------------------------- */

.dev-tag {
  font-family: var(--body);
  font-style: normal;
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--p1);
}

.theme-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s2);
}

.theme-chip {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  padding: var(--s2);
  text-transform: none;
  justify-content: flex-start;
}

/* The three colours that actually decide how a palette feels: the ground, the
   accent and the selection colour. Read straight off the theme, so a swatch
   cannot drift from what applying it does. */
.theme-swatch {
  display: flex;
  flex: 0 0 auto;
  border: 1px solid var(--edge-bright);
  overflow: hidden;
}

.theme-swatch i {
  display: block;
  width: 12px;
  height: 26px;
}

.theme-name {
  font-size: var(--t-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- How to Play: illustrated lessons -------------------------------------
   Each card is a board drawn by the real renderer, then a short caption. The
   boards are laid out by CSS: learn-ui.js overrides the inline size that
   renderBoard writes, so they scale to the column instead of overflowing it. */

/* Number and title across the top, then the board on the left with the words
   beside it. The board takes half the width; `minmax(0, …)` on both tracks
   because a canvas would otherwise refuse to shrink below its own width and
   push the caption off the screen. A rule between cards, so nine of them do
   not read as one long column. */
.lesson {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: var(--s1) var(--s2);
  padding-bottom: var(--s3);
  margin: 0 0 var(--s3);
  border-bottom: 1px solid var(--edge);
}

.lesson:last-child {
  border-bottom: 0;
}

.lesson-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin-bottom: var(--s1);
}

.lesson-title {
  color: var(--ink);
  font-family: var(--display);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.lesson-board {
  display: block;
  min-width: 0;
  border: 1px solid var(--edge);
  background: var(--board-surface);
}

.lesson-caption {
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--prose-ink);
}

/* The rules are numbered straight through, so one can be pointed at: "rule 5
   is the one about the fence". */
.lesson-number {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5em;
  height: 1.5em;
  padding: 0 0.35em;
  border-radius: 999px;
  background: var(--select-fill);
  color: var(--select-ink);
  font-family: var(--display);
  font-size: var(--t-sm);
  font-weight: 700;
  line-height: 1;
}


/* --- Expedition dialogue --------------------------------------------------
   A portrait, a name and one line at a time. The panel is fixed-height rather
   than growing with the text: a box that resizes between lines makes the
   Next button move under the thumb, which on a phone means mis-taps. The
   line length is bounded in dialogue.js (MAX_LINE) to fit this box. */

.panel-dialogue {
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.speaker-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 56px;
}

/* Placeholder until there is art. `data-portrait` carries the boss id, so a
   real image slots in here without moving anything around it. */

/* The `hidden` attribute is `display: none` in the UA stylesheet, which any
   author `display` beats on specificity — so setting `el.hidden = true` left
   the narrator wearing an empty frame. Restore it explicitly, above the rule
   that would otherwise win. */
.portrait[hidden] {
  display: none;
}

.portrait {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 56px;
  height: 56px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.5rem;
  color: var(--accent);
  background: linear-gradient(160deg, rgb(var(--accent-rgb) / 0.22), rgb(var(--panel-lo-rgb)));
  border: 1px solid rgb(var(--accent-rgb) / 0.55);
  clip-path: polygon(
    var(--chamfer) 0,
    100% 0,
    100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%,
    0 100%,
    0 var(--chamfer)
  );
}

.speaker-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--t-md);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
}

/* Three lines' worth, held whether the line needs it or not. */
.dialogue-text {
  min-height: 4.5em;
  margin: 0;
  font-size: var(--t-md);
  line-height: 1.5;
  color: var(--ink);
}

.dialogue-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}

.dialogue-dots {
  display: flex;
  gap: 6px;
}

.dialogue-dot {
  width: 7px;
  height: 7px;
  background: var(--edge-bright);
  transform: rotate(45deg);
}

.dialogue-dot.is-current {
  background: var(--accent);
}

.dialogue-actions {
  display: flex;
  gap: var(--s2);
}

/* --- Area tinting ---------------------------------------------------------
   The backdrop seam. Each area's banner and nodes carry `data-area`, so an
   area reads as its own place without any artwork. Real backdrops hook the
   same attribute. */

.zone-banner[data-area='fracture-line'] {
  background: linear-gradient(90deg, transparent, rgb(var(--p1-rgb) / 0.16), transparent);
  border-top-color: rgb(var(--p1-rgb) / 0.4);
}

.zone-banner[data-area='deep-cache'] {
  background: linear-gradient(90deg, transparent, rgb(var(--select-rgb) / 0.16), transparent);
  border-top-color: rgb(var(--select-rgb) / 0.4);
}

/* A mini boss sits between an ordinary rung and the area boss, and the map
   should say so before the player taps it. */
.map-node.is-mini {
  width: 70px;
  height: 70px;
}

/* --- The capture animation ------------------------------------------------ */

/* Over everything, catching nothing. Fixed rather than absolute so it does not
   move with any scroll, and above the overlays so a capture that finishes a
   game still plays over the panel rather than under it. */
.fx-layer {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

/* The score's own celebration, in CSS rather than on the canvas: the number is
   a DOM node, and scaling it here keeps the type crisp instead of rasterising
   it into the effects layer. `transform-origin` matters -- scaling from the
   left would shove the turn indicator sideways. */
@keyframes score-pop {
  0% {
    transform: scale(1);
    text-shadow: none;
  }
  35% {
    transform: scale(1.2);
    text-shadow:
      0 0 14px var(--celebrate),
      0 0 5px var(--bg-panel);
  }
  100% {
    transform: scale(1);
    text-shadow: none;
  }
}

.score-pop {
  display: inline-block;
  transform-origin: center;
  animation: score-pop 0.45s ease-out;
}

/* The same opt-out the board's pulse takes, both ways round: the in-app setting
   and the OS one. There is nothing to replace it with -- the score still counts
   up, it simply does not leap. */
body.reduce-motion .score-pop {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .score-pop {
    animation: none;
  }
}

/* --- The end-of-match announcement ---------------------------------------- */

/* A banner over the board rather than a panel in front of it: the board has to
   stay readable behind the words, because the pause exists so the player can
   look at the position they just won or lost. Under the effects layer, so a
   capture that ends the game still flies its souls over the top. */
.finale {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The gutter lives on the container, not on the words: a max-width on the
     span alone was not holding it, and a padded flex container cannot be
     argued with. */
  padding: 0 var(--s4);
  pointer-events: none;
}

.finale-title {
  /* Sized to the longest thing it ever says -- "You surrendered" at 375px, which
     is where an 11vw scale ran off both edges of the phone. The max-width lets
     it wrap rather than clip if a future line is longer still. */
  display: block;
  max-width: 100%;
  font-family: var(--display);
  font-size: clamp(1.4rem, 7.4vw, 2.8rem);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  line-height: 1.1;
  color: var(--ink);
  /* Legible over whatever the board is doing underneath, without a box. A panel
     here would hide the very thing the pause is for. */
  text-shadow:
    0 0 18px var(--bg-panel),
    0 0 6px var(--bg-panel),
    0 2px 2px var(--bg-panel);
}

/* Three tones, one shape. A win overshoots very slightly and carries the gold;
   a loss and a concession arrive without either; a draw sits between them. The
   difference is deliberately small — this is a board game, not a slot machine. */
@keyframes finale-in {
  0% {
    opacity: 0;
    transform: scale(0.86);
  }
  60% {
    opacity: 1;
    transform: scale(1.04);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes finale-in-quiet {
  0% {
    opacity: 0;
    transform: scale(1.06);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes finale-out {
  to {
    opacity: 0;
    transform: scale(1.06);
  }
}

/* Gold on a pale board needs something dark behind it, not more light: the
   panel-coloured halo the other tones use made it paler still. */
.finale-title.tone-triumph {
  color: var(--celebrate);
  text-shadow:
    0 0 26px var(--celebrate),
    0 1px 10px var(--ink),
    0 2px 3px var(--ink);
  animation: finale-in 0.42s cubic-bezier(0.22, 1.2, 0.36, 1) both;
}

.finale-title.tone-level {
  animation: finale-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.finale-title.tone-quiet {
  color: var(--ink-dim);
  animation: finale-in-quiet 0.36s ease-out both;
}

.finale-title.is-leaving {
  animation: finale-out 0.22s ease-in both;
}

/* Reduced motion keeps the words and drops the movement. The announcement is
   information; the scale is decoration. */
body.reduce-motion .finale-title,
body.reduce-motion .finale-title.is-leaving {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .finale-title,
  .finale-title.is-leaving {
    animation: none;
  }
}
