/*
 * Styling for the duality game pages.
 *
 * Two jobs. The page furniture matches the rest of danielgrimmer.github.io —
 * Roboto, the #002147 navy accent, and a dark mode that follows the reader's
 * system setting. The board itself is the V3.1 isometric look kept exactly:
 * the same three CSS-transformed faces per cube, the same palettes, and the
 * same lift on hover. Those transforms are tuned for a 40px tile, so the board
 * is never re-measured — it is scaled as a whole to fit its container, which is
 * also what fixes V3.1's board being a fixed size on phones.
 *
 * This file lives outside assets/css/, so purgecss never sees it.
 */

:root {
  --dg-bg: #ffffff;
  --dg-surface: #f7f7f7;
  --dg-text: #000000;
  --dg-text-muted: #828282;
  --dg-accent: #002147;
  --dg-accent-text: #ffffff;
  --dg-divider: #e0e0e0;
  --dg-tile: 40px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-dg-theme='light']) {
    --dg-bg: #1c1c1d;
    --dg-surface: #2b2b2c;
    --dg-text: #e8e8e8;
    --dg-text-muted: #9d9d9d;
    --dg-accent: #7fb2e5;
    --dg-accent-text: #1c1c1d;
    --dg-divider: #3a3a3c;
  }
}

:root[data-dg-theme='dark'] {
  --dg-bg: #1c1c1d;
  --dg-surface: #2b2b2c;
  --dg-text: #e8e8e8;
  --dg-text-muted: #9d9d9d;
  --dg-accent: #7fb2e5;
  --dg-accent-text: #1c1c1d;
  --dg-divider: #3a3a3c;
}

/*
 * `hidden` must actually hide.
 *
 * The browser implements it as `display: none` in its own stylesheet, which any
 * author rule setting `display` outranks. `.dg-btn-primary` sets inline-block
 * and `.dg-room-tools` sets flex, so both stayed on screen while reporting
 * hidden === true — which is how the Play the Real Game button came to sit
 * there from step one. Anything hidden in markup is hidden, full stop.
 */
[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------- page ---- */

body.dg {
  margin: 0;
  padding: 0 1rem 4rem;
  background: var(--dg-bg);
  color: var(--dg-text);
  font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
  line-height: 1.6;
}

.dg-wrap {
  max-width: 930px; /* $max-content-width from the site's main.scss */
  margin: 0 auto;
}

.dg-masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--dg-divider);
  margin-bottom: 2rem;
}

.dg-masthead a {
  color: var(--dg-accent);
  text-decoration: none;
}

.dg-masthead a:hover {
  text-decoration: underline;
}

.dg-masthead h1 {
  font-size: 1.5rem;
  font-weight: 500;
  margin: 0;
}

.dg-sub {
  color: var(--dg-text-muted);
  font-size: 0.9rem;
}

.dg h2 {
  font-weight: 500;
  margin: 2.5rem 0 0.75rem;
}

.dg p {
  max-width: 62ch;
}

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

.dg-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
  margin: 1.5rem 0;
}

.dg-btn {
  font: inherit;
  padding: 0.45rem 1.1rem;
  border: 1px solid var(--dg-accent);
  border-radius: 4px;
  background: transparent;
  color: var(--dg-accent);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.dg-btn:hover:not(:disabled) {
  background: var(--dg-accent);
  color: var(--dg-accent-text);
}

.dg-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The one button the page wants you to press. */
.dg-btn-primary {
  display: inline-block;
  background: var(--dg-accent);
  color: var(--dg-accent-text);
  text-decoration: none;
  margin-bottom: 0.75rem;
}

.dg-btn-primary:hover {
  opacity: 0.88;
  background: var(--dg-accent);
  color: var(--dg-accent-text);
}

.dg-intro {
  color: var(--dg-text-muted);
  margin-top: -1rem;
}

/* ------------------------------------------------------ room and presence ---- */

.dg-presence {
  color: var(--dg-text-muted);
  font-size: 0.9rem;
  min-height: 1.4em;
  margin-bottom: 0.5rem;
}

.dg-room-tools {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

.dg-join {
  font-size: 0.85rem;
  color: var(--dg-text-muted);
}

.dg-join-row {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.35rem;
}

.dg-join-row input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.9rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--dg-divider);
  border-radius: 4px;
  background: var(--dg-bg);
  color: var(--dg-text);
}

.dg-status {
  padding: 0.75rem 1rem;
  border: 1px solid var(--dg-divider);
  border-radius: 6px;
  background: var(--dg-surface);
  font-weight: 500;
  min-height: 1.6em;
}

.dg-status[data-tone='win'] {
  border-color: var(--dg-accent);
}

/* ---------------------------------------------------------------- board ---- */

.dg-board-frame {
  position: relative;
  margin: 1.5rem auto;
  /* Sized in JS: the board is transformed, and transforms do not affect layout. */
}

.dg-board {
  position: relative;
  transform-origin: top left;
  /* The flattening that makes the grid read as a field seen from a low angle. */
  transform: scaleX(calc(0.8 * var(--dg-fit, 1))) scaleY(calc(0.4 * var(--dg-fit, 1)));
}

.dg-cell {
  position: absolute;
  width: var(--dg-tile);
  height: var(--dg-tile);
  transform-origin: center;
  transition: transform 0.2s ease;
}

.dg-cell.is-playable {
  cursor: pointer;
}

/* The lift under the cursor. Worth keeping — it is most of the charm. */
.dg-cell.is-playable:hover {
  transform: translateY(-15px);
}

/*
 * Three faces of one cube, in the surface's own colours.
 *
 * `box-sizing: border-box` so the 2px border a lit square carries does not make
 * it a bigger cube than its neighbours, and the seal below — a hairline shadow
 * in the face's own colour — closes the sub-pixel seams the skew transforms
 * leave between the three faces. Without it the background shows through the
 * joins as a lit thread down every cube.
 */
.dg-face {
  position: absolute;
  box-sizing: border-box;
  width: var(--dg-tile);
  height: var(--dg-tile);
  border: 1px solid var(--dg-face-edge);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  font-size: 20px;
  color: var(--dg-dot);
}

.dg-face.dg-top {
  transform: rotate(45deg);
  background: var(--dg-face-top);
  box-shadow: var(--dg-legal-glow), 0 0 0 1.2px var(--dg-face-top);
}

.dg-face.dg-left {
  transform: scaleY(1.733) translateY(25.2px) translateX(-14.8px) rotate(60deg) scaleY(0.577) rotate(-45deg);
  background: var(--dg-face-left);
  box-shadow: 0 0 0 1.2px var(--dg-face-left);
}

.dg-face.dg-right {
  transform: scaleY(1.733) translateY(25.2px) translateX(14.8px) rotate(-60deg) scaleY(0.577) rotate(45deg);
  background: var(--dg-face-right);
  box-shadow: 0 0 0 1.2px var(--dg-face-right);
}

.dg-glyph {
  /* Counter the board's squash so an O looks like an O, not an ellipse. */
  display: block;
  transform: rotate(-45deg) scaleX(1.25) scaleY(2.5);
  line-height: 1;
}

/* Surfaces ----------------------------------------------------------------- *
 *
 * Chalk & Turf. Every surface is two shades of the same colour laid in mown
 * stripes, dark earth under the pitch and pale ice under the rink, and one gold
 * for everything the player is being told about. Values are the shared board
 * vocabulary; each theme only says what its own surface is made of.
 *
 * The sprites live in assets/games/img/ and are named, not versioned: this
 * stylesheet is cache-busted by its own `?v=` token but the images it points at
 * are not, so a redrawn ball under the same filename will keep loading from a
 * browser's cache for as long as it feels like. Redraw one, rename it.
 */

.dg-board {
  --dg-tile-edge: rgba(25, 24, 23, 0.3);
  --dg-wall: #232019;
  --dg-legal-bg: #f7efd0;
  --dg-legal-edge: #c8963e;
  /* Lit only under floodlights, and then only on the star — see .is-legal. */
  --dg-lit-glow: 0 0 0 0 transparent;
  --dg-legal-glow: 0 0 0 0 transparent;
  --dg-x: rgba(25, 24, 23, 0.45);
  /* The newest cross: the same ink, at full strength. */
  --dg-x-latest: rgba(25, 24, 23, 0.9);
}

.dg-board[data-theme='soccer'] {
  --dg-top-a: #7fb765;
  --dg-top-b: #77ac5c;
  --dg-side-l: #6b4a2f;
  --dg-side-r: #7d5636;
  --dg-dot: #a97d2c;
  --dg-ball-art: url('../img/ball-soccer.png');
  --dg-goal-far: url('../img/goal-far.png');
  --dg-goal-near: url('../img/goal-near.png');
}

.dg-board[data-theme='hockey'] {
  --dg-top-a: #e4f0f8;
  --dg-top-b: #dbebf5;
  --dg-side-l: #b3d3e4;
  --dg-side-r: #a4c8dc;
  /* Gold vanishes on ice; the approach dots are read off the black of a puck. */
  --dg-dot: #14181c;
  --dg-ball-art: url('../img/ball-puck.png');
  --dg-goal-far: url('../img/goal-far.png');
  --dg-goal-near: url('../img/goal-near.png');
}

.dg-board[data-theme='basketball'] {
  --dg-top-a: #dfbb85;
  --dg-top-b: #dfbb85;
  --dg-side-l: #981717;
  --dg-side-r: #f9c852;
  --dg-dot: #a97d2c;
  --dg-ball-art: url('../img/ball-basketball.png');
  /* A court has a hoop, not a cage. */
  --dg-goal-far: url('../img/hoop-far.png');
  --dg-goal-near: url('../img/hoop-near.png');
}

/* Floodlit: the same board, lit rather than daylit. */
@media (prefers-color-scheme: dark) {
  :root:not([data-dg-theme='light']) .dg-board {
    --dg-tile-edge: rgba(0, 0, 0, 0.42);
    --dg-wall: #05070a;
    --dg-legal-bg: #ffe9a8;
    --dg-legal-edge: #f5c451;
    --dg-lit-glow: 0 0 12px rgba(245, 196, 81, 0.7);
    --dg-x: rgba(0, 0, 0, 0.55);
    --dg-x-latest: rgba(0, 0, 0, 0.95);
  }
  :root:not([data-dg-theme='light']) .dg-board[data-theme='soccer'] {
    --dg-top-a: #356f42;
    --dg-top-b: #2c6138;
    /* Dirt, not grass in shadow: the pitch is laid on earth in both schemes. */
    --dg-side-l: #2e1e12;
    --dg-side-r: #3d2818;
  }
  :root:not([data-dg-theme='light']) .dg-board[data-theme='hockey'] {
    --dg-top-a: #d9edf8;
    --dg-top-b: #cee5f2;
    --dg-side-l: #5f8ba3;
    --dg-side-r: #527c95;
  }
}

[data-dg-theme='dark'] .dg-board {
  --dg-tile-edge: rgba(0, 0, 0, 0.42);
  --dg-wall: #05070a;
  --dg-legal-bg: #ffe9a8;
  --dg-legal-edge: #f5c451;
  --dg-lit-glow: 0 0 12px rgba(245, 196, 81, 0.7);
  --dg-x: rgba(0, 0, 0, 0.55);
  --dg-x-latest: rgba(0, 0, 0, 0.95);
}

[data-dg-theme='dark'] .dg-board[data-theme='soccer'] {
  --dg-top-a: #356f42;
  --dg-top-b: #2c6138;
  --dg-side-l: #2e1e12;
  --dg-side-r: #3d2818;
}

[data-dg-theme='dark'] .dg-board[data-theme='hockey'] {
  --dg-top-a: #d9edf8;
  --dg-top-b: #cee5f2;
  --dg-side-l: #5f8ba3;
  --dg-side-r: #527c95;
}

/*
 * A cube picks its three colours here and the faces above just paint them, so
 * the seal always matches whatever the face ended up being.
 */
.dg-cell {
  --dg-face-top: var(--dg-top-a);
  --dg-face-left: var(--dg-side-l);
  --dg-face-right: var(--dg-side-r);
  --dg-face-edge: var(--dg-tile-edge);
}

/* Mown stripes: the checker runs along row + col, set by the renderer. */
.dg-cell[data-shade='b'] {
  --dg-face-top: var(--dg-top-b);
}

/* Wall beside a goal mouth. */
.dg-cell.is-wall {
  --dg-face-top: var(--dg-wall);
  --dg-face-left: var(--dg-wall);
  --dg-face-right: var(--dg-wall);
  --dg-face-edge: var(--dg-wall);
}

/*
 * A square you may move to, and an arm of the star the trail has already
 * closed. Lit identically on purpose: the star is one shape, and what marks an
 * arm as spent is the ✕ sitting on it, not a duller square.
 */
.dg-cell.is-legal,
.dg-cell.is-blocked {
  --dg-face-top: var(--dg-legal-bg);
  --dg-face-edge: var(--dg-legal-edge);
  --dg-legal-glow: var(--dg-lit-glow);
}

.dg-cell.is-legal .dg-top,
.dg-cell.is-blocked .dg-top {
  border-width: 2px;
}


/*
 * The ball, the puck and the basketball are drawn sprites rather than stacked
 * gradients: a puck is a squat cylinder lying flat and a basketball has seams,
 * neither of which a radial gradient can say. No shadow and no ring — a dark
 * halo around an 18px sprite reads as dirt on the pitch.
 *
 * The counter-transform undoes the board's 0.8 x 0.4 squash: 1.25 x 0.8 and
 * 2.5 x 0.4 both come to 1, so the sprite renders at the size the renderer set.
 */
.dg-ball {
  position: absolute;
  /* Above every cube, so nothing can slice it; placed by the renderer. */
  z-index: 9999;
  background: var(--dg-ball-art) center / 100% 100% no-repeat;
  transform: scaleX(1.25) scaleY(2.5);
}

/*
 * Goal furniture standing in each mouth: a four-sided cage on turf and ice, a
 * backboard and rim on the court. Sized to one tile side, and counter-squashed
 * like the ball. The renderer gives each one the depth of the tile it stands
 * on, so a cube in front of it still paints over it.
 */
.dg-goal {
  position: absolute;
  width: 88px;
  height: 88px;
  background: center / 100% 100% no-repeat;
  transform: scaleX(1.25) scaleY(2.5);
  pointer-events: none;
}

.dg-goal-far {
  background-image: var(--dg-goal-far);
}

.dg-goal-near {
  background-image: var(--dg-goal-near);
}

.dg-cell.is-visited .dg-glyph {
  color: var(--dg-x);
  font-size: 17px;
}

/*
 * The last move, said with weight rather than with an arrow.
 *
 * Playtesters lost track of what their opponent had just done — the board
 * only ever gains crosses, and one more among a dozen is invisible. The
 * square the ball left last is drawn solid and a size up while the rest stay
 * translucent, so "it came from there to here" reads in one glance. An arrow
 * would have been a second kind of mark on a board whose whole subject is the
 * shape these crosses make.
 */
.dg-cell.is-latest .dg-glyph {
  color: var(--dg-x-latest);
  font-size: 21px;
}

/* ----------------------------------------------------------- chessboard ---- */

/*
 * Escher Chess is flat. The squares keep V1.2's wood palette in both colour
 * schemes: they are squares of a board, not page furniture, so they should not
 * follow the reader's dark mode any more than a real board does. Only the
 * labels and the promotion tray around them do.
 *
 * `--dg-square` and `--dg-gutter` are set by the renderer, in whole pixels.
 */
.dg-chess {
  --dg-sq-light: #f0d9b5;
  --dg-sq-dark: #b58863;
  --dg-sq-move: rgba(255, 206, 0, 0.42);
  position: relative;
  width: max-content;
  max-width: 100%;
  margin: 1rem auto;
}

/*
 * Two boards side by side, for the reveal and the sandbox.
 *
 * `minmax(0, 1fr)` rather than `1fr`: a column defaults to `min-width: auto`,
 * so a board wider than its share would stretch its own column — and the
 * renderer measures that column to decide how wide to be. The two would then
 * settle at different sizes depending on which drew first.
 */
.dg-chess-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
  align-items: start;
}

@media (max-width: 760px) {
  .dg-chess-pair {
    grid-template-columns: 1fr;
  }
}

/*
 * Labels on all four sides: rank numbers down both edges, file names along the
 * top and the bottom. On a ten-rank board a single row of letters underneath is
 * a long way from the far end, and naming a square out loud is how the game is
 * played.
 */
.dg-chess-grid {
  display: grid;
  grid-template-columns:
    var(--dg-gutter, 20px) repeat(var(--dg-files), var(--dg-square, 40px))
    var(--dg-gutter, 20px);
  grid-template-rows: auto repeat(var(--dg-ranks), var(--dg-square, 40px)) auto;
  border-radius: 3px;
  overflow: hidden;
}

.dg-chess-rank,
.dg-chess-file {
  display: grid;
  place-items: center;
  font-size: clamp(0.6rem, calc(var(--dg-square, 40px) * 0.3), 0.8rem);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--dg-text-muted);
}

.dg-chess-file {
  height: 1.5em;
}

/* The tutorial draws no file names, and should not reserve a row for them. */
.dg-chess-file:empty {
  height: 0;
}

.dg-square {
  position: relative;
  width: var(--dg-square, 40px);
  height: var(--dg-square, 40px);
  display: grid;
  place-items: center;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  font: inherit;
  background-color: var(--dg-sq-light);
  cursor: default;
}

.dg-square[data-shade='dark'] {
  background-color: var(--dg-sq-dark);
}

.dg-square:not(:disabled) {
  cursor: pointer;
}

/*
 * Where the last move went. Painted as a background *image* so the square's own
 * colour still shows through — the light and dark squares have to stay
 * distinguishable underneath it, or the trail reads as a third shade of board.
 */
.dg-square.is-from,
.dg-square.is-to {
  background-image: linear-gradient(var(--dg-sq-move), var(--dg-sq-move));
}

.dg-square.is-selected {
  box-shadow: inset 0 0 0 3px var(--dg-accent);
  z-index: 2;
}

/* A king in check, and never the piece attacking it. Working that out is the game. */
.dg-square.is-check {
  box-shadow: inset 0 0 0 3px #d64545;
  z-index: 2;
}

.dg-square:focus-visible {
  outline: 3px solid var(--dg-accent);
  outline-offset: -3px;
  z-index: 3;
}

/* Somewhere to go: a dot on an empty square, a ring round a piece to take. */
.dg-square.is-target::before {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 30%;
  height: 30%;
  border-radius: 50%;
  background: rgba(20, 20, 20, 0.34);
}

.dg-square.is-target.is-take::before {
  width: 88%;
  height: 88%;
  background: none;
  border: 3px solid rgba(20, 20, 20, 0.38);
}

.dg-square.is-target:hover::before {
  background: rgba(20, 20, 20, 0.5);
}

/*
 * A move that is legal but not the one being taught. Marked exactly like the
 * rest — seeing the whole pattern is the point — and simply inert, so the only
 * difference a player can feel is that the cursor does not change.
 */
.dg-square.is-locked {
  cursor: default;
}

.dg-square.is-target.is-take:hover::before {
  background: none;
  border-color: rgba(20, 20, 20, 0.62);
}

/*
 * The pieces are drawn, not typed — see ui/pieces-svg.js for why. Filled with
 * the side's colour and outlined in the other, so one set of shapes reads as
 * two sets of men, and no font is consulted at any point.
 */
.dg-man {
  display: block;
  width: 88%;
  height: 88%;
  pointer-events: none;
  position: relative;
  z-index: 1;
}

.dg-man svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.dg-man svg path {
  fill: currentColor;
  stroke: var(--dg-man-edge);
  stroke-width: 1.4;
  stroke-linejoin: round;
  paint-order: stroke fill;
}

/* A hole in the piece rather than more of it — the bishop's cross. */
.dg-man svg path[data-cut],
.dg-promo-choice svg path[data-cut] {
  fill: var(--dg-man-edge);
  stroke: none;
}

.dg-man[data-side='0'] {
  color: #fbfbf8;
  --dg-man-edge: #1b1b1b;
}

.dg-man[data-side='1'] {
  color: #17191d;
  --dg-man-edge: #f0f0ee;
}

.dg-promo {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.45rem;
  background: var(--dg-surface);
  border: 1px solid var(--dg-divider);
  border-radius: 8px;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.3);
  z-index: 6;
}

.dg-promo-choice {
  width: 2.6rem;
  height: 2.6rem;
  display: grid;
  place-items: center;
  padding: 0.15rem;
  cursor: pointer;
  background: var(--dg-sq-light, #f0d9b5);
  border: 1px solid var(--dg-divider);
  border-radius: 6px;
  color: #17191d;
  --dg-man-edge: #f0f0ee;
}

.dg-promo-choice svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.dg-promo-choice svg path {
  fill: currentColor;
  stroke: var(--dg-man-edge);
  stroke-width: 1.4;
  stroke-linejoin: round;
  paint-order: stroke fill;
}

.dg-promo-choice[data-side='0'] {
  color: #fbfbf8;
  --dg-man-edge: #1b1b1b;
}

.dg-promo-choice:hover {
  border-color: var(--dg-accent);
}

/* ---------------------------------------------------------------- coach ---- */

/*
 * Coach beside board on a wide screen, above it on a narrow one. The panel is
 * never modal: play is never blocked waiting for it to be dismissed.
 */
.dg-layout {
  display: grid;
  grid-template-columns: minmax(16rem, 20rem) 1fr;
  gap: 2rem;
  align-items: start;
  margin: 1.5rem 0;
}

@media (max-width: 780px) {
  .dg-layout {
    grid-template-columns: 1fr;
  }
}

.dg-coach {
  padding: 1.1rem 1.25rem 1.25rem;
  border: 1px solid var(--dg-divider);
  border-left: 3px solid var(--dg-accent);
  border-radius: 6px;
  background: var(--dg-surface);
  position: sticky;
  top: 1rem;
  /* Positioned siblings later in the DOM would otherwise paint over it. */
  z-index: 1;
}

/*
 * Sticky is for when the coach sits *beside* the board. Stacked on a phone it
 * pins itself to the top of the screen and the board scrolls up over it — the
 * cubes are positioned, so they win the paint. Above the board it should scroll
 * away like anything else.
 *
 * This has to come *after* the rule above rather than joining the layout query
 * further up: same specificity, so the later one wins, and up there it lost.
 */
@media (max-width: 780px) {
  .dg-coach {
    position: static;
  }
}

.dg-coach[data-done='true'] {
  border-left-color: #2e7d32;
}

.dg-coach-progress {
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dg-text-muted);
  margin-bottom: 0.4rem;
}

/*
 * `.dg h2` above is more specific than a bare class, so the panel's title was
 * inheriting the page's 2.5rem top margin and floating a long way from the
 * label that introduces it.
 */
.dg .dg-coach-title {
  font-size: 1.05rem;
  font-weight: 500;
  margin: 0 0 0.5rem;
}

.dg-coach-body {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
  /* Step copy uses blank lines between paragraphs; keep them without markup. */
  white-space: pre-line;
}

.dg-coach-hint {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
  font-style: italic;
  color: var(--dg-accent);
}

.dg-link {
  font: inherit;
  font-size: 0.85rem;
  background: none;
  border: 0;
  padding: 0;
  color: var(--dg-text-muted);
  text-decoration: underline;
  cursor: pointer;
}

.dg-link:hover {
  color: var(--dg-text);
}

.dg-play .dg-status {
  margin-bottom: 0.5rem;
}

/*
 * The board's cubes paint below their own tiles, and the frame reserves that —
 * but a board with a lifted cube under the cursor can still reach further. The
 * extra clearance keeps the buttons out of its way, and centring them under the
 * board reads better than hanging them off the left edge.
 */
.dg-play .dg-bar {
  justify-content: center;
  margin-top: 2.5rem;
}

/* --------------------------------------------------------------- replay ---- */

/*
 * Two full-size boards side by side need more room than the site's text column,
 * so the replay screen widens the wrapper. It is only ever applied once the
 * game is over, so the page furniture keeps the site's measure while there is
 * still reading to do.
 */
.dg-wrap.dg-wide {
  max-width: 1180px;
}

/*
 * The coach runs the full width here rather than sitting in a column beside the
 * board, which is what makes room for two boards at full size. Its paragraphs
 * still hold the site's measure, so it does not become a 1180px-wide slab of
 * text.
 */
.dg-replay-intro {
  max-width: 40rem;
  margin: 0 auto;
}

.dg-coach-band {
  position: static;
  margin: 1rem 0 0;
}

.dg-bar-main {
  justify-content: center;
  margin-top: 2.5rem;
}

.dg-replay-boards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
  align-items: start;
  margin-top: 1.5rem;
}

/*
 * Below this the two boards would be scaled down far enough that the trails
 * stop being readable, which defeats the comparison. Stacked, they are still
 * one above the other on the same screen.
 */
@media (max-width: 900px) {
  .dg-replay-boards {
    grid-template-columns: 1fr;
  }
}

.dg-replay-side {
  min-width: 0;
}

.dg-replay-head {
  border-bottom: 1px solid var(--dg-divider);
  padding-bottom: 0.4rem;
}

.dg-replay-label {
  font-weight: 500;
}

/*
 * The clearance is for the hover lift: a replay board takes no clicks, but its
 * cubes still rise under the cursor, and they must not reach the buttons.
 */
.dg-replay-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 2rem;
}

.dg-btn.dg-step {
  padding: 0.45rem 0.7rem;
  line-height: 1;
}

.dg-replay-scrub {
  flex: 1 1 14rem;
  min-width: 8rem;
  max-width: 26rem;
  accent-color: var(--dg-accent);
}

.dg-replay-count {
  flex: 1 0 100%;
  text-align: center;
  color: var(--dg-text-muted);
  font-size: 0.9rem;
}

.dg-board-frame[data-interactive='false'] .dg-cell.is-playable {
  cursor: default;
}

/* -------------------------------------------------------------- sandbox ---- */

.dg-dials {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin: 2rem 0 0.5rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--dg-divider);
  border-radius: 6px;
  background: var(--dg-surface);
}

.dg-dials label {
  font-size: 0.9rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.dg-dials input {
  font: inherit;
  width: 4.5rem;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--dg-divider);
  border-radius: 4px;
  background: var(--dg-bg);
  color: var(--dg-text);
}

.dg-dial-note {
  text-align: center;
  color: var(--dg-accent);
  font-size: 0.9rem;
  min-height: 1.4em;
  margin: 0 auto 0.5rem;
  max-width: 62ch;
}

/*
 * The move palette. Flat, on purpose: it is a diagram of displacements, not a
 * board, and drawing it isometrically would invite counting squares on it as
 * though the ball were standing there.
 */
.dg-palette-wrap {
  margin-top: 1rem;
  text-align: center;
  /* A nineteen-wide palette is wider than a phone. It scrolls inside its own
     box rather than pushing the page sideways. */
  overflow-x: auto;
}

.dg-palette-title {
  font-size: 0.85rem;
  color: var(--dg-text-muted);
  margin-bottom: 0.4rem;
}

.dg-palette {
  display: inline-block;
}

/*
 * Fixed cells rather than fractions. The palette sits in a shrink-wrapped box,
 * where `1fr` columns of empty buttons have no width to divide up and the whole
 * grid collapses to a few pixels. Fixed cells also make the palette grow with
 * the board: a nineteen-wide palette is visibly wider than an eleven-wide one,
 * which is the right thing for it to say.
 */
.dg-palette-grid {
  --dg-palette-cell: 22px;
  display: grid;
  grid-template-columns: repeat(var(--dg-palette-cols, 11), var(--dg-palette-cell));
  gap: 2px;
  margin: 0 auto;
}

.dg-palette-cell {
  width: var(--dg-palette-cell);
  height: var(--dg-palette-cell);
  padding: 0;
  border: 1px solid var(--dg-divider);
  border-radius: 2px;
  background: var(--dg-bg);
  cursor: pointer;
}

.dg-palette-cell:disabled {
  cursor: default;
}

.dg-palette-cell:hover:not(:disabled) {
  border-color: var(--dg-accent);
}

/* Lit exactly as a legal square is lit on the board, so the two read as one. */
.dg-palette-cell[data-on='true'] {
  background: #ffffcc;
  border-color: #ffd700;
}

/* The ball's own cell. It is not a move, so it does not toggle. */
.dg-palette-cell[data-centre='true'] {
  border-color: var(--dg-text);
}

.dg-palette[data-theme='soccer'] .dg-palette-cell[data-centre='true'] {
  background: radial-gradient(circle at 50% 50%, #1c1f24 0 22%, #ffffff 23%);
}

.dg-palette[data-theme='hockey'] .dg-palette-cell[data-centre='true'] {
  background: #23262b;
}

.dg-palette-note {
  font-size: 0.8rem;
  color: var(--dg-text-muted);
  min-height: 1.2em;
  margin-top: 0.35rem;
}

/*
 * The Escher sandbox's read-out: what each piece becomes once it crosses. The
 * three kinds are coloured because the *shape* of the list is the answer — a
 * good setting is mostly "same" with one swapped pair, and you can see that
 * without reading a word of it.
 */
.dg-duality {
  list-style: none;
  margin: 1rem auto 0;
  padding: 0;
  max-width: 40rem;
  display: grid;
  gap: 0.35rem;
}

.dg-duality-row {
  padding: 0.4rem 0.7rem;
  border-radius: 6px;
  border-left: 4px solid var(--dg-divider);
  background: var(--dg-surface);
  font-size: 0.95rem;
}

.dg-duality-row[data-kind='swap'] {
  border-left-color: var(--dg-accent);
}

.dg-duality-row[data-kind='same'] {
  border-left-color: #4f9d69;
}

.dg-duality-row[data-kind='alien'] {
  border-left-color: #b0762c;
}

.dg-dial-check {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.dg-dial-check input {
  width: auto;
  margin: 0;
}

/* ---------------------------------------------------------------- misc ---- */

.dg-legend {
  color: var(--dg-text-muted);
  font-size: 0.9rem;
}

/*
 * The whole rule set, folded shut, above the board rather than below it.
 *
 * Shut by default because the tutorial's job is to teach these one at a time;
 * open, it is the reference to check an argument against.
 */
.dg-rules {
  border: 1px solid var(--dg-divider);
  border-radius: 6px;
  background: var(--dg-surface);
  padding: 0.6rem 0.9rem;
  margin: 1rem 0 0;
}

.dg-rules > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.95rem;
}

.dg-rules > .dg-legend {
  margin: 0.75rem 0 0.25rem;
}

/*
 * The numbered form of the rules, and it wants the same measure the prose has:
 * `.dg p` is held to 62ch and a list left to itself would run the full width of
 * the box, so the block would read as two different documents.
 */
.dg-rules > ol.dg-legend {
  max-width: 62ch;
  padding-left: 1.4rem;
}

.dg-rules > ol.dg-legend li {
  margin: 0.35rem 0;
}

.dg-build {
  font-size: 0.78rem;
  opacity: 0.7;
  margin-top: 2rem;
}

.dg-seats {
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
  font-size: 0.95rem;
}

.dg-seat {
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--dg-divider);
  border-radius: 4px;
}

.dg-seat[data-active='true'] {
  border-color: var(--dg-accent);
  font-weight: 500;
}

@media (max-width: 600px) {
  .dg-masthead {
    padding: 1rem 0;
  }
  .dg h2 {
    margin-top: 2rem;
  }
}
