/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
img { max-width: 100%; display: block; }

:root {
  --color-bg: #fafaf8;
  --color-surface: #ffffff;
  --color-text: #1c1c1e;
  --color-text-muted: #5b5b60;
  --color-border: #dedede;
  --color-accent: #1f6f6b;
  --color-accent-contrast: #ffffff;
  --color-grid-line: #1c1c1e;
  --color-cell-filled: #3d3d3d;
  /* Deliberately not --color-accent - the cell highlight ring needs to read clearly against both
     pure black (filled) and pure white (blank) cells, which is a different job than a brand/button
     accent sitting on white page background; a dark teal barely shows up against near-black. */
  --color-highlight: #f5a623;
  --radius: 10px;
  --max-width: 960px;
}

/* Deliberately no @media (prefers-color-scheme: dark) override - the puzzle grids are designed to
   look like ink on paper (matching the print books), and inverting them for dark mode breaks that.
   The whole site is forced to always render light for now (may revisit with a grid-only-light,
   dark-mode-elsewhere approach later). */

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.5;
}

a { color: var(--color-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- Header / nav ---------- */
.site-header {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
}

.site-header__bar {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0.9rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.site-header__brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--color-text);
}

.site-header__brand-cover {
  height: 2rem;
  width: auto;
  border-radius: 4px;
  object-fit: cover;
}

.site-header__back {
  color: var(--color-text);
  font-weight: 500;
}

/* ---------- Main layout ---------- */
.site-main {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 2rem 1.25rem 3rem;
}

.page-title {
  font-size: 1.6rem;
  margin: 0 0 1.5rem;
}

/* ---------- Puzzle "frozen pane" layout (Have I Gone Wrong? / Example Solve) ----------
   The controls (title, zoom, step buttons) stay fixed in a compact header; the puzzle grid
   below gets the rest of the viewport and scrolls independently in both directions, like a
   frozen row/column in a spreadsheet - so the controls are always reachable regardless of how
   far you've scrolled/zoomed into the grid. */
body.puzzle-page {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* body.hub-page shares this header styling with body.puzzle-page (see wrong.html/example_solve.html)
   even though it doesn't use the rest of the frozen-pane layout below - the hub page sits directly
   between those two in the click-through path, and any difference in the header's own padding/
   border reads as a jarring pixel-shift of the cover image and brand text between one click and
   the next. */
body.puzzle-page .site-header,
body.hub-page .site-header {
  flex: none;
  /* the header is minimal here (just the brand + a back link) - the divider reads better moved
     down to bound the scrollable puzzle pane itself instead (see .puzzle-controls/.puzzle-zoom-footer).
     Without its own border, the header needs to match the page background (not the usual white
     surface) so it doesn't read as an odd two-tone strip above that line. */
  border-bottom: none;
  background: var(--color-bg);
}
body.puzzle-page .site-header__bar,
body.hub-page .site-header__bar {
  /* Matches .puzzle-controls/.puzzle-viewport/.puzzle-zoom-footer below: full-bleed (not centered
     in --max-width) and the same horizontal padding, so all the fixed bars align on wide screens.
     Bottom padding is also cut down - it otherwise stacks with .puzzle-controls's own top padding,
     making the gap above "Have I Gone Wrong?" much bigger than any other gap in the fixed header. */
  max-width: none;
  padding: 0.9rem 1rem 0.3rem;
}
body.puzzle-page .site-main {
  flex: 1 1 auto;
  /* Force a definite width rather than relying on flex "stretch", which loses out to the huge
     intrinsic width of the puzzle grid several levels down - min-width: 0 alone isn't enough. */
  width: 100%;
  /* No max-width/padding here (unlike the normal .site-main) - the controls/footer bars below
     span the full screen width, like the site header does, so their dividers read as structural
     boundaries of the app-like layout rather than stopping short of the edges. This must be
     explicit: the base .site-main rule's own padding (2rem 1.25rem 3rem) otherwise still applies,
     since nothing here was overriding it any more. */
  max-width: none;
  padding: 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.puzzle-controls {
  flex: none;
  padding: 0.6rem 1rem;
  border-bottom: 1px solid var(--color-border);
}
.puzzle-controls__title { font-size: 1.1rem; margin: 0 0 0.35rem; }
.puzzle-controls__hint { margin: 0 0 0.75rem; font-size: 0.85rem; color: var(--color-text-muted); }

.puzzle-controls__heading {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.puzzle-controls__heading .puzzle-controls__title,
.puzzle-controls__heading .puzzle-controls__hint {
  margin: 0;
}

body.puzzle-page .puzzle-viewport {
  flex: 1 1 auto;
  width: 100%;
  /* Flex items default to min-width/min-height: auto (won't shrink below their content's size) -
     without overriding this, the puzzle's true intrinsic size pushes this box (and the whole
     page) wider/taller than the viewport instead of scrolling internally. */
  min-width: 0;
  min-height: 0;
  padding: 0.75rem 1rem;
  /* White, rather than the page's grey background like the header/controls/footer around it -
     marks this out as the actual content pane, distinct from the fixed chrome framing it. */
  background: var(--color-surface);
}

.puzzle-zoom-footer {
  flex: none;
  display: flex;
  justify-content: center;
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--color-border);
}

/* ---------- Home page ---------- */
.intro {
  color: var(--color-text-muted);
  margin-bottom: 2rem;
  /* Avoids a single short word (e.g. "own") being left dangling alone on its own line - supported
     in current Chrome/Firefox, and simply ignored (falling back to normal wrapping) elsewhere. */
  text-wrap: pretty;
}
/* Inline links here sit mid-sentence in muted prose - the site's default accent color alone
   doesn't read as clickable against it, so underline unlike other links on the site. */
.intro a { text-decoration: underline; }

.book-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1.5rem;
  margin-bottom: 2.5rem;
}

.book-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  text-align: center;
}
.book-card:has(.book-card__link:hover) { border-color: var(--color-accent); }
.book-card__link { display: block; color: inherit; }
.book-card__link:hover { text-decoration: none; }

.book-card__cover {
  aspect-ratio: 3 / 4;
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
}
/* Only the text fallback (no cover image found) needs breathing room - a real cover image should
   fill this box edge-to-edge, not sit inside a padded frame that shows the background as a teal
   border around it. */
.book-card__cover span { padding: 1rem; }
.book-card__cover img { width: 100%; height: 100%; object-fit: cover; }

.book-card__label { padding: 0.75rem 0.75rem 0.25rem; font-size: 0.9rem; color: var(--color-text-muted); }
.book-card__buy { padding: 0 0.75rem 0.75rem; font-size: 0.8rem; font-weight: 600; color: var(--color-accent); }
.book-card__buy--disabled { color: var(--color-text-muted); font-weight: 400; }

.lookup-form {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1.5rem;
  max-width: 420px;
}
.lookup-form h2 { margin-top: 0; font-size: 1.15rem; }
.lookup-form label { display: block; font-size: 0.85rem; color: var(--color-text-muted); margin: 0.9rem 0 0.3rem; }
.lookup-form input, .lookup-form select {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  font-size: 1rem;
  background: var(--color-bg);
  color: var(--color-text);
}
.lookup-form button {
  margin-top: 1.25rem;
  width: 100%;
  padding: 0.65rem;
  border: none;
  border-radius: 6px;
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
}
.lookup-form button:hover { opacity: 0.9; }

.noscript-banner {
  background: #fff4e5;
  color: #6b4a00;
  border: 1px solid #f0c36d;
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  margin-bottom: 1rem;
}

/* ---------- Footer ---------- */
.site-footer {
  border-top: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 0.8rem;
  padding: 1.5rem 1.25rem;
}
.site-footer__inner { max-width: var(--max-width); margin: 0 auto; }
.site-footer__inner p { margin: 0 0 0.5rem; }
.site-footer__inner p:last-child { margin-bottom: 0; }
.site-footer__holder { font-weight: 600; color: var(--color-text); }
.site-footer__version { font-size: 0.7rem; }

/* ---------- Puzzle zoom controls ---------- */
.zoom-controls {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.zoom-controls button {
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 6px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.zoom-controls button:disabled { opacity: 0.4; cursor: not-allowed; }
.zoom-controls button:not(:disabled):hover { border-color: var(--color-accent); }
.zoom-controls__level { font-variant-numeric: tabular-nums; min-width: 6rem; text-align: center; color: var(--color-text-muted); }

/* ---------- Puzzle grid (Have I Gone Wrong? / Example Solve) ---------- */
.puzzle-viewport {
  max-width: 100%;
  overflow: auto;
  padding-bottom: 0.5rem;
  display: flex;
  /* "safe" centers the puzzle when it's smaller than the viewport, but falls back to start-aligned
     (unlike plain "center") once it overflows - without "safe", a centered element that overflows
     its scroll container becomes impossible to scroll all the way to its start in some browsers. */
  justify-content: safe center;
  align-items: safe center;
}

.puzzle {
  /* Every cell is exactly --cell-size square, in real pixels - not a percentage of the page -
     so zoom is predictable regardless of a puzzle's width/height. puzzle_zoom.js sets this;
     the value here is just the pre-JS/no-JS fallback (matches the default zoom level). */
  --cell-size: 18px;
  /* Border widths scale with cell size too (see puzzle_zoom.js, which sets these two alongside
     --cell-size) - otherwise borders are relatively huge when zoomed out and barely visible when
     zoomed in. These static values are just the pre-JS/no-JS fallback (100% zoom, matching
     --cell-size's default above). */
  --border-width: 0.667px;
  --thick-border-width: 2.333px;
  display: inline-grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto 1fr;
  /* .puzzle-viewport is a flex container (for the "safe center" alignment) - flex items shrink by
     default, which was compressing the row/column clue tracks down to nothing. The puzzle must
     always render at its natural intrinsic size; .puzzle-viewport's overflow:auto scrolls the rest. */
  flex: none;
}

.puzzle__corner { }

.puzzle__col-clues {
  display: grid;
  /* stretch (not "end") so every clue-stack's divider spans the full header height, matching the
     tallest column's stack - not just each column's own (shorter) clue count. */
  align-items: stretch;
  border-left: var(--thick-border-width) solid var(--color-grid-line);
}
.puzzle__col-clues .clue-stack {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  gap: 0.2em;
  font-size: clamp(6px, calc(var(--cell-size) * 0.65), 22px);
  line-height: 1.15;
  white-space: nowrap;
  padding-bottom: 2px;
  border-right: var(--border-width) solid var(--color-grid-line);
}
.puzzle__col-clues .clue-stack.thick-right { border-right-width: var(--thick-border-width); }

.puzzle__row-clues {
  display: grid;
  border-top: var(--thick-border-width) solid var(--color-grid-line);
}
.puzzle__row-clues .clue-line {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.4em;
  font-size: clamp(6px, calc(var(--cell-size) * 0.65), 22px);
  white-space: nowrap;
  padding-right: 0.5em;
  border-bottom: var(--border-width) solid var(--color-grid-line);
  overflow: hidden;
}
.puzzle__row-clues .clue-line.thick-bottom { border-bottom-width: var(--thick-border-width); }

.puzzle__cells {
  display: grid;
  border-top: var(--thick-border-width) solid var(--color-grid-line);
  border-left: var(--thick-border-width) solid var(--color-grid-line);
}

.puzzle__cell {
  all: unset;
  box-sizing: border-box;
  display: block;
  position: relative;
  border-right: var(--border-width) solid var(--color-grid-line);
  border-bottom: var(--border-width) solid var(--color-grid-line);
  background: var(--color-surface);
}
/* Solving Techniques diagrams (Block Match etc.) - a small amber number identifying which clue a
   cell's block corresponds to, overlaid on the cell itself rather than a separate annotation row,
   so it stays visually tied to the exact block it labels. Amber reads clearly against both the
   plain surface background and the dark filled-cell background used elsewhere in these diagrams. */
.puzzle__cell-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-highlight);
  font-weight: 700;
  font-size: clamp(8px, calc(var(--cell-size) * 0.62), 20px);
  pointer-events: none;
}
.puzzle__cell--unrevealed { background: var(--color-border); cursor: pointer; }
.puzzle__cell--unrevealed:hover { background: #b9b9b9; }
.puzzle__cell--filled { background: var(--color-cell-filled); }
.puzzle__cell--blank { position: relative; }
.puzzle__cell--blank::before,
.puzzle__cell--blank::after {
  content: "";
  position: absolute;
  top: 10%;
  bottom: 10%;
  left: 46%;
  right: 46%;
  background: var(--color-grid-line);
}
.puzzle__cell--blank::before { transform: rotate(45deg); }
.puzzle__cell--blank::after { transform: rotate(-45deg); }
.puzzle__cell--thick-right { border-right-width: var(--thick-border-width); }
.puzzle__cell--thick-bottom { border-bottom-width: var(--thick-border-width); }
.puzzle__cell--highlight {
  position: relative;
  z-index: 1;
  /* Scales with zoom instead of a fixed px width - at 250% zoom a fixed 3px reads as a hairline,
     and at 50% it would swallow most of a tiny cell. */
  box-shadow: inset 0 0 0 clamp(1.5px, calc(var(--cell-size) * 0.12), 5px) var(--color-highlight);
}
/* Solving Techniques diagrams (see how_to_solve.html) - "this is what the technique deduces",
   amber to match the highlight color rather than the real filled/blank colors, since these cells
   aren't yet actually known - they're the result being illustrated, not a real solved state. */
.puzzle__cell--preview-filled { background: var(--color-highlight); }
.puzzle__cell--preview-blank { position: relative; }
.puzzle__cell--preview-blank::before,
.puzzle__cell--preview-blank::after {
  content: "";
  position: absolute;
  /* A bit thicker than the real blank X (see .puzzle__cell--blank above, 8% thick) - amber on white
     has less natural contrast than black on white, so it needs some extra weight to read at a
     glance next to the solid preview-filled cells, but not so much it looks heavy-handed. Only ever
     used in these diagrams, never the live per-puzzle pages, so safe to diverge from the real blank
     X's weight. */
  top: 9%;
  bottom: 9%;
  left: 43%;
  right: 43%;
  background: var(--color-highlight);
}
/* Match the real blank X's weight to the preview one within a diagram, so a cell that's amber in
   the "Apply the rule" panel and black in "After" doesn't visibly change stroke thickness between
   the two - scoped to diagrams only, the live per-puzzle pages' blank X is untouched. */
.puzzle-viewport--diagram .puzzle__cell--blank::before,
.puzzle-viewport--diagram .puzzle__cell--blank::after {
  top: 9%;
  bottom: 9%;
  left: 43%;
  right: 43%;
}
.puzzle__cell--preview-blank::before { transform: rotate(45deg); }
.puzzle__cell--preview-blank::after { transform: rotate(-45deg); }

/* ---------- Example Solve controls ---------- */
.solve-controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.5rem;
  /* .puzzle-controls's own bottom padding is identical to wrong.html's, but a row of bordered
     buttons sitting right at that edge reads as less breathing room than plain hint text does at
     the same distance - so this page gets a bit extra to compensate. */
  padding-bottom: 0.35rem;
}
.solve-controls__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.solve-controls__counter {
  font-weight: 500;
  /* Fixed (not min-) width so the buttons either side don't shift as the digit count changes -
     "0 / 142" is narrower than "100 / 142", and the longest real puzzle currently has 820 steps. */
  width: 6rem;
  padding: 0 0.5rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.solve-controls button {
  padding: 0.5rem 0.7rem;
  border-radius: 6px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
}
.solve-controls button:disabled { opacity: 0.4; cursor: not-allowed; }
.solve-controls button:not(:disabled):hover { border-color: var(--color-accent); }
.solve-controls__explain {
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
  align-self: flex-start;
  max-width: 100%;
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--color-text-muted);
  cursor: pointer;
}
.solve-controls__explain input {
  /* Custom checkbox (rather than the browser default) to match the site's own accent color -
     appearance: none strips the native control so the checked state below can be drawn instead.
     position: relative so the checkmark below can be centred with absolute positioning rather
     than margins, which don't account for how `transform: rotate` shifts the visual bounding box. */
  position: relative;
  appearance: none;
  flex: none;
  width: 1rem;
  height: 1rem;
  margin: 0.15rem 0 0;
  border: 1.5px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-surface);
  cursor: pointer;
}
.solve-controls__explain input:checked {
  border-color: var(--color-accent);
  background: var(--color-accent);
}
.solve-controls__explain input:checked::after {
  content: "";
  position: absolute;
  left: 0.32rem;
  top: 0.14rem;
  width: 0.25rem;
  height: 0.45rem;
  border: solid var(--color-accent-contrast);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.solve-controls__explain input:hover { border-color: var(--color-accent); }
.solve-controls__explain-text {
  /* Label/colon/detail sit in their own no-gap group, separate from the checkbox<->text gap above -
     the colon needs to butt straight up against "Explain last step" with no space before it. */
  display: flex;
  min-width: 0;
}
.solve-controls__explain-label { flex: none; font-weight: 700; }
.solve-controls__explain-colon {
  /* Its own flex item (not a prefix character inside #solve-explain-detail's text) - and empty,
     not just hidden, when there's no explanation, so no colon shows with nothing after it. */
  flex: none;
  font-weight: 700;
}
#solve-explain-detail {
  /* Appended inline after the colon rather than on its own line, so toggling on/off never changes
     the row's height. Forced to a single line and truncated with an ellipsis rather than wrapping -
     some descriptions (e.g. edge-check ones) are long, and this row sits above the fixed-height
     "frozen pane" header (see body.puzzle-page), which must never change size underneath the user.
     Tapping/clicking it (see example_solve.js) toggles --expanded, since on mobile there's no hover
     to reveal the `title` tooltip - this is the only way to see the full text there.
     The gap after the colon is a margin, not a leading space character in the text content - a
     leading space shifts line 1's visible text right of the box edge without doing the same for
     the wrapped line below it, throwing off their alignment when --expanded wraps. */
  flex: 1 1 auto;
  min-width: 0;
  margin-left: 0.3rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
#solve-explain-detail.solve-controls__explain-detail--expanded {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* ---------- Puzzle hub actions ---------- */
.hub-options { display: flex; flex-wrap: wrap; gap: 1.25rem; }
.hub-option {
  flex: 1 1 240px;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.25rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}
.hub-option__description { margin: 0; flex: 1 1 auto; color: var(--color-text-muted); }
.hub-actions__link {
  display: inline-block;
  padding: 0.9rem 1.4rem;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  font-weight: 600;
  color: var(--color-text);
  text-align: center;
}
.hub-actions__link:hover { text-decoration: none; border-color: var(--color-accent); }

/* ---------- Solving Techniques page ---------- */
.technique-toc-group { margin: 0 0 1rem; }
.technique-toc-group__label {
  margin: 0 0 0.4rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.technique-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0;
  margin: 0;
  list-style: none;
}
.technique-toc li a {
  display: inline-block;
  padding: 0.4rem 0.8rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  font-size: 0.9rem;
}
.technique-toc li a:hover { border-color: var(--color-accent); text-decoration: none; }
.technique {
  padding: 1.75rem 0;
  border-top: 1px solid var(--color-border);
}
.technique:first-of-type { border-top: none; padding-top: 0; }
.technique-group-intro {
  padding: 2.5rem 0 0.5rem;
  border-top: 3px solid var(--color-accent);
}
.technique-group-intro--first { padding-top: 1.5rem; }
.technique-group-intro h2 {
  margin: 0 0 0.75rem;
  font-size: 1.7rem;
  color: var(--color-accent);
}
.technique__kicker {
  margin: 0 0 0.2rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.technique__name { margin: 0 0 0.75rem; }
.technique__diagram { margin: 1.25rem 0; }
.technique__diagram-caption {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  text-align: center;
  /* Centered text makes a one- or two-word widow on the last line read as an obvious gap -
     text-wrap: pretty asks the browser to balance line breaks to avoid that. Not supported
     everywhere yet, but degrades harmlessly to ordinary wrapping where it isn't. */
  text-wrap: pretty;
}
.puzzle-viewport--diagram { padding: 0; justify-content: flex-start; }
.technique__diagram { overflow-x: auto; }
.technique__diagram--sequence {
  display: flex;
  /* flex-end (not center) so the arrows line up with the puzzle row itself, not the vertical
     center of the whole step (label text + puzzle) - every step's label sits at the same height,
     so aligning the bottoms lines up every grid row, and the arrow along with them. */
  align-items: flex-end;
  justify-content: center;
  gap: 1rem;
  /* No flex-wrap here deliberately - see --stacked below. An ordinary wrap wouldn't split three
     panels plus two arrows evenly (e.g. two panels then one orphaned on its own line), so whether
     a diagram fits three-across is decided once, at build time (line_diagram_sequence, based on
     cell count at the fixed cell size), not left to the browser to discover mid-layout. */
}
.technique__diagram--sequence--stacked { flex-direction: column; align-items: center; gap: 0.5rem; }
.technique__diagram--sequence--stacked .diagram-step__arrow { height: auto; transform: rotate(90deg); }
.diagram-step { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
.diagram-step__label {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-text-muted);
}
.diagram-step__arrow {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Matches the puzzle row's own rendered height (cell size + its borders, set inline per
     diagram since cell_size can vary) so centering the glyph inside lines it up with the grid
     row itself, not the label above it - see the flex-end note on the container above. */
  height: var(--arrow-height, auto);
  font-size: 1.4rem;
  color: var(--color-text-muted);
}
/* A diagram that fits three-across on a desktop-width content column can still be too wide for a
   narrow (mobile) viewport - stack it there too, on top of whatever --stacked already decided. */
@media (max-width: 640px) {
  .technique__diagram--sequence { flex-direction: column; align-items: center; gap: 0.5rem; }
  .diagram-step__arrow { height: auto; transform: rotate(90deg); }
}
/* The full per-puzzle grid pages make the clue gutter read as "different from a cell" through
   context - a whole 2D grid, a corner box, thick borders converging. A single isolated line loses
   all of that, so the clue number can look like just another (oddly-sized) cell. Compensate with
   deliberately more breathing room and a tinted background, scoped to diagrams only - the real
   per-puzzle pages don't have this problem and shouldn't change. */
.puzzle-viewport--diagram .clue-line {
  min-width: 2.75em;
  padding: 0 0.9em 0 0.5em;
  background: var(--color-bg);
  font-weight: 600;
}
