/* Case board — a world you enter. Not a page. Quiet states. No rainbow. */

#caseBoard {
  /* The field is darker than the darkest face, so the monument always
     separates from the sky. Do not inherit --bg-*: the MD stage is lighter
     than the darkest stone and the silhouette dissolves. */
  --case-field-0: #06070a;
  --case-field-1: #0a0c10;
  --case-ink: var(--text-primary, #e8e6e1);
  --case-ink-dim: var(--text-secondary, #9ca3af);
  /* Chrome ink, as a triple so the alphas can stay where they are. Every rule
     that draws a keyline, a pill, a caption or a ring used this one paper white
     literally, which was invisible as a decision and fatal as one the moment
     the field went light: a white ring on cream is not a quiet ring, it is no
     ring. Naming it makes inverting the room a single declaration. */
  --case-chrome-ink: 232, 230, 225;
  --case-title-scrim: 0 12px 40px rgba(0, 0, 0, 0.7);
  --case-plate: rgba(255, 255, 255, 0.045);
  --case-plate-held: rgba(255, 255, 255, 0.07);
  --case-edge: rgba(255, 255, 255, 0.16);
  --case-edge-whisper: rgba(255, 255, 255, 0.08);
  /* One implied light, west. Baked bands — no shader, no gradient on a face.
     Load-bearing stone sits a band brighter than stone that is not.

     The whole room used to be seven stops under. The brightest surface anywhere
     was `l4` at 7.6% luminance, so there was nothing for the light to model form
     with and a block survived on its outline alone — which is why it read as a
     card lying on a surface. The bands below are measured: adjacent faces step
     at 1.50:1, the lit face lands at 36%, and the darkest face a piece can wear
     clears 3.36:1 against the field, against the 1.20:1 it used to manage.

     Warm light, cool stone. Faces turned to the source (l4–l6) carry a warm cast
     at 8–16% saturation; faces turned away (l1–l3) stay blue. That is a
     temperature gradient across one block rather than three samples of one hue,
     and it is why the stone is not warmed further: `#B45309` is hue 26°, so
     sandy stone would put the one raised voice into the family of everything it
     is raised above. At this chroma the break separates by 82 saturation points
     and stays the only saturated thing in the room. */
  --case-l1: #404652;
  --case-l2: #5e6578;
  --case-l3: #798299;
  --case-l4: #a8a198;
  --case-l5: #bbb3a9;
  --case-l6: #ccc4b9;
  /* The floor is lifted off the field just enough that a contact shadow has
     something to fall on — at 0.6% a dark patch on a dark plane was invisible,
     which meant the one cue that survives any scale in an axonometric world was
     not being drawn. Still 2.57:1 below the darkest face a piece can wear, so
     silhouettes hold against the ground as well as against the sky. */
  --case-ground: #24272d;
  --case-pave: #191d25;
  --gds-n900: #111827;
  --gds-n700: #374151;
  --gds-n500: #6B7280;
  --gds-n300: #D1D5DB;
  --gds-n100: #F3F4F6;
  --gds-white: #FFFFFF;
  --gds-warn: #B45309;
  --case-font-title: "Playfair Display", Georgia, "Times New Roman", serif;
  --case-font-label: Inter, Helvetica, system-ui, sans-serif;
  --case-u: 1px;
  --case-rail: 48px;
  --case-gutter-right: 80px;
  /* The crumbs, then the question, then the world: 76 + two lines of 28 + air.
     While the world was floored to a fraction of its sheet this reserve was
     notional, because the monument never reached its own top edge and nothing
     could collide. Fit the world to its window and the reserve has to be true. */
  --case-chrome-top: 142px;
  --case-chrome-bottom: 92px;
  --case-field-inset: 24px;
  --case-look-x: 0;
  --case-look-y: 0;
  /* Near-isometric and near-orthographic, like the reference. The authored
     angle is the whole illusion; look-parallax may only breathe against it. */
  --case-cam-x: 60deg;
  --case-cam-z: -45deg;
  --case-walk-x: 0;
  --case-walk-y: 0;
  --case-tilt: 10deg;
  --case-z: 0px;
  --case-rise: 28px;
  --case-face: 0deg;
  --case-lift: 0;
  --case-stone-top: var(--case-l4);
  --case-stone-south: var(--case-l2);
  --case-stone-west: var(--case-l3);
  --case-stone-held: var(--case-l5);
  --case-bill-scale: 1;
  --case-walk-zoom: 1;
  /* Signage ink. Set here against the night field it currently stands in; the
     daylight repaint moves these two and nothing else about the label. */
  --case-label-ink: #e8e6e1;
  --case-label-scrim: 0 1px 12px rgba(0, 0, 0, 0.9), 0 0 3px rgba(0, 0, 0, 0.7);
  --case-tier-ink: rgba(var(--case-chrome-ink), 0.5);
  --case-tier-rule: rgba(var(--case-chrome-ink), 0.22);
  /* The walk. Teal is the governing thought's family in the reference, so the
     path that reaches it is the same ink, not a third colour invented for a HUD. */
  --case-path: #8FD0C6;
  /* Type is signage, not stone. `--case-u` sizes the architecture and has to be
     free to fall to whatever a phone needs; a sentence carved at that scale is
     unreadable, and one number cannot serve both. So the labels follow the unit
     while there is room and stop at a size a person can read. Each tier keeps
     its own floor, or the hierarchy between a thought, a reason and a piece of
     evidence collapses on exactly the screen that needs it most.

     Once the sentence left the stone this stopped being a compromise and became
     the rule: a label is signage standing in the world, not lettering cut into
     a face, so its size is a reading decision and nothing else. It still
     breathes a little with the unit — a big room can afford a bigger sign — but
     it is clamped at both ends, because the blocks are now a fixed module and
     type that tracked them freely would swing from illegible to billboard.
     The clamp is the monument's. Everywhere else a plate is still sized around
     its own wrapped sentence by `boxHeight`, in plan units, so type that stops
     following the unit is type that no longer fits the box that was measured
     for it — the collect wall's claims overflowing their own plates. There the
     old rule stands: the lettering is cut into the plate and scales with it. */
  --case-type-stone: max(17px, calc(18 * var(--case-u)));
  --case-type-terrace: max(14px, calc(15 * var(--case-u)));
  --case-type-block: max(12px, calc(13 * var(--case-u)));

  position: fixed;
  inset: 0;
  z-index: 500;
  display: none;
  /* The light in the room. §5.1 asks for "a single horizon, not a lighting rig",
     and what stood here was a gradient from #0d1015 to #06070a — a 1.1:1
     transition, which is to say nothing at all, over a 1px rule pretending to be
     the horizon. A silhouette needs something to be a silhouette against, and at
     the outline of a piece there is no adjacent face to carry contrast, so this
     is the only mechanism that can separate a block's edge from the sky. Warm,
     because it is the source the lit faces are lit by; low and wide, because it
     is a horizon; and gradients only — `filter: blur()` is a main-thread paint
     on iOS and buys nothing a stop can't do. */
  background:
    radial-gradient(ellipse 62% 34% at 50% 62%,
      rgba(150, 128, 104, 0.30) 0%,
      rgba(96, 88, 84, 0.16) 34%,
      rgba(38, 42, 54, 0.05) 62%,
      transparent 82%),
    radial-gradient(ellipse 96% 70% at 50% 46%, var(--case-field-1) 0%, var(--case-field-0) 72%);
  color: var(--case-ink);
  font-family: var(--case-font-label);
}
#caseBoard::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 6;
  background:
    radial-gradient(ellipse 70% 60% at 50% 45%, transparent 40%, rgba(0, 0, 0, 0.45) 100%);
}
/* Same near-black ground as the MD stage — nothing from Start bleeds through. */
body.case-view-open {
  background: var(--case-field-0, var(--bg-0, #090a0c));
}
body.case-view-open.md-float-collapsed .stage {
  visibility: hidden;
  pointer-events: none;
}
/* [hidden] is display:none !important in the UA sheet — beat it while open. */
body.case-view-open #caseBoard { display: block !important; }
body.case-view-open #startOverlay {
  display: none !important;
}

/* The world is the field. No sheet, no page edge. */
.case-board-sheet {
  position: absolute;
  inset: 0;
  /* The frame the monument is fitted to, which is now the frame it actually
     gets. While the scale was floored this box was notional and the chrome
     simply overlapped whatever overflowed; once the world fits its sheet, the
     sheet has to exclude the title, the close, the plays and the pip or the
     world is fitted to a window it does not have. */
  top: var(--case-chrome-top);
  bottom: var(--case-chrome-bottom);
  left: var(--case-rail);
  right: var(--case-gutter-right);
  width: auto;
  height: auto;
  background: transparent;
  color: var(--case-ink);
  box-sizing: border-box;
  outline: none;
  overflow: hidden;
  /* Almost orthographic. The reference is an orthographic camera, and a short
     perspective is what makes plates read as cards floating at depths. */
  perspective: 9000px;
}
body:not(.case-exhibit) .case-board-sheet.is-monument {
  /* Perspective is a different camera for every cube. Two modules a storey
     apart, or a collar-width apart, no longer show the same three faces, which
     is the whole defect in the screenshots: the thought is a cube and the
     reason next to it is a slab. None is identity. */
  perspective: none;
}

.case-monument {
  position: absolute;
  inset: 0;
  transform-origin: var(--case-orig-x, 50%) var(--case-orig-y, 55%);
  transform-style: preserve-3d;
  /* The last term is the world's third axis catching up with its other two.
     Plan x and y are multiplied by the unit per piece; altitude and extrusion
     arrive from `case_pose` in pixels and were never scaled at all. That was
     invisible while the unit sat near 1, and the moment the world was allowed
     to fit a phone at 0.26 the plan shrank to a quarter while every storey
     stayed full height — blocks a storey above their own shadows. Scaling z
     alone, innermost so the camera turns the corrected geometry, is the whole
     correction and costs one multiply. */
  transform:
    translate3d(calc(var(--case-walk-px, 0) * 1px), calc(var(--case-walk-py, 0) * 1px), 0)
    scale(var(--case-walk-zoom))
    rotateX(calc(var(--case-cam-x) + var(--case-look-y) * 2.5deg))
    rotateZ(calc(var(--case-cam-z) + var(--case-look-x) * 3deg))
    scale3d(1, 1, var(--case-uu, 1));
  transition: transform 560ms cubic-bezier(0.22, 0.9, 0.24, 1), transform-origin 560ms cubic-bezier(0.22, 0.9, 0.24, 1);
}

.case-board-ground {
  position: absolute;
  left: 4%;
  top: 18%;
  width: 92%;
  height: 74%;
  pointer-events: none;
  transform: translateZ(-24px);
  /* A pool of light, not a slab. The ground exists to catch contact shadows, so
     it has to be brighter than a shadow — but a flat fill at that brightness is
     a hard-edged grey wedge across half the frame, and the reference floats its
     architecture on emptiness rather than standing it on a table. So the plane
     is lit under the monument and gone by its edges: the shadows have something
     to fall on, and the silhouette still ends in nothing. */
  background:
    radial-gradient(ellipse 46% 46% at 50% 50%,
      var(--case-ground) 0%,
      rgba(36, 39, 45, 0.62) 44%,
      rgba(24, 27, 33, 0.24) 72%,
      transparent 88%);
}

/* The far light. A thin bar of warm sky low behind the monument, so a block's
   top edge has something brighter than itself to sit against — the one place a
   silhouette can be read, and the one place the face ladder cannot reach. */
.case-board-horizon {
  position: absolute;
  left: -6%;
  right: -6%;
  top: 30%;
  height: 22%;
  pointer-events: none;
  z-index: 0;
  background:
    linear-gradient(180deg,
      transparent 0%,
      rgba(160, 137, 108, 0.10) 46%,
      rgba(196, 170, 134, 0.20) 62%,
      rgba(120, 112, 112, 0.07) 78%,
      transparent 100%);
}

/* Suns and a couple of distant birds — atmosphere behind the monument, never
   in it. Same reason as the horizon: nested in the rotated world they would
   turn with the camera. */
.case-board-field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}
.case-sun {
  position: absolute;
  border-radius: 50%;
  background: #FBE6D4;
}
.case-sun--a {
  left: 4%;
  top: 22%;
  width: 18%;
  aspect-ratio: 1;
  height: auto;
  opacity: 0.55;
}
.case-sun--b {
  left: 44%;
  top: 14%;
  width: 11%;
  aspect-ratio: 1;
  height: auto;
  opacity: 0.45;
}
.case-sun--c {
  right: 2%;
  top: 20%;
  width: 26%;
  aspect-ratio: 1;
  height: auto;
  opacity: 0.9;
  background: radial-gradient(circle at 50% 45%, #FFF3E0 0%, #F8D9BC 100%);
  box-shadow: 0 0 0 14px rgba(244, 207, 175, 0.4);
}
.case-birds {
  position: absolute;
  left: 0;
  right: 0;
  top: 6%;
  width: 100%;
  height: 22%;
  overflow: visible;
}

.case-board-title {
  position: absolute;
  /* Under the crumbs, which end at 67. The reserve below is this plus two lines
     of it plus air, so a question that wraps still clears the world. */
  top: 76px;
  left: 72px;
  right: 72px;
  z-index: 5;
  margin: 0;
  font-family: var(--case-font-title);
  font-size: 22px;
  font-weight: 500;
  line-height: 28px;
  color: rgba(var(--case-chrome-ink), 0.72);
  letter-spacing: 0.04em;
  text-align: center;
  pointer-events: none;
  text-shadow: var(--case-title-scrim);
}

.case-board-body {
  position: absolute;
  inset: 0;
  overflow: visible;
  transform-style: preserve-3d;
}

.case-board-connectors {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
  z-index: 0;
  /* Joins are pavement on the ground, not diagram lines strung between plates
     at a height. Stage B replaces these with real spans. */
  transform: translateZ(3px);
  transform-style: preserve-3d;
}
.case-board-connectors path {
  fill: none;
  stroke: var(--case-pave);
  stroke-width: max(10px, calc(16 * var(--case-u)));
  stroke-linecap: square;
  stroke-linejoin: miter;
}
/* A broken join is unpaved: the warm trace marks where the path should run
   and does not. The hue is spent on the break, never on a plate. */
.case-board-connectors path.is-broken {
  stroke: var(--gds-warn);
  stroke-width: max(2px, calc(3 * var(--case-u)));
  stroke-dasharray: calc(8 * var(--case-u)) calc(16 * var(--case-u));
  opacity: 0.7;
}

.case-board-footer {
  display: none;
  position: absolute;
  left: calc(96 * var(--case-u));
  right: calc(96 * var(--case-u));
  bottom: calc(48 * var(--case-u));
  top: auto;
  margin: 0;
  font-family: var(--case-font-label);
  font-size: calc(14 * var(--case-u));
  line-height: calc(20 * var(--case-u));
  color: var(--case-ink-dim);
}

.case-node {
  --case-z: 0px;
  --case-rise: 28px;
  --case-face: 0deg;
  --case-lift: 0;
  position: absolute;
  z-index: 1;
  box-sizing: border-box;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--case-ink);
  font-family: var(--case-font-label);
  font-size: calc(16 * var(--case-u));
  font-weight: 400;
  line-height: calc(20 * var(--case-u));
  white-space: pre-wrap;
  cursor: pointer;
  transform:
    translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u) + var(--case-hover, 0px)))
    rotateZ(var(--case-face));
  transform-style: preserve-3d;
  transition: transform 280ms ease, opacity 280ms ease;
}
.case-node:hover { --case-hover: 8px; }
/* The module does not leave the grid. A hover lift is a different cube. */
body:not(.case-exhibit) .case-board-world.is-monument .case-node:hover {
  --case-hover: 0px;
}
/* Hit padding. Picking follows the node box (`elementsFromPoint`), and the box
   used to be a 430-unit plate; a 130-unit module on a phone draws around 68px
   across the plan, which under the camera is a 96px lozenge about 48px tall —
   under the thumb. The `max()` is what scopes this: on a desktop the module is
   already 183px and the pad resolves to the block itself, so nothing grows
   where nothing needed to. Sized to stay inside the 46-unit ring gap, or blocks
   would start stealing each other's taps. */
body:not(.case-exhibit) .case-board-world .case-node::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 88px);
  height: max(100%, 88px);
  transform: translate(-50%, -50%);
  pointer-events: auto;
}
/* Contact, not a card shadow. A hard patch on the ground plane is what makes a
   piece sit ON something; a blur makes it float over a page — and blur is a
   main-thread paint on iOS for nothing. */
.case-prim-shadow {
  position: absolute;
  inset: 0;
  /* Dark enough to read on a lit floor, not so dark it becomes a hole in it.
     The walls now carry the block's depth, so the shadow only has to say the
     piece is resting on something. */
  background: rgba(6, 8, 11, 0.62);
  /* On the floor, not carried. The shadow used to travel with its piece at the
     piece's own altitude, so it hung in the air behind a block and read as a
     hole rather than a contact. Dropping it to the ground plane is what makes
     height legible without a single number: a terrace on the floor keeps its
     shadow tight beneath it, and a piece standing a storey up throws its shadow
     well clear. That is the one depth cue an axonometric camera has, and it is
     free — `--case-z` is already per-piece. The throw scales with the world:
     altitude is in pixels and the plan is in units, so on a phone an unscaled
     throw sends the shadow four times too far and it stops being a contact.
     The coefficients are a third of what they were, because altitude is now
     three storeys rather than one floor and two thicknesses: the same numbers
     threw the summit's shadow 116px clear of it, which stopped reading as a
     contact and started reading as a second, black tile lying on the ground. */
  transform: translate3d(
    calc(5 * var(--case-u) + 0.12 * var(--case-z) * var(--case-uu, 1)),
    calc(6 * var(--case-u) + 0.14 * var(--case-z) * var(--case-uu, 1)),
    calc(-1 * var(--case-z) - var(--case-lift) * var(--case-u) - 23px)
  );
  pointer-events: none;
  transition: transform 280ms ease, opacity 280ms ease;
}
.case-prim-top,
.case-prim-south,
.case-prim-west {
  position: absolute;
  box-sizing: border-box;
  pointer-events: none;
}
.case-prim-top {
  inset: 0;
  display: flex;
  align-items: flex-end;
  gap: calc(8 * var(--case-u));
  padding: calc(12 * var(--case-u));
  background: var(--case-stone-top);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
  transform: translateZ(var(--case-rise));
  /* Flat on purpose, and it is a rendering fix, not a simplification: with
     `preserve-3d` here, Chrome plane-splits the face's own background against
     its 3D context and, at walking distance, drops fragments of it — a rhombus
     of sky punched through the middle of every top face. Everything painted on
     the face (mark, cite) lies in the face's own plane, so flattening costs
     nothing; the one thing that stood off the plane, the billboard label, now
     lives on the node itself where the context is already preserved. */
  pointer-events: auto;
}
.case-prim-south {
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--case-rise);
  background: var(--case-stone-south);
  transform-origin: bottom center;
  transform: rotateX(-90deg);
}
/* The arch on the governing thought. Painted on the south wall, not as a
   billboard, so it turns with the cube and reads as a hole in the stone. */
.case-prim-door {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 32%;
  height: 52%;
  margin-left: -16%;
  background: #3E4A5A;
  border-radius: 999px 999px 0 0;
  pointer-events: none;
}
/* The goal ring. Billboarded, because the reference draws it as a circle in
   the page, not as an ellipse on the top face. */
.case-prim-ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(36 * var(--case-u));
  height: calc(36 * var(--case-u));
  margin-left: calc(-18 * var(--case-u));
  margin-top: calc(-18 * var(--case-u));
  border: calc(2.4 * var(--case-u)) solid #FFFFFF;
  border-radius: 50%;
  box-sizing: border-box;
  pointer-events: none;
  transform-origin: center center;
  transform:
    translateZ(calc(var(--case-rise) + 40 * var(--case-u)))
    rotateZ(calc(-1 * var(--case-cam-z)))
    rotateX(calc(-1 * var(--case-cam-x)));
}
/* Unbuilt summit: the ring is still the goal, dashed because the sentence is
   not yet in it. Solid white would say the thought is already won. */
.case-node.is-placeholder .case-prim-ring {
  border-style: dashed;
  border-color: var(--case-path);
}
/* The `?` the reference hangs over an empty thought. Billboarded with the
   ring, so it stays a mark in the page rather than a glyph carved into the cube. */
.case-prim-quest {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(36 * var(--case-u));
  margin-left: calc(-18 * var(--case-u));
  margin-top: calc(-10 * var(--case-u));
  text-align: center;
  font-family: var(--case-font-title);
  font-size: calc(22 * var(--case-u));
  font-weight: 700;
  line-height: 1;
  color: var(--case-path);
  pointer-events: none;
  transform-origin: center center;
  transform:
    translateZ(calc(var(--case-rise) + 62 * var(--case-u)))
    rotateZ(calc(-1 * var(--case-cam-z)))
    rotateX(calc(-1 * var(--case-cam-x)));
}
/* The X in a designed void. Same billboard as the ring: two strokes in the
   page, not a kind-glyph sitting on the top face. */
.case-prim-gapx {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(22 * var(--case-u));
  height: calc(22 * var(--case-u));
  margin-left: calc(-11 * var(--case-u));
  margin-top: calc(-11 * var(--case-u));
  pointer-events: none;
  transform-origin: center center;
  transform:
    translateZ(calc(var(--case-rise) + 10 * var(--case-u)))
    rotateZ(calc(-1 * var(--case-cam-z)))
    rotateX(calc(-1 * var(--case-cam-x)));
}
.case-prim-gapx-a,
.case-prim-gapx-b {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: max(1.5px, calc(2 * var(--case-u)));
  margin-left: calc(-1 * var(--case-u));
  background: var(--case-ink);
}
.case-prim-gapx-a { transform: rotate(45deg); }
.case-prim-gapx-b { transform: rotate(-45deg); }
/* West, not east. The camera is fixed at `rotateZ(-45deg)`, which puts the east
   wall's normal at -0.61 in screen depth — it is the back of the block and has
   never once been seen. The two faces a viewer actually gets are south and west,
   so the second wall belongs on the left edge. It cost nothing while the walls
   were being flattened out of existence; now that they are drawn, half the mass
   was being built on the hidden side. */
.case-prim-west {
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--case-rise);
  background: var(--case-stone-west);
  transform-origin: left center;
  transform: rotateY(-90deg);
}
/* A span is a join, and a join is the whole game: it exists only where you
   could walk it. Stone in the same 3D space as the pieces — never a line on a
   plane underneath them. Anchored at the parent end so a break swings on its
   hinge instead of detaching and floating away. */
.case-span {
  --case-z: 0px;
  --case-rise: 28px;
  --case-angle: 0deg;
  --case-lift: 0;
  position: absolute;
  z-index: 0;
  box-sizing: border-box;
  pointer-events: none;
  transform-origin: 0 50%;
  transform:
    translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
    rotateZ(var(--case-angle));
  transform-style: preserve-3d;
  transition: transform 460ms cubic-bezier(0.22, 0.9, 0.24, 1);
}
/* The three tiers, named once at the edge of the room rather than a glyph on
   every block. On the host, so they stay level with the screen instead of level
   with the plan; positioned per paint, because where a storey landed depends on
   where the camera is standing. */
.case-tiers {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
}
.case-tier {
  position: absolute;
  left: 22px;
  display: flex;
  align-items: center;
  gap: 10px;
  transform: translateY(-50%);
  transition: top 460ms cubic-bezier(0.22, 0.9, 0.24, 1), opacity 320ms ease;
}
.case-tier[hidden] { display: none; }
.case-tier-name {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--case-tier-ink);
}
/* The rule runs from the name to the stone it names. It is the leader line and
   nothing else — no arrowhead, no tick — because what it points at is a whole
   storey rather than a spot. */
.case-tier-lead {
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(
    90deg,
    var(--case-tier-rule) 0%,
    var(--case-tier-rule) 62%,
    transparent 100%);
}

/* Claim sentences, screen-space, the same grammar as the storey names: a
   leader and a caption pointing at mute stone. Never a child of the cube. */
.case-signs {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 4;
}
.case-sign {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  transform: translateY(-50%);
  opacity: 0;
  transition: opacity 280ms ease, top 460ms cubic-bezier(0.22, 0.9, 0.24, 1);
}
.case-sign.is-near { opacity: 1; }
.case-sign.is-far { opacity: 0; }
.case-sign.is-start { flex-direction: row-reverse; }
.case-sign-lead {
  flex: 0 0 28px;
  height: 1px;
  background: var(--case-tier-rule);
}
.case-sign-text {
  font-family: var(--case-font-label);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--case-label-ink, var(--case-ink));
  text-shadow: var(--case-label-scrim, 0 1px 0 rgba(255, 255, 255, 0.6));
  white-space: pre-wrap;
}

/* The walk, drawn in the page so standing back you see the argument rather
   than a pile of cubes. Under the signs, over the stone. */
.case-path {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  overflow: visible;
}
.case-path-walk {
  fill: none;
  stroke: var(--case-path);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 1.5 7;
}
.case-path-break,
.case-path-x-arm {
  fill: none;
  stroke: var(--case-ink);
  stroke-width: 1.6;
  stroke-linecap: round;
}
.case-path-break { stroke-dasharray: 4 5; }
.case-path-x-arm { stroke-dasharray: none; }

/* One sentence for the one place the stone gives out. Same grammar as a
   claim sign: a leader and a caption. Hidden when the monument is still. */
.case-defect {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  transform: translateY(-50%);
  pointer-events: none;
  z-index: 4;
  max-width: 220px;
  transition: opacity 280ms ease, top 460ms cubic-bezier(0.22, 0.9, 0.24, 1);
}
.case-defect[hidden] { display: none; }
.case-defect-lead {
  flex: 0 0 22px;
  height: 1px;
  background: var(--case-ink);
}
.case-defect-text {
  font-family: var(--case-font-label);
  font-size: 13px;
  font-weight: 500;
  font-style: italic;
  line-height: 1.35;
  color: var(--case-label-ink, var(--case-ink));
  text-shadow: var(--case-label-scrim, 0 1px 0 rgba(255, 255, 255, 0.6));
  white-space: nowrap;
}

/* One tread of the flight. A join that stays on its storey has exactly one of
   these across the whole run, which draws as the flat plank it always was; a
   join that crosses a storey has several, each a step higher than the last.
   Stairs are discrete in the reference — half-scale cubes, not a tilted ramp —
   and a ramp is what one sloped plate would read as. */
.case-span-tread {
  --case-tread-x: 0%;
  --case-tread-w: 100%;
  --case-tread-z: 0px;
  position: absolute;
  display: block;
  top: 0;
  bottom: 0;
  left: var(--case-tread-x);
  width: var(--case-tread-w);
  transform: translateZ(var(--case-tread-z));
  transform-style: preserve-3d;
}
.case-span-top,
.case-span-south,
.case-span-west {
  position: absolute;
  display: block;
}
.case-span-top {
  inset: 0;
  /* A stair is stone the light reaches less of, one band under the terrace it
     runs to — not a hole in the monument. */
  background: var(--case-l4);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  transform: translateZ(var(--case-rise));
}
.case-span-south {
  left: 0;
  right: 0;
  bottom: 0;
  /* Deep enough to close the riser between one tread and the next, so a flight
     reads as solid stone rather than as floating slats. */
  height: calc(var(--case-rise) + var(--case-tread-z));
  background: var(--case-l1);
  transform-origin: bottom center;
  transform: rotateX(-90deg);
}
.case-span-west {
  position: absolute;
  display: block;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--case-rise);
  background: var(--case-l2);
  transform-origin: left center;
  transform: rotateY(-90deg);
}
/* Raised: present, not yet load-bearing. You cannot walk across a stair that is
   still in the air, and standing back the amount of hover is the state of play. */
.case-span.is-raised {
  --case-lift: 16;
}
.case-span.is-raised .case-span-top {
  background-image: linear-gradient(rgba(6, 7, 10, 0.45), rgba(6, 7, 10, 0.45));
}
/* Broken: the span has swung off its hinge and the far end is where you would
   be standing, unable to continue. The one raised voice in the palette, spent
   on the place you cannot walk rather than on an outline round a box. */
/* The far end is what fails, so the mark goes on the last tread rather than on
   every one of them — a gradient repeated down a flight says the stair is
   broken four times instead of once, at the end. */
.case-span.is-broken .case-span-tread:last-child .case-span-top {
  background-image: linear-gradient(90deg, rgba(180, 83, 9, 0) 72%, rgba(180, 83, 9, 0.7) 100%);
  box-shadow: inset -3px 0 0 var(--gds-warn), inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.case-span.is-broken .case-span-tread:last-child .case-span-south {
  background-image: linear-gradient(90deg, rgba(180, 83, 9, 0) 72%, rgba(180, 83, 9, 0.45) 100%);
}
/* Cut: the join was never built. The stone runs out in mid-air, which is the
   thing the eye should hit before it finds any label. */
.case-span.is-cut .case-span-top {
  background: linear-gradient(90deg, var(--case-l2) 30%, rgba(47, 53, 66, 0) 100%);
  box-shadow: none;
}
.case-span.is-cut .case-span-south {
  background: linear-gradient(90deg, var(--case-l1) 30%, rgba(18, 21, 27, 0) 100%);
}
/* The refusal. You asked the world to carry you somewhere it will not, so the
   join flexes once on its hinge and lights, at the place you are standing. Last
   in the file because it must win over every settled state it can appear on. */
.case-span.is-refusing {
  animation: case-span-refuse 400ms cubic-bezier(0.3, 0.9, 0.3, 1);
}
/* The far end is what lights. Near the foot the stone stays stone, so the eye is
   pulled along the span to the join that will not carry you. */
.case-span.is-refusing .case-span-tread:last-child .case-span-top {
  background-image: linear-gradient(
    90deg,
    rgba(180, 83, 9, 0) 22%,
    rgba(180, 83, 9, 0.34) 66%,
    rgba(180, 83, 9, 0.95) 100%);
  box-shadow: inset -3px 0 0 var(--gds-warn);
}
.case-span.is-refusing .case-span-tread:last-child .case-span-south {
  background-image: linear-gradient(
    90deg,
    rgba(180, 83, 9, 0) 30%,
    rgba(180, 83, 9, 0.62) 100%);
}
@keyframes case-span-refuse {
  0%, 100% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-angle));
  }
  40% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(calc(var(--case-angle) + 6deg));
  }
}

/* Type is sacred: every sentence stands upright to the camera, a signpost cut
   into the stone — never rotated away with the architecture. */
.case-node-label {
  flex: 1 1 auto;
  /* Once type has its own floor, a sentence on a phone is wider than the plate
     it belongs to. It is allowed to overhang — the plate has no `overflow`, and
     a sign that is bigger than its post is still a sign. The floor here just
     stops the line collapsing into one word per row. */
  min-width: 12ch;
  position: relative;
  z-index: 1;
  transform-origin: left bottom;
  /* The lift comes last, so it happens in the counter-rotated frame — which is
     screen-parallel, which is the only frame in which "up" means up. Moving the
     sign in plan instead would slide it up the diagonal and off its own block. */
  transform:
    rotateZ(calc(-1 * (var(--case-cam-z) + var(--case-face, 0deg))))
    rotateX(calc(-1 * var(--case-cam-x)))
    scale(var(--case-bill-scale))
    translateY(calc(-1 * var(--case-bill-lift, 0px)));
  /* Dark ink on lit stone. Near-white type was correct when the brightest face
     in the room was 7.6%; on a face at 36–56% it drops to about 2:1 and stops
     being readable. Cut lettering on pale stone is what the reference does and
     it is what the surface now is. The lift is a hairline of light along the top
     edge, not a blurred dark halo — a halo is a paint on iOS and, on a light
     face, it is the wrong direction anyway. */
  color: #191d24;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.18);
  transition: transform 280ms ease, opacity 320ms ease;
}
/* Cut lettering has to stay on the stone it is cut into. A billboard is upright
   in screen space while its plate is a rhombus, so a line anchored at a corner
   runs off the edge — which was invisible while the ink was pale on a dark field
   and is fatal now that it is dark. Anchoring at the centre puts the text across
   the rhombus's widest chord and holding it to a fraction of the plate keeps it
   there. World only: the score is a document and sets its text at the baseline. */
/* The monument's type scale. A block is a fixed module and its sign is signage
   standing in the world, so its size is a reading decision and nothing else —
   clamped at both ends, because type that tracked a 130-unit cube freely would
   swing from illegible to billboard. */
body:not(.case-exhibit) .case-board-world.is-monument {
  --case-type-stone: clamp(16px, calc(10 * var(--case-u)), 22px);
  --case-type-terrace: clamp(13px, calc(8 * var(--case-u)), 18px);
  --case-type-block: clamp(11px, calc(6.5 * var(--case-u)), 15px);
}
body:not(.case-exhibit) .case-board-world.is-monument .case-node-label {
  /* Off the stone. The block is a module now and could not hold a sentence at
     any type size worth reading, so the sentence stands behind it as a sign
     instead of lying on it as an inscription. Absolute, so it leaves the top
     face's flex row without disturbing the score, which still sets its text in
     the flow. `.case-prim-top` has no `overflow`, so there is nothing to escape
     from — the label simply stops being bounded by the stone.

     The width is in `em` on purpose. Wrapping is decided in plan units by
     `wrapText`, so the breaks only hold if the rendered column keeps the same
     ratio to the rendered type: 320 units of column at 20 units of type is 16em
     whatever pixel size the type lands on. That is what lets type be sized for
     reading while the line breaks stay the ones the shared pipeline chose. */
  position: absolute;
  left: 50%;
  bottom: 100%;
  margin-bottom: calc(6 * var(--case-u));
  /* Clear of the stone, and no clearer. The sign hangs off the top face's north
     edge, but the face is a rhombus and its north corner draws higher than that
     edge's midpoint, so a sign anchored there lands on the block it is naming.
     Fourteen units lifts the last line off the corner. More would be tidier and
     is not available: the summit's sign is the tallest thing on the board and
     the sheet clips, so every unit spent here comes out of the headroom the
     governing thought needs to be read at all. */
  --case-bill-lift: calc(14 * var(--case-u));
  width: 16em;
  margin-left: -8em;
  min-width: 0;
  flex: none;
  text-align: center;
  /* Pivot on the bottom edge, not the middle. The billboard tips the text up out
     of the plate's plane, so a centre pivot sinks the lower half of the block
     below the stone and the last line is swallowed by the surface it is written
     on. Pivoting on the bottom edge lifts the whole sentence clear. */
  transform-origin: center bottom;
  /* The label is a child of the node now, not of the top face — the face went
     flat (see `.case-prim-top`) and a flat parent would crush the billboard
     into the stone. So the sign carries its own climb to the top face's plane:
     the same `translateZ(var(--case-rise))` the face itself performs, ahead of
     the counter-rotation that stands the text up to the camera. */
  transform:
    translateZ(var(--case-rise))
    rotateZ(calc(-1 * (var(--case-cam-z) + var(--case-face, 0deg))))
    rotateX(calc(-1 * var(--case-cam-x)))
    scale(var(--case-bill-scale))
    translateY(calc(-1 * var(--case-bill-lift, 0px)));
  /* A sign stands in the air, so its ink answers to the sky rather than to the
     face it used to be cut into. One variable, because that is the single thing
     the daylight repaint has to move. */
  color: var(--case-label-ink);
  text-shadow: var(--case-label-scrim);
}
/* The same 320-unit column, held against each tier's own type size. */
body:not(.case-exhibit) .case-board-world.is-monument .case-prim--stone .case-node-label {
  width: 13.34em;
  margin-left: -6.67em;
}
body:not(.case-exhibit) .case-board-world.is-monument .case-prim--block .case-node-label,
body:not(.case-exhibit) .case-board-world.is-monument .case-prim--void .case-node-label {
  width: 20em;
  margin-left: -10em;
}
/* A void has no stone to cut into: its face is the field showing through, so
   its sentence is the one that stays light. */
.case-prim--void .case-node-label {
  color: var(--case-ink-dim);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.85);
}
/* Text on demand. Standing back, the monument is architecture and marks: the
   question on the horizon and one sentence on the stone. A terrace speaks when
   you are standing on it. Never display:none — VoiceOver, selection and the
   exhibit export all read this text. */
.case-node.is-far .case-node-label {
  opacity: 0;
}
/* The third way a piece speaks, after standing on it and selecting it. It is
   CSS rather than a branch in `isNear` so that reading the room costs no state
   and no re-render — moving across six blocks would otherwise be six paints.
   A phone has no hover and gets the other two, which is the honest trade: on a
   phone you are touching things anyway. */
@media (hover: hover) {
  body:not(.case-exhibit) .case-board-world.is-monument .case-node.is-far:hover .case-node-label {
    opacity: 1;
  }
}
.case-node-mark {
  position: relative;
  flex: 0 0 calc(14 * var(--case-u));
  width: calc(14 * var(--case-u));
  height: calc(14 * var(--case-u));
  margin-top: calc(7 * var(--case-u));
  /* Cut into the stone with the sentence it belongs to, not floated over it. */
  color: rgba(25, 29, 36, 0.62);
  pointer-events: none;
}
.case-prim--void .case-node-mark { color: var(--case-ink-dim); }
.case-node-mark::before,
.case-node-mark::after {
  content: "";
  position: absolute;
  box-sizing: border-box;
}

/* Reason: branch stub ⊢ */
.case-node--reason .case-node-mark::before {
  left: calc(3 * var(--case-u));
  top: 0;
  bottom: 0;
  width: 0;
  border-left: calc(2 * var(--case-u)) solid currentColor;
}
.case-node--reason .case-node-mark::after {
  left: calc(3 * var(--case-u));
  top: 50%;
  width: calc(9 * var(--case-u));
  border-top: calc(2 * var(--case-u)) solid currentColor;
}

/* Evidence: citation bars */
.case-node--evidence .case-node-mark {
  margin-top: calc(4 * var(--case-u));
}
.case-node--evidence .case-node-mark::before,
.case-node--evidence .case-node-mark::after {
  top: calc(2 * var(--case-u));
  bottom: calc(2 * var(--case-u));
  width: calc(2 * var(--case-u));
  background: currentColor;
}
.case-node--evidence .case-node-mark::before { left: calc(3 * var(--case-u)); }
.case-node--evidence .case-node-mark::after { left: calc(8 * var(--case-u)); }

/* Gap: open circle */
.case-node--gap .case-node-mark {
  margin-top: calc(4 * var(--case-u));
  border: calc(1.5 * var(--case-u)) solid currentColor;
  border-radius: 50%;
  background: transparent;
}

/* Claim: empty square */
.case-node--claim .case-node-mark {
  border: calc(1.5 * var(--case-u)) solid currentColor;
  border-radius: calc(2 * var(--case-u));
  background: transparent;
}

.case-node.is-held .case-node-mark { color: var(--case-ink-dim); }
.case-node.is-proposed .case-node-mark { color: rgba(var(--case-chrome-ink), 0.35); }
.case-node.is-contested .case-node-mark { color: var(--gds-warn); }
.case-node.is-stale .case-node-mark { color: var(--case-ink-dim); opacity: 0.45; }
.case-node.is-empty .case-node-mark { color: rgba(var(--case-chrome-ink), 0.28); }

.case-node.is-held.case-node--reason .case-node-mark::before,
.case-node.is-held.case-node--reason .case-node-mark::after {
  border-width: calc(2 * var(--case-u));
}
.case-node.is-proposed.case-node--reason .case-node-mark::before,
.case-node.is-proposed.case-node--reason .case-node-mark::after,
.case-node.is-stale.case-node--reason .case-node-mark::before,
.case-node.is-stale.case-node--reason .case-node-mark::after {
  border-width: calc(1 * var(--case-u));
}
.case-node.is-contested.case-node--reason .case-node-mark::before,
.case-node.is-contested.case-node--reason .case-node-mark::after {
  border-width: calc(3 * var(--case-u));
}

.case-node.is-held.case-node--gap .case-node-mark,
.case-node.is-held.case-node--claim .case-node-mark {
  background: currentColor;
}
.case-node.is-proposed.case-node--gap .case-node-mark,
.case-node.is-proposed.case-node--claim .case-node-mark,
.case-node.is-stale.case-node--gap .case-node-mark,
.case-node.is-stale.case-node--claim .case-node-mark {
  background: transparent;
}

.case-node-cite {
  position: absolute;
  top: calc(10 * var(--case-u));
  right: calc(10 * var(--case-u));
  width: calc(7 * var(--case-u));
  height: calc(9 * var(--case-u));
  border-right: calc(1.5 * var(--case-u)) solid rgba(25, 29, 36, 0.42);
  border-bottom: calc(1.5 * var(--case-u)) solid rgba(25, 29, 36, 0.42);
  pointer-events: none;
}
.case-prim--void .case-node-cite {
  border-right-color: rgba(var(--case-chrome-ink), 0.28);
  border-bottom-color: rgba(var(--case-chrome-ink), 0.28);
}
.case-node:focus {
  outline: none;
}
.case-node:hover .case-prim-top {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 0 0 1px rgba(255, 255, 255, 0.12);
}
.case-node:focus-visible .case-prim-top {
  box-shadow: 0 0 0 2px var(--case-ink);
}
.case-node.is-selected {
  --case-bill-scale: 1.22;
  z-index: 5;
}
/* z-index on a preserve-3d child is a stacking context. The compositor then
   paints that whole cube as a 2D layer, so the selected terrace is a different
   block from its siblings and the summit (z-index 3) eats the collar in front
   of it. In the world, document order is the painter — see `tileDepth`. */
body:not(.case-exhibit) .case-board-world.is-monument .case-node,
body:not(.case-exhibit) .case-board-world.is-monument .case-node.is-selected,
body:not(.case-exhibit) .case-board-world.is-monument .case-prim--stone,
body:not(.case-exhibit) .case-board-world.is-monument .case-span,
body:not(.case-exhibit) .case-board-world.is-monument .case-marker {
  z-index: auto;
}
.case-node.is-selected .case-prim-top {
  background: var(--case-stone-held);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 0 0 2px rgba(var(--case-chrome-ink), 0.55);
}
/* Standing back dims the stone, not the sentence.
   Never put opacity or filter on a face or on .case-node: both create a
   grouping context, which flattens the counter-rotated label back down onto
   the plate and turns every receded sentence into an unreadable sliver. Shade
   is painted into the face instead, so the stone stays opaque and the text
   keeps standing up. */
.case-node.is-receded .case-prim-top,
.case-node.is-receded .case-prim-south,
.case-node.is-receded .case-prim-west {
  background-image: linear-gradient(rgba(6, 7, 10, 0.55), rgba(6, 7, 10, 0.55));
}
.case-node.is-receded .case-node-mark,
.case-node.is-receded .case-prim-shadow { opacity: 0.5; }
/* Where you are standing. Stone with the light on it, not a second ring — the
   figure is already the marker, and a ring here would fight the selected one. */
.case-node.is-standing .case-prim-top {
  background-image: linear-gradient(rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.07));
}
.case-node.is-socket .case-prim-top {
  box-shadow:
    0 0 0 2px rgba(var(--case-chrome-ink), 0.5),
    inset 0 0 0 1px rgba(var(--case-chrome-ink), 0.2);
}
.case-node.is-dragging { opacity: 0.55; }
.case-node.is-commissioned .case-prim-top::after {
  content: "";
  position: absolute;
  left: calc(8 * var(--case-u));
  top: calc(8 * var(--case-u));
  width: calc(7 * var(--case-u));
  height: calc(7 * var(--case-u));
  border-radius: 50%;
  background: rgba(var(--case-chrome-ink), 0.55);
  box-shadow: 0 0 0 4px rgba(var(--case-chrome-ink), 0.12);
}

/* One material, lit once. The plaza is the brightest thing in the room because
   it is the high ground catching the most light, not because it is made of
   something else — so it lifts its lit face and shares every shaded face with
   the rest of the monument. Terraces and evidence blocks are told apart by size,
   height and where they stand, which is the job §4.1 gives geometry back after
   taking tint away from it: three tints spent on hierarchy left nothing for
   lighting, and lighting is what makes a block read as a block. */
.case-prim--stone {
  --case-stone-top: var(--case-l5);
  --case-stone-held: var(--case-l6);
  z-index: 3;
}
.case-prim--stone .case-prim-top {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
/* The tier's type scale is named on the label as well as on the face, because
   the label no longer always lives inside the face: in the world it is the
   node's child (see `.case-prim-top` on why), and a sign that stopped
   inheriting its storey's size would fall back to the node's raw module font
   and double in size. */
.case-prim--stone .case-prim-top,
.case-prim--stone .case-node-label {
  font-size: var(--case-type-stone);
  font-weight: 600;
  line-height: 1.32;
}
.case-prim--terrace .case-prim-top,
.case-prim--terrace .case-node-label {
  font-size: var(--case-type-terrace);
  line-height: 1.34;
}
.case-prim--block .case-prim-top,
.case-prim--block .case-node-label {
  font-size: var(--case-type-block);
  line-height: 1.36;
}
.case-prim--void .case-prim-top {
  background: transparent;
  box-shadow: inset 0 0 0 1px rgba(var(--case-chrome-ink), 0.22);
}
.case-prim--void .case-prim-top,
.case-prim--void .case-node-label {
  font-size: var(--case-type-terrace);
  line-height: 1.34;
  font-style: italic;
  color: var(--case-ink-dim);
}
.case-prim--void .case-prim-south,
.case-prim--void .case-prim-west {
  background: rgba(22, 24, 29, 0.35);
}
.case-prim--void .case-prim-shadow { opacity: 0.2; }

.case-node.is-held .case-prim-top { background: var(--case-stone-held); }
/* A hovering piece and its seat separate. The gap under the block is the one
   "waiting on you" signal — no badge, no dot. The seat shows where it lands. */
.case-node.is-proposed .case-prim-shadow,
.case-node.is-empty .case-prim-shadow {
  background: transparent;
  box-shadow: inset 0 0 0 max(1px, calc(1.5 * var(--case-u))) rgba(var(--case-chrome-ink), 0.22);
  /* Detached, and further out than a seated piece: the shadow has left the
     stone, which is what hovering looks like from a fixed camera. */
  transform: translate3d(
    calc(14 * var(--case-u) + 0.34 * var(--case-z) * var(--case-uu, 1)),
    calc(18 * var(--case-u) + 0.4 * var(--case-z) * var(--case-uu, 1)),
    calc(-1 * var(--case-z) - var(--case-lift) * var(--case-u) - 23px)
  );
}
/* Stale carries nothing, so it casts nothing. */
.case-node.is-stale .case-prim-shadow { opacity: 0; }
/* The break, and only the break, gets the hue: the edge that no longer meets
   the stone. Never an outline around a plate. */
.case-node.is-contested .case-prim-top {
  box-shadow:
    inset max(2px, calc(3 * var(--case-u))) 0 0 var(--gds-warn),
    inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.case-node.is-contested .case-prim-south {
  background: var(--case-l1);
}
.case-node.is-stale {
  color: var(--case-ink-dim);
}
/* Painted shade, not opacity: a face carries the label, so dimming the face
   would create a grouping context and flatten the counter-rotated sentence back
   onto the plate. */
.case-node.is-stale .case-prim-top,
.case-node.is-stale .case-prim-south,
.case-node.is-stale .case-prim-west {
  background-image: linear-gradient(rgba(6, 7, 10, 0.45), rgba(6, 7, 10, 0.45));
}
/* Cut off from the stone. Last of the shades, so on a piece that is also
   receded or stale this one wins — being unreachable is the more important
   thing to see, and two shades painted over each other would put the sentence
   out of reach as well. */
.case-node.is-unreachable .case-prim-top,
.case-node.is-unreachable .case-prim-south,
.case-node.is-unreachable .case-prim-west {
  background-image: linear-gradient(rgba(5, 6, 9, 0.58), rgba(5, 6, 9, 0.58));
}
.case-node.is-unreachable .case-node-mark { opacity: 0.4; }
.case-node.is-unreachable .case-prim-shadow { opacity: 0.25; }
/* Empty is unbuilt stone, not a ghost of a cube. Opacity on the node flattens
   `preserve-3d` and punches a courtyard tile through the summit — the live
   graph's whole defect. The score may still dim a plate; the world paints a
   dashed face and keeps the hue. */
.case-board-sheet:not(.is-monument) .case-node.is-empty .case-prim-top,
.case-board-sheet:not(.is-monument) .case-node.is-empty .case-prim-south,
.case-board-sheet:not(.is-monument) .case-node.is-empty .case-prim-west {
  background-image: linear-gradient(rgba(6, 7, 10, 0.35), rgba(6, 7, 10, 0.35));
}
.case-node.is-placeholder {
  font-style: italic;
  color: var(--case-ink-dim);
}

.case-version-rail {
  position: absolute;
  left: 12px;
  top: 48px;
  bottom: 72px;
  width: 8px;
  overflow: hidden;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 7px;
}
.case-version-rail.is-collapsed { width: 8px; opacity: 0.35; }
.case-version-tick {
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(var(--case-chrome-ink), 0.45);
  color: transparent;
  font-size: 0;
  cursor: pointer;
}
.case-version-rail:not(.is-collapsed) {
  width: 36px;
}
.case-version-rail:not(.is-collapsed) .case-version-tick {
  width: auto;
  height: auto;
  border-radius: 2px;
  background: transparent;
  color: rgba(var(--case-chrome-ink), 0.55);
  font: 10px/1 var(--case-font-label);
}

/* The principal. Not a cursor and not a HUD dot — the figure whose walk is the
   soundness test. It stands on the top surface of a piece, lives inside the
   monument so it takes the camera and the occlusion for free, and moves in plan
   space one transform at a time. */
.case-marker {
  --case-mx: 0;
  --case-my: 0;
  --case-mz: 0px;
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  z-index: 5;
  pointer-events: none;
  visibility: hidden;
  transform-style: preserve-3d;
  transform: translate3d(
    calc(var(--case-mx) * var(--case-u)),
    calc(var(--case-my) * var(--case-u)),
    var(--case-mz));
  transition: transform 90ms linear;
}
.case-marker.is-placed { visibility: visible; }
.case-marker-body {
  position: absolute;
  left: calc(-8 * var(--case-u));
  bottom: 0;
  width: calc(16 * var(--case-u));
  height: calc(36 * var(--case-u));
  background: transparent;
  transform-origin: center bottom;
  transform:
    translateZ(2px)
    rotateZ(calc(-1 * var(--case-cam-z)))
    rotateX(calc(-1 * var(--case-cam-x)));
  filter: drop-shadow(calc(2.4 * var(--case-u)) 0 0 rgba(74, 59, 82, 0.22));
}
.case-marker-ida {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.case-board-close {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 7;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--ui-glass-border, rgba(255, 255, 255, 0.11));
  border-radius: 50%;
  background: var(--ui-glass-fill, rgba(32, 34, 38, 0.44));
  backdrop-filter: blur(var(--ui-glass-blur, 40px)) saturate(var(--ui-glass-saturate, 160%));
  -webkit-backdrop-filter: blur(var(--ui-glass-blur, 40px)) saturate(var(--ui-glass-saturate, 160%));
  box-shadow: var(--ui-glass-shadow, inset 0 1px 0 rgba(255, 255, 255, 0.15));
  color: var(--ui-glass-icon, rgba(255, 255, 255, 0.92));
  font-size: 22px;
  line-height: 34px;
  cursor: pointer;
}
.case-board-close:hover {
  background: var(--ui-glass-fill-hover, rgba(44, 47, 52, 0.54));
}

.case-voice-pip {
  position: absolute;
  right: 22px;
  bottom: 22px;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 0.5px solid rgba(255, 255, 255, 0.16);
  background:
    url("wholegrain-logo.png") center / 62% no-repeat,
    rgba(255, 255, 255, 0.06);
  cursor: pointer;
  z-index: 3;
}

/* The real #mdFloatPill sits on this corner (z 620). Keep the board pip as a
   fallback hit target underneath, not a second grain. */
body.case-view-open.md-companion-open .case-voice-pip,
body.case-view-open.md-mobile-companion .case-voice-pip {
  pointer-events: none;
}

body.case-view-open .md-float-pill {
  pointer-events: auto;
  z-index: 620;
}

/* The board pip is the orb. Keep the living avatar out of the field. */
body.case-view-open .avatar.case-orb-to-pip {
  visibility: hidden;
}

body.case-view-open.case-exhibit #caseBoard {
  --case-gutter-right: 0px;
  --case-rail: 0px;
}
body.case-view-open.case-exhibit .case-board-sheet {
  left: 0;
  right: 0;
  /* An exhibit has no plays bar and no pip to stand clear of — only its own
     title — so it gets the page back. */
  top: 56px;
  bottom: 24px;
}
body.case-view-open.case-exhibit .case-board-footer:not(:empty) {
  display: block;
}
/* No crumbs to sit under, so the title takes the top of the page back and the
   exhibit's own sheet geometry is left exactly as it was. */
body.case-view-open.case-exhibit .case-board-title {
  top: 20px;
}
body.case-view-open.case-exhibit .case-marker,
body.case-view-open.case-exhibit .case-voice-pip,
body.case-view-open.case-exhibit .case-version-rail,
body.case-view-open.case-exhibit .case-layer,
body.case-view-open.case-exhibit .case-plays { display: none; }

.case-layer {
  position: absolute;
  top: 54px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 8px;
  pointer-events: auto;
}
.case-layer-sep {
  color: rgba(var(--case-chrome-ink), 0.35);
  font: 13px/1 Inter, system-ui, sans-serif;
}
.case-layer-crumb {
  border: 0;
  padding: 0;
  background: transparent;
  color: rgba(var(--case-chrome-ink), 0.55);
  font: 13px/1 Inter, system-ui, sans-serif;
  cursor: pointer;
}
.case-layer-crumb.is-here {
  color: rgba(var(--case-chrome-ink), 0.95);
}
/* The legal moves. Off the world for pointer users — legality is rendered as
   sockets, lift and seats, and anything sayable is playable through the host.
   Keyboard and assistive tech get the same list, revealed on focus, so nothing
   reachable by pointer is unreachable by tab. */
.case-plays {
  position: absolute;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: min(720px, calc(100% - 168px));
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}
#caseBoard.is-keying .case-plays,
.case-plays:focus-within,
#caseBoard:has(.case-node:focus-visible) .case-plays {
  opacity: 1;
}
#caseBoard.is-keying .case-plays-verbs,
.case-plays:focus-within .case-plays-verbs,
#caseBoard:has(.case-node:focus-visible) .case-plays-verbs {
  pointer-events: auto;
}
.case-plays-verbs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  pointer-events: none;
}
.case-play {
  border: 0;
  border-radius: 999px;
  padding: 8px 14px;
  background: rgba(var(--case-chrome-ink), 0.12);
  color: #eceae4;
  font: 500 13px/1 Inter, system-ui, sans-serif;
  cursor: pointer;
}
.case-play:hover,
.case-play:focus {
  background: rgba(var(--case-chrome-ink), 0.22);
}
.case-play--hold {
  background: rgba(var(--case-chrome-ink), 0.2);
}
.case-play--contest,
.case-play--commission {
  box-shadow: inset 0 0 0 1px rgba(232, 140, 64, 0.55);
}
.case-node.is-holdable .case-prim-top {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    0 0 0 2px rgba(var(--case-chrome-ink), 0.28);
}
.case-node.is-holding .case-prim-top {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 0 0 3px rgba(var(--case-chrome-ink), 0.7);
}
.case-figure.is-aimed .case-figure-body {
  box-shadow: 0 0 0 2px rgba(var(--case-chrome-ink), 0.55);
}

/* Exhibit is the score, not the world: flatten the camera and the stone. */
body.case-view-open.case-exhibit .case-monument { transform: none; }
body.case-view-open.case-exhibit .case-node-label,
body.case-view-open.case-exhibit .case-figure-body,
body.case-view-open.case-exhibit .case-figure-name,
body.case-view-open.case-exhibit .case-figure-stance { transform: none; }
body.case-view-open.case-exhibit .case-prim-top { transform: none; }
/* The score is text. Tiers are a property of the world, not the exhibit: the
   document says what a row is in the row itself, and a storey named at the edge
   of a printed page is naming a storey that is not drawn. */
body.case-view-open.case-exhibit .case-tiers,
body.case-view-open.case-exhibit .case-signs,
body.case-view-open.case-exhibit .case-path,
body.case-view-open.case-exhibit .case-defect,
body.case-view-open.case-exhibit .case-board-field,
body.case-view-open.case-exhibit .case-prim-door,
body.case-view-open.case-exhibit .case-prim-ring,
body.case-view-open.case-exhibit .case-prim-quest,
body.case-view-open.case-exhibit .case-prim-gapx { display: none; }
body.case-view-open.case-exhibit .case-node.is-far .case-node-label { opacity: 1; }
body.case-view-open.case-exhibit .case-prim-south,
body.case-view-open.case-exhibit .case-prim-west,
body.case-view-open.case-exhibit .case-prim-shadow { display: none; }
body.case-view-open.case-exhibit .case-board-ground,
body.case-view-open.case-exhibit .case-board-horizon,
body.case-view-open.case-exhibit .case-board-field { display: none; }
body.case-view-open.case-exhibit .case-figure.is-far .case-figure-stance { opacity: 1; }
/* Causeways are a property of the world. The score keeps GDS connectors. */
body.case-view-open.case-exhibit .case-board-connectors { transform: none; }
body.case-view-open.case-exhibit .case-board-connectors path {
  stroke: #1a1d22;
  stroke-width: max(10px, calc(14 * var(--case-u)));
}
body.case-view-open.case-exhibit .case-board-connectors path.is-broken {
  stroke: var(--gds-warn);
  stroke-width: max(8px, calc(12 * var(--case-u)));
  stroke-dasharray: calc(18 * var(--case-u)) calc(22 * var(--case-u));
  opacity: 1;
}

.case-stance-card {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 420;
  min-width: 16rem;
  max-width: 22rem;
  padding: 14px 16px;
  background: #1a1a1a;
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.88);
}
.case-stance-title { font-size: 0.8rem; opacity: 0.7; }
.case-stance-thought { margin: 8px 0 12px; font-size: 1.05rem; }
.case-stance-open {
  background: transparent;
  color: inherit;
  border: 1px solid rgba(255, 255, 255, 0.28);
  padding: 4px 10px;
  cursor: pointer;
}

/* The cuts. Small, low and to one side: turning the structure is a move you
   make when the structure will not carry you, not the first thing in the room.
   It reads as a row of names rather than a count, because a count is a fact
   about the world and a name is a way into one. */
.case-frame-switcher {
  position: absolute;
  left: 16px;
  bottom: 16px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--case-font-label);
}
.case-frame-cut {
  border: 0;
  border-radius: 999px;
  padding: 5px 11px;
  background: rgba(var(--case-chrome-ink), 0.08);
  color: rgba(var(--case-chrome-ink), 0.55);
  font: 500 12px/1 var(--case-font-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}
.case-frame-cut:hover,
.case-frame-cut:focus-visible {
  background: rgba(var(--case-chrome-ink), 0.18);
  color: #eceae4;
}
/* Where you are. Not a highlight on a tab — the one you are standing in is the
   one that is lit, the same rule the stone follows. */
.case-frame-cut.is-here {
  background: rgba(var(--case-chrome-ink), 0.2);
  color: #eceae4;
  cursor: default;
}

@media (max-width: 899px) {
  #caseBoard {
    --case-rail: 16px;
    --case-gutter-right: 16px;
    --case-field-inset: 16px;
    --case-chrome-top: 128px;
    --case-chrome-bottom: 104px;
  }
  /* The question ran to a 72px inset on both sides and straight under the close
     button. On a 390px screen that is 246px of column for a full sentence. */
  .case-board-title {
    top: 74px;
    left: 16px;
    right: 60px;
    font-size: 17px;
    line-height: 22px;
    text-align: left;
  }
  /* At the unit a phone ends up with, a sentence in the world is set three words
     wide and five lines tall over a plate half that height. `12ch` is the floor
     against one word per line; 160px is the floor against a column no phone can
     read. Desktop plates are wider than this, so it would only push their text
     off its own stone — hence phone only. */
  .case-board-world .case-node-label { min-width: max(12ch, 160px); }
  .case-version-rail { display: none; }
  /* One row that scrolls, not eleven that stack. The verbs are capped at
     `100% - 168px` so they clear the close button and the pip, which on a 390px
     screen leaves a 222px column — and a dozen chips wrapped into it took the
     bottom half of the phone and buried the world behind the list of things you
     could do to it. Assistive tech and keyboard users get the same verbs in the
     same order; they just travel sideways. */
  .case-plays {
    left: 16px;
    right: 16px;
    transform: none;
    max-width: none;
    align-items: stretch;
  }
  .case-plays-verbs {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 2px;
  }
  .case-plays-verbs::-webkit-scrollbar { display: none; }
  .case-play { flex: 0 0 auto; }
}

.case-board-cast {
  position: absolute;
  left: calc(96 * var(--case-u));
  right: calc(96 * var(--case-u));
  bottom: calc(36 * var(--case-u));
  display: flex;
  gap: calc(36 * var(--case-u));
  align-items: flex-end;
  z-index: 4;
  pointer-events: none;
  transform: translateZ(48px);
  transform-style: preserve-3d;
}
.case-figure {
  pointer-events: auto;
  min-width: calc(72 * var(--case-u));
  max-width: calc(140 * var(--case-u));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(6 * var(--case-u));
  color: var(--case-ink);
  cursor: pointer;
  transform-style: preserve-3d;
}
.case-figure-body {
  position: relative;
  width: calc(14 * var(--case-u));
  height: calc(36 * var(--case-u));
  background: #d8d4cc;
  border-radius: calc(3 * var(--case-u)) calc(3 * var(--case-u)) 0 0;
  transform-origin: center bottom;
  transform:
    translateZ(4px)
    rotateZ(calc(-1 * var(--case-cam-z)))
    rotateX(calc(-1 * var(--case-cam-x)));
  box-shadow: calc(4 * var(--case-u)) 0 0 rgba(0, 0, 0, 0.25);
}
.case-figure-body::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(-11 * var(--case-u));
  width: calc(11 * var(--case-u));
  height: calc(11 * var(--case-u));
  margin-left: calc(-5.5 * var(--case-u));
  border-radius: 50%;
  background: #d8d4cc;
}
.case-figure-plinth {
  width: calc(46 * var(--case-u));
  height: calc(13 * var(--case-u));
  background: var(--case-stone-top);
  box-shadow:
    calc(9 * var(--case-u)) calc(7 * var(--case-u)) 0 var(--case-stone-west),
    0 calc(8 * var(--case-u)) calc(16 * var(--case-u)) rgba(0, 0, 0, 0.4);
  transform: translateZ(8px);
}
.case-figure-mark {
  width: calc(10 * var(--case-u));
  height: calc(10 * var(--case-u));
  border: max(1px, calc(1.5 * var(--case-u))) solid rgba(var(--case-chrome-ink), 0.55);
  border-radius: 50%;
  background: transparent;
  order: 5;
}
.case-figure--artifact .case-figure-mark,
.case-figure--artifact .case-figure-body {
  border-radius: calc(2 * var(--case-u));
}
.case-figure--agent .case-figure-mark {
  border-radius: 0;
  transform: rotate(45deg);
}
.case-figure--meeting .case-figure-mark {
  border-radius: 50% 50% 50% 0;
}
.case-figure-tag {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(3 * var(--case-u));
  transform-origin: center bottom;
  transform: rotateZ(calc(-1 * var(--case-cam-z))) rotateX(calc(-1 * var(--case-cam-x)));
}
/* A name is identity and stays legible at any unit. A stance is prose, and
   prose resolves at conversational distance like every other sentence. */
.case-figure-name {
  font-family: var(--case-font-label);
  font-size: max(12px, calc(11 * var(--case-u)));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--case-ink-dim);
  text-align: center;
}
.case-figure-stance {
  font-family: var(--case-font-label);
  font-size: max(12px, calc(12 * var(--case-u)));
  line-height: 1.35;
  text-align: center;
  color: var(--case-ink);
  max-width: max(20ch, calc(160 * var(--case-u)));
  transition: opacity 320ms ease;
}
.case-figure.is-far .case-figure-stance { opacity: 0; }
/* Same law as the stone: dim the figure, never the group, or the name and
   stance flatten onto the ground. */
.case-figure.is-silent .case-figure-body,
.case-figure.is-silent .case-figure-plinth,
.case-figure.is-silent .case-figure-mark {
  opacity: 0.42;
}
.case-figure.is-silent .case-figure-name { color: var(--case-ink-dim); }
.case-figure.is-silent .case-figure-stance { display: none; }
.case-figure.is-out .case-figure-body,
.case-figure.is-out .case-figure-mark { opacity: 0; }
.case-figure.is-out .case-figure-plinth {
  box-shadow:
    calc(8 * var(--case-u)) calc(6 * var(--case-u)) 0 var(--case-stone-west),
    0 0 0 1px rgba(var(--case-chrome-ink), 0.2);
}
/* Asked and not yet answered. The figure is present — unlike out, where the
   plinth stands empty — but stepped forward off its plinth and holding, which
   is what being mid-answer looks like from across the room. */
.case-figure.is-asked .case-figure-body,
.case-figure.is-asked .case-figure-mark {
  opacity: 1;
  transform: translateZ(calc(7 * var(--case-u)));
}
.case-figure.is-asked .case-figure-plinth {
  box-shadow:
    calc(8 * var(--case-u)) calc(6 * var(--case-u)) 0 var(--case-stone-west),
    0 0 0 1px rgba(var(--case-chrome-ink), 0.28);
}
.case-figure.is-asked .case-figure-name { color: var(--case-ink); }
.case-figure.is-asked .case-figure-stance { display: none; }

/* A figure with a stance stands at that node's terrace, not in a lobby row. */
.case-figure.is-placed {
  position: absolute;
  margin: 0;
  min-width: calc(64 * var(--case-u));
  max-width: calc(150 * var(--case-u));
  z-index: 4;
  transform: translateZ(2px);
}

/* ==========================================================================
   The ballets. Motion is mechanism.

   Every piece already transitions between poses, so the world was never still —
   it was *uniform*, and a stance arriving, a reason being contested and a piece
   being held all looked like the same 280ms slide. These are the differences.

   A beat class is worn only while that beat is running, so each rule is a
   one-shot animation over the settled pose, never a state. Transform and
   opacity only, and opacity only ever on a leaf: a face or a label. Putting it
   on a piece would flatten the 3D children underneath it, which is the same law
   that governs every other shade in this file.
   ========================================================================== */

/* Turn. The crank: a contested terrace swings off the stone and takes its path
   with it. It overshoots because a hinge under load does, and because the
   overshoot is what makes the break read as a mechanism rather than a fade. */
@keyframes case-turn {
  0% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(0deg);
  }
  62% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(calc(var(--case-face) * 1.35));
  }
  100% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-face));
  }
}
.case-node.case-beat--face {
  animation: case-turn 320ms cubic-bezier(0.3, 0.85, 0.28, 1);
}
/* The span goes with it. Same hinge, same moment — if the join lagged behind
   the terrace the two would read as separate events instead of one. */
@keyframes case-span-open {
  0% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-angle));
  }
  62% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(calc(var(--case-angle) + 9deg));
  }
  100% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-angle));
  }
}
.case-span.case-beat--face {
  animation: case-span-open 320ms cubic-bezier(0.3, 0.85, 0.28, 1);
}

/* Seat. A hovering piece comes to rest against the stone: down out of the air,
   a little into the stone, and settled. It has to be the whole piece and not
   the top face — a face moving on its own would take the box apart, since the
   south and east faces are pinned to the same block. */
@keyframes case-seat {
  0% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u) + 9 * var(--case-u)))
      rotateZ(var(--case-face));
  }
  64% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u) - 3px))
      rotateZ(var(--case-face));
  }
  100% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-face));
  }
}
.case-node.case-beat--settle {
  animation: case-seat 240ms cubic-bezier(0.32, 0.9, 0.3, 1);
}

/* Arrive. A figure settles onto its plinth: down onto the stone and into the
   light. This is the only beat the room plays for somebody rather than for a
   piece — the finding it brought inks in on the same beat, so the arrival and
   what arrived are one event. */
@keyframes case-figure-arrive {
  0% {
    opacity: 0;
    transform:
      translateZ(calc(4px + 10 * var(--case-u)))
      rotateZ(calc(-1 * var(--case-cam-z)))
      rotateX(calc(-1 * var(--case-cam-x)));
  }
  100% {
    opacity: 1;
    transform:
      translateZ(4px)
      rotateZ(calc(-1 * var(--case-cam-z)))
      rotateX(calc(-1 * var(--case-cam-x)));
  }
}
/* Only somebody who is actually there arrives. A silent figure is dimmed and an
   absent one has an empty plinth, so fading either of them up to full and then
   dropping back to their settled shade would end the ballet on a flinch. */
.case-figure.is-spoken.case-beat--arrive .case-figure-body,
.case-figure.is-asked.case-beat--arrive .case-figure-body {
  animation: case-figure-arrive 360ms cubic-bezier(0.24, 0.9, 0.26, 1);
}
.case-figure.is-spoken.case-beat--arrive .case-figure-mark,
.case-figure.is-asked.case-beat--arrive .case-figure-mark,
.case-figure.is-spoken.case-beat--arrive .case-figure-stance {
  animation: case-ink 360ms ease-out;
}

/* Ink. A sentence appearing on the stone. Used wherever the world gains words
   it did not have: a proposal, a contest mark, a void being named, a merge
   writing its survivor. Never on a piece you are standing back from — text on
   demand outranks any ballet, or standing back would flicker with prose. */
@keyframes case-ink {
  from { opacity: 0; }
  to { opacity: 1; }
}
.case-beat--ink_write .case-node-label,
.case-beat--ink_circle .case-node-label,
.case-beat--write .case-node-label,
.case-beat--write_gap .case-node-label,
.case-beat--arrive .case-node-label {
  animation: case-ink 420ms ease-out;
}
.case-node.is-far.case-beat--ink_write .case-node-label,
.case-node.is-far.case-beat--ink_circle .case-node-label,
.case-node.is-far.case-beat--write .case-node-label,
.case-node.is-far.case-beat--write_gap .case-node-label,
.case-node.is-far.case-beat--arrive .case-node-label {
  animation: none;
}

/* Open space. A void is cut into the architecture, so the architecture has to
   make room for it — it grows out of the ground rather than fading up over it,
   because a gap is a hole in something, not a thing on top of it. */
@keyframes case-open-space {
  0% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-face))
      scale(0.72);
  }
  100% {
    transform:
      translate3d(0, 0, calc(var(--case-z) + var(--case-lift) * var(--case-u)))
      rotateZ(var(--case-face))
      scale(1);
  }
}
.case-node.case-beat--open_space {
  animation: case-open-space 280ms cubic-bezier(0.28, 0.88, 0.3, 1);
}

/* Fold. The same argument cut another way. One transition over two whole
   paintings, because there is no single actor to animate — every piece in the
   world is somewhere else afterwards.

   The name is worn only while folding, and that is not a tidiness preference.
   `view-transition-name` applies containment to the element that carries it, and
   `.case-monument` is the element that owns the 3D rendering context — so naming
   it permanently flattened the world. A flattened context still draws a face
   that is only translated in z, which is why the plates survived; it draws a
   face rotated 90° out of the plane at zero height, which is why every south and
   east wall in the monument silently stopped existing. That is the whole of
   "nothing has mass": the sides were never dark, they were never drawn, and no
   amount of relighting a top face would have found it. Wear the name for the
   520ms the fold needs it and the stone is solid the rest of the time. */
/* ---------------------------------------------------------------------------
   Daylight.

   The room was seven stops under a night sky and the architecture survived on
   its silhouette. The reference is the opposite claim: a warm afternoon, forms
   separated by hue rather than by outline, and shadow used sparingly because
   there is light to spare. Everything below is that one decision and the things
   it forces.

   Scoped to the world. `.case-board-world` is on the sheet in the exhibit too,
   so the guard has to be the body class — the score is a printed document and
   nothing here may reach it. The `#caseBoard` in each selector is not
   decoration: the defaults are written against the id, so a body-class selector
   alone loses the specificity contest and silently does nothing.
   --------------------------------------------------------------------------- */
body.case-view-open:not(.case-exhibit) #caseBoard {
  /* Sky, not field. Brighter toward the sun and warmer at the edges, which is
     the same two-stop gradient the night version had, inverted. */
  --case-field-0: #F2CFC6;
  --case-field-1: #FFF6EA;
  /* Plum, not near-white. On this sky it measures about 9:1 — better than dark
     ink on stone ever managed, and far better than the 2:1 pale ink was getting
     once the faces came up to 36%. */
  --case-ink: #4A3B52;
  --case-ink-dim: rgba(74, 59, 82, 0.6);
  /* Chrome plates were white washes on a black field. On cream a white wash is
     nothing at all, so every one of them inverts to a plum wash. This is not
     part of the palette so much as the cost of moving the field: the title, the
     crumbs, the plays bar and the rail are all light-on-dark by construction. */
  --case-chrome-ink: 74, 59, 82;
  --case-title-scrim: 0 1px 0 rgba(255, 255, 255, 0.5);
  --case-plate: rgba(74, 59, 82, 0.06);
  --case-plate-held: rgba(74, 59, 82, 0.1);
  --case-edge: rgba(74, 59, 82, 0.2);
  --case-edge-whisper: rgba(74, 59, 82, 0.09);
  --case-label-ink: #4A3B52;
  /* A sign in daylight needs no halo, only enough lift to survive crossing a
     block edge. The night scrim was a black blur, which on cream is a smudge. */
  --case-label-scrim: 0 1px 0 rgba(255, 255, 255, 0.6);
  --case-tier-ink: rgba(74, 59, 82, 0.55);
  --case-tier-rule: rgba(74, 59, 82, 0.26);
  --case-ground: rgba(198, 156, 124, 0.4);
  --case-pave: rgba(198, 156, 124, 0.24);
  --case-path: #2F6E68;
}
body.case-view-open:not(.case-exhibit) #caseBoard {
  background:
    radial-gradient(ellipse 70% 40% at 50% 64%,
      rgba(255, 236, 206, 0.55) 0%,
      rgba(250, 222, 196, 0.28) 38%,
      transparent 70%),
    linear-gradient(180deg, #FFF6EA 0%, #FBE3D2 55%, #F2CFC6 100%);
}
/* The vignette was a black radial. On cream that is dirt on the lens, so it
   inverts: the edges of the room deepen into warm shade rather than into
   nothing. */
body.case-view-open:not(.case-exhibit) #caseBoard::after {
  background:
    radial-gradient(ellipse 72% 62% at 50% 48%,
      transparent 42%,
      rgba(203, 148, 112, 0.22) 100%);
}
/* The low sun. On a dark field this was the one bright thing and had to carry
   the whole silhouette; on a light field it is the brightest band in an already
   light room, so it reads as where the light is coming from rather than as a
   bar drawn behind the stone. */
body.case-view-open:not(.case-exhibit) .case-board-horizon {
  top: 26%;
  height: 30%;
  background:
    linear-gradient(180deg,
      transparent 0%,
      rgba(255, 240, 214, 0.34) 40%,
      rgba(255, 247, 232, 0.72) 58%,
      rgba(255, 230, 200, 0.3) 76%,
      transparent 100%);
}
/* The pool the shadows fall on. Its falloff was hardcoded dark; on a lit floor
   the pool is a warm shade under the mass, so the whole ramp inverts. */
body.case-view-open:not(.case-exhibit) .case-board-ground {
  background:
    radial-gradient(ellipse 46% 46% at 50% 50%,
      var(--case-ground) 0%,
      rgba(198, 156, 124, 0.24) 44%,
      rgba(198, 156, 124, 0.1) 72%,
      transparent 88%);
}
/* Near-black at 62% was a hole punched in a dark floor and read as one. The
   reference's shadows are warm greys at roughly a fifth of that weight: enough
   to say a block is resting on something, not enough to be a second object. */
body.case-view-open:not(.case-exhibit) .case-prim-shadow {
  background: rgba(150, 96, 74, 0.16);
}

/* ---------------------------------------------------------------------------
   Hue per tier.

   One shared luminance ladder was the right answer to a real problem: three
   tints spent on hierarchy had left nothing for lighting, and lighting is what
   makes a block read as a block. It solved that and paid for it by making every
   storey the same material, so the only thing separating a thought from a piece
   of evidence was where it stood.

   Four families fix both at once, because each family carries the ladder rather
   than replacing it. Every one of these steps about 1.5:1 from face to face —
   the figure already measured as correct — so the modelling is untouched and
   the hue is free. Which tier a block belongs to becomes the first thing you
   read rather than something you work out from altitude.

   Every selector carries `#caseBoard`, because `.case-prim--stone` alone loses
   to the id-scoped defaults it is trying to replace.
   --------------------------------------------------------------------------- */
/* Teal — the governing thought, and the only cool family in the room. The
   summit is the one block that is always visible and always speaking, so it is
   the one that is allowed to sit against everything else rather than with it. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--stone {
  --case-stone-south: #47857E;
  --case-stone-west: #5FA89E;
  --case-stone-top: #8FD0C6;
  --case-stone-held: #AFDFD8;
}
/* Rose — the key line. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--terrace {
  --case-stone-south: #A05F72;
  --case-stone-west: #C97C86;
  --case-stone-top: #E9A0A2;
  --case-stone-held: #F3BFBE;
}
/* Terracotta — support, the widest and lowest tier, and the warmest. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--block {
  --case-stone-south: #B96B58;
  --case-stone-west: #D98E68;
  --case-stone-top: #EFB68C;
  --case-stone-held: #F7D2B2;
}
/* Cream — the treads. A walkway belongs to no tier; it is the thing between
   them, so it is the one surface with no hue of its own. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-span-top {
  background: #F7EFE1;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span-south {
  background: #C3A992;
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span-west {
  background: #DEC9B4;
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-cut .case-span-top {
  background: linear-gradient(90deg, #DEC9B4 30%, rgba(222, 201, 180, 0) 100%);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-cut .case-span-south {
  background: linear-gradient(90deg, #C3A992 30%, rgba(195, 169, 146, 0) 100%);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-cut .case-span-west {
  background: linear-gradient(90deg, #DEC9B4 30%, rgba(222, 201, 180, 0) 100%);
}

/* The sentence is a screen-space sign now. This copy stays in the tree for
   VoiceOver and for the score; drawing it on the cube is what put type through
   the stone. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-board-world.is-monument .case-node-label {
  opacity: 0;
  pointer-events: none;
}

/* The state washes invert rather than recolour.

   `is-stale`, `is-receded` and `is-unreachable` all darkened toward black,
   which is what receding means when the field is black — the piece goes back
   into the room. On a light field the same gradient is the piece coming
   forward, painted with soot. Receding is now fading toward the sky, and the
   direction of every one of these is the only thing that changed. Still painted
   into the face rather than applied as opacity, because opacity on a face makes
   a grouping context and flattens the counter-rotated sign onto the stone.

   They also weigh about half what their night counterparts did. Painting a face
   toward black keeps the face: the three faces of a block darken together and
   the ladder between them survives, so the block is still a block. Painting a
   face toward the sky washes the hue and the ladder out at the same rate, and a
   block at the old weights was a rumour. Half is enough to say "back there" and
   leaves the modelling standing. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-receded .case-prim-top,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-receded .case-prim-south,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-receded .case-prim-west {
  background-image: linear-gradient(rgba(253, 242, 230, 0.3), rgba(253, 242, 230, 0.3));
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-stale .case-prim-top,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-stale .case-prim-south,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-stale .case-prim-west {
  background-image: linear-gradient(rgba(253, 242, 230, 0.28), rgba(253, 242, 230, 0.28));
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-unreachable .case-prim-top,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-unreachable .case-prim-south,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-unreachable .case-prim-west {
  background-image: linear-gradient(rgba(253, 242, 230, 0.44), rgba(253, 242, 230, 0.44));
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-raised .case-span-top {
  background-image: linear-gradient(rgba(253, 242, 230, 0.28), rgba(253, 242, 230, 0.28));
}
/* A void is the field showing through, and the field is now bright, so its
   outline and its walls invert with everything else. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--void .case-prim-top {
  box-shadow: inset 0 0 0 1px rgba(74, 59, 82, 0.34);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--void .case-prim-south,
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--void .case-prim-west {
  background: rgba(150, 96, 74, 0.16);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-prim--void .case-node-label {
  color: var(--case-ink-dim);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
/* The seat a hovering piece will land on. A white keyline on black becomes a
   plum keyline on cream. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-proposed .case-prim-shadow,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-empty .case-prim-shadow {
  box-shadow: inset 0 0 0 max(1px, calc(1.5 * var(--case-u))) rgba(74, 59, 82, 0.3);
}
/* Unbuilt, and still a cube. A light sky wash on the faces — never opacity on
   the node — plus a dashed inset on the top, the same grammar as a contest:
   the stone is there and the writing is not. Contested wins if both apply. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-empty .case-prim-top,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-empty .case-prim-south,
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-empty .case-prim-west {
  background-image: linear-gradient(rgba(253, 242, 230, 0.22), rgba(253, 242, 230, 0.22));
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-empty:not(.is-contested) .case-prim-top::before {
  content: "";
  position: absolute;
  inset: calc(5 * var(--case-u));
  border: max(1.5px, calc(2 * var(--case-u))) dashed var(--case-ink);
  opacity: 0.55;
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   Breakage becomes drawn, not coloured.

   Amber earned its place as the one raised voice in a room of grey stone. In a
   room of terracotta it is another warm block: #B45309 against #D98E68 is close
   enough in hue and light that a broken join reads as a slightly darker stair.
   The signal has to move off hue entirely, so it moves onto line — a dashed
   edge where the stone should be continuous and does not, and a struck mark at
   the point of failure, both in the same plum as every other piece of ink.
   Dashes and a cross survive any face they land on, which is the property the
   colour has lost.

   The score keeps amber. A printed document is read on white with no hue
   ladder underneath it, and there the one warm mark is exactly right.

   Three touchpoints rather than the four scoped: the contested node's kind mark
   is now the fourth only in the exhibit, because a world cube carries no glyph
   at all since marks came off the stone. The rule is written anyway, so that no
   route back to a world mark can bring amber with it.
   --------------------------------------------------------------------------- */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-contested .case-node-mark {
  color: var(--case-ink);
}
/* A contested block is sound stone with a bad joint, so the block keeps its
   family and the joint is what is drawn: a dashed line round the face it is
   standing on. The inset amber bar goes, and with it the near-black south face
   the night palette gave it — that band is a hole on a lit floor. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-contested .case-prim-top {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-contested .case-prim-south {
  background: var(--case-stone-south);
}
/* Drawn on ::before so the state washes keep ::after and keep the background:
   a contested block that is also receded or stale must still be able to show
   both, and a mark painted into the face would take the one slot they use. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-node.is-contested .case-prim-top::before {
  content: "";
  position: absolute;
  inset: calc(5 * var(--case-u));
  border: max(1.5px, calc(2 * var(--case-u))) dashed var(--case-ink);
  opacity: 0.8;
  pointer-events: none;
}
/* A broken join is the path that should run and stops. The far end of the last
   tread is struck: a dashed edge where the next tread would have started, and a
   cross on the tread you would have been standing on. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-broken .case-span-tread:last-child .case-span-top {
  background-image: none;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-broken .case-span-tread:last-child .case-span-south {
  background-image: none;
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-broken .case-span-tread:last-child .case-span-top::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: max(2px, calc(3 * var(--case-u)));
  background: repeating-linear-gradient(
    180deg,
    var(--case-ink) 0 calc(6 * var(--case-u)),
    rgba(255, 255, 255, 0) calc(6 * var(--case-u)) calc(12 * var(--case-u)));
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-broken .case-span-tread:last-child .case-span-top::before,
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-refusing .case-span-tread:last-child .case-span-top::before {
  content: "";
  position: absolute;
  top: 50%;
  right: calc(9 * var(--case-u));
  width: calc(18 * var(--case-u));
  height: calc(18 * var(--case-u));
  margin-top: calc(-9 * var(--case-u));
  background-image:
    linear-gradient(45deg,
      transparent calc(50% - max(1px, 1.5 * var(--case-u))),
      var(--case-ink) calc(50% - max(1px, 1.5 * var(--case-u))),
      var(--case-ink) calc(50% + max(1px, 1.5 * var(--case-u))),
      transparent calc(50% + max(1px, 1.5 * var(--case-u)))),
    linear-gradient(-45deg,
      transparent calc(50% - max(1px, 1.5 * var(--case-u))),
      var(--case-ink) calc(50% - max(1px, 1.5 * var(--case-u))),
      var(--case-ink) calc(50% + max(1px, 1.5 * var(--case-u))),
      transparent calc(50% + max(1px, 1.5 * var(--case-u))));
}
/* The refusal keeps its hinge — the movement is the answer and it needs no
   colour to carry it. What lights at the far end is ink: a solid edge under the
   same cross, so a join that will not carry you and a join that is already
   broken are the same mark, one of them arriving with a flex. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-refusing .case-span-tread:last-child .case-span-top {
  background-image: linear-gradient(
    90deg,
    rgba(74, 59, 82, 0) 30%,
    rgba(74, 59, 82, 0.2) 100%);
  box-shadow: inset -3px 0 0 var(--case-ink);
}
body.case-view-open:not(.case-exhibit) #caseBoard .case-span.is-refusing .case-span-tread:last-child .case-span-south {
  background-image: linear-gradient(
    90deg,
    rgba(74, 59, 82, 0) 40%,
    rgba(74, 59, 82, 0.24) 100%);
}
/* The causeway that marks a break was already drawn rather than coloured — a
   dashed trace across the gap — so only its ink moves. */
body.case-view-open:not(.case-exhibit) #caseBoard .case-board-connectors path.is-broken {
  stroke: var(--case-ink);
  opacity: 0.55;
}

#caseBoard.is-folding .case-monument { view-transition-name: case-monument; }
::view-transition-old(case-monument),
::view-transition-new(case-monument) {
  animation-duration: 520ms;
  animation-timing-function: cubic-bezier(0.22, 0.9, 0.24, 1);
}

@media (prefers-reduced-motion: reduce) {
  .case-board-sheet,
  .case-monument,
  .case-board-body,
  .case-board-connectors,
  .case-node,
  .case-node-label,
  .case-sign,
  .case-defect,
  .case-prim-shadow,
  .case-span,
  .case-marker,
  .case-plays,
  .case-figure {
    transition: none;
  }
  /* The refusal still has to be seen, so it keeps its colour and loses only its
     movement — reduced motion is the final pose, not the absence of an answer. */
  .case-span.is-refusing { animation: none; }
  /* And the ballets do not run at all. beatsFor already collapses them to a
     single end_pose, so this is the belt to that pair of braces: nothing can
     animate even if a beat class survives a race. */
  .case-node[class*="case-beat--"],
  .case-span[class*="case-beat--"],
  [class*="case-beat--"] .case-prim-top,
  [class*="case-beat--"] .case-figure-body,
  [class*="case-beat--"] .case-figure-mark,
  [class*="case-beat--"] .case-figure-stance,
  [class*="case-beat--"] .case-node-label {
    animation: none;
  }
}
