/* DAM BREAK — OPUS C owns this file.
   Instrument-panel UI over a canvas game world: dark blue-slate, restrained,
   legible at a glance. Mobile-first (360×640 baseline), widened at 720px+.
   No build step, no external assets/fonts — system font stack only. */

/* ==================== TOKENS ==================== */
:root {
  /* surfaces */
  --bg: #0d1b26;
  --surface: #10222f;
  --surface-2: #16344a;   /* raised: buttons, cards */
  --surface-3: #1c3f57;   /* raised + hovered/active/selected */
  --border: #26445a;
  --border-strong: #3d6683;

  /* text */
  --text: #dfe9f0;
  --text-dim: #93a7b5;
  --text-faint: #61798c;

  /* accent + semantic */
  --accent: #35a7ff;
  --accent-dim: #1d5f8a;
  --accent-soft: rgba(53, 167, 255, 0.14);
  --accent-glow: rgba(53, 167, 255, 0.45);
  --warn: #ff9b35;
  --warn-soft: rgba(255, 155, 53, 0.16);
  --danger: #ff5a3c;
  --danger-soft: rgba(255, 90, 60, 0.18);
  --danger-glow: rgba(255, 90, 60, 0.5);
  --good: #7fff9a;
  --good-soft: rgba(127, 255, 154, 0.16);
  --sandbox: #b39dff; /* freebuild/sandbox badge — neutral accent, not in the core palette */
  --sandbox-soft: rgba(179, 157, 255, 0.16);
  --on-accent: #06202e; /* dark text for use on bright accent-filled buttons */

  /* the "release water" button gets its own warm identity — the single
     most dangerous-looking control in the game */
  --release-bg: #7a2818;
  --release-bg-hot: #9a3620;
  --release-border: var(--danger);

  /* materials — must match src/build/materials.js colours exactly */
  --mat-timber: #c8954a;
  --mat-steel: #9fb2c4;
  --mat-concrete: #b9c4cc;
  --mat-cable: #e8d44d;

  /* shape + shadow */
  --radius-sm: 8px;
  --radius: 12px;
  --radius-lg: 18px;
  --shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
  --scrim: rgba(0, 0, 0, 0.35);       /* dark overlay for contrast on light swatches/badges */
  --scrim-screen: rgba(8, 16, 24, 0.9); /* the dimmed backdrop behind full-screen overlays */

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* THE TOP-RIGHT CORNER, MEASURED ONCE.
     The hub's three plates — menu, sound, fullscreen — are fixed to the page
     and are there on every screen and right through a run (see #corner-tools),
     so everything else that wants that corner is positioned off ITS box rather
     than off a number somebody guessed. */
  --corner-inset: 12px;   /* the cluster's own distance from the top and right edges */
  --corner-w: 154px;      /* 5 strip padding + 3 × 44 plate + 2 × 6 gap + 5 */
  --corner-h: 54px;       /* 5 + 44 + 5 */

  /* The width of that whole column, which #hud-top keeps clear on its right so
     the budget block can never end up under the cluster. ONE token, so the HUD
     and the cluster cannot drift apart into an overlap — the readouts running
     under the menu button was a real regression once. */
  --hud-menu-gap: calc(var(--corner-inset) + var(--corner-w) + 8px);
}

/* ==================== RESET / BASE ==================== */
* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  /* A game, not a document (hub CLAUDE.md §2). On a tablet a resting thumb
     is a long press and a drag that starts on a level card or a HUD label is
     a text selection, so Safari highlights words and offers Copy / Look Up
     in the middle of laying a beam. Nothing selects and nothing opens a
     callout; `manipulation` drops double-tap zoom and keeps pinch. The
     canvas below goes further with touch-action:none, and
     src/touch-guard.js is the half CSS is not honoured for everywhere. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
/* No text field ships today (the faders are ranges), but the day one does it
   must be able to select what was typed in it. */
input,
textarea,
[contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

#wrap { position: fixed; inset: 0; }

#game {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  touch-action: none; /* the player drags on the canvas to build — never let the browser pan/zoom it */
  display: block;
}

.hidden { display: none !important; }

/* ==================== BUTTONS ==================== */
/* Shared button system. Every clickable control in the HUD and screens
   inherits from this baseline, then layers on size/colour modifiers. */
.big-btn, .small-btn, .tool-btn, .speed-btn, .level-card {
  font: inherit;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  cursor: pointer;
  min-height: 44px;
  min-width: 44px;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
  transition: transform 0.08s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

.big-btn {
  font-size: clamp(16px, 4.4vw, 20px);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: clamp(12px, 3vw, 14px) clamp(20px, 6vw, 34px);
  box-shadow: var(--shadow-sm);
}
.small-btn {
  font-size: clamp(13px, 3.4vw, 15px);
  font-weight: 600;
  padding: 10px 18px;
}
.tool-btn {
  font-size: 17px;
  padding: 0;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* Icon tools draw with inline SVG rather than a glyph where no glyph says the
   right thing (a beam between two nodes; a dashed box with an ×). currentColor
   throughout, so they inherit every state colour below for free. */
.tool-btn > svg { width: 22px; height: 22px; display: block; pointer-events: none; }
/* The hub's CSS trap: [hidden] is only a presentation hint and loses to the
   inline-flex above, so the fullscreen plate would show on exactly the iPhone
   it is hidden from. Written out, every time. */
.tool-btn[hidden] { display: none; }
.tool-btn.active { border-color: var(--accent); background: var(--surface-3); color: var(--accent); }
.tool-btn.danger.active { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.speed-btn {
  font-size: 14px;
  font-weight: 700;
  padding: 0 10px;
  height: 44px;
  flex: 0 0 auto;
}
.speed-btn.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

/* colour modifiers — usable standalone or combined with .small-btn */
.primary-btn { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 700; }
.ghost-btn { background: transparent; border-color: var(--border); color: var(--text-dim); box-shadow: none; }

.update-btn { border-color: var(--good); color: var(--good); background: var(--surface-2); animation: update-glow 2.4s ease-in-out infinite; }

/* Leaving the game, as opposed to leaving a level — the hub's one way out,
   which since 2026-10-11 looks and behaves the same in every cabinet (hub §2,
   "One button"). A GHOST plate: the toolbar's rim with nothing poured into it,
   because leaving is never the recommended action and must not compete with
   PLAY; but a visible rim, because it was borderless until then and a way out
   that looks like a caption is one nobody finds. The shared back arrow sits in
   front of the words, stroked like the corner glyphs (1.9, round caps), and
   the words are sentence case — no ◂ and no capitals, for this button only.
   [hidden]-style hiding is .hidden here, which is display:none !important, so
   the inline-flex below cannot resurrect it on a browser that should not see
   it. */
.quit-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text-dim);
  box-shadow: none;
}
.quit-glyph {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.quit-btn:not(:disabled):hover,
.quit-btn:focus-visible { color: var(--text); border-color: var(--accent); }
.quit-btn:disabled { opacity: 0.7; }

/* A menu's column of doors, laid out as one: a grid that stretches every
   member to the widest, so the column has two shared edges and no magic width
   to keep in step with the longest Norwegian label (maxgear's .title-actions).
   The way out is always its last member and stands a little apart, so it is
   found in the same place every time and never read as one more setting. */
.menu-col {
  display: grid;
  justify-items: stretch;
  gap: 10px;
  width: min(100%, 340px);
}
.menu-col > .quit-btn { margin-top: 8px; }

.big-btn.release {
  background: var(--release-bg);
  border-color: var(--release-border);
  color: var(--text);
  box-shadow: 0 0 0 1px var(--danger-soft), 0 6px 22px var(--danger-soft);
  animation: release-glow 2.2s ease-in-out infinite;
}

.pulse { animation: pulse 1.6s infinite; }

@keyframes pulse { 50% { box-shadow: 0 0 26px var(--accent-glow); } }
@keyframes release-glow {
  0%, 100% { box-shadow: 0 0 0 1px var(--danger-soft), 0 6px 18px var(--danger-soft); }
  50% { box-shadow: 0 0 0 1px var(--danger-glow), 0 6px 30px var(--danger-glow); }
}
@keyframes update-glow { 50% { opacity: 0.7; } }

/* .level-card is a <div role="button">, not a <button> (see the levels block for
   why), so its "disabled" state is aria-disabled — hence every rule below
   matches BOTH the real :disabled pseudo-class and the ARIA attribute. */
.big-btn:not(:disabled):active,
.small-btn:not(:disabled):active,
.tool-btn:not(:disabled):active,
.speed-btn:not(:disabled):active,
.level-card:not([aria-disabled="true"]):active { transform: scale(0.96); }

.big-btn:disabled, .small-btn:disabled, .tool-btn:disabled, .speed-btn:disabled,
.level-card[aria-disabled="true"] {
  opacity: 0.4;
  cursor: default;
  box-shadow: none;
}

.big-btn:focus-visible, .small-btn:focus-visible, .tool-btn:focus-visible,
.speed-btn:focus-visible, .level-card:focus-visible, .mat-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ==================== SCREENS — SHARED ==================== */
.screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* SAFE centring, and it is load-bearing on a short landscape phone: a flex
     container that centres content TALLER than its own box overflows in both
     directions, and the half that goes off the top edge is unreachable — no
     amount of scrolling brings it back. `safe center` degrades to flex-start
     exactly in that case. The plain `center` above stays as the fallback for
     engines that do not know the keyword. */
  justify-content: safe center;
  gap: 14px;
  background: var(--scrim-screen);
  text-align: center;
  padding: calc(20px + env(safe-area-inset-top, 0px)) calc(20px + env(safe-area-inset-right, 0px))
            calc(20px + env(safe-area-inset-bottom, 0px)) calc(20px + env(safe-area-inset-left, 0px));
  z-index: 10; /* above #hud (z-index 5) */
  overflow-y: auto; /* safety net on very short viewports; #screen-levels re-locks this below */
  -webkit-overflow-scrolling: touch;
}

.screen-head { display: flex; flex-direction: column; gap: 4px; margin-bottom: 4px; }
.screen-head h2 { font-size: clamp(20px, 5.5vw, 26px); letter-spacing: 0.06em; }

.tagline { color: var(--text-dim); font-size: clamp(13px, 3.6vw, 15px); }
.hint { color: var(--text-faint); font-size: 13px; margin-top: 4px; }
.version-tag { position: absolute; bottom: calc(10px + env(safe-area-inset-bottom, 0px)); right: calc(14px + env(safe-area-inset-right, 0px)); color: var(--text-faint); font-size: 11px; }

/* ---- title screen ----
   The backdrop is no longer CSS: src/ui/titleScene.js runs a live fluid diorama
   on the game canvas behind this screen. So this screen's scrim is a WINDOW, not
   a wall — heavy enough behind the text column to keep every glyph legible,
   thin enough at the edges that the reservoir, the bowing timber and the breach
   are all clearly visible. Two layers do that: a radial pool of shade centred on
   the text, and a top/bottom vignette that anchors the header and the version
   tag. Nothing here may go opaque; the diorama is the point. */
#screen-title {
  /* Lift the whole text block into the upper half. The diorama's waterline sits
     at ~63% of the canvas height and the crest at ~60%, so a vertically centred
     menu puts the tagline exactly on the moving surface of the reservoir. This
     is the other half of that fix (the first half is title.sillAt): text above
     the crest line, water below it.

     CLAMPED, and that clamp is load-bearing. .screen centres with
     justify-content, and a flex container whose content is taller than its box
     overflows in BOTH directions — the top of the wordmark goes above the
     viewport and no amount of scrolling brings it back. A flat 24vh did exactly
     that on a 420 px-tall landscape phone. The max() term is the available slack
     above the ~420 px the text block needs, so the lift fades to nothing rather
     than pushing the logo off the top. */
  padding-bottom: calc(20px + min(24vh, max(0px, 100vh - 420px)) + env(safe-area-inset-bottom, 0px));
  background:
    /* A TIGHT pool of shade under the wordmark, and nothing else. Two earlier
       versions covered 76% and then 112% of the frame and both had the same
       result: measured at the reservoir, the diorama was arriving 65-70%
       dimmed, which is to say not arriving. The scrim only has to serve the
       text block, so it is sized to the text block. */
    radial-gradient(56% 29% at 50% 36%,
      rgba(6, 13, 20, 0.74) 0%,
      rgba(6, 13, 20, 0.44) 50%,
      rgba(6, 13, 20, 0) 100%),
    /* top and bottom vignette: anchors the update button and the version tag,
       and keeps the waterline from running into the frame edge */
    linear-gradient(to bottom,
      rgba(6, 13, 20, 0.44) 0%,
      rgba(6, 13, 20, 0.02) 24%,
      rgba(6, 13, 20, 0.02) 70%,
      rgba(6, 13, 20, 0.48) 100%);
}
/* With the scrim this thin, contrast has to come from the type itself: a hard
   shadow for edge definition and a wide soft one to lift the glyphs off
   whatever the water is doing behind them. Cheaper than opacity, and it does
   not cost the picture anything. */
#screen-title .logo h1 { text-shadow: 0 2px 4px rgba(4, 10, 16, 0.95), 0 6px 34px rgba(4, 10, 16, 0.8); }
#screen-title .tagline,
#screen-title .hint,
#screen-title .vol-label { text-shadow: 0 1px 3px rgba(4, 10, 16, 0.98), 0 2px 16px rgba(4, 10, 16, 0.9); }
#screen-title .tagline { color: var(--text); }
#screen-title .hint { color: var(--text-dim); }

.logo h1 {
  /* 13vh in the middle term as well as 11vw: on a short landscape phone the
     width is generous and the height is not, and an 84 px wordmark plus buttons
     does not fit in 420 px. Scaling on the smaller of the two axes is what keeps
     the block inside the viewport there. */
  font-size: clamp(40px, min(11vw, 13vh), 84px);
  letter-spacing: 0.05em;
  line-height: 1;
  font-weight: 800;
}
.logo .accent { color: var(--accent); }

/* THE BROKEN RULE. Two beams under the wordmark: a long one, then a short one
   dropped and tilted off the end of it, as if the line itself had failed in the
   middle. It is the whole logo idea — the name says BREAK, so the rule breaks —
   and it costs two empty <i> elements. The fault sits under the gap between
   "DAM" and "BREAK", which is where the eye already is. */
.logo-rule {
  position: relative;
  width: min(66vw, 300px);
  height: 16px;
  margin: 10px auto 12px;
}
.logo-rule i {
  position: absolute;
  top: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  box-shadow: 0 0 14px var(--accent-glow);
}
.logo-rule i:first-child { left: 0; width: 50%; }
.logo-rule i:last-child {
  right: 0;
  width: 34%;
  background: var(--accent-dim);
  box-shadow: none;
  transform: translateY(7px) rotate(2.6deg);
}

/* ---- levels screen ---- */
/* The same diorama is still running behind this screen (dimmed and pushed to the
   lower third by titleScene.js), but here ten cards of small text have to be
   scannable, so the scrim stays a wall rather than a window — only the top and
   bottom edges thin out enough to admit that the world is still there. */
#screen-levels {
  overflow-y: hidden;      /* the screen itself must never scroll away BACK — only the grid does */
  align-items: stretch;
  background: linear-gradient(to bottom,
    rgba(8, 16, 24, 0.80) 0%,
    rgba(8, 16, 24, 0.93) 16%,
    rgba(8, 16, 24, 0.93) 84%,
    rgba(8, 16, 24, 0.80) 100%);
}
/* align-items:stretch (above) is only there so #level-grid can flex-grow to
   fill the space between header and BACK — pull the header text and the
   BACK button back to a natural centred width instead of stretching them */
#screen-levels > .screen-head { align-self: center; }
/* The foot is BACK alone now — the language row and the way out moved to the
   pause menu when the HUD's menu plate stopped opening this screen. */
.levels-foot {
  align-self: center;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#levels-progress { margin-bottom: 2px; }

#level-grid {
  flex: 1 1 auto;
  min-height: 0;
  width: min(100%, 900px);
  margin: 0 auto;
  display: grid;
  /* One column on a narrow phone, two or more the moment there is room, with no
     media query: minmax(min(100%, 260px), 1fr) means "260px unless 260px is
     wider than the track, in which case take the track". The bare 260px form
     overflows the grid on a 320px screen; the min() is what stops that. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  /* THIS LINE IS THE BUG FIX. It is not a tidy-up.
     The rows used to be `auto`, i.e. minmax(auto, auto). This grid is a SCROLL
     container with a DEFINITE height (it is a flex item with flex:1 1 auto), and
     when the sum of a definite-height grid's auto rows exceeds that height,
     Chromium shrinks every auto row towards its MINIMUM instead of overflowing
     it. The minimum here was .level-card's own min-height: 60px. Then the
     default align-items:stretch clamped every card to exactly that 60px while
     its content wanted 73px — so the subtitle painted 13px BELOW the card's own
     background, on top of the gap and the next card down, and a card with a BEST
     line put the best cost outside the box entirely.
     max-content rows are sized by the card's content and are never shrunk, so
     the card box always contains the card. (Everything still stretches to the
     row, so two cards side by side in the 2-column layout stay equal height.) */
  grid-auto-rows: max-content;
  align-content: start;
  gap: 10px;
  padding: 4px 2px 12px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* ---- THE LEVEL CARD ----
   Two changes here, and it is worth being precise about which one fixed what,
   because the obvious suspect was innocent.

   WHAT ACTUALLY CAUSED THE OVERFLOW: the row sizing on #level-grid above, not
   this element. Measured proof: a card pinned to 60px with 73px of content
   sprang to its correct 63px the instant `align-self: start` was set on it, and
   every row track came out right the instant #level-grid got
   `grid-auto-rows: max-content`. Neither of those touches the card's own
   internals, and both work with the card still being a <button>. The card was
   being CLAMPED from outside, not failing to grow from inside.

   WHY IT IS A <div role="button"> ANYWAY: the card is a wrapping, multi-line,
   grid-structured block of five bits of information. That is a layout job, and a
   <button> is the one element you least want doing layout — its baseline
   alignment, intrinsic sizing and child-layout behaviour all have special cases,
   which is exactly why the button theory was plausible in the first place. A div
   with the role, the tab stop and Enter/Space handling (screens.js) behaves like
   a button to a user and a screen reader, and like a plain box to the layout
   engine. Its "disabled" is aria-disabled, hence the attribute selectors above.

   The layout itself is a two-column grid rather than the old wrapping flex row,
   so the subtitle has a REAL row of its own and cannot be pushed anywhere: the
   number spans both rows on the left, everything else stacks in column 2. */
.level-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 12px;
  row-gap: 3px;
  text-align: left;
  padding: 11px 14px;
  border-radius: var(--radius);
  background: var(--surface-2);
  min-height: 60px;
  /* Deliberately NO `overflow: hidden` here. It was tried, and it converts the
     bug rather than fixing it: an under-sized card stops bleeding onto its
     neighbour and starts guillotining its own subtitle mid-word instead, which
     is the other half of what the player reported. The row sizing above is what
     makes the box fit the content; tests/ui-layout.mjs asserts scrollHeight
     never exceeds clientHeight so a regression shows up as a failure rather
     than as either symptom. */
}
.level-num {
  grid-column: 1;
  grid-row: 1 / span 2;
  font-size: clamp(22px, 6vw, 28px);
  font-weight: 800;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  min-width: 1.4em;
  text-align: center;
}
/* Row 1: the name, then the tag cluster pushed to the right. Both wrap, and
   because .level-tags is one flex item the badge and the best cost travel
   together to a second line instead of splitting up raggedly. */
.level-head {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px 10px;
  min-width: 0;
}
.level-name {
  font-size: clamp(14px, 3.8vw, 16px);
  font-weight: 700;
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: break-word;   /* a long level name breaks, it never clips mid-word */
}
.level-tags { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; margin-left: auto; }
/* Row 2: the subtitle, in its own grid row, wrapping freely inside the card. */
.level-sub {
  grid-column: 2;
  grid-row: 2;
  font-size: clamp(11px, 3.2vw, 13px);
  color: var(--text-dim);
  min-width: 0;
  overflow-wrap: break-word;
  line-height: 1.35;
}
.level-best { font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.level-lock { font-size: 13px; opacity: 0.6; }

.badge {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 3px 8px;
  border-radius: 999px;
  flex: 0 0 auto;
}
.badge.countdown { background: var(--warn-soft); color: var(--warn); }
.badge.freebuild { background: var(--accent-soft); color: var(--accent); }
.badge.sandbox { background: var(--sandbox-soft); color: var(--sandbox); }

.level-card.done { border-left: 4px solid var(--good); background: linear-gradient(90deg, var(--good-soft), var(--surface-2) 18%); }
.level-card.locked {
  background: var(--surface);
  color: var(--text-faint);
  box-shadow: none;
}
.level-card.locked .level-name { color: var(--text-faint); }
/* The padlock is a real element in .level-tags now, not an absolutely
   positioned ::after — the old one sat on top of the FREE BUILD badge. */

/* ---- result screen ---- */
.end-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 26px 28px;
  max-width: 92vw;
  width: min(440px, 92vw);
  box-shadow: var(--shadow);
}
#result-heading { font-size: clamp(22px, 6vw, 30px); letter-spacing: 0.03em; margin-bottom: 4px; }
#result-heading.win { color: var(--good); }
#result-heading.fail { color: var(--danger); }
#result-cause { color: var(--warn); }
.result-note { color: var(--text-dim); font-size: 13px; margin-top: 4px; }

.end-stats {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 7px 20px;
  text-align: left;
  margin: 18px 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.stat-k { font-size: 12px; color: var(--text-dim); letter-spacing: 0.03em; align-self: center; }
.stat-v { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; text-align: right; }
.stat-v.good { color: var(--good); }
.stat-v.bad { color: var(--danger); }
.stat-v.warn { color: var(--warn); }

.result-btns { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }

/* ==================== THE CORNER CLUSTER ====================
   The hub's three plates, top right, ON EVERY SCREEN AND THROUGH EVERY RUN:
   menu, sound (a live mute), fullscreen, reading left to right.

   It borrows the toolbar's own vocabulary rather than inventing one: .tool-btn
   plates inside a bordered strip, which is exactly what #tool-group is at the
   foot of the build screen. This game draws its controls as square plates
   bolted onto a blueprint; a soft round glass pill would belong to some other
   game in the arcade, not to this one. 44px targets come with .tool-btn, so a
   tablet is already served.

   FIXED TO THE PAGE, not to whatever the page is currently showing. Until
   2026-10-11 the menu plate lived one row down inside the HUD and the title
   had no menu at all; now all three are here, always, and the HUD keeps the
   column under them clear (--hud-menu-gap).

   Above .screen (10) and above #hud (5): this outlives both, so nothing either
   of them does may cover it. */
#corner-tools {
  position: fixed;
  top: calc(var(--corner-inset) + env(safe-area-inset-top, 0px));
  right: calc(var(--corner-inset) + env(safe-area-inset-right, 0px));
  z-index: 30;
  display: flex;
  gap: 6px;
  padding: 5px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}

/* The fullscreen glyph carries BOTH states in one <svg> — four corners pushing
   out, and the same four pulled in — and aria-pressed chooses. src/ui/screen.js
   writes that attribute and the accessible label from one paint(), so what is
   drawn and what is announced cannot drift apart. */
#corner-tools svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The speaker carries both of ITS states the same way: the waves while
   audible, the slash while muted (aria-pressed="true", written by
   src/ui/screens.js with the label). */
#btn-sound .icon-slash { display: none; }
#btn-sound[aria-pressed="true"] .icon-waves { display: none; }
#btn-sound[aria-pressed="true"] .icon-slash { display: inline; }
#btn-sound[aria-pressed="true"] { color: var(--text-dim); }

#btn-screen .icon-in { display: none; }
#btn-screen[aria-pressed="true"] .icon-out { display: none; }
#btn-screen[aria-pressed="true"] .icon-in { display: inline; }
#btn-screen[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }

/* ==================== THE MENU'S SOUND ROWS ====================
   Two faders and the language row in the menu panel (#screen-pause), set
   once, behind the three bars, because in-play chrome competes with the valley
   for attention. */
.vol-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 14px 0 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);   /* the same rule .end-stats draws above the numbers */
}

/* ONE gutter for both rows, so MUSIC and EFFECTS put their sliders at the same
   x — the hub's .row/.label/.slider shape, under this game's own names.
   `.label` on its own is too generic to introduce into a stylesheet that
   already carries .hud-label, .mat-name and .level-name. 84px because the
   longest label in either language — EFFEKTER, and LANGUAGE on the language
   row that shares this gutter — measures ~74px at this size and tracking. */
.vol-row { display: flex; align-items: center; gap: 12px; }
.vol-label {
  flex: 0 0 84px;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--text-dim);
}
.vol-slider { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 10px; min-height: 44px; }
/* Tabular numerals and a fixed basis: the readout must not shove the slider
   sideways as it counts past 9 and 99. */
.vol-slider output {
  flex: 0 0 34px;
  text-align: right;
  font-size: 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* A bare range input is an OS control and belongs to no game, so the track
   becomes a girder and the thumb becomes a plate — the same two shapes the
   rest of this UI is built from. Both vendor pseudo-elements are written out;
   there is no selector that addresses them together. */
.vol-slider input[type="range"] {
  flex: 1 1 0;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 28px;
  margin: 0;
  background: transparent;
  cursor: pointer;
  /* pan-y, so a finger that lands on the fader can still scroll the panel on a
     very short viewport instead of being captured by a control it missed. */
  touch-action: pan-y;
}
.vol-slider input[type="range"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
/* The track's 1px rim is an inset shadow rather than a border, so the box stays
   exactly 6px tall and the thumb's negative margin below stays predictable. */
.vol-slider input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
.vol-slider input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
/* -11px = (6px track - 28px thumb) / 2. Firefox centres its own thumb. */
.vol-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 26px;
  margin-top: -11px;
  border-radius: 4px;
  background: var(--accent);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 0 10px var(--accent-glow);
}
.vol-slider input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 26px;
  border-radius: 4px;
  background: var(--accent);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 0 10px var(--accent-glow);
}

/* The line under the faders, drawn as a margin note. It was amber — the HUD's
   correcting colour — for as long as it had to admit the game was silent; now
   it only says what the speaker does, so it takes the quiet rule. */
.sound-note {
  margin: 0 0 16px;
  padding-left: 10px;
  border-left: 2px solid var(--border-strong);
  line-height: 1.45;
  color: var(--text-dim);
  text-align: left;
}

/* ==================== THE MENU ====================
   The game's one menu panel, both faces (in play: PAUSE, RESUME, LEVELS;
   elsewhere: SETTINGS, BACK). The same plate as the result card, so pausing
   reads as another page of the same document. One column, top to bottom: the
   doors back in, the faders and the language row on one label gutter, the
   speaker note, and the way out LAST, full width, a little apart. A grid with
   align-content: start (the hub's iPad lesson) so no row is ever stretched. */
.pause-card {
  width: min(420px, 92vw);
  display: grid;
  align-content: start;
  justify-items: stretch;
  text-align: left;
}
.pause-card .screen-head { text-align: center; }
.pause-card .menu-col { width: 100%; margin-top: 12px; }
.pause-card > .quit-btn { margin-top: 2px; }

/* ---- THE LANGUAGE ROW ----
   Two equal plates in one segmented row, Norsk first, each a flag and its
   endonym. The same plate as a .tool-btn — surface, rim, and the accent rim
   for the one that is lit, read off aria-pressed — so it belongs to the same
   blueprint as the toolbar rather than being a stock toggle.

   flex: 1 1 0 on the plates, never auto: "Norsk" is shorter than "English",
   and auto would draw the default as the smaller, lesser choice. The row may
   WRAP: on a phone under ~370px the pair cannot sit beside the 84px label, and
   rather than shrink the plates below a thumb it drops them onto a line of
   their own, still full width and still equal. */
.lang-row { flex-wrap: wrap; row-gap: 6px; }
.lang-seg { flex: 1 1 200px; min-width: 0; display: flex; gap: 8px; }
.lang-btn {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 10px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.lang-btn[aria-pressed="true"] {
  color: var(--accent);
  background: var(--surface-3);
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-soft), 0 0 12px var(--accent-soft);
}
.lang-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lang-btn .flag { flex: 0 0 auto; height: 15px; border-radius: 2px; box-shadow: 0 0 0 1px var(--scrim); }
.lang-btn[data-lang="nb"] .flag { width: 21px; }   /* 22:16 */
.lang-btn[data-lang="en"] .flag { width: 30px; }   /* 2:1 */

/* ==================== HUD — TOP BAR & METERS ==================== */
#hud { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
/* Only real controls opt back in to receiving input — everything else lets
   pointer events fall through to the canvas underneath. #toolbar is deliberately
   NOT in this list any more: it is a full-width row in the bottom stack now, and
   an `auto` box there would swallow every drag along the foot of the play area.
   The buttons take their own hits; #material-strip needs it to be touch-
   scrollable; .tut-card needs it so you cannot build through the card. */
#hud button, #material-strip, .tut-card { pointer-events: auto; }

#hud-top {
  position: absolute;
  top: env(safe-area-inset-top, 0px);
  left: 0; right: 0;
  display: flex;
  flex-wrap: nowrap; /* keep this a single predictable row so other HUD elements can be positioned relative to it */
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(6px, 2.5vw, 16px);
  padding: 10px clamp(10px, 3vw, 16px);
  /* AND THEN THE CORNER'S COLUMN ON TOP OF THAT. The cluster is fixed to the
     page and is up through build and sim as well as on the menus, so this row
     has to stop short of it rather than run underneath: that reservation is
     what lets "$5,470 LEFT OF $8,000" and a fullscreen plate share one edge.
     It costs the row ~124px of width; the three columns' min-contents come to
     about 160px, so a 320px phone still fits all three. */
  padding-right: calc(var(--hud-menu-gap) + env(safe-area-inset-right, 0px));
}

/* nowrap is load-bearing. "LEFT OF $5,470" broke onto THREE lines when the top
   row got tight (a 320px screen with the countdown timer column showing), and
   the third line landed on top of the menu button (then one row below the
   corner; it is in the corner itself now). */
.hud-label { display: block; font-size: 10px; color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap; }

/* The squeeze order in this row is explicit. #hud-level is the ONLY column that
   may lose width, because it is the only one that degrades gracefully — it
   ellipsises. The timer and the budget hold their min-content (their labels do
   not wrap any more), so they can neither wrap into the row below nor spill
   their own box. Sum of the min-contents is ~160px, so this still fits a 280px
   screen with all three columns showing. */
#hud-level { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 auto; max-width: 44%; }
#hud-level-name { font-size: clamp(13px, 3.8vw, 16px); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#hud-objective { font-size: clamp(10px, 2.8vw, 12px); color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#objective-meter { width: 100%; max-width: 160px; margin-top: 2px; }

#hud-timer { display: flex; flex-direction: column; align-items: center; text-align: center; flex: 0 0 auto; min-width: min-content; }
#timer-text { font-size: clamp(20px, 6.5vw, 30px); font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
#hud-timer.urgent #timer-text, #hud-timer.urgent .hud-label { color: var(--danger); }
#hud-timer.urgent { animation: timer-urgent 0.8s ease-in-out infinite; }
@keyframes timer-urgent { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }

#hud-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; min-width: min-content; flex: 0 1 auto; max-width: 52%; }
#hud-budget { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; width: 100%; }
#budget-left { font-size: clamp(14px, 3.8vw, 17px); font-weight: 700; font-variant-numeric: tabular-nums; }
#budget-meter { width: 100%; max-width: 150px; }

/* Readouts must NEVER shrink below their text: a 3-digit load like 248% in a
   flex-shrunk box overflows its own bounds and prints on top of the neighbour
   (they don't wrap — there is no break opportunity inside "248%"). flex:0 0 auto
   + nowrap keeps each column exactly as wide as its content. */
/* No gutter of its own: the corner column is kept clear by #hud-top as a
   whole (its padding-right, --hud-menu-gap), so nothing in this box can reach
   the cluster, and padding it a second time would only squeeze two readouts
   that have to fit a 320px screen. */
#hud-readouts {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.readout { display: flex; flex-direction: column; align-items: flex-end; flex: 0 0 auto; }
.readout > span { white-space: nowrap; }
.readout > span:first-child { font-size: clamp(12px, 3.2vw, 14px); font-weight: 700; font-variant-numeric: tabular-nums; }

/* thin horizontal bars; JS sets the fill's width via inline style */
.meter { position: relative; width: 120px; max-width: 36vw; height: 6px; border-radius: 4px; background: var(--border); overflow: hidden; flex: 0 0 auto; }
.meter > i { display: block; height: 100%; width: 0%; border-radius: 4px; transition: width 0.25s ease; }
#budget-fill { background: linear-gradient(90deg, var(--accent-dim), var(--accent)); }
#objective-fill { background: linear-gradient(90deg, var(--accent), var(--good)); }

/* live states hud.js toggles: budget nearly gone, objective slipping, dam
   overloaded. Never colour alone — the number itself is the primary signal. */
#hud-budget.low #budget-left { color: var(--warn); }
#hud-budget.low #budget-fill { background: linear-gradient(90deg, var(--warn), var(--danger)); }
#objective-meter.bad > i { background: linear-gradient(90deg, var(--warn), var(--danger)); }
#stat-load.warn { color: var(--warn); }
#stat-load.bad { color: var(--danger); }

/* toast: centred near top-middle, purely informational, never blocks input */
#hud-toast {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 60px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  pointer-events: none;
  z-index: 1;
}
.toast {
  padding: 8px 18px;
  border-radius: 999px;
  font-size: clamp(13px, 3.6vw, 15px);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-sm);
  animation: toast-in-out 1.6s ease forwards;
}
.toast.win { color: var(--good); border-color: var(--good); background: var(--good-soft); }
.toast.fail { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); }
.toast.info { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
@keyframes toast-in-out {
  0% { opacity: 0; transform: translateY(6px); }
  12%, 78% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-10px); }
}

/* ==================== HUD — THE BOTTOM STACK ==================== */
/* Everything that lives at the foot of the screen is a row in ONE bottom-
   anchored flex column: tutorial, build hint, toolbar (materials + tools +
   RELEASE WATER), sim controls. See the long comment on #hud-bottom in
   index.html for why. Two consequences worth stating here:
     · overlap between any two of them is structurally impossible, in either
       orientation, at any width — there are no reserved-pixel offsets left;
     · the column grows UPWARDS, so a hint or the tutorial appearing above the
       toolbar never shifts the controls already under the player's thumb. */
#hud-bottom {
  position: absolute;
  left: env(safe-area-inset-left, 0px);
  right: env(safe-area-inset-right, 0px);
  bottom: 0;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 0 10px calc(10px + env(safe-area-inset-bottom, 0px));
  pointer-events: none;   /* children opt back in — see the #hud rule above */
}

/* Default (narrow portrait) shape: materials on their own row, then the tool
   cluster, then RELEASE WATER full-width across the bottom. Full-width is not a
   compromise — it is the best thumb target on the screen for the one
   irreversible action in the game. */
#toolbar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  min-width: 0;
}

/* (a) MATERIALS — the ONLY thing here allowed to scroll, because it is the only
   thing where "there is more, swipe for it" is an acceptable answer. The inset
   lives in PADDING, not in a margin: an overflow container clips at its padding
   box, so a child flush against the edge (and its focus ring / active glow)
   gets cut off — that is what clipped the first material label originally. */
#material-strip {
  display: flex;
  min-width: 0;
  padding: 4px 2px;
  scroll-padding-inline: 4px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
#material-strip::-webkit-scrollbar { display: none; }
/* Scroll affordance. hud.js measures scrollWidth vs clientWidth and toggles
   these, so the fade appears only when something really is hidden — and only on
   the side it is hidden on. The mask applies to the scroll container's own box
   (not its scrolled content), which is exactly what makes it read as an edge. */
#material-strip.fade-r {
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 30px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 30px), transparent 100%);
}
#material-strip.fade-l {
  -webkit-mask-image: linear-gradient(to left, #000 0, #000 calc(100% - 30px), transparent 100%);
  mask-image: linear-gradient(to left, #000 0, #000 calc(100% - 30px), transparent 100%);
}
#material-strip.fade-l.fade-r {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
}
#material-bar { display: flex; gap: 6px; flex: 0 0 auto; }

/* (b) TOOLS — fixed. 6 × 44px + gaps = 294px, which fits inside a 320px
   viewport, so this cluster NEVER scrolls and never hides. The separator splits
   "what am I doing" (build/erase/box) from "take it back" (undo/redo/clear). */
#tool-group { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.tool-sep { flex: 0 0 auto; width: 1px; align-self: stretch; margin: 5px 2px; background: var(--border-strong); }

/* material buttons — built by hud.js as .tool-btn.mat-btn with swatch/name/cost/key children */
.mat-btn {
  width: auto;
  height: auto;
  min-height: 44px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 6px 10px;
  position: relative;
}
.mat-swatch {
  width: 15px;
  height: 15px;
  border-radius: 4px;
  display: block;
  border: 1px solid var(--scrim);
  transition: transform 0.15s ease;
}
/* fallback/backup fill so the swatch reads correctly even a frame before
   hud.js sets its inline background colour — inline styles still win once set */
.mat-btn[data-mat="timber"] .mat-swatch { background: var(--mat-timber); }
.mat-btn[data-mat="steel"] .mat-swatch { background: var(--mat-steel); }
.mat-btn[data-mat="concrete"] .mat-swatch { background: var(--mat-concrete); }
.mat-btn[data-mat="cable"] .mat-swatch { background: var(--mat-cable); }
.mat-name { font-size: clamp(9px, 2.6vw, 11px); font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; white-space: nowrap; }
.mat-cost { font-size: clamp(8px, 2.2vw, 10px); color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* head rating — what this material BUYS you, so it is accent-coloured against
   the dim cost line right above it: one row is a price, the other a capability.
   Optional: hud.js only creates it when the material has a headRating, and the
   column layout closes up on its own when it is absent. */
.mat-head {
  font-size: clamp(8px, 2.2vw, 10px);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.1;
}
.mat-btn.unaffordable .mat-head { color: var(--text-dim); }
.mat-key {
  position: absolute;
  top: 3px; right: 4px;
  font-size: 9px;
  line-height: 1;
  color: var(--text-faint);
  background: var(--scrim);
  border-radius: 3px;
  padding: 1px 3px;
}
@media (hover: none) { .mat-key { display: none; } } /* hotkeys are meaningless on a touch-only device */

.mat-btn.active {
  border-color: var(--accent);
  background: var(--surface-3);
  box-shadow: 0 0 0 1px var(--accent-soft) inset, 0 0 14px var(--accent-glow);
}
.mat-btn.active .mat-swatch { transform: scale(1.3); }
.mat-btn.unaffordable { opacity: 0.42; }

/* ==================== HUD — SIM CONTROLS, RELEASE, MENU ==================== */
/* Also rows in the bottom stack. WRAPS rather than scrolls: a speed control the
   player cannot find is worse than two short rows of them, and unlike materials
   there is no "more of the same" to swipe for — every one of these is unique. */
#sim-controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  min-width: 0;
}

#btn-release {
  width: 100%;              /* portrait: the full width of the stack */
  font-size: clamp(13px, 3.6vw, 18px);
  padding: clamp(10px, 2.8vw, 15px) clamp(14px, 4vw, 26px);
}

/* build hint: a correction ("over budget") or a calm level hint. A row in the
   bottom stack ABOVE the toolbar, so it can never sit on the release button (it
   used to, at bottom:70px) and its appearing never nudges the controls. */
#build-hint {
  align-self: center;
  max-width: min(340px, 86vw);
  text-align: center;
  padding: 6px 16px;
  border-radius: 999px;
  font-size: clamp(11px, 3.2vw, 13px);
  font-weight: 600;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
#build-hint.warn { color: var(--warn); border-color: var(--warn); background: var(--warn-soft); }
#build-hint.info { color: var(--text-dim); border-color: var(--border); background: var(--surface); }

/* ==================== TUTORIAL ==================== */
/* A row at the TOP of the bottom stack, not a free-floating overlay with a
   magic 100px bottom padding. The old offset was a guess at the toolbar's
   height; in the stack the card simply cannot reach the controls it is
   describing, whatever the toolbar reflows to. Only the card itself takes
   pointer events — the player must be able to keep dragging beside it. */
/* The tutorial is the ONLY row in the stack allowed to shrink — see the flex
   weights just below. Everything else would clip a control if it gave way. */
#tutorial {
  display: flex;
  justify-content: center;
  flex: 0 1 auto;
  min-height: 0;
  overflow: hidden;
  pointer-events: none;
}
#build-hint, #toolbar, #sim-controls { flex: 0 0 auto; }
.tut-card {
  width: min(420px, 92vw);
  /* Caps against the stack, and the stack caps against the viewport, so on a
     short screen the card gives up height rather than pushing the toolbar off.
     The card is a flex COLUMN and only #tut-text scrolls: capping the card as a
     whole and letting it scroll (which was tried) cuts SKIP and GOT IT off the
     bottom, and a tutorial you cannot dismiss is worse than one you cannot
     finish reading. */
  max-height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: 14px 16px;
  box-shadow: var(--shadow);
  animation: tut-in 0.3s ease;
}
.tut-step { display: block; font-size: 10px; letter-spacing: 0.12em; color: var(--accent); font-weight: 700; margin-bottom: 4px; flex: 0 0 auto; }
#tut-title { font-size: clamp(15px, 4vw, 18px); margin-bottom: 4px; flex: 0 0 auto; }
#tut-text {
  font-size: clamp(12px, 3.4vw, 14px);
  color: var(--text-dim);
  line-height: 1.4;
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;                 /* the copy scrolls; the buttons never move */
  -webkit-overflow-scrolling: touch;
}
.tut-foot { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex: 0 0 auto; }
.tut-dots { display: flex; gap: 5px; flex: 1 1 auto; }
.tut-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--border-strong); display: block; }
.tut-dots span.on { background: var(--accent); }
@keyframes tut-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }

/* ==================== RESPONSIVE ==================== */

/* ---- TOOLBAR: ONE ROW ----
   Two very different viewports want the same shape. A tablet/desktop has the
   width to spare, and a LANDSCAPE PHONE has no height to spare — three stacked
   rows would eat half of a 390px-tall screen. So: materials | tools | RELEASE
   on a single line.

   The flex weights are the guarantee the old layout never had. #material-strip
   is the only flexible item (flex:1 1 auto, min-width:0); the tool cluster and
   the release button are flex:0 0 auto. Squeeze the viewport and the MATERIALS
   give up space and scroll — the tools and the release button cannot be shrunk,
   cannot be pushed off the edge, and cannot end up underneath each other.

   The 560px floor on the short-viewport arm matters: without it a hypothetical
   320×500 window would take the one-row branch, where tools + release are
   wider than the screen. Every real landscape phone clears 560px. */
@media (min-width: 640px), (max-height: 520px) and (min-width: 560px) {
  #toolbar { flex-direction: row; align-items: flex-end; gap: 10px; }
  /* 0 1 auto, not 1 1 auto: the strip may SHRINK (that is the whole point — it
     is the designated loser when space runs out) but must not GROW, or on a
     wide desktop it stretches and shoves the tool cluster halfway across the
     screen, away from the materials it belongs beside. */
  #material-strip { flex: 0 1 auto; }
  #btn-release { width: auto; flex: 0 0 auto; margin-left: auto; }
}

/* ---- SHORT VIEWPORT (landscape phones) ----
   Every vertical pixel here is one the player cannot see the valley through, so
   this block is a height budget: fold the material head-rating line onto the
   cost line, tighten paddings, pull the toast and the menu button up under a
   shorter top bar. Touch targets stay at 44px — that minimum is never the thing
   that gets sacrificed. */
@media (max-height: 520px) {
  /* The corner cluster tightens with everything else, and the column the HUD
     reserves narrows with it on its own. THE PLATES THEMSELVES DO NOT SHRINK.
     Every other game in the hub takes its corner down to 38px sideways; here
     the 44px target is the one thing this block has never been allowed to
     spend (tests/ui-layout.mjs enforces it), so the strip gives up its padding
     and its distance from the edge instead. */
  :root {
    --corner-inset: 6px;
    --corner-w: 146px;   /* 3 + 3 × 44 + 2 × 4 gap + 3 */
    --corner-h: 50px;    /* 3 + 44 + 3 */
  }
  #corner-tools { padding: 3px; gap: 4px; }

  /* The shorthand would reset the corner's reservation, so it is restated in
     the same breath — the row must never run under the cluster, sideways least
     of all, where there is the least height to lose to a second line. */
  #hud-top {
    padding: 6px clamp(8px, 2.5vw, 14px);
    padding-right: calc(var(--hud-menu-gap) + env(safe-area-inset-right, 0px));
  }
  #hud-toast { top: calc(env(safe-area-inset-top, 0px) + 46px); }
  #objective-meter, #budget-meter { display: none; }  /* the numbers carry it */

  #hud-bottom { gap: 6px; padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); }
  #toolbar { gap: 8px; }
  #material-strip { padding: 3px 2px; }
  /* THE FOLD: the material button stops being a four-line column and becomes a
     2×2 grid — swatch beside the name, price beside the head rating. Measured on
     a 844×303 viewport: 68px tall unfolded, 52px folded, and the 44px touch
     minimum still governs the rest. It is done here in CSS, with the four spans
     left as direct children of the button, precisely so the markup hud.js emits
     does not have to change shape with the viewport. */
  .mat-btn {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas: "sw name" "cost head";
    align-items: center;
    justify-items: center;
    column-gap: 5px;
    row-gap: 1px;
    padding: 4px 8px;
  }
  .mat-swatch { grid-area: sw; width: 13px; height: 13px; }
  .mat-name { grid-area: name; font-size: 10px; letter-spacing: 0; }
  .mat-cost { grid-area: cost; font-size: 9px; }
  .mat-head { grid-area: head; font-size: 9px; }
  #btn-release { font-size: 14px; padding: 11px 18px; }

  .tut-card { padding: 10px 12px; }
  .tut-foot { margin-top: 8px; }
  .screen { gap: 8px; padding-top: calc(10px + env(safe-area-inset-top, 0px)); padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }
  .end-card { padding: 12px 16px; }
  .end-stats { margin: 7px 0; padding-top: 7px; gap: 4px 14px; }
  #result-heading { font-size: clamp(18px, 4.4vw, 22px); }
  #level-grid { gap: 8px; }
  .level-card { min-height: 52px; padding: 8px 12px; }
  /* The faders keep their 44px rows — that minimum is never what gets
     sacrificed — so the height comes back out of the gaps around them. */
  .vol-rows { margin: 10px 0 12px; padding-top: 10px; gap: 4px; }
  .sound-note { margin-bottom: 12px; }
}

/* ---- RESULT CARD ON A SHORT, WIDE VIEWPORT (landscape phone) ----
   The card is a tall single column: heading, cause, note, SEVEN stat rows,
   four buttons. On a 303px-tall viewport that is 427px of card, and while the
   screen scrolls (so nothing is unreachable) RETRY — the thing the player wants
   — starts below the fold, which is a bad enough failure on a result screen to
   be worth its own layout. Widen the card and pair the stats up two-across:
   seven rows become four, and the whole card lands inside the fold. */
@media (max-height: 560px) and (min-width: 560px) {
  .end-card { width: min(700px, 94vw); max-width: 94vw; }
  .end-stats { grid-template-columns: 1fr auto 1fr auto; }
  /* A card this wide and this tall reaches up under the corner's three plates
     on a 390px-tall phone (the win card's top edge landed at 48px, the strip
     ends at 58). So the two card screens keep the corner's column clear on
     the right, exactly as #hud-top does, and the card centres in what is
     left. The title is left centred: its column is narrow and never gets
     there. */
  #screen-result, #screen-pause {
    padding-right: calc(var(--hud-menu-gap) + env(safe-area-inset-right, 0px));
  }
  #screen-result .end-card, #screen-pause .end-card { max-width: 100%; }
  .result-btns { gap: 8px; }

  /* ---- AND THE MENU, SIDEWAYS ----
     Same trick, same reason, and the shape maxgear arrived at: a phone on its
     side has width exactly where it has no height, so the card becomes a
     two-column grid laid over the existing markup BY GRID AREA. Nothing moves
     in index.html, portrait is untouched, and a tablet — taller than 560px
     whichever way up — keeps the stack. The heading, the doors back in and the
     speaker note go down the left; the faders, the language and the way out
     down the right, so the way out is still last — bottom right — and a
     390px-tall phone never has to scroll a menu to find it. */
  .pause-card {
    width: min(680px, 94vw);
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    grid-template-areas:
      "head rows"
      "acts rows"
      "note quit";
    column-gap: 22px;
    align-items: start;
  }
  .pause-card .screen-head { grid-area: head; text-align: left; margin: 0 0 6px; }
  .pause-card .menu-col { grid-area: acts; margin-top: 0; }
  .pause-card .vol-rows { grid-area: rows; margin: 0; padding-top: 0; border-top: 0; }
  .pause-card .sound-note { grid-area: note; margin: 10px 0 0; align-self: end; }
  .pause-card > .quit-btn { grid-area: quit; margin-top: 8px; align-self: end; }
}

/* ---- VERY NARROW (≤ 379px) ----
   6 tool buttons at 44px need 294px with 6px gaps; a 320px viewport has 300px
   inside the stack's padding. Tighten the gaps and the padding so the cluster
   still fits on ONE row with room to spare, rather than scrolling. */
@media (max-width: 379px) {
  #hud-bottom { padding-inline: 8px; }
  #tool-group { gap: 4px; }
  .tool-sep { margin-inline: 0; }
  /* The label gutter gives up twelve pixels here so the fader itself stays
     long enough to aim at. EFFEKTER and LANGUAGE, the longest in either
     language, still fit on one line at 72px. */
  .vol-label { flex-basis: 72px; letter-spacing: 0.1em; }
  .vol-row { gap: 8px; }
}

@media (min-width: 720px) {
  #level-grid { max-height: 60vh; }
  .big-btn { padding: 16px 40px; }
  .end-card { padding: 32px 40px; }
}

@media (hover: hover) {
  .lang-btn:hover { border-color: var(--accent); color: var(--text); }
  .lang-btn[aria-pressed="true"]:hover { color: var(--accent); }
  .big-btn:hover, .small-btn:hover, .tool-btn:hover, .speed-btn:hover, .mat-btn:hover {
    background: var(--surface-3);
    border-color: var(--accent);
  }
  .big-btn.release:hover { background: var(--release-bg-hot); }
  .primary-btn:hover { background: var(--accent); filter: brightness(1.1); }
  .ghost-btn:hover { background: var(--surface-2); color: var(--text); }
  .level-card:not([aria-disabled="true"]):hover { border-color: var(--accent); transform: translateY(-1px); }
  .level-card[aria-disabled="true"]:hover { border-color: var(--border-strong); transform: none; }
}

/* ==================== THE OPENING STING ====================
   src/intro.js's canvas, which the script creates and removes. Above
   everything, the inline splash (z-index 100) and the corner plates
   included, because it is a curtain over the whole page for its three
   seconds; a tap anywhere on it skips, and touch-action keeps that tap from
   becoming a scroll or a zoom. Reduced motion never creates it at all. */
#intro {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 110;
  touch-action: none;
  cursor: pointer;
}

/* ==================== REDUCED MOTION ==================== */
@media (prefers-reduced-motion: reduce) {
  .pulse, .big-btn.release, #hud-timer.urgent, .toast, #splash .sp-water span, .update-btn {
    animation: none !important;
  }
  /* The diorama itself is honest motion (it is the game running), so it keeps
     playing — but the splash shimmer and every UI pulse stop. */
  .toast { opacity: 1; transform: none; }
  .tut-card { animation: none; }
  * { scroll-behavior: auto !important; }
}
