/* ============================================================
   THEME FRAMEWORK
   ------------------------------------------------------------
   The whole app is styled through CSS variables. A theme is just
   a set of values for those variables, selected by the data-theme
   attribute on <body> (set in index.html, and swappable at runtime
   later so the look can advance with the hunt's "acts").

   To add a theme (e.g. Act II / Act III): copy the
   [data-theme="training"] block, rename it, and change the values.
   The structural CSS below never needs to change — that's the
   "same shapes, recolored" idea made literal.
   ============================================================ */

:root {
  --maxw: 480px;
  --radius: 12px;
  --radius-lg: 16px;
}

/* ---------- Act I: Agent Training Portal (light, clean, cyan) ---------- */
[data-theme="training"] {
  --font-display: "Trebuchet MS", "Segoe UI", system-ui, sans-serif;
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --bg: #e6f2f5;
  --bg-gradient: linear-gradient(170deg, #edf7f9 0%, #dcecf0 100%);
  --surface: #ffffff;
  --surface-2: #f1f9fb;
  --border: #cde4e9;
  --border-strong: #23c3d1;

  --text: #0e2f38;
  --text-muted: #5c7c84;

  --accent: #16b8c6;
  --accent-strong: #0e97a4;
  --accent-soft: #daf3f6;
  --accent-contrast: #ffffff;

  --danger: #c0433b;
  --danger-soft: #fbecea;

  --warn-bg: #fff8ea;
  --warn-border: #f0d98c;
  --warn-text: #8a6d00;

  --success: #1a6b5c;

  --header-bg: rgba(255, 255, 255, 0.92);
  --toast-bg: #0e2f38;
  --toast-text: #ffffff;

  --shadow: 0 2px 12px rgba(14, 47, 56, 0.08);
  --shadow-card: 0 1px 3px rgba(14, 47, 56, 0.06);

  --btn-transform: uppercase;
  --btn-spacing: 0.06em;

  /* Dossier / briefing page. The landing screen is dressed as a
     declassified file; these are the only extra colors that needs, so an
     Act II/III theme can restyle the whole thing by overriding them. */
  --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  --stamp: #c0433b;
  --redact: #16323a;
  --paper: #fbfdfe;
  --tape: #f2c14e;

  /* Strands solved-word palette. Chosen to stay distinguishable from each
     other at tile size (and reasonably so for common color-vision
     deficiencies — pair color with position, never rely on hue alone to
     convey something essential). */
  --sc-green: #2e9e5b;
  --sc-gold: #e8c14a;
  --sc-blue: #2b7fd4;
  --sc-purple: #7a4fd0;
  --sc-orange: #e07a2b;
  --sc-pink: #d64d95;
  --sc-red: #cc4436;
  --sc-teal: #12a3ab;
  --sc-gray: #6b7c82;
}

/* ============================================================
   STRUCTURAL / COMPONENT STYLES  (theme-agnostic — all colors
   come from the variables above)
   ============================================================ */

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  background-image: var(--bg-gradient);
  background-attachment: fixed;
  color: var(--text);
}

.app { max-width: var(--maxw); margin: 0 auto; padding: 0 16px 80px; }
.screen.hidden, .hidden { display: none !important; }

h1 { font-size: 1.4rem; margin: 20px 0 16px; font-family: var(--font-display); letter-spacing: 0.02em; }
h2 { font-size: 1.2rem; margin: 0 0 8px; font-family: var(--font-display); }
h3 { font-size: 0.95rem; margin: 0 0 8px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }

/* ---------- brand bar ---------- */
.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 4px 14px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--header-bg);
  backdrop-filter: blur(6px);
  z-index: 10;
}
.brand-mark {
  color: var(--accent);
  width: 40px; height: 40px;
  flex: 0 0 40px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft);
  border-radius: 10px;
}
.brand-mark svg { width: 30px; height: 30px; display: block; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; letter-spacing: 0.12em; color: var(--text); }
.brand-sub { font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-muted); }

/* ---------- briefing / dossier (landing page) ----------
   Dressed as a declassified file. Everything here is driven by the same
   theme variables as the rest of the app, so a new act recolors it
   without touching this block. */
.dossier {
  position: relative;
  background: var(--paper);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 32px 20px 20px;
  margin: 14px 0 24px;
  overflow: hidden;
}
/* Faint horizontal banding — reads as a photocopied page rather than a
   clean web card. Kept very low contrast so body text stays crisp. */
.dossier::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(14, 47, 56, 0.013) 0 2px,
    transparent 2px 6px
  );
}
.dossier > * { position: relative; }

/* Strip of tape holding the page down. Hangs off the top edge and gets
   clipped by the card's overflow, so it reads as tape rather than a
   floating rectangle. */
.dossier-tape {
  position: absolute;
  top: -9px;
  left: 50%;
  width: 108px;
  height: 22px;
  margin-left: -54px;
  background: var(--tape);
  opacity: 0.45;
  transform: rotate(-2.2deg);
  border-radius: 1px;
}

.classification {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 16px;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--stamp);
}
.classification-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--stamp);
  flex: 0 0 7px;
  animation: pulse-dot 2.4s ease-in-out infinite;
}
@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

.dossier-meta {
  margin: 0 0 20px;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.5;
}
.dossier-meta > div { display: flex; gap: 10px; }
.dossier-meta > div + div { margin-top: 3px; }
.dossier-meta dt {
  flex: 0 0 76px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
/* flex:1 gives dd a definite width for the redaction bar's max-width to
   resolve against; min-width:0 lets it shrink below its content instead of
   pushing the row wider than the card. */
.dossier-meta dd { flex: 1 1 auto; min-width: 0; margin: 0; color: var(--text); font-weight: 600; }

.redacted {
  display: inline-block;
  /* Capped, so it can't punch through the card edge on a narrow screen. */
  width: 104px;
  max-width: 100%;
  height: 0.82em;
  vertical-align: -0.1em;
  background: var(--redact);
  border-radius: 1px;
}

.dossier-eyebrow {
  margin: 0 0 2px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.dossier-title {
  margin: 0 0 16px;
  font-family: var(--font-display);
  font-size: 1.5rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--text);
}
/* The initials that spell the acronym. */
.dossier-title b {
  color: var(--accent-strong);
  font-weight: 700;
}

.dossier-body p {
  margin: 0 0 13px;
  font-size: 0.93rem;
  line-height: 1.62;
  color: var(--text);
}

.mission {
  margin: 18px 0;
  padding: 14px 16px;
  background: var(--accent-soft);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
}
.mission h2 {
  margin: 0 0 7px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--accent-strong);
}
.mission p {
  margin: 0;
  font-size: 0.93rem;
  line-height: 1.55;
  color: var(--text);
}
.mission-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.mission-tags li {
  padding: 4px 10px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--accent-strong);
}

.stamp {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: fit-content;
  margin: 4px 6px 6px auto;
  padding: 7px 16px;
  border: 3px double var(--stamp);
  border-radius: 6px;
  color: var(--stamp);
  font-family: var(--font-display);
  text-transform: uppercase;
  transform: rotate(-7deg);
  opacity: 0.72;
}
.stamp-main { font-size: 1.05rem; font-weight: 700; letter-spacing: 0.13em; line-height: 1.1; }
.stamp-sub  { font-size: 0.58rem; font-weight: 600; letter-spacing: 0.22em; }

.dossier-rule {
  height: 0;
  margin: 16px 0;
  border: 0;
  border-top: 1px dashed var(--border);
}
.dossier-cta-note {
  margin: 0 0 12px;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-muted);
}

/* ---------- registration form (same dossier shell) ---------- */
/* No tape on this one, so it doesn't need the extra top padding. */
.dossier--form { padding-top: 24px; }

.form-title {
  font-size: 1.32rem;
  margin-bottom: 6px;
}
.form-lede {
  margin: 0 0 22px;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--text-muted);
}

/* Overrides the global `label` block — these are form rows, not captions. */
.field { display: block; margin: 0 0 18px; }
.field-label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
}
/* Field number, as on a real intake form. */
.field-num {
  flex: 0 0 auto;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-style: normal;
  font-size: 0.64rem;
  letter-spacing: 0.06em;
}
.field-hint {
  display: block;
  margin-top: 6px;
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--text-muted);
}

/* Typed-in-the-field look. Scoped so puzzle inputs elsewhere are untouched. */
#join-screen input[type="text"],
#join-screen select {
  margin-top: 0;
  padding: 13px 12px;
  font-family: var(--font-mono);
  font-size: 0.95rem;
  background: var(--surface);
  border-color: var(--border-strong);
}
#join-screen input[type="text"]::placeholder {
  color: var(--text-muted);
  opacity: 0.62;
}

#join-screen .cta { margin-top: 22px; }
#join-screen .error { margin: 10px 0 0; text-align: center; }

/* ---------- briefing room (phase 1 → phase 2 gate) ---------- */
/* Same dossier shell, but flagged: the red rule across the top is the one
   visual cue that separates "here is your file" from "stakes just went up".
   Act II will take this much further; this keeps it inside Act I's palette. */
.briefing-room { margin-bottom: 24px; }
.dossier--room {
  padding-top: 24px;
  border-top: 3px solid var(--stamp);
  border-radius: var(--radius-lg);
}
.dossier--room .dossier-title { margin-bottom: 14px; }
.dossier--room .dossier-meta { margin-bottom: 20px; }
.dossier--room strong { font-weight: 700; }
/* Wide enough for "Class rank" but no wider — the values here are long
   enough that every extra pixel of label column wraps them. */
.dossier--room .dossier-meta dt { flex-basis: 88px; }

/* Stakes callout. Reuses the theme's existing warn-* variables, so it
   stays legible in any act without new colors. */
.alert {
  display: block;
  margin: 18px 0;
  padding: 14px 16px;
  background: var(--warn-bg);
  border: 1px solid var(--warn-border);
  border-left: 4px solid var(--warn-border);
  border-radius: var(--radius);
}
.alert h3 {
  margin: 0 0 7px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--warn-text);
}
.alert p {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text);
}

/* Terminal-style button: [ INITIATE FINAL ASSESSMENT ]
   Slightly smaller than a normal CTA so the whole command fits on one
   line at 375px — wrapping strands the closing bracket on its own row. */
.cta--terminal {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
}
.cta--terminal::before { content: "[ "; }
.cta--terminal::after  { content: " ]"; }

/* Staggered reveal — the file "assembling" as it opens. Purely additive:
   with reduced motion the same content just appears.

   Note the fill-mode `both` rather than a standalone `opacity: 0` plus
   `forwards`. Both hide the element during its delay, but a hard
   `opacity: 0` is a declaration that outlives the animation: if the
   animation never runs, the page stays permanently blank. With `both`
   the hidden state lives only inside the keyframe, so anything that
   stops the animation from running leaves the content visible instead.

   Scoped to the briefing: the registration form shares the .dossier shell
   but not this reveal. Someone who just tapped "Begin Training" wants to
   start typing, not watch a form assemble itself. */
@media (prefers-reduced-motion: no-preference) {
  #briefing-screen .dossier > *:not(.dossier-tape) {
    animation: dossier-in 0.44s ease-out both;
  }
  #briefing-screen .dossier > .classification   { animation-delay: 0.05s; }
  #briefing-screen .dossier > .dossier-meta     { animation-delay: 0.14s; }
  #briefing-screen .dossier > .dossier-eyebrow  { animation-delay: 0.22s; }
  #briefing-screen .dossier > .dossier-title    { animation-delay: 0.26s; }
  #briefing-screen .dossier > .dossier-body     { animation-delay: 0.34s; }
  #briefing-screen .dossier > .mission          { animation-delay: 0.42s; }
  /* Sits below the mission block, so it must not arrive before it. */
  #briefing-screen .dossier > .dossier-closing  { animation-delay: 0.5s; }
  #briefing-screen .dossier > .stamp            { animation: stamp-in 0.3s cubic-bezier(0.2, 1.5, 0.4, 1) 0.72s both; }
  #briefing-screen .dossier > .dossier-rule,
  #briefing-screen .dossier > .dossier-cta-note,
  #briefing-screen .dossier > .cta              { animation-delay: 0.5s; }
  .dossier-tape {
    animation: dossier-in 0.4s ease-out both;
  }
  /* The form gets one quick fade, no stagger. */
  .dossier--form { animation: dossier-in 0.3s ease-out both; }
}
@keyframes dossier-in {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
/* Lands harder than the rest — it's a stamp. Ends at the same rotation
   and opacity the static rule sets. */
@keyframes stamp-in {
  from { opacity: 0; transform: rotate(-7deg) scale(1.7); }
  to   { opacity: 0.72; transform: rotate(-7deg) scale(1); }
}

/* ---------- forms ---------- */
label {
  display: block;
  margin-bottom: 16px;
  font-size: 0.9rem;
  color: var(--text-muted);
}
input[type="text"], select {
  display: block;
  width: 100%;
  padding: 12px;
  margin-top: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 1rem;
  background: var(--surface);
  color: var(--text);
}
input[type="text"]:focus, select:focus {
  outline: none;
  border-color: var(--border-strong);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Negative top margin tucks this under the field it annotates on the
   join screen — but inside a puzzle it pulled the text up under the
   button above it, so reset it there. */
.hint { color: var(--text-muted); font-size: 0.8rem; margin: -8px 0 16px; }
#modal-body .hint { margin: 12px 0 4px; }

button {
  width: 100%;
  padding: 14px;
  background: var(--accent);
  color: var(--accent-contrast);
  border: none;
  border-radius: var(--radius);
  font-size: 1rem;
  font-weight: 700;
  font-family: var(--font-display);
  letter-spacing: var(--btn-spacing);
  text-transform: var(--btn-transform);
  cursor: pointer;
  transition: filter 0.12s ease;
}
button:hover { filter: brightness(1.05); }
button:active { filter: brightness(0.94); }
button:disabled { opacity: 0.55; cursor: default; filter: none; }

.error { color: var(--danger); min-height: 1.2em; font-size: 0.9rem; }

/* ---------- hunt header ---------- */
.hunt-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 18px 0 20px;
  font-weight: 600;
}
#team-label { font-family: var(--font-display); letter-spacing: 0.04em; text-transform: capitalize; }
.leave-btn {
  width: auto;
  padding: 6px 12px;
  background: transparent;
  color: var(--danger);
  border: 1px solid var(--danger);
  font-size: 0.72rem;
  font-weight: 600;
}

/* ---------- fragment tray ---------- */
.fragment-tray {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 20px;
}
.fragment-list { display: flex; gap: 8px; flex-wrap: wrap; }
.fragment-chip {
  background: var(--accent);
  color: var(--accent-contrast);
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* ---------- puzzle cards ---------- */
.puzzle-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }
.puzzle-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
  padding: 16px;
  cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.puzzle-card:active { transform: scale(0.99); }
.puzzle-card h3 { margin-bottom: 4px; color: var(--text); text-transform: none; letter-spacing: 0; font-size: 1rem; font-family: var(--font-display); }
.puzzle-card p { margin: 0; color: var(--text-muted); font-size: 0.9rem; }
.puzzle-card.done { opacity: 0.5; pointer-events: none; }
.puzzle-card.pending {
  background: var(--warn-bg);
  border-color: var(--warn-border);
  border-left-color: var(--warn-border);
}
.puzzle-card.pending p { color: var(--warn-text); font-weight: 600; }

.pending-notice {
  background: var(--warn-bg);
  border: 1px solid var(--warn-border);
  color: var(--warn-text);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 0.85rem;
  margin-bottom: 12px;
}

/* ---------- major puzzle (priority panel) ---------- */
.major-puzzle {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow);
}
.major-puzzle h2 { color: var(--accent-strong); }
.major-puzzle input { margin-bottom: 12px; }

.finale { text-align: center; padding: 40px 0; }

/* ---------- toast ---------- */
.toast {
  position: fixed;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 32px);
  background: var(--toast-bg);
  color: var(--toast-text);
  padding: 12px 16px;
  border-radius: var(--radius);
  font-size: 0.9rem;
  text-align: center;
  z-index: 1000;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
}

/* ---------- modal ---------- */
.modal {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex; align-items: flex-end;
}
.modal-content {
  background: var(--surface);
  color: var(--text);
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  max-height: 85vh;
  overflow-y: auto;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 24px;
  position: relative;
}
.modal-content h2 { font-family: var(--font-display); }
.modal-close {
  position: fixed; top: 16px; right: 16px;
  width: 40px; height: 40px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.55); color: white;
  border-radius: 50%;
  font-size: 1.4rem; line-height: 1;
  z-index: 1100;
}
#modal-body input[type="file"] { margin-bottom: 12px; }
#modal-body video { max-height: 45vh; border-radius: 8px; }

/* ---------- strands (word-find) puzzle ---------- */
.strands-theme {
  font-family: var(--font-display);
  font-weight: 700;
  text-align: center;
  font-size: 1.05rem;
  letter-spacing: 0.04em;
  margin: 4px 0 2px;
  color: var(--accent-strong);
}
.strands-progress {
  text-align: center;
  font-size: 0.8rem;
  color: var(--text-muted);
  margin: 0 0 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.strands-board {
  display: grid;
  grid-template-columns: repeat(var(--cols, 6), 1fr);
  gap: 6px;
  /* Tracing must not scroll the sheet underneath. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  margin: 0 auto 12px;
  max-width: 340px;
  /* Positions the trace ribbon, and makes cell offsetLeft/offsetTop
     relative to the board so the ribbon can measure them. */
  position: relative;
}

/* The ribbon threading through the letters — the thing that makes a
   traced word read as traced rather than as a set of lit-up tiles.
   Sits *behind* the circles (z-index 0 against the cells' 1) so it only
   shows in the gaps between them, which is why the grid gap above is 6px
   rather than hairline. Geometry is measured from the live cell boxes,
   not derived from row/col maths, because the gap means evenly divided
   coordinates would drift off-centre. */
.strands-trace {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  overflow: visible;
}
.strands-trace-line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Same palette as the solved tiles, so a word's ribbon and its circles
   always agree. */
.strands-trace-line[data-color="active"] { stroke: var(--accent); }
.strands-trace-line[data-color="green"]  { stroke: var(--sc-green); }
.strands-trace-line[data-color="gold"]   { stroke: var(--sc-gold); }
.strands-trace-line[data-color="blue"]   { stroke: var(--sc-blue); }
.strands-trace-line[data-color="purple"] { stroke: var(--sc-purple); }
.strands-trace-line[data-color="orange"] { stroke: var(--sc-orange); }
.strands-trace-line[data-color="pink"]   { stroke: var(--sc-pink); }
.strands-trace-line[data-color="red"]    { stroke: var(--sc-red); }
.strands-trace-line[data-color="teal"]   { stroke: var(--sc-teal); }
.strands-trace-line[data-color="gray"]   { stroke: var(--sc-gray); }
.strands-cell {
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  /* Above the trace ribbon, so the ribbon reads as passing behind. */
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 50%;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
  transition: background 0.1s ease, color 0.1s ease, transform 0.1s ease;
}
.strands-cell.selected {
  background: var(--accent);
  color: var(--accent-contrast);
  border-color: var(--accent);
  transform: scale(1.06);
}
/* Solved words. Color is authored per word (data-color) so it can carry
   meaning beyond "solved" — e.g. a second puzzle hidden in the grouping.
   Solid fills on purpose: the earlier pale tint read too close to the
   unsolved tiles. Default is green; the spangram defaults to gold. */
.strands-cell.solved {
  background: var(--sc-green);
  color: #ffffff;
  border-color: transparent;
  font-weight: 700;
}
.strands-cell.solved[data-color="green"]  { background: var(--sc-green);  color: #fff; }
.strands-cell.solved[data-color="gold"]   { background: var(--sc-gold);   color: #4a3a00; }
.strands-cell.solved[data-color="blue"]   { background: var(--sc-blue);   color: #fff; }
.strands-cell.solved[data-color="purple"] { background: var(--sc-purple); color: #fff; }
.strands-cell.solved[data-color="orange"] { background: var(--sc-orange); color: #fff; }
.strands-cell.solved[data-color="pink"]   { background: var(--sc-pink);   color: #fff; }
.strands-cell.solved[data-color="red"]    { background: var(--sc-red);    color: #fff; }
.strands-cell.solved[data-color="teal"]   { background: var(--sc-teal);   color: #fff; }
.strands-cell.solved[data-color="gray"]   { background: var(--sc-gray);   color: #fff; }

.strands-status { min-height: 1.3em; text-align: center; font-size: 0.9rem; margin: 0 0 10px; color: var(--text-muted); }
.strands-status.good { color: var(--success); font-weight: 600; }
.strands-status.bad { color: var(--danger); }

/* ---------- connections puzzle ---------- */
.conn-solved { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.conn-group {
  border-radius: var(--radius);
  padding: 10px 12px;
  text-align: center;
  color: #fff;
  background: var(--sc-gray);
}
.conn-group strong { display: block; font-size: 0.8rem; letter-spacing: 0.06em; text-transform: uppercase; }
.conn-group span { font-size: 0.85rem; opacity: 0.95; }
.conn-group[data-color="yellow"] { background: var(--sc-gold); color: #4a3a00; }
.conn-group[data-color="green"]  { background: var(--sc-green); }
.conn-group[data-color="blue"]   { background: var(--sc-blue); }
.conn-group[data-color="purple"] { background: var(--sc-purple); }
.conn-group[data-color="red"]    { background: var(--sc-red); }
.conn-group[data-color="orange"] { background: var(--sc-orange); }
.conn-group[data-color="teal"]   { background: var(--sc-teal); }
.conn-group[data-color="pink"]   { background: var(--sc-pink); }

.conn-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 12px; }
.conn-tile {
  width: 100%;
  min-height: 58px;
  padding: 4px 2px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-transform: none;
  word-break: break-word;
  line-height: 1.15;
}
.conn-tile.selected { background: var(--text); color: var(--surface); border-color: var(--text); }

/* ---------- crossword ---------- */
.xw-board {
  display: grid;
  grid-template-columns: repeat(var(--cols, 5), 1fr);
  gap: 2px;
  /* Never wider than the sheet, whatever the grid size. */
  max-width: min(340px, 100%);
  width: 100%;
  /* Explicit block layout + clear separation: the grid was crowding the
     status line beneath it on smaller screens. */
  margin: 0 auto 16px;
  clear: both;
}
/* Bigger grids get smaller type so 7+ columns stay legible on a phone. */
.xw-board { font-size: 0.95rem; }
input[type="text"].xw-input { font-size: min(0.95rem, calc(240px / var(--cols, 5) * 0.42)); }
.xw-num { font-size: min(0.5rem, calc(240px / var(--cols, 5) * 0.22)); }
.strands-status { clear: both; }
.xw-cell {
  position: relative;
  aspect-ratio: 1;
  background: var(--surface);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
}
.xw-cell.block { background: var(--text); border-color: var(--text); }
/* Distinct tint per direction — the label alone wasn't enough to tell
   Across from Down at a glance while solving. */
.xw-board[data-dir="across"] .xw-cell.active { background: #d8eef7; }
.xw-board[data-dir="down"]   .xw-cell.active { background: #f6e6c8; }
.xw-board[data-dir="across"] .xw-cell.cursor { background: #2b7fd4; }
.xw-board[data-dir="down"]   .xw-cell.cursor { background: #d99a1f; }
.xw-cell.cursor .xw-input { color: #fff; }
.xw-cell.cursor .xw-num { color: #fff; }
.xw-num {
  position: absolute; top: 1px; left: 2px;
  font-size: 0.5rem; line-height: 1; color: var(--text-muted);
  pointer-events: none; z-index: 1;
}
/* A real input per square so the mobile keyboard works natively.
   Selector must out-specify the global `input[type="text"]` rule (which
   is specificity 0,1,1) — otherwise its 12px padding inflates every
   square and wide grids overflow off-screen. */
input[type="text"].xw-input {
  display: block;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  border: none; border-radius: 0;
  background: transparent;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--text);
  text-transform: uppercase;
  caret-color: transparent;
  min-width: 0;
}
input[type="text"].xw-input:focus { outline: none; box-shadow: none; }

.xw-cluebar {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  margin-bottom: 10px;
  font-size: 0.85rem;
  min-height: 40px;
}
.xw-cluebar strong { color: var(--accent-strong); white-space: nowrap; font-size: 0.78rem; }
.xw-cluebar span { flex: 1; }
.xw-flip {
  width: auto; flex: 0 0 auto;
  padding: 4px 10px;
  background: transparent;
  color: var(--accent-strong);
  border: 1px solid var(--border-strong);
  font-size: 0.9rem;
}

.xw-clues { margin-bottom: 8px; }
.xw-clues h3 { margin: 10px 0 4px; }
.xw-clue {
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--text);
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 8px 4px;
  font-size: 0.85rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.xw-clue strong { color: var(--accent-strong); margin-right: 4px; }
.xw-clue em { color: var(--text-muted); font-size: 0.78rem; }

/* ---------- sudoku ---------- */
.sd-board {
  display: grid;
  grid-template-columns: repeat(var(--n, 9), 1fr);
  gap: 0;
  max-width: min(340px, 100%);
  width: 100%;
  margin: 0 auto 14px;
  border: 2px solid var(--text);
  border-radius: 6px;
  overflow: hidden;
}
button.sd-cell {
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  color: var(--accent-strong);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: min(1rem, calc(300px / var(--n, 9) * 0.5));
  letter-spacing: 0;
  text-transform: none;
}
/* Givens read as fixed; entered digits are the accent colour. */
button.sd-cell.given { color: var(--text); background: var(--surface-2); }
button.sd-cell.box-left { border-left: 2px solid var(--text); }
button.sd-cell.box-top { border-top: 2px solid var(--text); }
button.sd-cell.peer { background: var(--accent-soft); }
button.sd-cell.selected { background: var(--accent); color: var(--accent-contrast); }
button.sd-cell.conflict { color: var(--danger); background: var(--danger-soft); }
button.sd-cell.selected.conflict { background: var(--danger); color: #fff; }

.sd-pad {
  display: grid;
  grid-template-columns: repeat(var(--n, 9), 1fr);
  gap: 4px;
  margin-bottom: 12px;
}
button.sd-key {
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: var(--font-display);
  font-size: min(1rem, calc(320px / var(--n, 9) * 0.5));
  font-weight: 700;
}
button.sd-key.erase {
  color: var(--danger);
  border-color: var(--danger);
  /* The digit keys fill --n columns exactly, so erase always wrapped onto
     a row by itself and read as a layout accident. Span it instead. */
  grid-column: 1 / -1;
  aspect-ratio: auto;
  padding: 8px 0;
}

/* ---------- kenken ---------- */
/* Same cell/pad interaction as Sudoku; the difference is that the cage
   outlines carry the clue structure, so they're the heaviest line on the
   board and the thin grid recedes behind them. */
.kk-board {
  display: grid;
  grid-template-columns: repeat(var(--n, 4), 1fr);
  gap: 0;
  width: 100%;
  max-width: min(340px, 100%);
  margin: 0 auto 14px;
  background: var(--surface);
  border-radius: 4px;
  overflow: hidden;
  touch-action: manipulation;
}
.kk-cell {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 0;
  font-family: var(--font-display);
  font-weight: 700;
  /* Scales with the grid so a 6x6 doesn't overflow its digits. */
  font-size: min(1.5rem, calc(320px / var(--n, 4) * 0.46));
  cursor: pointer;
}
/* Cage boundaries. Drawn per-edge from JS, which compares each cell's
   cage with its neighbour's. */
.kk-cell.cage-t { border-top-color: var(--text); border-top-width: 2.5px; }
.kk-cell.cage-b { border-bottom-color: var(--text); border-bottom-width: 2.5px; }
.kk-cell.cage-l { border-left-color: var(--text); border-left-width: 2.5px; }
.kk-cell.cage-r { border-right-color: var(--text); border-right-width: 2.5px; }

.kk-clue {
  position: absolute;
  top: 2px;
  left: 4px;
  font-family: var(--font-mono);
  font-size: min(0.68rem, calc(320px / var(--n, 4) * 0.22));
  font-weight: 700;
  line-height: 1;
  color: var(--accent-strong);
  pointer-events: none;
}
.kk-val { line-height: 1; }

.kk-cell.selected { background: var(--accent-soft); box-shadow: inset 0 0 0 2px var(--accent); }
.kk-cell.peer { background: var(--surface-2); }
/* Only Latin-square clashes are flagged. Whether a cage adds up is the
   puzzle, so highlighting that would solve it for them. */
.kk-cell.conflict { color: var(--danger); background: var(--danger-soft); }

/* ---------- geo target ---------- */
/* One georeferenced image, not a slippy map — so there's no tile layer to
   style, just a dark chart with a HUD over it. */
.geo-hud {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 9px 12px;
  margin-bottom: 8px;
  background: #0d2028;
  border-radius: var(--radius) var(--radius) 0 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: #8fd4c2;
}
.geo-hud-label { letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.7; }
.geo-hud-value { font-weight: 700; color: #d6f5ea; }
.geo-hud-tol { margin-left: auto; color: var(--sc-gold); }

.geo-stage {
  position: relative;
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
  aspect-ratio: 1;
  overflow: hidden;
  background: #06222e;
  border: 1px solid #1c3b48;
  /* Tapping must place a pin, not scroll the sheet. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: crosshair;
}
.geo-map { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }

.geo-crosshair {
  position: absolute;
  width: 0; height: 0;
  pointer-events: none;
}
.geo-crosshair .geo-ring,
.geo-crosshair .geo-dot { position: absolute; border-radius: 50%; transform: translate(-50%, -50%); }
.geo-crosshair .geo-ring {
  width: 34px; height: 34px;
  border: 2px solid var(--sc-gold);
  box-shadow: 0 0 10px rgba(232, 193, 74, 0.6);
}
.geo-crosshair .geo-dot { width: 6px; height: 6px; background: var(--sc-gold); }
/* Cross arms, drawn off the ring so the exact point stays visible. */
.geo-crosshair::before,
.geo-crosshair::after {
  content: "";
  position: absolute;
  background: var(--sc-gold);
  transform: translate(-50%, -50%);
}
.geo-crosshair::before { width: 1px; height: 56px; }
.geo-crosshair::after  { width: 56px; height: 1px; }
.geo-crosshair.on-target .geo-ring { border-color: var(--sc-green); box-shadow: 0 0 14px rgba(46,158,91,0.8); }

/* Magnifier, so a few-pixel target is still tappable without adding a
   pinch-zoom gesture that would fight tap-to-place. */
.geo-loupe {
  position: absolute;
  border-radius: 50%;
  border: 2px solid #8fd4c2;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
  background-repeat: no-repeat;
  pointer-events: none;
}
.geo-loupe::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border: 1px solid var(--sc-gold);
  border-radius: 50%;
}

.geo-pulse {
  position: absolute;
  width: 0; height: 0;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
}
.geo-pulse.run { animation: geo-ping 0.9s ease-out 2; }
@keyframes geo-ping {
  from { opacity: 0.85; box-shadow: 0 0 0 0 rgba(232, 193, 74, 0.55); }
  to   { opacity: 0;    box-shadow: 0 0 0 130px rgba(232, 193, 74, 0); }
}

.geo-drawer {
  max-width: 420px;
  margin: 0 auto 8px;
  padding: 10px 12px;
  background: #0d2028;
  border-radius: 0 0 var(--radius) var(--radius);
}
.geo-readout {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin: 0 0 10px;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: #d6f5ea;
}
.geo-submit {
  background: var(--sc-gold);
  color: #3a2c00;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.1em;
}

/* ---------- spectrum lens ---------- */
/* Two stacked copies of the page: a dimmed one, and a lit one clipped to
   a circle that follows the finger. The reveal layer sits inside the clip
   with the lit copy, so the lens shows page + hidden marks together. */
.spec-stage {
  position: relative;
  width: 100%;
  max-width: 420px;
  margin: 0 auto 12px;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  background: #ded9cc;
  /* Dragging must sweep the lens, not scroll the sheet behind it. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: crosshair;
}
.spec-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  pointer-events: none;
}
/* Unlit page: readable enough to navigate, flat enough that the lens
   obviously reveals something. */
.spec-base { filter: brightness(0.72) contrast(0.82) saturate(0.5); }

.spec-lens {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Starts closed; JS opens it on the first paint. */
  clip-path: circle(0px at 50% 50%);
}
.spec-wash {
  position: absolute;
  inset: 0;
  mix-blend-mode: multiply;
  opacity: 0.28;
}
.spec-stage[data-color="purple"] .spec-wash { background: #6c3fd0; }
.spec-stage[data-color="red"]    .spec-wash { background: #d0432f; }
.spec-stage[data-color="green"]  .spec-wash { background: #2e9e5b; }
.spec-stage[data-color="blue"]   .spec-wash { background: #2b7fd4; }

/* Ring around the lens, so the light has an edge.
   A separate element rather than a border on .spec-lens: that element IS
   the clip, so anything drawn at its edge is exactly what clip-path
   removes. JS positions this one to match the circle. */
.spec-ring {
  position: absolute;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 14px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  transition: border-color 0.12s ease;
}
/* Over a findable mark, the ring goes gold and thickens. This is the
   real signal — the haptic is a bonus, since iOS Safari has no
   Vibration API at all. */
.spec-stage.over-target .spec-ring {
  border-color: var(--sc-gold);
  border-width: 4px;
  box-shadow: 0 0 18px rgba(232, 193, 74, 0.75);
}

.spec-modes { display: flex; gap: 8px; margin-bottom: 10px; }
.spec-mode {
  flex: 1;
  width: auto;
  padding: 10px 8px;
  background: var(--surface);
  color: var(--text-muted);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
}
.spec-mode.active { color: var(--accent-contrast); border-color: transparent; }
.spec-mode.active[data-color="purple"] { background: #6c3fd0; }
.spec-mode.active[data-color="red"]    { background: #d0432f; }
.spec-mode.active[data-color="green"]  { background: var(--sc-green); }
.spec-mode.active[data-color="blue"]   { background: var(--sc-blue); }

.spec-lock { margin-bottom: 4px; }

.spec-tray:not(:empty) {
  margin: 12px 0 4px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.spec-clues { display: flex; flex-wrap: wrap; gap: 6px; }
.spec-clue {
  padding: 5px 11px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.06em;
}

/* ---------- solved puzzles ---------- */
/* Finished puzzles stay in the list, receded rather than removed, so a
   team can go back and re-read one. */
.puzzle-card.solved {
  background: var(--surface-2);
  border-style: dashed;
  opacity: 0.75;
}
.puzzle-card.solved h3 { color: var(--success); }

/* Banner across the top of a finished board. */
.solved-banner {
  margin: 0 0 14px;
  padding: 10px 12px;
  background: var(--accent-soft);
  border: 1px solid var(--border);
  border-left: 4px solid var(--success);
  border-radius: var(--radius);
  font-size: 0.86rem;
  line-height: 1.45;
  font-weight: 600;
  color: var(--success);
}

/* A reviewed board is inert. Controls keep the global disabled styling —
   a submit button that still looks pressable is worse than one that
   clearly isn't — but the BOARD itself must stay at full strength, since
   the finished grid is the thing the player came back to look at. */
#modal-body input:disabled {
  color: var(--text);
  opacity: 1;
}
#modal-body .strands-cell:disabled,
#modal-body .sd-cell:disabled,
#modal-body .conn-tile:disabled,
#modal-body .xw-input:disabled {
  opacity: 1;
  cursor: default;
}

/* ---------- puzzle feedback animation ----------
   Payoff and rejection, for the two puzzles where a guess is a discrete
   moment. Entirely inside a no-preference query: with reduced motion the
   tiles simply change state, which is already legible on its own.
   Nothing here loops, and nothing flashes a large area. */
@media (prefers-reduced-motion: no-preference) {
  /* Each letter of a found word pops in turn — JS staggers the delay so
     the word reads left-to-right along the path you traced. */
  .strands-cell.just-solved { animation: pz-pop 0.36s ease-out both; }

  /* A rejected trace: short, small, and over quickly. */
  .strands-board.pz-reject,
  .conn-grid.pz-reject { animation: pz-shake 0.34s ease-in-out; }

  /* A newly solved Connections group dropping into place. */
  .conn-group.just-solved { animation: pz-drop 0.36s cubic-bezier(0.2, 1.3, 0.4, 1) both; }
}

@keyframes pz-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}
@keyframes pz-shake {
  0%, 100% { transform: translateX(0); }
  22%      { transform: translateX(-5px); }
  45%      { transform: translateX(5px); }
  70%      { transform: translateX(-3px); }
}
@keyframes pz-drop {
  from { opacity: 0; transform: translateY(-8px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

.strands-controls { display: flex; gap: 8px; margin-top: 4px; }
.strands-controls button.secondary {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  flex: 0 0 34%;
}

/* ============================================================
   W.O.R.C. SIGNAL INTERCEPT
   ------------------------------------------------------------
   A hostile overlay, so it deliberately ignores the theme
   variables — the whole point is that it is NOT B.E.A.C.H.'s
   interface. Its palette is scoped to .breach so it can be
   retuned without touching any act.

   On motion: there is no full-screen strobe anywhere in here.
   Rapidly flashing large areas can trigger photosensitive
   seizures (WCAG 2.3.1 puts the threshold at 3 flashes/sec), and
   this is going out to a house full of guests on unknown phones.
   The effect is carried by a slow sweep, static scanlines and
   small text jitter instead — and all of it is switched off
   under prefers-reduced-motion, where the text simply appears.
   ============================================================ */
.breach {
  --w-bg: #07090c;
  --w-red: #ff3546;
  --w-red-dim: #9c1725;
  --w-text: #e9eff3;
  --w-mute: #7f8d97;

  position: fixed;
  inset: 0;
  z-index: 2000;
  background: var(--w-bg);
  color: var(--w-text);
  font-family: var(--font-mono);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 34px 18px 40px;
}
.breach:focus { outline: none; }
body.breach-open { overflow: hidden; }

.breach-inner { max-width: 560px; margin: 0 auto; position: relative; z-index: 1; }

/* Static scanlines + a slow sweep. Both decorative, both cheap. */
.breach-scan {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.045) 0 1px,
    transparent 1px 3px
  );
}
.breach-scan::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 22vh;
  background: linear-gradient(to bottom, transparent, rgba(255, 53, 70, 0.07), transparent);
}

/* No skip control on purpose — the transmission is the point, and a
   button in the corner is too easy to hit by accident on a phone. Phase 2
   waits on "Close transmission", so nobody is held longer than the two
   short automated phases. Escape still aborts on a physical keyboard. */

/* ---------- phase 1 / 3: terminal lines ---------- */
.breach-boot, .breach-restore {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  line-height: 1.85;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--w-red);
}
.breach-restore { color: #4ad07f; }
/* Blinking block cursor. 1.06s cycle — nowhere near flash territory,
   and it's a few characters wide, not an area. */
.breach-boot::after, .breach-restore::after {
  content: "▊";
  margin-left: 2px;
}
/* Once the transmission itself is on screen the boot log is history:
   dim it and drop the cursor so it stops competing for attention. */
.breach-boot.settled { opacity: 0.4; margin-bottom: 18px; }
.breach-boot.settled::after { content: none; }

/* ---------- phase 2: the transmission ---------- */
.breach-message { padding-bottom: 8px; }

.breach-source {
  margin: 0 0 18px;
  padding: 8px 10px;
  border: 1px solid var(--w-red-dim);
  border-left: 3px solid var(--w-red);
  background: rgba(255, 53, 70, 0.07);
  color: var(--w-red);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1.6;
}

.breach-title {
  position: relative;
  margin: 0 0 20px;
  font-family: var(--font-mono);
  font-size: 1.28rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.02em;
  color: var(--w-text);
}

.breach-body p {
  margin: 0 0 15px;
  font-family: var(--font-body);
  font-size: 0.97rem;
  line-height: 1.68;
  color: var(--w-text);
}
.breach-body strong { color: var(--w-red); font-weight: 700; }
.breach-emphasis {
  padding: 12px 14px;
  border-left: 3px solid var(--w-red);
  background: rgba(255, 53, 70, 0.09);
  font-weight: 600;
}
.breach-sign {
  margin: 22px 0 0;
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--w-red);
}

.breach-continue {
  margin-top: 26px;
  background: transparent;
  color: var(--w-red);
  border: 1px solid var(--w-red);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.14em;
}
.breach-continue::before { content: "[ "; }
.breach-continue::after  { content: " ]"; }
.breach-continue:hover { background: rgba(255, 53, 70, 0.12); filter: none; }

/* ---------- motion ---------- */
@media (prefers-reduced-motion: no-preference) {
  /* Slow downward sweep — 7s for a full pass. */
  .breach-scan::after { animation: breach-sweep 7s linear infinite; }

  /* Chromatic split on the headline. Two offset copies of the same text,
     nudged a couple of pixels. No luminance flashing, just displacement. */
  .breach-title::before,
  .breach-title::after {
    content: attr(data-text);
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .breach-title::before { color: var(--w-red); animation: breach-shift-a 3.1s steps(2, end) infinite; }
  .breach-title::after  { color: #35d7ff; animation: breach-shift-b 2.7s steps(2, end) infinite; }

  .breach-message { animation: breach-in 0.5s ease-out both; }
}
@keyframes breach-sweep {
  from { top: -25vh; }
  to   { top: 100vh; }
}
/* Held still for most of the cycle, so it reads as an occasional
   glitch rather than a constant shimmer. */
@keyframes breach-shift-a {
  0%, 92%  { transform: none; opacity: 0; }
  93%, 96% { transform: translate(-2px, 1px); opacity: 0.75; }
  97%,100% { transform: none; opacity: 0; }
}
@keyframes breach-shift-b {
  0%, 90%  { transform: none; opacity: 0; }
  91%, 95% { transform: translate(2px, -1px); opacity: 0.6; }
  96%,100% { transform: none; opacity: 0; }
}
@keyframes breach-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- aftermath ---------- */
/* Pinned watermark. Sits above content but below the overlay itself. */
.worc-mark {
  position: fixed;
  right: 10px;
  bottom: 10px;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 6px 7px;
  border: 1px solid rgba(156, 23, 37, 0.4);
  border-radius: 6px;
  background: rgba(7, 9, 12, 0.05);
  color: #9c1725;
  opacity: 0.42;
  pointer-events: none;
}
.worc-mark svg { width: 20px; height: 20px; display: block; }
.worc-mark span {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  font-weight: 700;
  letter-spacing: 0.1em;
}

/* B.E.A.C.H.'s official "nothing happened" notice. */
.agent-notice {
  margin: 0 0 20px;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
}
.agent-notice h3 {
  margin: 0 0 5px;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  color: var(--accent-strong);
}
.agent-notice p {
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-muted);
}
