/*
 * The Shortcut Dojo's chrome. Its own file rather than more of app.css, because
 * none of it is live in explore mode and the two share only the colour tokens.
 *
 * The one colour decision worth stating: amber is the graph's accent and it is
 * the game's too. A correct answer floods the path amber on the canvas, so the
 * card that says so is edged in the same colour, and nothing else *during a
 * run* is allowed to be — the eye should be able to follow one hue from the
 * card to the key it is about. The miss colour is deliberately not
 * red-on-the-canvas: the canvas never turns red, so a red edge on the card
 * cannot be confused for something the graph is saying.
 *
 * The five doors on the menu are the one exception, and they can be: no card is
 * on screen while the menu is up, so their green→red ramp is never in the same
 * frame as a verdict. See the note above the ramp.
 */

:root {
  --amber:     #f5b54a;
  --amber-dim: #7a5a22;
  --wrong:     #e0574f;
}

.game {
  position: fixed;
  inset: 0;
  z-index: 40;
  /* The overlay is a frame around the board, not a lid on it: everything in
     here that is not itself interactive lets the pointer through to the canvas
     underneath, and each panel turns it back on for itself. */
  pointer-events: none;
  font: 13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
}

.game-scrim {
  position: absolute;
  inset: 0;
  background: rgba(8, 9, 10, 0.72);
  backdrop-filter: blur(2px);
  pointer-events: auto;
}

/* ----------------------------------------------------------- mode switch */

.mode-switch {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(13, 13, 13, 0.86);
  z-index: 6;
}

.mode-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-muted);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.02em;
  padding: 5px 16px;
  border-radius: 999px;
  cursor: pointer;
}

.mode-btn:hover { color: var(--ink-2); }

.mode-btn.is-on {
  background: var(--plane-2);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

/* In train mode the switch is not a switch — there is one button and it is
   the way out. Train goes, and Explore (relabelled Exit by app.js) reads as an
   action rather than the half of a pair that happens to be off: it is plainly
   legible on its own instead of the muted grey that only makes sense next to a
   lit sibling. */
.mode-switch.is-game #modeGame { display: none; }

.mode-switch.is-game #modeExplore { color: var(--ink-2); }

.mode-switch.is-game #modeExplore:hover {
  background: var(--plane-2);
  color: var(--ink);
}

/* ---------------------------------------------------------------- dialog */

.game-dialog {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(520px, calc(100vw - 48px));
  max-height: calc(100vh - 64px);
  /* Only a window too short to hold the dialog ever scrolls this, and when it
     does the bar should be the app's own thin one rather than the OS default —
     a chunky grey gutter down a floating card reads as a bug. `auto` and not
     `hidden`: on that short window the doors below the fold have to stay
     reachable. */
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  padding: 22px 24px 20px;
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  background: var(--plane);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  pointer-events: auto;
  /* Slides in rather than appearing: the graph stays where it was and the
     dialog arrives over it, which is what says the game is a layer on the map
     rather than a different page. */
  animation: game-in 220ms cubic-bezier(0.2, 0.9, 0.3, 1);
}

.game-dialog::-webkit-scrollbar { width: 9px; }
.game-dialog::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 5px;
  border: 2px solid var(--plane);
}

@keyframes game-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 14px)); }
}

/* The menu is the one dialog that is five doors wide; the rest hold a
   paragraph and a couple of buttons and stay at the narrow width. */
#gameMenu { width: min(880px, calc(100vw - 48px)); }

.game-dialog header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.game-dialog h2 { font-size: 17px; letter-spacing: 0.01em; }
.game-dialog h3 {
  margin: 18px 0 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.game-lead {
  margin: 14px 0 5px;
  color: var(--ink);
  font-size: 15px;
}

/* The method, under the instruction it explains. Tucked up against the lead
   rather than spaced off it — the two are one thought, and the gap that used to
   sit under the lead belongs below the pair, holding the doors off. */
.game-method { margin: 0 0 16px; }

/* Five cards, side by side, in ladder order. A column of rows read as a list of
   options; a row of cards reads as five doors, which is what they are — and
   laid out across the page the colour ramp below has somewhere to run. They
   only fall back to a wrapping grid when the dialog is too narrow to hold
   five. */
.game-buckets {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 9px;
  margin-bottom: 18px;
}

@media (max-width: 780px) {
  .game-buckets { grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }
}

/*
 * The ladder, said in colour: green at the bottom rung through to red at the
 * top, the way every difficulty selector says it, so the order is legible
 * before a single word is read. The ramp passes through the app's own amber at
 * the middle rung and lands on the miss red at the far end — both already
 * tokens in this file, so the five are one family rather than a picked-out
 * rainbow, and the steps are luminance-matched so no rung shouts louder than
 * the one beside it.
 *
 * `--tier` is the whole of it: stripe, name, hover edge and progress bar all
 * read from that one property, so a door renamed or inserted needs one line.
 * `--rung` keys off the *order*, not the tier letter, for the same reason.
 *
 * Set on the rung itself rather than on the door, because the card in play
 * wears the same five colours to say which tier the shortcut it is asking
 * belongs to — one ramp, read in both places.
 *
 * Nothing else may declare a `--tier` of its own: a custom property set on
 * `.game-bucket` or `.game-tier` ties with these on specificity and wins on
 * source order, which paints every rung the one fallback colour. Defaults ride
 * on the `var()` at each use site instead.
 */
[data-rung="1"] { --tier: #56c88a; }
[data-rung="2"] { --tier: #a8c95a; }
[data-rung="3"] { --tier: #f5b54a; }
[data-rung="4"] { --tier: #ef8b45; }
[data-rung="5"] { --tier: #e0574f; }

.game-bucket {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  overflow: hidden;
  text-align: left;
  padding: 13px 13px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--plane-2);
  color: var(--ink-2);
  font: inherit;
  cursor: pointer;
  transition: border-color 120ms, background 120ms, transform 120ms;
}

/* The rung's colour, across the top of its card. A bar rather than a tint on
   the whole card: five filled panels would fight the graph showing through the
   scrim behind them, and the ramp reads just as well off five stripes. */
.game-bucket::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--tier, var(--amber));
}

.game-bucket:hover:not(:disabled),
.game-bucket:focus-visible {
  border-color: var(--tier, var(--amber));
  background: #17181a;
  transform: translateY(-2px);
  outline: none;
}

/* Nothing behind this door: no colour either, or an empty tier would still
   read as a step on the ladder. */
.game-bucket:disabled { opacity: 0.3; cursor: default; }
.game-bucket:disabled::before { background: var(--line-strong); }

.bucket-name {
  color: var(--tier, var(--amber));
  font-weight: 600;
  font-size: 14px;
  text-transform: capitalize;
  margin-top: 2px;
}
.bucket-count {
  color: var(--ink-2);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.bucket-count b { color: var(--ink); font-weight: 600; }
.bucket-blurb {
  margin-top: 4px;
  color: var(--ink-muted);
  font-size: 11.5px;
  line-height: 1.4;
}

/* What the bucket has been worth so far — a record, not a starting line. Pushed
   to the foot of the card by the margin, so the five stay aligned however many
   lines the blurbs above them run to. */
.bucket-was {
  margin-top: auto;
  padding-top: 8px;
  color: var(--ink-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* How far out of the tier the adaptive stream has carried you. A bar because
   it is a distance along a scale, and the number for it alone in a card this
   narrow was a third line of small print. */
.bucket-reach {
  width: 100%;
  height: 2px;
  margin-top: 5px;
  border-radius: 2px;
  background: var(--line);
}
.bucket-reach i {
  display: block;
  width: var(--reach, 0%);
  height: 100%;
  border-radius: 2px;
  background: var(--tier, var(--amber));
}

/* ----------------------------------------------------------- the scope */

/*
 * The Advanced fold: eighty-odd contexts, which is more than a dialog can hold
 * upright. Two columns and a fixed height turn it into a panel rather than a
 * page — the doors above it stay on screen while it is open, which is the
 * whole reason to tick a context in the first place: the counts on them move
 * as you do it.
 */
.game-scope { padding-bottom: 10px; }

.scope-tools {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 8px 0 6px;
}

#scopeSearch {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  background: var(--plane-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  padding: 5px 9px;
}
#scopeSearch::placeholder { color: #6a6a64; }
#scopeSearch:focus { outline: none; border-color: var(--accent); }
#scopeSearch::-webkit-search-cancel-button { display: none; }

.scope-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0 14px;
  max-height: 208px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  /* The gutter the scrollbar sits in, so the right-hand column's counts are
     not underneath it. */
  padding-right: 4px;
}

.scope-list::-webkit-scrollbar { width: 9px; }
.scope-list::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 5px;
  border: 2px solid var(--plane);
}

/* A context name is long ("Paint Face Mask (Weight, Vertex, Texture)") and the
   count beside it is the part that must never be pushed off; the name gives
   way first, and the full one is on the row's title. */
.scope-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A picked row, so the choice is legible down the column without reading the
   boxes one at a time. */
.scope-list li.is-on .scope-name { color: var(--ink); }

.scope-empty { margin-top: 8px; }

/* ------------------------------------------------------------- the wipe */

/*
 * The bottom of the Advanced fold. Everything above the rule narrows a
 * session; what is under it throws away every session there has ever been, so
 * it gets a line across the fold rather than just more space — and it is the
 * last thing in the dialog, as far from the doors as the layout can put it.
 */
.scope-reset {
  margin-top: 12px;
  padding-top: 11px;
  border-top: 1px solid var(--line);
}

.scope-reset h3 {
  margin: 0 0 7px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.scope-reset .hint { margin: 7px 0 0; font-size: 10.5px; }

/* Red on sight. It is out in the open now rather than shut inside a corner,
   and the colour is what says so before it is read — the arming press is a
   deepening of the same red, not the moment it first turns. */
.fx-reset {
  padding: 6px 12px;
  border: 1px solid color-mix(in srgb, var(--wrong) 55%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--wrong) 9%, transparent);
  color: var(--wrong);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
  transition: color 120ms, border-color 120ms, background 120ms;
}

.fx-reset:hover {
  border-color: var(--wrong);
  background: color-mix(in srgb, var(--wrong) 16%, transparent);
}

/* Armed: the same red, filled in. */
.fx-reset.armed {
  border-color: var(--wrong);
  background: color-mix(in srgb, var(--wrong) 26%, transparent);
  color: var(--ink);
}

/*
 * The foot of the menu: the last word about Esc, and nothing else beside it.
 * The gear that used to sit in the right corner is gone with the switches it
 * opened.
 */
.game-foot { margin-top: 16px; }

.game-foot .hint { margin: 0; }

.game-actions {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 18px;
}

.game-go {
  appearance: none;
  border: 1px solid var(--amber-dim);
  border-radius: var(--radius);
  background: rgba(245, 181, 74, 0.12);
  color: var(--amber);
  font: inherit;
  font-weight: 600;
  padding: 8px 20px;
  cursor: pointer;
}

.game-go:hover { background: rgba(245, 181, 74, 0.2); }

.game-stats {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 20px;
  margin: 14px 0 0;
}
.game-stats dt { color: var(--ink-muted); }
.game-stats dd {
  margin: 0;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.game-weak { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.game-weak li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1px 12px;
  align-items: baseline;
}
.weak-action { color: var(--ink); }
.weak-ctx { color: var(--ink-muted); font-size: 12px; }
.weak-caps { grid-column: 1 / -1; }

/* ------------------------------------------------------------------- HUD */

.game-hud { position: absolute; inset: 0; }

/*
 * The card sits high rather than centred: the answer is going to land in the
 * middle of the screen, on the canvas, and a card parked over it would be in
 * front of the one thing the game is about.
 */
.game-card {
  position: absolute;
  top: 58px;
  left: 50%;
  transform: translateX(-50%);
  width: min(560px, calc(100vw - 40px));
  padding: 14px 20px 16px;
  border: 1px solid var(--line-strong);
  border-top: 2px solid var(--line-strong);
  border-radius: 10px;
  background: rgba(13, 13, 13, 0.92);
  text-align: center;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
  transition: border-color 140ms, box-shadow 140ms;
}

.game-card.is-right {
  border-color: var(--amber-dim);
  border-top-color: var(--amber);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 30px rgba(245, 181, 74, 0.12);
}
.game-card.is-wrong {
  border-color: #4a2320;
  border-top-color: var(--wrong);
}

/* Where the shortcut is, and how deep it is: two pills on one line, centred
   over the name. */
.game-tags {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
  margin: 0;
}

/* The context, tinted with its own cluster's hue by game-ui.js — the same hue
   the canvas paints that tree in, so the tag and the place are one thing. */
.game-tag {
  display: inline-block;
  padding: 1px 9px;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-size: 11px;
  letter-spacing: 0.04em;
  opacity: 0.9;
}

/*
 * The tier, in its rung's colour off the same ramp the five doors carry. Worn
 * lighter than the context tag — no border, and the word set small — because
 * the place a shortcut lives is part of the question and how deep it sits is
 * only ever an aside. The dot is what makes it read as a mark rather than a
 * second label competing with the context beside it.
 */
.game-tier {
  display: flex;
  align-items: center;
  gap: 5px;
  border: 0;
  padding: 1px 2px;
  /* The fallback rides on the `var()` rather than on a `--tier` of its own: a
     custom property set here would sit at the same weight as the rung's and
     win on source order, and the ramp would never show. */
  color: var(--tier, var(--ink-muted));
  text-transform: capitalize;
  opacity: 0.85;
}
.game-tier::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}
/* Nothing to say when the tier is unknown, and an empty pill would still draw
   the eye. */
.game-tier:empty { display: none; }

.game-action {
  margin: 9px 0 0;
  font-size: 28px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
}

/* Two words set large is the whole card, and most of them are two words. The
   few that carry a bracket or two are set down a step so they still read as one
   name rather than as a line of type. game-ui.js picks by length. */
.game-action.is-long { font-size: 23px; line-height: 1.25; }
.game-action.is-longer { font-size: 19px; line-height: 1.3; }

/* The chord, and the only place it is ever written. Never on an open question. */
.game-answer {
  margin: 12px 0 0;
  display: flex;
  flex-wrap: wrap;              /* four chords answer some cards; they wrap */
  justify-content: center;
  align-items: center;
  gap: 5px;
}

.cap {
  display: inline-block;
  min-width: 24px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--plane-2);
  color: var(--ink);
  font: 600 12px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif;
  text-align: center;
}

.cap-plus { color: var(--ink-muted); margin: 0 3px; font-size: 11px; }

/* Between two whole chords that both answer the card, where `+` joins the keys
   inside one. Set in words rather than as a symbol so the row cannot be read as
   one long chord. */
.cap-or {
  color: var(--ink-muted);
  margin: 0 7px;
  font-size: 11px;
  font-style: italic;
  letter-spacing: .04em;
}

.game-hint {
  position: absolute;
  top: 178px;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: 5px 14px;
  border-radius: 999px;
  background: rgba(13, 13, 13, 0.9);
  color: var(--ink-muted);
  font-size: 12px;
  animation: game-in 200ms ease-out;
}

.game-streak {
  position: absolute;
  top: 58px;
  right: 26px;
  font: 700 30px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  text-shadow: 0 0 22px rgba(245, 181, 74, 0.35);
}

.game-streak.is-pop { animation: combo-pop 260ms cubic-bezier(0.2, 1.4, 0.4, 1); }

@keyframes combo-pop {
  0%   { transform: scale(1.5); }
  100% { transform: scale(1); }
}

.game-note {
  position: absolute;
  bottom: 22px;
  left: 50%;
  transform: translateX(-50%);
  max-width: 460px;
  margin: 0;
  padding: 9px 15px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: rgba(13, 13, 13, 0.95);
  color: var(--ink-2);
  font-size: 12px;
  text-align: center;
}

/* ------------------------------------------------------------ countdown */

.game-count {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font: 700 96px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  text-shadow: 0 0 60px rgba(0, 0, 0, 0.8);
}

.game-count.is-beat { animation: count-beat 640ms ease-out; }

@keyframes count-beat {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
  18%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0.15; transform: translate(-50%, -50%) scale(0.94); }
}

.pause-keys {
  display: flex;
  gap: 6px;
  align-items: center;
  min-height: 34px;
  margin: 10px 0 2px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--plane-2);
}

/* ------------------------------------------------- screen-level feedback */

/* The streak glow. A vignette rather than a flash: it is on for as long as the
   streak is, and something that is on for a minute has to be something you can
   stop noticing. */
.game-vignette {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 420ms ease-out;
  background: radial-gradient(ellipse at center,
              rgba(245, 181, 74, 0) 45%,
              rgba(245, 181, 74, 0.09) 78%,
              rgba(245, 181, 74, 0.2) 100%);
}

/* The miss shake, on #app so the canvas moves with it — the board is what got
   hit. Small and fast: a big shake on a game you play for twenty minutes is a
   headache, and this only has to register. */
#app.is-shake { animation: shake 300ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }

@keyframes shake {
  10%, 90% { transform: translate3d(-2px, 0, 0); }
  20%, 80% { transform: translate3d(4px, 0, 0); }
  30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
  40%, 60% { transform: translate3d(6px, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  #app.is-shake { animation: none; }
  .game-dialog, .game-hint { animation: none; }
  .game-count.is-beat, .game-streak.is-pop { animation: none; }
}

/* In game mode the panels fold away entirely: the board is the whole screen,
   and the controls belong to the mode that is about reading the graph. */
#app.game-on { grid-template-columns: 0 minmax(0, 1fr) 0; }
#app.game-on .pane-reopen,
#app.game-on .stage-controls,
#app.game-on .stage-status { display: none; }

/* Under 940px the panels fold along the other axis — app.css stacks the three
   into rows and gives the columns up. Collapsing only the columns there leaves
   two empty rows still holding their share of the height, and the board loses a
   third of the screen to nothing. Every collapsed variant has to be restated in
   here for the same reason app.css restates its own: the game is a fourth
   state of the same fold. */
@media (max-width: 940px) {
  #app.game-on,
  #app.game-on.sidebar-off,
  #app.game-on.detail-off,
  #app.game-on.sidebar-off.detail-off {
    grid-template-columns: 1fr;
    grid-template-rows: 0 minmax(0, 1fr) 0;
  }
}
