/* =============================================================
   compass.css — Session D7 (states-lab)
   D7: tag bump only (compass unchanged; scroll.css now points
       to the D7 vellum render; compass.css loads after the game's
       own styles but D7 doesn't change any compass rules).
   D6: tag bump only (parchment re-render; no compass changes).
   D5: D4's clip-path revert (the ring was the sheet, not the compass);
       no other compass.css changes.
   D1: navy backdrop -> dark warm backdrop for the parchment
   (previous: Session C53)
   C53: #compass clips horizontally without becoming a scroll container.
   C52 used overflow-x:hidden, which in CSS forces the other axis to
   compute as auto — turning #compass into a scroll container and
   giving it its own scrollbar (Jeff saw it on the right of the disc
   whenever the mouse moved, growing when the drawer came out).
   overflow-x:clip clips the same 1-30 px phone h-scroll without a
   scroll container: overflow-y stays visible, so the drawer still
   shows below the disc.  (Chrome/Edge 90+, Safari 16+, Firefox 81+;
   in older browsers the property is ignored and the only cost is C52's
   intermittent 1-30 px phone h-scroll, which is harmless.)
   Cache: compass.css?v=c53 in index.html (compass.js stays at its
   c51 tag — no JS change this session).
   C52 (previous): the compass fills phone screens (94.5vw).
   --size was max(300px, min(86vw, 62vh, 560px)) — the 86vw term left
   the disc at 354 px on a 412 px phone (28.8 px navy gutter each side,
   ~7%).  The 86vw term is now 94.5vw: on phones the disc runs
   edge-to-edge with a 2.75% gutter each side (11 px at 412, 10.7 px
   at 390 — inside the 2%..3.5% band); 300px floor, 62vh cap and the
   560px desktop cap are untouched, so 1920x1080 still resolves to
   exactly 560 px and short phones still cap by 62vh.
   Secondary: overflow-x:hidden added to #compass.  At the larger
   phone size the rotating rings' SVG bounding boxes (86% of S,
   extending to 60.8% of S from centre at 45°) cross the disc edge
   and — with overflow visible — extend the page's horizontal scroll
   area by 1-30 px at certain rotation angles.  Clipping the compass
   container's x-overflow removes that without cutting any visible
   geometry (the rings' circles are 43% of S, well inside the 50%
   radius; the drawer extends below, not sideways).
   Cache: compass.css?v=c52 in index.html (compass.js stays at its
   c51 tag — no JS change this session).
   C51 (previous): tag bump only
   C49 (previous, states-lab): the leather ring continues
   behind the window (body v41 re-render; this file: tag bump only).
   C48 — the morph you can SEE.
   .vpTitle and .vpDesc are now width:100% and .compassViewport has no
   side padding, so each text line's overflow:hidden box IS the glass
   width: the flying letters are clipped at the glass edges instead of
   at the text's own 232 px box (the "rectangle around the text" Jeff
   saw in C47).  Everything else unchanged; the C47/C46 notes below
   still describe the assets.  Cache: compass.css/compass.js at
   ?v=c48; body.webp?v=c46, crystal-*.webp?v=c44, fittings.webp?v=c40
   unchanged.
   Previous header (C47, body v40): ROSE_TIP_R corrected to
   0.468 units (23.4% of S); the leather band now runs 22.15%..24.65% of S,
   centred on the rose's long-point tips measured against the brass rim's
   diameter D.  Crystal unchanged from v38 (c44).  Fittings stay at the v10
   image (c40 tag — unchanged this session).
   Previous header (C44, body v38 — now superseded in this file):
   crystal v38: the window frame no longer flattens the tube: the tube
   runs untouched up to CRYS_FILLET_W 0.04 units from the frame outline;
   there the glass climbs from its local surface (dome or tube) up to
   CRYS_PAD_TOP (the dome height at the frame's bottom edge +
   CRYS_PAD_RISE 0.018) with a (1 - d/W)^2 meniscus profile — no separate
   lip block, no separate meniscus.  Body v38: red leather ring
   (LEATHER_IN 0.635 .. LEATHER_OUT 0.685 = 63.5%..68.5% of the radius),
   recessed into the dial, pebble-grained (Voronoi grain LEATHER_GRAIN 90),
   matte, stopping at the window pocket.
   Previous header (C43, crystal v37 — now superseded in this file):
   crystal v37: a WIDE tube.  The roll now
   runs from the INNER tick ring (0.70) to the wall (0.90), crest at 0.81 (40.5%
   of S is the crest; the tube spans 35%..45% of S, groove a gentle dip from
   33% to 35%), so it covers the middle and outer tick rings. Lighting as in
   v36 (beams CRYS_BEAM_L/R 4.0 x 2.4 units, low at the sides, LEFT E18 /
   RIGHT E12, TRACKED to the centre; each half of the inner slope mirrors the
   beam opposite it; case lamps low in the crystal layer; ring fill 0.35) —
   unchanged. Body: v10 (C33) image — no hoods, no lamps — UNCHANGED this
   session; Fittings: v10 (C34) image — window frame + three button
   collars only — UNCHANGED this session.
   Cache: compass.css/compass.js and the crystal-*.webp URLs at
   ?v=c43; body.webp?v=c40 and fittings.webp?v=c40 KEEP their c40
   tags (assets unchanged).
   C42 (v36 — git working tree, superseded by v37): every gradient emitter
   (CrystalKey/Strip/beams) lies FLAT because transform_apply(scale) before a
   TRACK_TO constraint leaves the constraint evaluating to identity in
   Blender 4.3; v36's emitter() fixed the beams (kept scale on the object,
   tracked to centre). Key and Strip are deliberately flat — the blob and
   the streak are tuned flat.
   C33 (v27): rim bead tube; groove; roll. C32 (v26): ring emitter
   E 10, tube 0.5→0.35. --- C27 (v20 — git history) ---
   crystal re-render with v20: the window pad is a raised block the exact shape of the
   frame outline + a 0.012 lip, vertical walls, flat top flush with
   the dome at the frame's bottom edge (proud only toward the top
   corners), clipped inside the dome (never touches the rim). Crisp
   thin lip of light/shadow hugs the frame where it rises; nothing
   along the bottom; no wide shoulder, no arcs. C24 (v17) render at ceca74d;
   C26 (v19) blend-pad look superseded.
   --- C24 (layered rim, calmer — superseded) ---
   crystal re-render with v17: the C21 layered rim is back (light
   bounces inside the glass are allowed again; case lamps 55% so the
   bands stay bright but not clipped); dimmer blob/streak and the
   three scratches now scatter so they read along their length.
   --- C22 (calmer crystal, v16 render — now only in git history) ---
   calmer crystal (v16 render), hairline scratches, and: fittings
   ABOVE the crystal (mounted on the glass — z-index 6 -> 8; modeCard
   8 -> 9 so it stays above the fittings).
   --- C21 (history below) ---
   C20: rendered crystal over the dial.  The C8 CSS glass lens
   (#compass .compassGlass: radial-gradient sheen + inset shadow ring)
   is REPLACED by the two rendered layers from assets/crystal-*.webp
   (v14 tools/render_body.py, --layer crystal, 1200x1200):
   .compassGlass = transmission (black, alpha = reflected fraction,
   normal compositing, darkens what it fails to transmit) and the
   new .crystalLight = reflection (mix-blend-mode:plus-lighter,
   screen fallback), both inset:0 / 100% / z-index 7 (dark below,
   light above).  No CSS shine remains.
   Cache-bust ?v=c19 -> ?v=c20 in index.html (both compass tags).
   --- C19 (history below) ---
   C19: rendered button caps.  The .cap background is now the
   RENDERED cap assets/cap.webp (v13 tools/render_body.py, --layer
   cap, 512x512: shallow gunmetal dome, raised brass bezel,
   highlight upper-left), stretched center / 100% 100% over each
   .cap the way drawer.webp is stretched over the drawer.  The
   CSS radial-gradient face and the two INSET box-shadow entries
   that drew the brass ring + inner bevel (inset 1px white,
   inset 1.5px #9a7a36) are REMOVED; the outer 0 3px 6px drop
   shadow stays.  The pressed/selected box-shadow rules lose their
   INSET ring/bevel entries the same way but KEEP their cyan ring
   (inset 0 0 0 3.5px rgba(70,220,255,.85)) and outer glow, plus
   the color:#bff3ff lettering change.  Lettering (color / font /
   text-shadow), size, border-radius 50%, transitions, :active
   scale(.94), and the .selected behaviour are all unchanged.
   Cache-bust ?v=c17 -> ?v=c19 in index.html (both compass tags).
   --- C17: constant glow, no click-in.  (history below) ---
   C17: constant glow, no click-in.  The glow is a drop-shadow
   filter on .vpTitle / .vpDesc at ALL times — weaker at rest
   (title .35em / desc .3em alpha), stronger while .morphing
   (title .6 / desc .45 alpha) — transitioned over .35s, so the
   ONLY thing that changes at the end of a morph is the glow
   strength, smoothly.  .morphing no longer sets `color:` or
   `text-shadow:` (both were sources of the end-of-morph shift and
   the description colour jump); it carries just the stronger
   filter.  .vpTitle.text-shadow is now `none` (the C16 .45em blue
   halo is replaced by the filter), the C13/C16 colour + text-shadow
   transition is replaced by `transition:filter .35s ease-out`, and
   the will-change on the character spans is dropped (they no
   longer composite after a settle cancels their animation).  The
   character spans STAY (span rule keeps display:inline-block); see
   compass.js C17 for the settle that never rebuilds them.
   Everything else (viewport geometry, rings, rose, drawer, .cap,
   #startBtn) as C15/C16.
   =============================================================
   compass.css — Session C16 (states-lab)
   C16: unclipped morph glow — WHILE MORPHING the text carries NO
   text-shadow at all; the C13/C15 .morphing text-shadow rules are
   REPLACED by a drop-shadow filter on the (static, non-moving)
   .vpTitle / .vpDesc element itself, whose filter region grows to
   fit the shadow.  Cause fixed: the glow was a text-shadow
   inherited by the ANIMATED character spans — each span is a
   composited layer that clips its own shadow to its own box,
   producing the hard-edged lighter rectangle behind each word.
   The new spans carry .vpTitle.morphing .w span /
   .vpDesc.morphing .w span { text-shadow: none; } so nothing on a
   moving layer is shadowed.  The resting .vpTitle / .vpDesc
   rules (their normal text-shadow, color, the .15s transition)
   are UNCHANGED.
   Description string: index.html capitals card data-desc is now
   "Match all 50 state capitals\nwith their states" (one \n, no
   period).  Cache: compass.css?v=c16 (compass.js tag stays c15).
   Everything else as C15.
   =============================================================
   compass.css — Session C15 (states-lab)
   C15: drawer travel PULLED BACK — .compass.hasSelection .compassDrawer
   top 84% -> 80% (retracted 53% unchanged) so the strip of brass
   between the bar frame's top edge and the case edge is only a
   HAIR wider than the brass between the bar frame's sides and the
   lip; #startScreen bottom padding 0.26 -> 0.22 of --size to match.
   assets/drawer.webp is NOT re-rendered (the v11 render is kept;
   the arc changes by under 1 px at the new travel).  Lettering:
   the -webkit-text-stroke line is REMOVED from .cap and #startBtn
   (bright streaks inside some glyphs on Android chips — the dark
   upper edge now comes only from the 0 0 1px first shadow stop);
   the 4-stop text-shadow GAINS that 0 0 1px rgba(0,0,0,.9) stop
   as its first entry (all other colors/offsets unchanged).
   Soft morph: the C14 span rule ".vpTitle span, .vpDesc span
   { will-change: transform, opacity, filter; }" is DELETED and
   REPLACED by .vpDesc { white-space: pre-line; } (hard line
   breaks in the description strings render as line breaks), plus
   nowrap word spans .vpTitle .w / .vpDesc .w { display:inline-block;
   white-space:nowrap; } and .vpTitle .w span / .vpDesc .w span
   { display:inline-block; will-change:transform, opacity; }
   (no filter — the blur filter clipped the glow to a hard-edged
   rectangle; the morph now animates transform+opacity only).
   Everything else as C14: drawer 46% x 40%, retracted top 53%,
   transition 520ms cubic-bezier(0.22,1,0.36,1); strike palette
   #d7dbe2 with the 4-stop shadow; drawer.webp?v=c14 (UNCHANGED
   from C14 — the C15 bump only hits compass.css/compass.js);
   navy page background; hidden header h1; z-order unchanged.
   =============================================================
   compass.css — Session C14 (states-lab)
   C14: the drawer travels FURTHER — .hasSelection top 76% -> 84%
   (retracted 53% unchanged), redrawn with the v11 render script
   (tools/render_body.py, UNMODIFIED: DRW_EXT_TOP 0.84, arc radius
   0.74 S) so the curved bottom stays concentric at the new travel;
   assets/drawer.webp?v=c14, drawer transition 520ms (same
   cubic-bezier(0.22, 1, 0.36, 1)); #startScreen bottom padding
   18% -> 26% of --size of room under the disc; the C13 glyph
   "digitize" is REPLACED by a soft morph (compass.js: old chars
   drift apart/blur/fade 260 ms, new chars sweep in from the left
   and right edges 380 ms + 240 ms stagger, Web Animations API,
   reduced motion = instant) — its flare is SOFTENED (title
   0.9em glow alpha .6 -> .35, desc .4em alpha .6 -> .35) and
   .vpTitle/.vpDesc spans get will-change:transform, opacity,
   filter.  Everything else as C12/C13.
   C13: the viewport title and desc TEXT MORPHED when it changed
   (glow flared via .morphing rules + 0.15s color/text-shadow
   transitions) — superseded by the C14 soft morph above.
   C12: the drawer bottom is now a CURVE concentric with the case
   (v10 render: arc bottom, bar pocket moved up + narrowed, assets
   /drawer.webp?v=c12).  Deeper struck lettering on #startBtn + .cap:
   #d7dbe2 unchanged, -webkit-text-stroke 0.5px rgba(0,0,0,.75),
   four-stop text-shadow (0 -2px 0 rgba(0,0,0,.95),
                          0 -1px 0 rgba(0,0,0,.7),
                          0  1px 0 rgba(255,255,255,.28),
                          0  2px 1px rgba(255,255,255,.12)).
   Bar repositioned into the new pocket: left 13.04% / width 73.91%
   / bottom 16.25% / height 22.5% (of the 46%x40% drawer box).
   Deep navy page background: body radial-gradient(ellipse at 50% 35%,
   #0c142c, #070d1e 55%, #03060f 100%) fixed — loads after the game's
   own styles so it wins; #gameScreen/#startScreen backgrounds
   UNCHANGED.  Header: title + icon hidden (header h1 display:none),
   bar chrome removed (transparent / no shadow / no border) yet the
   Change Mode button + game badge stay.  drawer.webp cache-busted
   to ?v=c12 (body.webp + fittings.webp stay ?v=c10).
   C11: the drawer is a TRAY (v9 render: raised lip around the
   edge + raised brass frame around the bar pocket, assets
   /drawer.webp?v=c11).  The .cap rest transform is now `none`
   (C10 was translateY(-4%), which sat the cap HIGH in its
   collar); pressed / selected is scale(.94) only (no
   translateY).  Etched lettering on all four buttons:
   #d7dbe2 + -webkit-text-stroke 0.35px rgba(0,0,0,.55) +
   text-shadow 0 -1px 0 rgba(0,0,0,.9),
                     0  1px 0 rgba(255,255,255,.22) — a dark
   upper edge and a faint lit lower edge that reads as cut
   into the face, on both #startBtn and .cap.  Selected .cap
   keeps #bff3ff (cyan) but the same stroke + shadows.
   drawer.webp cache-busted to ?v=c11 (body.webp and
   fittings.webp UNCHANGED at ?v=c10).
   C10: the Start drawer is the RENDERED brass slide
   (assets/drawer.webp, C10 render: brass drawer 552x480)
   instead of the CSS brass plate; drawer 1, body 2, ring 3,
   rose 4, viewport 5, fittings 6, glass 7, modeCard 8 (z-order
   UNCHANGED).
   C9: modeCard diameter 17% (gap closed), brass push-bar.
   C8: fittings layer (brass collars + window frame, rendered in
   C7) now sits between the glass and the buttons; buttons are
   SEAT + latching CAP (the cap sinks into the rendered brass
   seat on press/select); rose is faceted (16 half-facets +
   outline); rings pulled inward (86/78/70).
   C7: body + fittings rendered as separate layers
   (assets/body.webp + assets/fittings.webp, both 1200x1200,
   cached as ?v=c7).
   C6: brass case, dark pitted gunmetal dial, button collars
   and window frame from the Blender render.
   B12: inset bevel on the viewport + fixed height 20%.
   Geometry contract:
     --size: max(300px, min(86vw, 62vh, 560px)).
     Every element inside #compass is positioned by PERCENTAGE of
     --size.  No pixel minimums or clamp()-with-px-minimums inside
     the compass.
   Stacking (explicit, every child positioned with a z-index):
     .compassDrawer    1   .compassFittings  6  (C7 render: brass
     .compassBody      2     collars + window
     .compassRing      3     frame)
     .compassRose      4   .compassGlass     7
     .compassViewport  5   .modeCard         8  (C8: cap latches
                                    inside the rendered seat; glass
                                    at 7)
     .rotateNotice  30 (fixed overlay, outside
                          #compass, only for landscape
                          where the compass is hidden)
   ============================================================= */

/* ---- container: column layout, centered; bottom padding = drawer room ---- */
#startScreen{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:0;
  width:100%;
  /* 22% of --size of room beneath the disc for the slide-out drawer
     (C15: 0.26 -> 0.22 as the drawer travel pulled back 84% -> 80%).
     The extended drawer reaches top 80% + height 40% = 120% of the
     disc (20% past its bottom edge).  The compass is centered in the
     flex area, so the extra space needed below is roughly 0.5*size;
     per the contract we keep at least 22% + slack and let the
     flex centering handle the rest (verified 1920x1080, 390x844,
     360x640). */
  padding-bottom:calc(var(--size, 300px) * 0.22 + 8px);
  background:none;
  border:none;
  border-radius:0;
  box-shadow:none;
  max-width:100vw;
  overflow:visible;
}

/* ---- compass disc: always a perfect square, never shrinks inside the flex column ---- */
#compass{
  /* D11: --size vertical term 100vh - 275px (rolls ~100 px from each edge + 8 px clearance
     + #startScreen's constant 74 px padding + its 20 px top offset).
     #startScreen's padding uses var(--size)'s 300px fallback (constant 74 px)
     since --size is scoped to #compass, not an ancestor.
     Derivation: S + 74 <= 100vh - 201  →  100vh - S >= 275.
     S table: 1920x1080=560(cap) 1536x864=560(cap) 1366x768=493 1280x720=445 phone=94.5vw
     Drawer overlap of the bottom roll when a mode is selected is allowed (Jeff). */
  --size:max(300px, min(94.5vw, calc(100vh - 275px), 560px));
  width:var(--size);
  height:var(--size);
  position:relative;
  isolation:isolate;   /* z-order contained: children only compete with each other */
  /* D45 (iOS): explicit stacking context on the engine side as well —
     position (already) + z-index + isolation:isolate, so #compass's
     children can never be promoted or reordered by a parent's or
     sibling's paint-order differences. Children already use
     non-negative z-indices in paint order (drawer 1, body 2, rings 3,
     rose 4, viewport 5, crystal 7, fittings 8, modeCard 9).
     No negative z-index anywhere in #compass. No 3D layering:
     transform-style:flat is set here as a guard; nothing uses
     preserve-3d or translateZ. */
  z-index:1;
  transform-style:flat;
  border-radius:50%;
  aspect-ratio:1 / 1;
  flex-shrink:0;
  /* C53: the scribed rings rotate inside #compass; their SVG bounding
     boxes occasionally cross the disc edge on the 45-degree diagonals.
     overflow-x:clip (not hidden) clips that overflow without turning
     #compass into a scroll container — when one axis is clip the other
     stays visible, so the drawer still shows below the disc and no
     scrollbar can ever appear on #compass.  (Chrome/Edge 90+, Safari
     16+, Firefox 81+; in older browsers the property is ignored and
     the only cost is C52's intermittent 1-30 px phone h-scroll.) */
  overflow-x:clip;
}

/* ---- body: the rendered case + dial (C7 render, cached ?v=c7) ----
   The whole face/rim/case is the Blender render
   (assets/body.webp).  The layer is a transparent window onto the
   rendered disc.  The brass collars and window frame do NOT live
   in this layer — they are the separate fittings layer (C8) so the
   rings pass UNDER the collars and frame. */
#compass .compassBody{
  position:absolute;
  inset:0;
  box-sizing:border-box;
  border-radius:50%;
  background:url("assets/body.webp?v=c49") center / 100% 100% no-repeat;
  background-color:transparent;
  border:none;
  box-shadow:none;
  outline:none;
  filter:none;   /* D2: no filter on #compass — the shadow is a static element in the parchment layer (see scroll.css) */
  z-index:2;   /* above the drawer (1), below the rings (3) */
}

/* ---- the rose (SVG 16-vertex star, faceted — C8) — slow spin ----
   B4: slender 16-vertex star.  Long (N/E/S/W) tips r40, short
   diagonal tips r26, all inner corners at r9 (22.5deg spacing).
   C8: the single flat star path is replaced by 16 triangular
   facets (alternating light #e3c46e / dark #8c6a26) + one outline
   path (original 16-vertex d, fill none, stroke #2a1d08 w 0.6).
   The <svg> has 21 children: 16 facets + 1 outline + 4 <text>.
   Square 62% of --size, centered via top/left 19%.  The <svg> uses
   viewBox 0 0 100 100, so 1 unit = 1% of the rose = 0.62% of --size.
   N/E/S/W letters: font-size 8, bold, fill #f3e3b3, centered
   (text-anchor middle + dominant-baseline central) on the cardinal
   axes at r46, i.e. just beyond each long tip. */
#compass .compassRose{
  position:absolute;
  top:19%;
  left:19%;
  width:62%;
  height:62%;
  transform:rotate(0deg);
  transform-origin:50% 50%;
  pointer-events:none;
  animation:cmpSpin 120s linear infinite;
  z-index:4;
}
@keyframes cmpSpin{
  from{ transform:rotate(0deg); }
  to  { transform:rotate(360deg); }
}
/* Three scribed rings BEHIND the rose (z:3).  Each .compassRing is a
   centered square with an inline SVG (viewBox 0 0 100 100): a thin
   circle r48 (stroke #8d7238 w0.6) + a second r48 circle whose
   stroke-dasharray forms tick marks (stroke #b8944a w3, 360/n so
   each dash is 1 tick and each gap n-1), + one short radial index
   mark at the 12-o'clock position.  The rose (z4) occludes the
   center, so only the annulus from ~35% to ~43-48% of --size is
   visible.  C8: sizes pulled inward to 86/78/70 (were 92/84/76) so
   the outer ring does NOT ride on the rim, and the index mark
   (y1 0.3 -> y2 4.2) covers the tick band only (no longer poking
   into the next inner ring). */
#compass .compassRing{
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  pointer-events:none;
  z-index:3;   /* above the body (2), below the rose (4) */
}
#compass .compassRing svg{
  width:100%;
  height:100%;
  display:block;
}
#compass .compassRing .ringTicks{
  fill:none;
  stroke:#b8944a;
  stroke-width:3;
}
#compass .compassRing .ringIndex{
  stroke:#f1d68a;
  stroke-width:3;
  stroke-linecap:round;
}
/* ringA: 86% of --size, 90s clockwise (C8: was 92%) */
#compass .compassRing.ringA{
  width:86%;
  height:86%;
  animation:cmpRingA 90s linear infinite;
}
/* ringB: 78% of --size, 140s counter-clockwise (C8: was 84%) */
#compass .compassRing.ringB{
  width:78%;
  height:78%;
  animation:cmpRingB 140s linear infinite;
}
/* ringC: 70% of --size, 200s clockwise (C8: was 76%) */
#compass .compassRing.ringC{
  width:70%;
  height:70%;
  animation:cmpRingC 200s linear infinite;
}
@keyframes cmpRingA{
  from{ transform:translate(-50%,-50%) rotate(0deg); }
  to  { transform:translate(-50%,-50%) rotate(360deg); }
}
@keyframes cmpRingB{
  from{ transform:translate(-50%,-50%) rotate(0deg); }
  to  { transform:translate(-50%,-50%) rotate(-360deg); }
}
@keyframes cmpRingC{
  from{ transform:translate(-50%,-50%) rotate(0deg); }
  to  { transform:translate(-50%,-50%) rotate(360deg); }
}
@media (prefers-reduced-motion: reduce){
  #compass .compassRose,
  #compass .compassRing{ animation:none; }
}

/* ---- viewport: INSET bevel — the window is CUT INTO the face (B12) ----
   C8: the window reads as GLASS OVER A DARK POCKET, not a plate.
   The solid dark two-layer background is replaced by a single
   165deg white-sheen gradient over a semi-opaque dark base
   (rgba(5,7,11,.80)) so the rendered pocket and frame shadow show
   through; the three inset shadows are reduced to ONE top inset
   shadow (the near wall of the recess); border stays 0.  Height is
   FIXED at 20% of --size with overflow hidden; text is vertically
   centered and clamped to fit (title 1 line, desc 2 lines).
   z-index 5 — below the fittings (6) and the glass (7), above the
   rose (4).  Top 17% and width 56% UNCHANGED. */
#compass .compassViewport{
  position:absolute;
  top:17%;
  left:50%;
  transform:translateX(-50%);
  width:56%;
  height:calc(var(--size) * 0.20);
  min-width:0;
  box-sizing:border-box;
  text-align:center;
  padding:calc(var(--size) * 0.012) 0;   /* C48: no side padding — the text
                 lines span the glass so their own overflow boxes
                 reach the glass edges (letters fly to/from them) */
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:calc(var(--size) * 0.006);
  overflow:hidden;
  background:
    linear-gradient(165deg, rgba(255,255,255,0.10) 0%,
      rgba(255,255,255,0.03) 38%, rgba(255,255,255,0) 55%),
    rgba(5,7,11,0.80);
  border:0;   /* C6: rendered brass frame; the rendered pocket +
                 frame shadow show through the semi-opaque glass */
  border-radius:calc(var(--size) * 0.045);
  box-shadow:
    inset 0 calc(var(--size) * 0.012) calc(var(--size) * 0.02)
      rgba(0,0,0,0.7);   /* C8: single near-wall shadow */
  z-index:5;
  pointer-events:none;
}
#compass .vpTitle{
  font-size:calc(var(--size) * 0.055);
  font-weight:600;
  letter-spacing:0.05em;
  color:#d6ecff;
  text-shadow:none;
  filter:drop-shadow(0 0 1px rgba(0,0,0,.9))
          drop-shadow(0 0 .35em rgba(120,200,255,.35));
  transition:filter .35s ease-out;
  margin:0;
  line-height:1.15;
  white-space:nowrap;     /* .vpTitle nowrap per spec */
  overflow:hidden;
  text-overflow:ellipsis;
  width:100%;             /* C48: the line's box IS the glass width, so its
                             overflow clip sits at the glass edges, not at
                             the text's own edges */
  max-width:100%;
}
#compass .vpDesc{
  font-size:calc(var(--size) * 0.036);
  color:#a9bfd8;
  text-shadow:none;
  filter:drop-shadow(0 0 1px rgba(0,0,0,.9))
          drop-shadow(0 0 .3em rgba(120,190,240,.25));
  transition:filter .35s ease-out;
  line-height:1.15;
  width:100%;             /* C48: see .vpTitle */
  max-width:100%;
  /* two lines max: clamp to 2 lines via -webkit-line-clamp */
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}
/* C17: the character spans stay in place after a morph — the
   resting text IS the spans, so nothing is rebuilt (nothing
   shifts) when the animation ends and .morphing is removed.
   .morphing therefore touches ONLY the glow: a drop-shadow
   filter at all times (weaker at rest, stronger while morphing),
   transitioned .35s.  The only thing that changes at the end is
   the glow strength — smoothly. */
#compass .vpTitle.morphing{
  filter:drop-shadow(0 0 1px rgba(0,0,0,.9))
          drop-shadow(0 0 .35em rgba(120,200,255,.6));
}
#compass .vpDesc.morphing{
  filter:drop-shadow(0 0 1px rgba(0,0,0,.9))
          drop-shadow(0 0 .3em rgba(120,190,240,.45));
}
/* C15: description hard line breaks + word-safe morph spans.
   .vpDesc: white-space pre-line so the "\n" inside the data-desc
   strings renders as a real line break (spaces still collapse).
   .w: one no-break word (inline-block, white-space:nowrap) —
   the viewport text can only ever wrap at spaces.
   spans: one inline-block span per character inside a .w, with
   will-change for transform+opacity (NO filter — the C14 blur
   keyframe clipped the glow to a hard-edged rectangle, so it is
   gone entirely from both compass.css and compass.js). */
#compass .vpDesc{ white-space:pre-line; }
#compass .vpTitle .w, #compass .vpDesc .w{
  display:inline-block;
  white-space:nowrap;
}
#compass .vpTitle .w span, #compass .vpDesc .w span{
  display:inline-block;
}

/* ---- fittings: the rendered brass collars + window frame (C8; C22: ABOVE the crystal) ----
   C22: the window frame's top corners were being swallowed by the
   crystal's rim: the fittings layer (window frame + button collars)
   now moves ABOVE the crystal, as if mounted on the glass itself.
   z-index 6 -> 8.  The C7 render (assets/fittings.webp) still
   carries the brass button collars and the window frame as a
   transparent PNG over the dial; it now sits above the crystal
   layers (7), below the buttons/modeCard (9).  pointer-events none
   — it never intercepts clicks. */
#compass .compassFittings{
  position:absolute;
  inset:0;
  background:url("assets/fittings.webp?v=c40") center / 100% 100% no-repeat;
  pointer-events:none;
  z-index:8;
}

/* ---- glass lens: crystal over the display window (B10/C8) ----
  The glass over the dial is RENDERED (C20): assets/crystal-dark.webp
  (v14, tools/render_body.py --layer crystal, 1200x1200).  This element
  is the TRANSMISSION layer: a black image whose alpha is the fraction
  of light the glass reflects away — what it fails to transmit darkens
  the rose and rings under it.  The REFLECTION layer lives on
  .crystalLight (below), composited with plus-lighter.  z-index 7
  (was the CSS-shine lens in C8): same slot in the stack — the two
  crystal layers share z-index 7, dark first, light on top.
  pointer-events none so it never intercepts clicks. */
#compass .compassGlass{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  z-index:7;
  pointer-events:none;
  border-radius:0;
  background:url(assets/crystal-dark.webp?v=c44) center / 100% 100% no-repeat;
}
/* C20: reflection layer — the rendered highlights (soft blob left of
   centre, thin streak lower-right).  plus-lighter adds it onto the
   layers below; where the image is black it changes nothing.
   Screen fallback for engines without plus-lighter. */
#compass .crystalLight {
  position:absolute; inset:0;
  width:100%; height:100%;
  z-index:7; pointer-events:none;
  background: url(assets/crystal-light.webp?v=c44)
    center / 100% 100% no-repeat;
  mix-blend-mode: plus-lighter;
}
@supports not (mix-blend-mode: plus-lighter) {
  #compass .crystalLight { mix-blend-mode: screen; }
}

/* ---- mode buttons: ringed around the compass (C8: seat + cap) ----
   B3: ring radius is 40% of --size.  Button diameter is 16% of
   --size.  No pixel minimums.  The transform chain rotates around
   the disc center and pushes each button out to its ring
   position, then counter-rotates so the button stays upright.
   C8: the single flat dark cap with a brass border (C6) is
   replaced by a SEAT (the button itself: dark recessed well) + a
   CAP (an inner .cap span: the movable face).  The brass collar
   around each button is RENDERED in assets/fittings.webp (C7);
   the CSS border is GONE.  Pressing (or selecting) the button
   moves ONLY the .cap down into the rendered brass seat
   (translateY(2%) scale(.94)) — the seat itself (and the button's
   position on the ring) never moves.  The selected cyan ring
   lives ON THE CAP (inset box-shadows + a small outer glow) and
   no longer spills out over the collar.  The :focus-visible ring
   is an outline on the seat (inside its rim).  No shadow
   extends past 49.5% of --size from center (button center at 40%
   with radius 8% + the cap's inset ring). */
#compass .modeCard{
  position:absolute;
  top:50%;
  left:50%;
  /* beat the old .modeCard { min-width:190px } and flex sizing */
  min-width:0;
  max-width:none;
  width:calc(var(--size) * 0.17);
  height:calc(var(--size) * 0.17);
  aspect-ratio:1 / 1;
  transform:
    translate(-50%,-50%)
    rotate(var(--a))
    translateY(calc(var(--size) * -0.40))
    rotate(calc(var(--a) * -1));
  margin:0;
  /* C8: the SEAT — a dark recessed well.  No border (brass collar
     is rendered), no old dark-cap gradient, no old cyan outer ring.
     display:grid + place-items:center centers the .cap;
     overflow:visible so the cap's drop shadow reads as depth. */
  padding:0;
  display:grid;
  place-items:center;
  overflow:visible;
  background:#0a0d13;
  border:0;
  border-radius:50%;
  box-shadow: inset 0 2px 5px rgba(0,0,0,0.95),
              inset 0 0 0 1px #000;
  color:#e8ebef;
  font-size:calc(var(--size) * 0.05);
  font-weight:700;
  line-height:1;
  -webkit-tap-highlight-color:transparent;
  cursor:pointer;
  z-index:9;   /* C22: top of the stack (body 2, rings 3, rose 4,
                 viewport 5, crystal 7, fittings 8) */
  will-change: transform;
}
/* C8: the CAP — the movable face.  84% of the seat, brass inset
   ring, dark gunmetal radial gradient, a drop shadow that reads
   as a raised cap sitting ABOVE the seat.  On press/select it
   sinks (translateY(2%) scale(.94)) and the shadow flattens —
   the latched state.  The cap never covers the collar. */
#compass .modeCard .cap{
  width:84%;
  height:84%;
  border-radius:50%;
  display:grid;
  place-items:center;
  font:inherit;
  background:url("assets/cap.webp?v=c19") center / 100% 100% no-repeat;
  box-shadow: 0 3px 6px rgba(0,0,0,0.75);
  transform:none;   /* C11: cap rests centered (was translateY(-4%) HIGH) */
  color:#d7dbe2;   /* C11: etched lettering (C15: stroke removed) */
  text-shadow:0 0 1px rgba(0,0,0,.9),
              0 -2px 0 rgba(0,0,0,.95),
              0 -1px 0 rgba(0,0,0,.7),
              0  1px 0 rgba(255,255,255,.28),
              0  2px 1px rgba(255,255,255,.12);
  transition:transform .12s ease, box-shadow .12s ease,
             filter .12s ease;
}
#compass .modeCard:hover .cap{ filter:brightness(1.15); }
#compass .modeCard:active .cap,
#compass .modeCard.selected .cap{   /* pressed / latched */
  transform:scale(.94);   /* C11: scale only, no translateY — cap stays centered */
  box-shadow: 0 0 0 rgba(0,0,0,0);
}
#compass .modeCard.selected .cap{
  box-shadow: 0 0 0 rgba(0,0,0,0),
              inset 0 0 0 3.5px rgba(70,220,255,0.85),
              0 0 10px rgba(70,220,255,0.45);
  color:#bff3ff;
}
#compass .modeCard:focus-visible{
  outline:2px solid rgba(70,220,255,0.9);
  outline-offset:-4px;
}

/* ---- slide-out Start drawer ----
   C10: the drawer is the RENDERED brass slide (assets/drawer.webp).
   Width 46% of --size, height 40% of --size (was 17% in C9),
   centered.  RETRACTED: top 53%.  EXTENDED (#compass.hasSelection):
   top 76% — the slide extends below the disc bottom by exactly
   (76% + 40% - 100%) = 16% of --size.  Drawn BEHIND the compass
   body (z:1 vs body z:2); z-order unchanged.  The C9 CSS brass
   plate (gradient, border-radius, box-shadow) is REMOVED — the
   render carries all the brass.  The Start bar sits in the
   rendered milled pocket (see #startBtn below). */
#compass .compassDrawer{
  position:absolute;
  top:53%;
  left:50%;
  transform:translateX(-50%);
  width:calc(var(--size) * 0.46);
  height:calc(var(--size) * 0.40);
  pointer-events:none;
  transition:top 520ms cubic-bezier(0.22, 1, 0.36, 1);
  z-index:1;   /* below the body, so it appears from BEHIND the disc */
  overflow:visible;
  background:url("assets/drawer.webp?v=c14") center / 100% 100% no-repeat;
  will-change: transform;   /* D47: own compositor layer — iOS painted the drawer under #constitution's layer after the slide (see NOTES SAFARI / iOS) */
}
/* When any .modeCard is .selected, compass gets .hasSelection and the drawer slides out. */
#compass.hasSelection .compassDrawer{
  top:80%;   /* C15: 84% -> 80% — brass band above the bar frame now a hair wider than the band beside it */
  pointer-events:auto;
}
@media (prefers-reduced-motion: reduce){
  #compass .compassDrawer{ transition:none; }
}

/* startBtn is the Start bar set DOWN INTO the rendered milled
   pocket of the brass drawer slide (C10).  It is positioned in
   PERCENTAGES OF THE DRAWER BOX: left 10.87%, width 78.26%,
   bottom 8.75%, height 22.5% — matching the pocket geometry in
   assets/drawer.webp (pocket spans x 10.87%..89.13%,
   y 69.58%..92.08% of the image).  The C9 outer drop shadow is
   GONE — the bar is inside the pocket, so its shadow is the
   pocket's own (inset top shadow + brass bezel rim).  Scoped to
   #compass .compassDrawer so the game's OTHER .primary buttons
   (#playAgainBtn, #reviewRestartBtn) keep their blue. */
#compass .compassDrawer #startBtn{
  box-sizing:border-box;
  min-width:0;
  min-height:0;
  margin:0;   /* C10: kill the old .primary UA/margin so the pocket fit is exact */
  padding:0;
  font-size:calc(var(--size) * 0.045);
  font-weight:700;
  line-height:1;
  overflow:hidden;
  white-space:nowrap;
  text-overflow:ellipsis;
  cursor:pointer;
  position:absolute;
  left:13.04%;
  width:73.91%;
  bottom:16.25%;
  height:22.5%;
  background:radial-gradient(ellipse at 40% 30%, #3b4457 0%,
    #1a2030 60%, #0d1119 100%);
  border:0;
  border-radius:calc(var(--size) * 0.02);
  box-shadow:inset 0 2px 5px rgba(0,0,0,0.85),
             inset 0 0 0 1.5px #9a7a36;   /* C10: inside the pocket — no outer drop */
  color:#d7dbe2;   /* C11: etched lettering (C15: stroke removed) */
  text-shadow:0 0 1px rgba(0,0,0,.9),
              0 -2px 0 rgba(0,0,0,.95),
              0 -1px 0 rgba(0,0,0,.7),
              0  1px 0 rgba(255,255,255,.28),
              0  2px 1px rgba(255,255,255,.12);
  transition:transform .12s ease, box-shadow .12s ease,
             filter .12s ease, color .12s ease;
}
#compass .compassDrawer #startBtn:hover:not(:disabled){
  filter:brightness(1.15);
  color:#bff3ff;
}
#compass .compassDrawer #startBtn:active:not(:disabled){
  transform:translateY(1px);   /* C10: sink 1px into the pocket */
  filter:brightness(0.9);
}
#compass .compassDrawer #startBtn:disabled{
  opacity:0.55;
}
#compass .compassDrawer #startBtn:focus-visible{
  outline:calc(var(--size) * 0.006) solid #7fd8ff;
  outline-offset:calc(var(--size) * 0.006);
}

/* ---- rotate-notice (landscape phones) ----
   Hidden by default.  Shown (centered) only when:
   orientation:landscape AND max-height 500px.  It is a fixed overlay
   OUTSIDE #compass (the compass is display:none in that state), so
   nothing in #compass competes with its z-index. */
#startScreen .rotateNotice{
  display:none;
  position:fixed;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  padding:20px 28px;
  font-size:15px;
  font-weight:600;
  color:#e8ecf4;
  background:rgba(14,17,23,0.90);
  border:1px solid rgba(255,255,255,0.10);
  border-radius:12px;
  text-align:center;
  z-index:30;
}
@media (orientation: landscape) and (max-height: 500px){
  #startScreen #compass{ display:none; }
  #startScreen .rotateNotice{ display:block; }
}

/* ---- C12: page background (compass.css loads after the game's own
   <style>, so this body rule wins) ----
   Deep navy radial wash over the whole page: a little brighter at the
   upper-centre (behind the compass) falling to near-black at the edges.
   The game sets a background on #gameScreen / #startScreen — those are
   LEFT ALONE per the C12 contract; only body changes. */
body{
  /* D1: navy backdrop replaced by a dark warm brown-black (walnut/soot),
     a little lighter toward the centre so the brown sheet sits on it
     naturally.  scroll.css (loaded after this file) sets the same
     value, so this is a safety net for the brief moment before
     scroll.css is parsed. */
  background:radial-gradient(ellipse at 50% 40%,
    #2a211a 0%, #17110c 58%, #0a0806 100%) fixed;
}

/* ---- C12: header — hide the title + icon, keep the bar ----
   The Change Mode button (#changeModeBtn) and the game badge
   (#progressBadge) stay in place; only the title/icon block
   (header h1) and the bar's own chrome go. */
header{
  background:transparent;
  box-shadow:none;
  border:0;
}
header h1{
  display:none;
}
