/* ==========================================================================
   Wandernote — REALMS OF LIGHT  (css/realm.css)
   Round 4. The app stops being panels and becomes a world: eight painted
   realms, notes hanging in them as vessels of light, and writing that happens
   ON the scene instead of on paper.

   Loads after css/app.css and css/atmosphere.css so the world rules out-order
   the page/paper rules they replace at equal specificity — there is not one
   !important in this file. Everything that is CHROME takes its colour from
   tokens.css; the paintings in app.html are the only place literal art values
   live (spec: scene-internal art is exempt).

   Contents
     0. Z-SCALE + THE VEIL CONTRAST DERIVATION   (read 0.B before touching alpha)
     1. #world geometry, the eight scenes, the hour veils
     2. World mode: the chrome whispers, the editor renders over the realm
     3. The veil of light, the prose in it, the sheen
     4. Vessels (tiers, states, birth, opening, labels)
     5. Portals + scene crossfade
     6. Rituals (the opening flare, the sealing sigil, the dim)
     7. Enchanted reading
     8. The compass + its radial menu
     9. The realms panel
    10. The Ledger drawer
    11. Hush
    12. The round-1/2 overlays, over the world
    13. Responsive
    14. Reduced motion
    15. Wayfinding — the landmark, the Shelf, the realm ring, the carried vessel
   ==========================================================================

   0.A  Z-SCALE INSIDE #world
   app.css documents --z-base:0 … --z-toast:1000 for the app shell. #world sits
   at --z-world (below .workspace), and everything below is local to it: #world
   carries a z-index, so it forms a stacking context and none of these numbers
   can ever collide with the app scale.

        scene            0      the painting (img over SVG)
        sky              1      moon, shooting stars
        aurora           2      ribbons
        canvas           3      particles (rain, snow, stars, embers, motes)
        ground           4      blossoms sprouting along the bottom band
        fog              5      drifting bands
        flash / ripple   6      lightning, water rings
        veil            10      the haze the words are read on
        sheen           11      the slow diagonal light sweep
        portal          14      travel rings
        reading         15      the realm dims while a note reads itself
        ritual          16      the opening lantern flare

   0.A2  WHY #vessels IS NOT IN THAT LIST  (cross-fix P1 — a vessel has to be
         clickable, and the stacking above made that impossible)

   The scenery belongs BEHIND the app: #world is --z-world (-1), so the words
   are never read through a painting. But a vessel is not scenery — it is the
   only way a mouse opens a note from the realm, and inside a stacking context
   at -1 no local z-index can lift it out. `#vessels { z-index: 8 }` therefore
   put every vessel under #editor-pane (z-index 1 in world mode) and under the
   56px .chrome row: `elementFromPoint` at a vessel's own centre returned
   #note-title or .chrome, and only the keyboard could open a note.

   So #vessels is the ONE layer of the world that lives in the app's scale
   instead of the world's, and in app.html it is a sibling of #world rather
   than a child of it (realm.js finds it with getElementById, so nothing in the
   controller moved). Its own box is still the viewport, still clipped, still
   pointer-transparent; only the .vessel buttons take a pointer.

        #world                  -1     scenery + the veil + the rituals
        .workspace / pane        1     the prose is read ON the world
        #vessels                39     ABOVE the pane, BELOW the chrome
        .chrome                 40     its controls outrank a vessel
        #shelf                  50     the landmark's panel, over the sky
        #compass, popovers      60
        #ledger + its scrim     99/100
        #realm-ring            106     the top of #app — over the Ledger and the
                                       compass. It cannot out-paint the round-1
                                       overlays (siblings of an isolated #app),
                                       so the realms panel steps aside for it
        a carried vessel       108     the one thing above the ring is the thing
                                       being dropped into it
        toasts                1000     unchanged, and still the last word
        #ledger + its scrim     99/100 the drawer covers the sky

   Three consequences, all deliberate:
     · .app.world-mode .workspace drops to `z-index: auto`, so #ledger's
       z-index (--z-overlay) is no longer trapped inside a z-index:0 context at
       the bottom of the pile — the drawer and its scrim now really do cover the
       vessels, instead of being covered by them. #editor-pane keeps its own
       z-index (1) and therefore its own stacking context, so the atmosphere
       layers and the editor's chrome rows keep the exact order atmosphere.css
       gives them.
     · .chrome and #editor-pane become pointer-TRANSPARENT in world mode, with
       pointer-events restored on the rows that are really controls. The world
       is underneath them; their empty space must not swallow a click meant for
       it. (The chrome's hover reveal now triggers from .chrome__brand /
       .chrome__tools — i.e. from the controls — rather than from the whole row.)
     · where a vessel overlaps the editor's own column the VESSEL takes the
       click. That is the trade the realms' anchors are drawn for (js/logic.js
       keeps them in the sky and along the edges, out of the centre band where
       the veil's text floats); the 44px disc is the only thing it costs.

   0.B  THE CONTRAST GUARANTEE  (spec §5 — do not change a number here without
        redoing this derivation; tokens.css points at this block)

   The words are set in light over an unknown painting, so nothing about the
   painting can be assumed. The veil is what makes the text legible, and its
   core alpha is chosen against the WORST CASE pixel the painting could put
   behind it: pure white under a dark veil, pure black under a light veil.

   Method: sRGB compositing C = a·haze + (1-a)·worst, then WCAG 2.1 relative
   luminance L = 0.2126·R' + 0.7152·G' + 0.0722·B' (channels linearised), then
   ratio = (L_hi + 0.05) / (L_lo + 0.05).

   DARK VEIL — core #0B1020 (--veil-dark-core), shipped alpha 0.64 (floor 0.62)
     composite over #FFFFFF:  rgb(98.8, 102.0, 112.3)        L = 0.13330
     required text luminance for 4.5:1:                      L >= 0.77487
     cream reference #F5EEDD   L = 0.85781  ->  4.95:1
     the SIX luminous realm tints (tokens.css), worst case first:
       wisp    #E0EBFC  L=0.8229  4.76:1      petal   #F9EBD0  L=0.8411  4.86:1
       shrine  #E6F2EE  L=0.8650  4.99:1      library #FAEFD6  L=0.8691  5.01:1
       canal   #FBEEDB  L=0.8677  5.01:1      moon    #EAF1FA  L=0.8731  5.04:1
     Hearthlight's tints are all brighter still (worst 4.99:1, wisp).
     LIBRARY is the old candle-cream #FAEFD6, kept unchanged when the Silent
     Vault became the Great Library: lamp-cream over mahogany and shadow.
     CANAL is the lantern-cream that replaced the old drowned-silver when the
     Drowned Cathedral became the Lantern Canals — warmer, because every light
     in that realm is a paper lantern. (Hearthlight #FDF4E6 -> 5.25:1; at the
     floor alpha 0.62, 4.66:1 Daylight / 4.89:1 Hearthlight.) The other two
     realm tints — COUNCIL and MAPLE — are not in this table at all; they are
     ink, and they are audited under the light veil below.
     At the floor alpha 0.62 the same cream is 4.62:1 — still AA, which is why
     0.62 is the floor and 0.64 is what ships.

   LIGHT VEIL — core #FAF3E2 (--veil-light-core), shipped alpha 0.72 (floor .70)
     composite over #000000:  rgb(180.0, 175.0, 162.7)       L = 0.42982
     permitted ink luminance for 4.5:1:                      L <= 0.05663
     --veil-ink #221A10  L = 0.0112  ->  7.84:1   (Hearthlight #1E1810 8.04:1)
     At the floor alpha 0.70 the same ink is 7.26:1.

     THE TWO INK TINTS. The Council Room and the Maple Valley are the only
     realms whose LIGHT is ink rather than a luminous tint: both are daylit
     places (a late-afternoon room, a valley held at sunset), and a cream
     light-form would vanish into either one. Their tints are therefore read on
     this same cream core, and re-derived here with the method above:
       council #2A1E13  L = 0.01468  ->  7.42:1   (Hearthlight #231A10  7.82:1)
       maple   #2E1C0E  L = 0.01443  ->  7.45:1   (Hearthlight #271708  7.92:1)
     At the floor alpha 0.70 (composite L = 0.40376): council 7.02:1,
     maple 7.04:1 (Hearthlight 7.40:1 / 7.49:1). Every number clears the 4.5:1
     bar with a wide margin, and both clear 7:1 (AAA for body text) as well.
     Note that neither realm ever shows the DARK core — both are veil:'light'
     with no day painting, so realmVeilMode() can only ever return 'light'.
     The council refresh (crimson carpet, sage-teal curtains, brass) changed
     only the PAINTING and the realms-panel card, never the haze core or the
     ink, so the council line above is unchanged by it.

   WHY THE FLOOR HOLDS IN PRACTICE: a blurred shape loses alpha at its centre
   when the blur radius approaches half its size, so the haze is built in two
   blurred layers instead of one. ::before is the CORE — inset 4px, a radial
   with a wide solid plateau, blur 26px — and the audit above measures that
   layer alone. ::after is the 48px halo that kills the edge, and it composites
   ON TOP of the core, so every pixel of the halo is pure headroom. The minimum
   box size below (260x150) keeps the core plateau wider than 2x26px in both
   axes, so the core can never be thinned by its own blur. Raising either blur,
   shrinking the minimum box, or lowering either alpha re-opens this audit.

   THE CORE BLUR IS 26px ON PURPOSE, and the spec asks for >= 40px. The spec's
   number is about the EDGE: "the haze never has a hard edge". A single 40px+
   blur delivers that and eats the centre alpha of any small block, which is the
   one thing the floor cannot survive. Splitting the job keeps both promises: the
   26px core is never the outer edge of anything (the halo extends 44px beyond it
   on every side), and the visible edge of the veil is the halo's own blur(48px),
   which is over the spec's floor. Nothing anywhere on this element transitions
   from opaque to transparent in less than 48px.

   WHAT js/realm.js MUST DO — THE PADDING CONTRACT. The floor is guaranteed for
   the plateau, not for the last pixel of the box. The core's alpha begins to
   fall at 62% of the gradient radius and the blur softens 26px inward from the
   clipped edge, so a glyph sitting within ~48px of the box edge is NOT on the
   audited plateau. Therefore: realm.js must pad the measured text rect by
   >= 48px on ALL FOUR SIDES before writing left/top/width/height. 34px is the
   floor if the halo is credited (it adds ~0.30 alpha of the same core over that
   band, which carries 34..48px), but 48px is the number to implement — it is
   cheap, it is the difference between 4.95:1 and an unaudited first line, and
   the veil has no visible cost for being larger than the words.
   ========================================================================== */

:root {
  /* the world paints above the app background and strictly below .workspace */
  --z-world: -1;

  /* inside #world */
  --z-w-scene:   0;
  --z-w-sky:     1;
  --z-w-aurora:  2;
  --z-w-canvas:  3;
  --z-w-ground:  4;
  --z-w-fog:     5;
  --z-w-weather: 6;
  /* not inside #world any more — it is on the APP's scale, one step under the
     chrome, so a vessel can actually be clicked (see 0.A2) */
  --z-vessels:  calc(var(--z-chrome) - 1);
  /* round 4b (wayfinding): the two new surfaces, both in the APP's scale for
     the same reason #vessels is (0.A2) — they are things a pointer uses, not
     scenery. The Shelf sits between the vessels it describes and the compass
     that must stay reachable over it; the ring sits above the compass, because
     the compass is the ring's own centre and the ring is being aimed at it. A
     LIFTED vessel goes one step above the ring: it is the thing in the hand. */
  --z-shelf:    calc(var(--z-chrome) + 10);      /* 50 — over vessels (39) + chrome (40) */
  /* REVIEW FIX (HIGH). The ring sat at 66, under everything in the overlay
     scale — including the Ledger drawer (99/100), which is inside #app with
     it. It is raised here so that within #app the ring is genuinely the top.
     WHAT THIS CANNOT DO, and the reason the realms panel is handled another
     way: `.app` carries `isolation: isolate` (css/app.css), so it is a
     stacking context, and the round-1 overlays — #realms-panel among them —
     are siblings of #app, not descendants. No z-index given to a child of
     #app can lift it above them. The panel therefore STEPS ASIDE for the ring
     (js/realm.js onRealmsClick closes it, runRing brings it back) rather than
     being painted under it. Both values stay well below --z-toast (1000), and
     closeWorldSurfaces() still puts the ring away when a host overlay opens. */
  --z-ring:     calc(var(--z-overlay) + 6);      /* 106 — over the realms panel (100) */
  --z-carried:  calc(var(--z-overlay) + 8);      /* 108 — the vessel being carried */
  --z-w-veil:   10;
  --z-w-sheen:  11;
  --z-w-portal: 14;
  --z-w-read:   15;
  --z-w-ritual: 16;

  /* world chrome geometry */
  --compass-size:   56px;
  --compass-inset:  28px;
  --compass-radius: 244px;   /* menu arc radius, see §8 */
  --ledger-w:       calc(var(--sidebar-w) + var(--list-w));

  /* the active realm's tint. Defaults to the home realm so the world is
     correct before js/realm.js has said a word. (--veil-lumen vs --veil-text:
     see the comment above the per-realm rules in §1.) */
  --veil-lumen: var(--veil-petal-text);
  --veil-text:  var(--veil-petal-text);
  --veil-glow:  var(--veil-petal-glow);
  /* the halo under TEXT only; flips with the veil mode (see §1), unlike
     --veil-glow, which is the realm's hue and belongs to the world */
  --veil-text-glow: var(--veil-petal-glow);
  /* --veil-shade and --veil-pit are tokens now (tokens.css, the --veil- family) */
}

/* ==========================================================================
   1. THE WORLD
   ========================================================================== */
#world {
  position: fixed;
  inset: 0;
  z-index: var(--z-world);
  overflow: hidden;
  pointer-events: none;            /* vessels opt back in, §4 */
  background: var(--bg-sunken);    /* never a flash of white before a scene */
  contain: paint;
}

/* Outside world mode the world is not merely hidden, it is not painted at all:
   eight full-viewport SVGs are not free. */
.app:not(.world-mode) #world { display: none; }

/* ---------- the eight scenes ---------- */
#realm-scenes { position: absolute; inset: 0; z-index: var(--z-w-scene); }

.realm-scene {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transform: scale(1.012);
  transition: opacity 900ms var(--ease-soft),
              transform 1400ms var(--ease-soft),
              visibility 0s linear 900ms;
}
.realm-scene.is-current {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  transition-delay: 0s, 0s, 0s;
}

.realm-scene__svg,
.realm-scene__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* the painting, when one exists, sits ABOVE its code-painted fallback */
.realm-scene__img { z-index: 1; }
.realm-scene__img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  opacity: 0;
  transition: opacity 1200ms var(--ease-soft);
}
.realm-scene__img img.is-loaded { opacity: 1; }

/* ---------- the hour veil -------------------------------------------------
   Two washes over the scene and nothing else: a vertical hour gradient, and a
   bloom in the realm's own tint. No text is ever read through these (the veil
   in §3 owns that), so their only constraint is restraint. 3s crossfade, per
   spec §4, when the clock turns the hour over. -------------------------- */
#realm-scenes::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: none;
  transition: background 3s linear, opacity 3s linear;
}
#world[data-hour="dawn"] #realm-scenes::after {
  background:
    radial-gradient(70% 50% at 22% 78%, color-mix(in srgb, var(--veil-glow) 42%, transparent), transparent 70%),
    linear-gradient(180deg, rgba(58, 44, 86, 0.26), rgba(244, 186, 140, 0.15) 62%, rgba(255, 214, 164, 0.11));
  opacity: 0.66;
}
#world[data-hour="day"] #realm-scenes::after {
  background: radial-gradient(80% 60% at 50% 18%, rgba(255, 248, 228, 0.12), transparent 76%);
  opacity: 0.6;
}
#world[data-hour="dusk"] #realm-scenes::after {
  background:
    radial-gradient(66% 48% at 78% 74%, color-mix(in srgb, var(--veil-glow) 42%, transparent), transparent 72%),
    linear-gradient(180deg, rgba(28, 24, 58, 0.30), rgba(134, 84, 120, 0.16) 58%, rgba(226, 148, 104, 0.12));
  opacity: 0.68;
}
#world[data-hour="night"] #realm-scenes::after {
  background:
    radial-gradient(60% 44% at 50% 14%, color-mix(in srgb, var(--veil-glow) 34%, transparent), transparent 66%),
    linear-gradient(180deg, rgba(8, 10, 26, 0.34), rgba(12, 16, 38, 0.16) 58%, rgba(6, 8, 20, 0.30));
  opacity: 0.68;
}
/* the two realms with a day painting want less wash when the painting arrives */
#world[data-hour="day"][data-realm="tree"] #realm-scenes::after,
#world[data-hour="day"][data-realm="lake"] #realm-scenes::after { opacity: 0.34; }

/* ---------- THE TWO REALMS THAT ARE NEVER DARK.
   The Maple Valley is always sunset and the Council Room is always late
   afternoon: the hour does not take their light away, it only changes its
   colour. So these eight rules replace the four above for those two realms,
   and not one of them carries a dark band. The Maple Valley never even cools
   much — its dawn is rose, its day is a high clear gold, its dusk is the deep
   amber the painting already lives in, and its night is the one hour that
   cools at all: a thin blue-violet laid OVER a canopy that stays lit.
   Specificity (1,2,0) beats the generic (1,1,0) hour rules in any order. */
#world[data-realm="maples"][data-hour="dawn"] #realm-scenes::after {
  background:
    radial-gradient(80% 58% at 50% 76%, rgba(255, 198, 158, 0.38), transparent 72%),
    linear-gradient(180deg, rgba(255, 212, 178, 0.22), rgba(255, 232, 200, 0.12) 60%, rgba(255, 244, 220, 0.08));
  opacity: 0.58;
}
#world[data-realm="maples"][data-hour="day"] #realm-scenes::after {
  background: radial-gradient(84% 62% at 50% 12%, rgba(255, 246, 214, 0.26), transparent 78%);
  opacity: 0.44;
}
#world[data-realm="maples"][data-hour="dusk"] #realm-scenes::after {
  background:
    radial-gradient(74% 56% at 50% 66%, rgba(255, 178, 92, 0.44), transparent 74%),
    linear-gradient(180deg, rgba(255, 206, 150, 0.18), rgba(255, 186, 124, 0.14) 58%, rgba(255, 216, 168, 0.12));
  opacity: 0.62;
}
#world[data-realm="maples"][data-hour="night"] #realm-scenes::after {
  background:
    radial-gradient(66% 50% at 50% 62%, rgba(255, 190, 124, 0.30), transparent 72%),
    linear-gradient(180deg, rgba(132, 140, 200, 0.24), rgba(186, 168, 194, 0.14) 58%, rgba(226, 196, 176, 0.10));
  opacity: 0.58;
}

#world[data-realm="council"][data-hour="dawn"] #realm-scenes::after {
  background:
    radial-gradient(74% 54% at 28% 70%, rgba(255, 222, 184, 0.32), transparent 72%),
    linear-gradient(180deg, rgba(255, 236, 210, 0.18), rgba(255, 246, 224, 0.09) 62%, rgba(255, 250, 234, 0.05));
  opacity: 0.52;
}
#world[data-realm="council"][data-hour="day"] #realm-scenes::after {
  background: radial-gradient(78% 58% at 62% 16%, rgba(255, 248, 224, 0.22), transparent 78%);
  opacity: 0.44;
}
#world[data-realm="council"][data-hour="dusk"] #realm-scenes::after {
  background:
    radial-gradient(66% 50% at 80% 62%, rgba(255, 198, 116, 0.44), transparent 74%),
    linear-gradient(180deg, rgba(255, 226, 172, 0.18), rgba(255, 210, 146, 0.14) 58%, rgba(255, 232, 192, 0.10));
  opacity: 0.62;
}
#world[data-realm="council"][data-hour="night"] #realm-scenes::after {
  background:
    radial-gradient(60% 44% at 64% 18%, rgba(228, 236, 255, 0.30), transparent 70%),
    linear-gradient(180deg, rgba(150, 166, 214, 0.22), rgba(182, 194, 230, 0.15) 60%, rgba(212, 218, 242, 0.10));
  opacity: 0.56;
}

/* ---------- THE LANTERN CANALS are a night city, and they are night at every
   hour: hundreds of paper lanterns are what light that realm, so the clock can
   only warm or cool them a little. All four washes are amber-rose over the
   water; none of them is the generic indigo, and none goes to black. */
#world[data-realm="canals"][data-hour="dawn"] #realm-scenes::after {
  background:
    radial-gradient(62% 48% at 50% 84%, rgba(255, 186, 132, 0.34), transparent 72%),
    linear-gradient(180deg, rgba(58, 40, 70, 0.24), rgba(140, 88, 74, 0.14) 58%, rgba(48, 28, 32, 0.20));
  opacity: 0.58;
}
#world[data-realm="canals"][data-hour="day"] #realm-scenes::after {
  background:
    radial-gradient(60% 46% at 50% 82%, rgba(255, 204, 156, 0.30), transparent 74%),
    linear-gradient(180deg, rgba(48, 36, 62, 0.18), transparent 56%, rgba(40, 24, 28, 0.18));
  opacity: 0.5;
}
#world[data-realm="canals"][data-hour="dusk"] #realm-scenes::after {
  background:
    radial-gradient(66% 50% at 50% 80%, rgba(255, 166, 104, 0.40), transparent 72%),
    linear-gradient(180deg, rgba(44, 30, 62, 0.26), rgba(150, 90, 76, 0.16) 58%, rgba(44, 24, 30, 0.24));
  opacity: 0.62;
}
#world[data-realm="canals"][data-hour="night"] #realm-scenes::after {
  background:
    radial-gradient(58% 46% at 50% 86%, rgba(255, 176, 116, 0.34), transparent 70%),
    linear-gradient(180deg, rgba(26, 24, 58, 0.28), rgba(112, 70, 70, 0.14) 58%, rgba(28, 16, 22, 0.26));
  opacity: 0.6;
}

/* ---------- THE GREAT LIBRARY is dark, but it is never BLACK: the lamps burn
   at every hour and the dome only ever changes the colour of what falls
   through it. So the hour here is amber all the way down, with a silver
   column from the dome at night instead of the generic indigo wash. */
#world[data-realm="library"][data-hour="dawn"] #realm-scenes::after {
  background:
    radial-gradient(62% 50% at 50% 8%, rgba(255, 230, 186, 0.26), transparent 70%),
    linear-gradient(180deg, rgba(60, 38, 20, 0.20), rgba(120, 78, 38, 0.12) 58%, rgba(40, 24, 12, 0.22));
  opacity: 0.56;
}
#world[data-realm="library"][data-hour="day"] #realm-scenes::after {
  background:
    radial-gradient(58% 48% at 50% 6%, rgba(255, 246, 220, 0.30), transparent 72%),
    linear-gradient(180deg, rgba(52, 34, 18, 0.14), transparent 56%, rgba(34, 20, 10, 0.18));
  opacity: 0.5;
}
#world[data-realm="library"][data-hour="dusk"] #realm-scenes::after {
  background:
    radial-gradient(64% 50% at 50% 10%, rgba(255, 216, 150, 0.30), transparent 70%),
    linear-gradient(180deg, rgba(48, 28, 14, 0.24), rgba(134, 82, 36, 0.14) 58%, rgba(36, 20, 10, 0.26));
  opacity: 0.6;
}
#world[data-realm="library"][data-hour="night"] #realm-scenes::after {
  background:
    radial-gradient(46% 60% at 50% 4%, rgba(214, 228, 255, 0.26), transparent 68%),
    linear-gradient(180deg, rgba(30, 20, 12, 0.26), rgba(96, 60, 28, 0.14) 58%, rgba(22, 14, 8, 0.28));
  opacity: 0.6;
}

/* ---------- per-realm tint, set where BOTH the world and the editor can read
   it. The editor pane is not inside #world, so the tokens are bound on .app
   via :has() — one rule per realm, then one more for a light veil.

   TWO tints, not one, and the difference matters:
     --veil-lumen  the realm's LIGHT. Never flips. Vessels, the compass, the
                   chrome and every glyph out in the world are made of it,
                   because they are lights in a scene, not marks on a page.
     --veil-text   what is read ON THE VEIL. Flips to ink under a light haze,
                   because that is the only place a cream tint would be read
                   against cream. Only .prose, the title, the footer and the
                   empty state — everything the haze actually backs — use it.
   --veil-core / --veil-alpha follow the veil mode too, so the soft backings
   behind the title and the footer (§2) are always the same material, and the
   same audit (0.B), as the haze itself. ---------------------------------- */
.app { --veil-core: var(--veil-dark-core); --veil-alpha: var(--veil-dark-alpha); }

.app:has(#world[data-realm="tree"])      { --veil-lumen: var(--veil-petal-text);   --veil-text: var(--veil-petal-text);   --veil-glow: var(--veil-petal-glow); --veil-text-glow: var(--veil-petal-glow); }
.app:has(#world[data-realm="canals"])    { --veil-lumen: var(--veil-canal-text);   --veil-text: var(--veil-canal-text);   --veil-glow: var(--veil-canal-glow); --veil-text-glow: var(--veil-canal-glow); }
.app:has(#world[data-realm="lake"])      { --veil-lumen: var(--veil-moon-text);    --veil-text: var(--veil-moon-text);    --veil-glow: var(--veil-moon-glow); --veil-text-glow: var(--veil-moon-glow); }
.app:has(#world[data-realm="pines"])     { --veil-lumen: var(--veil-wisp-text);    --veil-text: var(--veil-wisp-text);    --veil-glow: var(--veil-wisp-glow); --veil-text-glow: var(--veil-wisp-glow); }
.app:has(#world[data-realm="shrine"])    { --veil-lumen: var(--veil-shrine-text);  --veil-text: var(--veil-shrine-text);  --veil-glow: var(--veil-shrine-glow); --veil-text-glow: var(--veil-shrine-glow); }
.app:has(#world[data-realm="council"])   { --veil-lumen: var(--veil-council-text); --veil-text: var(--veil-council-text); --veil-glow: var(--veil-council-glow); --veil-text-glow: var(--veil-council-glow); }
.app:has(#world[data-realm="maples"])    { --veil-lumen: var(--veil-maple-text);   --veil-text: var(--veil-maple-text);   --veil-glow: var(--veil-maple-glow); --veil-text-glow: var(--veil-maple-glow); }
.app:has(#world[data-realm="library"])   { --veil-lumen: var(--veil-library-text); --veil-text: var(--veil-library-text); --veil-glow: var(--veil-library-glow); --veil-text-glow: var(--veil-library-glow); }

/* THE TWO INK REALMS (the Council Room, the Maple Valley). Everything in
   §2 and §4 that lifts a light-form off a bright painting does it with
   --veil-pit, a near-black drop under the form. Under an INK form that is the
   wrong direction — it deepens the mark instead of separating it — so for
   these two realms the pit becomes a warm white and lifts the ink exactly the
   same way. Nothing else about the light changes: --veil-lumen is still "the
   realm's light", it simply is not a bright colour in a bright place. */
.app:has(#world[data-realm="council"]),
.app:has(#world[data-realm="maples"]) {
  --veil-pit:   rgba(255, 250, 236, 0.74);
  --veil-shade: rgba(255, 252, 244, 0.62);
}

/* (1,3,0) beats the (1,2,0) realm rules above regardless of source order */
.app:has(#veil-layer .veil-haze.veil--light) {
  --veil-text:       var(--veil-ink);
  /* the halo under ink on cream is a warm white, not the realm's hue: that is
     what --veil-ink-glow is for, and this is the only place it is read */
  --veil-text-glow:  var(--veil-ink-glow);
  --veil-shade:      rgba(255, 252, 244, 0.62);
  --veil-core:       var(--veil-light-core);
  --veil-alpha:      var(--veil-light-alpha);
}

/* ==========================================================================
   2. WORLD MODE — the chrome gets out of the way
   The compass is the control now, so the chrome bar stops being a bar. Round 4
   shipped it at opacity 0.3, revealed on hover — which meant a ghost row of
   round-1 furniture floating over every painting, and a pointer crossing the
   top band lit it up on the way past. It rests at ZERO now: the 56px row stays
   (the world shows through it) so nothing below it ever shifts, but there is
   nothing there to see.

   Two ways back, and the reader chose both of them:
     · :focus-within — Tab reaches it, so the bar is never keyboard-unreachable.
       (A mouse user has the compass, and the command palette carries every
       action the bar held, the wordmark's link to the landing page included.)
     · .ledger-open — the drawer IS the round-1 workspace, so while it is open
       the round-1 bar belongs with it, at full strength.
   ========================================================================== */
.app.world-mode .chrome {
  background: none;
  border-bottom-color: transparent;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-soft);
}
.app.world-mode .chrome:focus-within { opacity: 1; }
.app.world-mode .chrome:focus-within,
.app.world-mode.ledger-open .chrome {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--bg) 62%, transparent),
    transparent);
}
/* With the Ledger open the bar is furniture again: fully lit, fully clickable,
   and lifted ABOVE the drawer and its scrim. The drawer starts at --chrome-h so
   it never covers the row geometrically, but the scrim's stacking context would
   otherwise decide who paints last. */
.app.world-mode.ledger-open .chrome {
  opacity: 1;
  z-index: calc(var(--z-overlay) + 1);
}
.app.world-mode .wordmark,
.app.world-mode .chrome .icon-btn {
  color: var(--veil-lumen);
  text-shadow: 0 1px 3px var(--veil-pit), 0 1px 14px var(--veil-pit);
}
.app.world-mode .wordmark__mark { color: var(--veil-lumen); }
.app.world-mode .chrome__sep { background: color-mix(in srgb, var(--veil-lumen) 28%, transparent); }
/* the faintest possible ground under the chrome row: without it, cream chrome
   over a bright realm (the Shrine at noon) has nothing to read against. */
.app.world-mode .chrome {
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--veil-dark-core) 42%, transparent), transparent);
}
/* ...and the opposite ground under the two INK realms: their chrome is a warm
   brown, so a dark band would bury it. (1,4,0) — wins wherever it stands. */
.app.world-mode:has(#world[data-realm="council"]) .chrome,
.app.world-mode:has(#world[data-realm="maples"]) .chrome {
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--veil-light-core) 66%, transparent), transparent);
}

/* In the world the light is PAINTED — the realm, the hour veil, the vessels and
   the veil itself all emit it. The round-3 overlay candle (#light-layer, a
   multiply wash across the whole pane) would sit on top of that and haze the
   painting into lavender milk, so it stands down, exactly as it did on the
   desk. Its siblings (ink, caret, motes) are point effects and stay. */
.app.world-mode #light-layer { display: none; }

/* the workspace and the editor pane become windows onto the world */
.app.world-mode .workspace,
.app.world-mode #editor-pane { background: none; }
.app.world-mode #editor-pane { z-index: 1; }
.app.world-mode .workspace { grid-template-columns: 1fr; }
/* 0.A2: the workspace stops being a stacking context so #ledger's --z-overlay
   is real — the drawer has to cover a sky that is now painted above the pane.
   #editor-pane keeps its own z-index, and therefore its own context, so
   atmosphere.css's layer order inside the pane is untouched. */
.app.world-mode .workspace { z-index: auto; }

/* ---------- the world is UNDERNEATH the chrome and the pane, so their empty
   space must not swallow a click meant for it (0.A2). Everything that is
   really a control takes a pointer again; the transparent space between them
   does not. ------------------------------------------------------------- */
.app.world-mode .chrome { pointer-events: none; }
/* A bar at opacity 0 must not be clickable: an invisible control that swallows
   a click meant for a vessel is worse than no control. Its buttons take a
   pointer again only when the bar is actually on screen — focused into, or
   standing beside an open Ledger. (The gaps between the buttons never do: they
   are sky, and a vessel hanging in the top band has to be reachable through
   them — at 390px the brand group alone is a quarter of the row.) */
.app.world-mode .chrome .icon-btn,
.app.world-mode .chrome .wordmark { pointer-events: none; }
.app.world-mode .chrome:focus-within .icon-btn,
.app.world-mode .chrome:focus-within .wordmark,
.app.world-mode.ledger-open .chrome .icon-btn,
.app.world-mode.ledger-open .chrome .wordmark { pointer-events: auto; }

.app.world-mode #editor-pane { pointer-events: none; }
.app.world-mode #editor-pane > .editor__head,
.app.world-mode #editor-pane > #toolbar,
.app.world-mode #editor-pane > .editor__scroll,
.app.world-mode #editor-pane > #backlinks,
.app.world-mode #editor-pane > #thread-chip,
.app.world-mode #editor-pane > #editor-footer,
.app.world-mode #editor-pane > #editor-empty,
.app.world-mode #editor-pane > #outline-panel { pointer-events: auto; }
/* (.wn-mini needs no rule: js/app.js appends the rune-bar to <body>, outside
   the pane, so the pane's pointer-events never reach it.) */

/* The four round-1 action icons beside the title (outline, pin, favorite, more)
   are the last hard-edged furniture left in the frame: four opaque glyphs on a
   painting, every one of them a thing you touch rarely. They sit back to a
   whisper and come forward the moment the row is reached for — by pointer or by
   keyboard. Nothing is hidden and nothing moves: opacity only, so the row keeps
   its size, its hit targets and its tab order exactly as they were.

   The BUTTONS carry the opacity, not the .editor__actions box: the note menu is
   a child of that box, and fading the box would have taken an open popmenu down
   with it. */
.app.world-mode .editor__head .editor__actions .icon-btn {
  opacity: 0.35;
  transition: opacity var(--dur-mid, 220ms) var(--ease-soft);
}
.app.world-mode .editor__head:hover .editor__actions .icon-btn,
.app.world-mode .editor__head:focus-within .editor__actions .icon-btn { opacity: 1; }

/* ---------- the Thread's chip, on a painting (spec §2.3) -------------------
   In the Ledger this is a pale amber pill on parchment. Over a realm there is
   no parchment to sit on, so it takes the veil's vocabulary instead — the same
   dark backing, blur and lumen the compass and the vessels wear — or it is a
   line of faint amber text on a painting, which is no line at all.

   It also has to be REACHABLE: .app.world-mode #editor-pane is
   pointer-events:none and every interactive child is listed back in above, so
   a chip left off that list would be a control nobody could press. */
.app.world-mode #thread-chip {
  color: var(--veil-lumen);
  background: color-mix(in srgb, var(--veil-dark-core) 62%, transparent);
  border-color: color-mix(in srgb, var(--veil-lumen) 26%, transparent);
  backdrop-filter: blur(10px);
  box-shadow: 0 6px 22px var(--veil-pit), 0 0 18px var(--veil-glow);
  text-shadow: 0 1px 2px var(--veil-shade);
}
.app.world-mode #thread-chip .thread-chip__mark,
.app.world-mode #thread-chip .thread-chip__text { color: var(--veil-lumen); }
.app.world-mode #thread-chip .thread-chip__btn { color: var(--veil-lumen); }
.app.world-mode #thread-chip .thread-chip__btn--quiet { color: color-mix(in srgb, var(--veil-lumen) 64%, transparent); }
.app.world-mode #thread-chip .thread-chip__btn:focus-visible { outline-color: var(--veil-lumen); }
/* hush hides every control in the world; the chip is one of them. */
.app.hush #thread-chip { opacity: 0; pointer-events: none; }

/* CROSS-FIX P2 — the 23px of horizontal scroll at 390px. The backing blobs
   below (.editor__head::after / #editor-footer::after) are deliberately WIDER
   than the row they back: `inset: -26px -6%`. On a 390px viewport that is
   23.4px of ink outside the element on each side, and the right-hand side of it
   is scrollable overflow — it propagated all the way to the documentElement
   (scrollWidth 413 against a 390 client width) and the page really did scroll
   sideways. The blob is decoration with no scroll value, so the pane clips it:
   `clip` rather than `hidden` so it can never become a scroll container, and
   x-only so .editor__scroll keeps owning the vertical scroll. §13 additionally
   pulls the horizontal inset to 0 under 640px, where the clip would otherwise
   cut the blur at the viewport edge. */
.app.world-mode #editor-pane { overflow-x: clip; }

/* the page is gone: no paper, no lift, no edge. Just a column of light. */
.app.world-mode #editor-pane .editor__scroll { padding-top: 0; }
.app.world-mode #editor-pane .editor {
  max-width: 760px;
  margin-inline: auto;
}
.app.world-mode #editor-pane .prose {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: var(--space-7) var(--space-6) var(--space-8);
  font-size: 1.22rem;
  line-height: 1.82;
  color: var(--veil-text);
  text-shadow: 0 0 26px var(--veil-text-glow), 0 1px 2px var(--veil-shade);
}
.app.world-mode #editor-pane .prose.texture-paper::after { content: none; }
.app.world-mode #editor-pane .prose h1,
.app.world-mode #editor-pane .prose h2,
.app.world-mode #editor-pane .prose h3 {
  color: var(--veil-text);
  border: none;
  text-shadow: 0 0 34px var(--veil-text-glow), 0 1px 2px var(--veil-shade);
}
.app.world-mode #editor-pane .prose blockquote {
  border-left-color: color-mix(in srgb, var(--veil-text) 44%, transparent);
  color: color-mix(in srgb, var(--veil-text) 88%, transparent);
  background: none;
}
.app.world-mode #editor-pane .prose code {
  color: var(--veil-text);
  background: color-mix(in srgb, var(--veil-shade) 40%, transparent);
  border-color: color-mix(in srgb, var(--veil-text) 26%, transparent);
}
.app.world-mode #editor-pane .prose mark {
  background: var(--veil-glow);
  color: var(--veil-text);
  border-radius: 3px;
}
.app.world-mode #editor-pane .prose hr {
  border-color: color-mix(in srgb, var(--veil-text) 30%, transparent);
}
.app.world-mode #editor-pane .prose a { color: var(--veil-text); }
.app.world-mode #editor-pane .note-title {
  color: var(--veil-text);
  text-shadow: 0 0 30px var(--veil-text-glow), 0 1px 2px var(--veil-shade);
  background: none;
  border: none;
}
.app.world-mode #editor-pane .note-title::placeholder {
  color: color-mix(in srgb, var(--veil-text) 52%, transparent);
}
.app.world-mode #editor-pane .editor__head {
  width: min(760px, 100%);
  margin-inline: auto;
  padding-top: var(--space-5);
}
.app.world-mode #editor-pane .editor__actions .icon-btn { color: var(--veil-text); }

/* ---------- the title row and the glyph footer get their own breath of veil --
   Everything in the editor column is read ON light, so everything in it needs
   the backing the prose has — otherwise a light-veil realm puts ink on an
   unlit painting. Both are blurred blobs of the ACTIVE veil material
   (--veil-core at --veil-alpha), so they carry the same guarantee as §3.

   REVIEW FIX (Important 2). These two used the veil's own 26px blur and its
   52%-plateau gradient, which is correct for a 780px-tall block and wrong for a
   ~103px one: at that height the plateau is thinner than 2x the blur and the
   composited alpha fell to ~0.61 (head) and ~0.57 (footer) — under the 0.62
   floor, before the footer's group opacity halved it again. Three changes fix
   it and the numbers are re-derived in 0.B's terms:
     1. blur 26px -> 14px. These blobs are small; 14px still leaves no hard edge
        (28px of falloff) and cannot reach the centre of a >= 60px-tall plateau.
     2. the plateau now covers the WHOLE box (extents 116%/104%, solid to 60%),
        so every pixel under a glyph is at full --veil-alpha and the falloff
        happens at the clip, where the blur is.
     3. the vertical insets grew to 26px, so the blob extends past the text.
   Effective core alpha at the text: --veil-alpha (0.64 dark / 0.72 light), the
   same number §0.B audits. Blurred blobs, never boxes: nothing here has a
   corner. ------------------------------------------------------------- */
.app.world-mode #editor-pane .editor__head::after,
.app.world-mode #editor-pane #editor-footer::after {
  content: "";
  position: absolute;
  inset: -26px -6%;
  z-index: -1;
  pointer-events: none;
  border-radius: 46% 54% 50% 50% / 62% 38% 62% 38%;
  background: radial-gradient(116% 104% at 50% 50%,
    color-mix(in srgb, var(--veil-core) calc(var(--veil-alpha) * 100%), transparent) 0 60%,
    color-mix(in srgb, var(--veil-core) calc(var(--veil-alpha) * 52%), transparent) 86%,
    transparent 100%);
  filter: blur(14px);
}

/* ---------- the full toolbar hides; Ctrl+/ floats it back for 6s ---------- */
.app.world-mode #toolbar {
  width: fit-content;
  max-width: min(760px, 100%);
  margin-inline: auto;
  padding: var(--space-1) var(--space-2);
  background: color-mix(in srgb, var(--veil-dark-core) 56%, transparent);
  backdrop-filter: blur(10px);
  border: 1px solid color-mix(in srgb, var(--veil-lumen) 18%, transparent);
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-quick) var(--ease-out);
}
.app.world-mode #toolbar .tool { color: var(--veil-lumen); }
.app.world-mode.toolbar-summoned #toolbar { opacity: 1; pointer-events: auto; }

/* ---------- the rune-bar: the selection mini-toolbar, restyled as glyphs ----
   js/app.js builds .wn-mini and styles it from an injected <style> at (0,1,0).
   These are (0,3,0), so they win without touching that file. --------------- */
.app.world-mode .wn-mini {
  gap: 2px;
  padding: 5px 7px;
  background: color-mix(in srgb, var(--veil-dark-core) 72%, transparent);
  border: 1px solid color-mix(in srgb, var(--veil-lumen) 22%, transparent);
  border-radius: 999px;
  box-shadow: 0 10px 30px var(--veil-pit), 0 0 26px var(--veil-glow);
  backdrop-filter: blur(12px);
}
.app.world-mode .wn-mini button {
  min-width: 30px;
  height: 30px;
  border-radius: 50%;
  color: var(--veil-lumen);
  font-family: var(--font-display);
  font-size: 0.96rem;
  letter-spacing: 0.02em;
  transition: background var(--dur-quick) var(--ease-out),
              transform 140ms var(--ease-out);
}
.app.world-mode .wn-mini button:hover {
  background: color-mix(in srgb, var(--veil-lumen) 18%, transparent);
  color: var(--veil-lumen);
}
.app.world-mode .wn-mini button:active { transform: scale(0.94); }

/* ---------- the glyph footer: word count and save state as a line of light -- */
.app.world-mode #editor-footer {
  width: min(760px, 100%);
  margin-inline: auto;
  padding-bottom: var(--space-5);
  background: none;
  border-top: none;
  color: var(--veil-text);
  font-family: var(--font-ui);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: lowercase;
  /* only hush ever animates this, and only between 0 and 1 (see S11) */
  transition: opacity var(--dur-slow) var(--ease-soft);
}
/* REVIEW FIX (Important 2): the footer used to recede with `opacity: .62` on the
   whole element, which multiplied its own veil backing down to ~0.40 AND faded
   the text it exists to make readable. Group opacity is for DECORATION only —
   the hairline and the dots below — and the word count is always at full
   --veil-text on a full-strength backing. */
.app.world-mode #editor-footer .editor-foot__left { gap: var(--space-3); }
.app.world-mode #editor-footer .editor-foot__dot {
  background: color-mix(in srgb, var(--veil-text) 60%, transparent);
}
.app.world-mode #editor-footer .goal-lantern,
.app.world-mode #editor-footer .save-indicator,
.app.world-mode #editor-footer .word-count,
.app.world-mode #editor-footer .reading-time { color: inherit; }
.app.world-mode #editor-footer .whisper__text,
.app.world-mode #editor-footer .whisper__source {
  color: var(--veil-text);
  text-shadow: 0 0 22px var(--veil-text-glow);
}
/* a hairline of light under the glyph row, fading at both ends. THIS is what
   recedes when the pointer is away — decoration, never the reading. */
.app.world-mode #editor-footer::before {
  content: "";
  opacity: 0.55;
  transition: opacity var(--dur-slow) var(--ease-soft);
  position: absolute;
  left: var(--space-5);
  right: var(--space-5);
  top: 0;
  height: 1px;
  background: linear-gradient(90deg,
    transparent,
    color-mix(in srgb, var(--veil-text) 34%, transparent) 22%,
    color-mix(in srgb, var(--veil-text) 34%, transparent) 78%,
    transparent);
}
.app.world-mode #editor-pane:hover #editor-footer::before,
.app.world-mode #editor-pane:focus-within #editor-footer::before { opacity: 1; }
.app.world-mode #editor-footer .editor-foot__dot { opacity: 0.7; }

/* the no-note state reads as part of the world, not a card on it */
.app.world-mode #editor-empty {
  background: none;
  color: var(--veil-text);
}
.app.world-mode #editor-empty .editor-empty__title,
.app.world-mode #editor-empty .editor-empty__copy {
  color: var(--veil-text);
  text-shadow: 0 0 24px var(--veil-text-glow), 0 1px 2px var(--veil-shade);
}

/* ==========================================================================
   3. THE VEIL OF LIGHT
   js/realm.js measures the text block and writes left/top/width/height inline
   on one .veil-haze per open note. Nothing here is rectangular: the shape is a
   four-corner-radius blob and both of its layers are blurred (see 0.B).
   ========================================================================== */
#veil-layer {
  position: absolute;
  inset: 0;
  z-index: var(--z-w-veil);
  pointer-events: none;
}

/* The min sizes exist so the core plateau always outruns its own 26px blur.
   NOTE FOR js/realm.js: they are floors on the BOX, not on the inline width and
   height it writes — a rect smaller than 260x150 will grow down/right from its
   inline left/top and sit off-centre behind the text. Either pad the rect to at
   least 260x150 yourself (the >= 48px padding in 0.B already does this for any
   real text block) or, when you cannot, recentre by subtracting half the
   overflow from left/top. */
.veil-haze {
  position: absolute;
  min-width: 260px;
  min-height: 150px;
  pointer-events: none;
  /* breathes with the candle rhythm js/realm.js mirrors onto #world */
  opacity: calc(0.9 + 0.1 * var(--flame, 0.35));
  transition: opacity 700ms var(--ease-soft);
  animation: veil-form 700ms var(--ease-out) both;
}
.veil-haze::before,
.veil-haze::after {
  content: "";
  position: absolute;
  /* an organic blob, not an oval and never a rectangle: the radii hug the text
     block closely enough to back all of it, and the two blurs do the rest. */
  border-radius: 24% 28% 22% 26% / 32% 26% 30% 28%;
}
/* THE CORE — the layer the contrast audit measures. Wide solid plateau. */
.veil-haze::before {
  inset: 4px;
  filter: blur(26px);
}
/* THE HALO — kills every edge. A wide, faint skirt OUTSIDE the core, so the
   veil ends in a long falloff instead of a rim. Pure headroom on top of it. */
.veil-haze::after {
  inset: -44px;
  z-index: -1;
  filter: blur(48px);
}

.veil-haze::before {
  background: radial-gradient(104% 100% at 50% 50%,
    color-mix(in srgb, var(--veil-dark-core) calc(var(--veil-dark-alpha) * 100%), transparent) 0 62%,
    color-mix(in srgb, var(--veil-dark-core) calc(var(--veil-dark-alpha) * 58%), transparent) 82%,
    transparent 100%);
}
.veil-haze::after {
  background: radial-gradient(90% 86% at 50% 50%,
    color-mix(in srgb, var(--veil-dark-core) 30%, transparent) 0 46%,
    color-mix(in srgb, var(--veil-dark-core) 13%, transparent) 74%,
    transparent 100%);
}
/* REVIEW FIX (minor 6) — THE :has() FALLBACK.
   The prose learns that the haze went light only through the `.app:has(...)`
   rule in §1. Where :has() is unsupported that rule never matches, the text
   stays cream — and a cream core under cream text is the one combination with
   no contrast at all. So the light core is itself gated on :has() support:
   without it, `.veil--light` simply paints the DARK core and the cream text is
   correct. The degraded world is "every realm has a dark veil", which is a
   world, not a bug. The base rules above (which target .veil-haze::before /
   ::after with no mode class) already paint the dark core for both modes. */
@supports selector(:has(*)) {
  .veil-haze.veil--light::before {
    background: radial-gradient(104% 100% at 50% 50%,
      color-mix(in srgb, var(--veil-light-core) calc(var(--veil-light-alpha) * 100%), transparent) 0 62%,
      color-mix(in srgb, var(--veil-light-core) calc(var(--veil-light-alpha) * 58%), transparent) 82%,
      transparent 100%);
  }
  .veil-haze.veil--light::after {
    background: radial-gradient(90% 86% at 50% 50%,
      color-mix(in srgb, var(--veil-light-core) 36%, transparent) 0 46%,
      color-mix(in srgb, var(--veil-light-core) 16%, transparent) 74%,
      transparent 100%);
  }
}
/* The realm's hue reaches the veil through the TEXT's glow (§2), not through a
   halo on the haze: a spread shadow in --veil-glow here reads as milk poured
   over the painting, which is exactly the thing the world is not. */

/* ---------- the rush of light: the vessel hands the note to the veil --------
   js/realm.js builds the spark with inline styles and animates it with the Web
   Animations API along a distance it measures at the moment of the click, so
   this rule owns only what WAAPI cannot: where the spark lives in the stack,
   that it never takes a pointer, and a CSS fallback for the (rare) engine with
   no element.animate — a WAAPI animation outranks a CSS one, so the keyframes
   below are invisible whenever the real path is available. */
.rush {
  position: absolute;
  z-index: calc(var(--z-w-veil) + 1);
  pointer-events: none;
  will-change: transform, opacity;
  animation: rush-bloom 700ms var(--ease-out) both;
}
@keyframes rush-bloom {
  0%   { opacity: 0; scale: 0.6; }
  35%  { opacity: 1; scale: 1.5; }
  100% { opacity: 0; scale: 2.6; }
}

@keyframes veil-form {
  from { opacity: 0; transform: scale(0.965); }
  to   { opacity: 1; transform: scale(1); }
}

/* ---------- the sheen: one slow diagonal sweep, every ~14s (spec §6) ------- */
#sheen-layer {
  position: absolute;
  inset: 0;
  z-index: var(--z-w-sheen);
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
}
.app.world-mode.is-writing #sheen-layer { opacity: 1; }
#sheen-layer::before {
  content: "";
  position: absolute;
  top: -40%;
  left: -60%;
  width: 42%;
  height: 180%;
  background: linear-gradient(100deg, transparent, var(--veil-glow) 46%, transparent);
  mix-blend-mode: screen;
  opacity: 0.34;
  filter: blur(26px);
  transform: translate3d(0, 0, 0) rotate(14deg);
  animation: sheen-sweep 14s var(--ease-in-out) infinite;
}
@keyframes sheen-sweep {
  0%, 62%  { transform: translate3d(0, 0, 0) rotate(14deg); opacity: 0; }
  70%      { opacity: 0.34; }
  100%     { transform: translate3d(260%, 0, 0) rotate(14deg); opacity: 0; }
}

/* ---------- THE LIVING CALLIGRAPHY (spec §6) --------------------------------
   The caret becomes a wisp of the realm's own light, it grows a three-dot comet
   tail, and a finished word sends up a rising mote instead of the round-3 boxy
   ink bloom. js/atmosphere.js owns the nodes, the coordinates and the class
   writes; this is the whole of their look. It was injected from js/app.js's
   injectStyles() while this file had no §3 calligraphy (task-5 report §5
   defect 3) — it lives here now, in tokens, with its own reduced-motion block
   in §14, and the SELECTORS ARE UNCHANGED so every class atmosphere.js writes
   still lands:
     #caret-layer .quill-caret.is-wisp  (+ .is-idle / .is-writing)
     #caret-layer .caret-trail.caret-trail--1 | --2 | --3
     #ink-layer   .bloom.is-mote

   THE NIB'S HEIGHT IS AN INLINE STYLE — atmosphere.js writes one line's height
   onto it every time it measures the caret — so a `height` rule here could only
   win with !important, and there is not one in this file. Instead the nib stops
   painting and the orb is a pseudo-element pinned to the caret's head: the same
   nine pixels in the same place, with no specificity war. -------------------- */
.app.world-mode #caret-layer .quill-caret.is-wisp {
  width: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.app.world-mode #caret-layer .quill-caret.is-wisp::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 9px;
  height: 9px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: radial-gradient(circle,
    var(--veil-lumen) 0 34%,
    color-mix(in srgb, var(--veil-lumen) 55%, transparent) 62%,
    transparent 100%);
  box-shadow: 0 0 14px 3px var(--veil-glow);
  filter: blur(0.2px);
}
.app.world-mode #caret-layer .quill-caret.is-wisp.is-idle {
  animation: wisp-drift 3.6s var(--ease-in-out) infinite;
}
.app.world-mode #caret-layer .quill-caret.is-wisp.is-writing { animation: none; opacity: 1; }
@keyframes wisp-drift {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.62; }
}

/* The tail: three dots given the SAME coordinates as the nib in the same frame.
   The lag is entirely their transition-duration — keep the three values apart,
   there is no second rAF loop behind them. */
.app.world-mode #caret-layer .caret-trail {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, var(--veil-lumen) 0 40%, transparent 78%);
  will-change: transform;
}
.app.world-mode #caret-layer .caret-trail--1 {
  width: 6px; height: 6px; margin: -3px 0 0 -3px;
  opacity: 0.55;
  transition: transform 200ms var(--ease-out);
}
.app.world-mode #caret-layer .caret-trail--2 {
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  opacity: 0.34;
  transition: transform 340ms var(--ease-out);
}
.app.world-mode #caret-layer .caret-trail--3 {
  width: 4px; height: 4px; margin: -2px 0 0 -2px;
  opacity: 0.2;
  transition: transform 480ms var(--ease-out);
}
/* the dots are built lazily and kept; outside the world they simply stop */
.app:not(.world-mode) #caret-layer .caret-trail { display: none; }

/* The rising mote: one spark at the top-centre of a finished word, 120px up. */
.app.world-mode #ink-layer .bloom.is-mote {
  width: 5px;
  height: 5px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--veil-lumen) 0 42%, transparent 78%);
  box-shadow: 0 0 9px 2px var(--veil-glow);
  animation: mote-lift 1400ms var(--ease-out) both;
}
.app.world-mode #ink-layer .bloom.is-mote::before { content: none; }
@keyframes mote-lift {
  0%   { transform: translate3d(0, 0, 0) scale(0.6); opacity: 0; }
  18%  { opacity: 0.95; }
  100% { transform: translate3d(var(--mote-drift, 6px), -120px, 0) scale(1.05); opacity: 0; }
}

/* ==========================================================================
   4. VESSELS — a note, as the realm makes one
   js/realm.js writes --ax / --ay (0..100 percentages of the viewport) per
   vessel and adds .vessel--<type>, .tier-1|2|3 and the state classes.
   ========================================================================== */
/* A SIBLING of #world, not a child of it (see 0.A2): the scenery is painted
   behind the app and the vessels are painted in front of it, so the same box
   has to be described twice. `fixed` + `inset: 0` reproduces #world's box
   exactly — realm.js writes --ax/--ay as percentages of the viewport — and the
   clip reproduces #world's `overflow: hidden`, so an edge anchor (x: 95) can
   never give the document 30px of horizontal scroll. */
#vessels {
  position: fixed;
  inset: 0;
  z-index: var(--z-vessels);
  overflow: clip;
  pointer-events: none;            /* .vessel opts back in, below */
}
/* the world is not painted at all outside world mode, and neither is its sky */
.app:not(.world-mode) #vessels { display: none; }
/* During the opening ritual the world rises over the whole app (§6). The sky
   rises one step FURTHER, because #world is opaque from its scene up: left at
   39 the vessels would be buried for the ritual's 1.8s and then pop in. Above
   it, they are the first lights in the ritual's darkness — which is the
   ritual's own idea, and the flare outshines them either way. */
.app.is-ritual #vessels { z-index: calc(var(--z-overlay) + 13); }

.vessel {
  position: absolute;
  left: calc(var(--ax, 50) * 1%);
  top: calc(var(--ay, 50) * 1%);
  width: var(--vessel-size, 44px);
  height: var(--vessel-size, 44px);
  margin: calc(var(--vessel-size, 44px) / -2);
  padding: 0;
  border: none;
  background: none;
  color: var(--veil-lumen);
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* REVIEW FIX (MEDIUM-LOW). The spec §5 says touch-action goes on a vessel
     "only while lifted"; the browser does not allow that. touch-action is read
     at POINTERDOWN, and a vessel is not lifted until 8px later — by which time
     the touch has been claimed as a scroll and the drag arrives as a
     pointercancel. It is declared here instead, on every vessel, always. The
     cost is nil: #vessels is a fixed, clipped box over a page that does not
     scroll, and only the 34-66px discs take a pointer at all. */
  touch-action: none;
  transition: transform 260ms var(--ease-out), opacity 420ms var(--ease-soft),
              filter 260ms var(--ease-out);
  animation: vessel-bob 7s var(--ease-in-out) infinite;
}
.vessel.tier-1 { --vessel-size: 34px; }
.vessel.tier-2 { --vessel-size: 48px; }
.vessel.tier-3 { --vessel-size: 66px; }

.vessel__glyph,
.vessel .vessel-glyph {
  width: 100%;
  height: 100%;
  display: block;
  fill: currentColor;
  stroke: none;
  filter: drop-shadow(0 1px 2px var(--veil-pit))
          drop-shadow(0 0 10px var(--veil-glow))
          drop-shadow(0 0 26px var(--veil-glow));
}

/* a 44px touch target even for a tier-1 blossom (Apple HIG / Material) */
.vessel::before {
  content: "";
  position: absolute;
  inset: min(0px, calc((var(--vessel-size, 44px) - 48px) / 2));
  border-radius: 50%;
}

.vessel:hover,
.vessel:focus-visible { transform: scale(1.12); }
.vessel:active { transform: scale(0.96); }

/* states */
.vessel.is-favorite { color: color-mix(in srgb, var(--veil-lumen) 62%, var(--accent-soft)); }
.vessel.is-active {
  filter: brightness(1.18);
  animation: vessel-bob 7s var(--ease-in-out) infinite, vessel-active 3.6s var(--ease-in-out) infinite;
}
.vessel.is-pinned::after {
  content: "";
  position: absolute;
  inset: -22%;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--veil-lumen) 52%, transparent);
  box-shadow: 0 0 18px var(--veil-glow);
  opacity: 0.8;
}
/* tag colour, when a note has one: js sets --vessel-tint from the tag token */
.vessel[style*="--vessel-tint"] { color: var(--vessel-tint); }

/* the title, in light, on hover or focus. Never a tooltip box. */
.vessel__label {
  position: absolute;
  left: 50%;
  top: calc(100% + 8px);
  max-width: 240px;
  transform: translate(-50%, 4px);
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--veil-lumen);
  text-shadow: 0 0 18px var(--veil-glow), 0 1px 3px var(--veil-pit);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
.vessel:hover .vessel__label,
.vessel:focus-visible .vessel__label { opacity: 1; transform: translate(-50%, 0); }

/* birth (900ms, spec §2) and opening (the rush of light) */
.vessel.is-born { animation: vessel-born 900ms var(--ease-out) both, vessel-bob 7s var(--ease-in-out) 900ms infinite; }
.vessel.is-opening { animation: vessel-open 700ms var(--ease-out) both; }
.vessel.is-dimmed { opacity: 0.14; transform: scale(0.9); }

@keyframes vessel-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 calc(-6px * var(--motion-ok)); }
}
@keyframes vessel-active {
  0%, 100% { filter: brightness(1.1); }
  50%      { filter: brightness(1.34); }
}
@keyframes vessel-born {
  0%   { opacity: 0; scale: 0.3; filter: brightness(2.2); }
  55%  { opacity: 1; scale: 1.16; }
  100% { opacity: 1; scale: 1; filter: brightness(1); }
}
@keyframes vessel-open {
  0%   { filter: brightness(1); }
  35%  { filter: brightness(2.4) drop-shadow(0 0 40px var(--veil-glow)); scale: 1.22; }
  100% { filter: brightness(1.18); scale: 1; }
}

/* ==========================================================================
   5. PORTALS — rings out of the travel point, then the new realm blooms in
   js/realm.js writes --px / --py (percentages) on #portal-layer and adds
   .is-travelling for the 1.4s of the transition.
   ========================================================================== */
#portal-layer {
  position: absolute;
  inset: 0;
  z-index: var(--z-w-portal);
  pointer-events: none;
  opacity: 0;
}
#portal-layer.is-travelling { opacity: 1; }

.portal-ring {
  position: absolute;
  left: var(--px, 50%);
  top: var(--py, 50%);
  width: 36px;
  height: 36px;
  margin: -18px;
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--veil-lumen) 64%, transparent);
  box-shadow: 0 0 40px var(--veil-glow), inset 0 0 30px var(--veil-glow);
  opacity: 0;
}
#portal-layer.is-travelling .portal-ring {
  animation: portal-out 1200ms var(--ease-out) both;
}
#portal-layer.is-travelling .portal-ring:nth-child(2) { animation-delay: 140ms; }
#portal-layer.is-travelling .portal-ring:nth-child(3) { animation-delay: 280ms; }

@keyframes portal-out {
  0%   { opacity: 0;    scale: 0.2; }
  18%  { opacity: 0.85; }
  100% { opacity: 0;    scale: 56; }
}

/* the outgoing realm gives way: the vessels dim first (§6), then the scene */
#world.is-travelling .realm-scene.is-current { opacity: 0.55; transform: scale(1.02); }

/* ==========================================================================
   6. RITUALS — the opening flare, the sealing sigil, the dim
   ========================================================================== */
#ritual-layer {
  position: absolute;
  inset: 0;
  z-index: var(--z-w-ritual);
  pointer-events: none;
}
/* While the ritual runs, the world outranks everything: the app opens in
   darkness and the flare is the first thing there is. app.css gives .app
   isolation, so this cannot leak past the app shell. */
.app.is-ritual #world { z-index: calc(var(--z-overlay) + 12); }

.ritual__dark,
.ritual__flare,
.ritual__ray {
  position: absolute;
  pointer-events: none;
}
.ritual__dark {
  inset: 0;
  background: var(--veil-dark-core);
  animation: ritual-dark 1800ms var(--ease-soft) both;
}
.ritual__flare {
  left: 50%;
  top: 50%;
  width: 40px;
  height: 40px;
  margin: -20px;
  border-radius: 50%;
  background: radial-gradient(circle,
    #FFF6E2 0 18%,
    color-mix(in srgb, var(--accent-soft) 80%, transparent) 42%,
    transparent 72%);
  filter: blur(4px);
  animation: ritual-flare 1800ms var(--ease-out) both;
}
.ritual__ray {
  left: 50%;
  top: 50%;
  width: 10px;
  height: 10px;
  margin: -5px;
  border-radius: 50%;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-soft) 50%, transparent);
  animation: ritual-ray 1800ms var(--ease-out) both;
}
@keyframes ritual-dark {
  0%, 22% { opacity: 1; }
  100%    { opacity: 0; }
}
@keyframes ritual-flare {
  0%   { opacity: 0; scale: 0.2; }
  16%  { opacity: 1; scale: 1; }
  46%  { opacity: 1; scale: 7; }
  100% { opacity: 0; scale: 26; }
}
@keyframes ritual-ray {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-soft) 60%, transparent); opacity: 0; }
  20%  { opacity: 1; }
  100% { box-shadow: 0 0 0 120vmax color-mix(in srgb, var(--accent-soft) 0%, transparent); opacity: 0; }
}

/* the sealing sigil, drawn over the veil on close (600ms, spec §9) */
.seal-sigil {
  position: absolute;
  z-index: calc(var(--z-w-veil) + 1);
  pointer-events: none;
  color: var(--veil-lumen);
  filter: drop-shadow(0 0 16px var(--veil-glow));
  animation: seal-fade 1100ms var(--ease-soft) 600ms both;
}
.seal-sigil path,
.seal-sigil circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-dasharray: var(--len, 600);
  stroke-dashoffset: var(--len, 600);
  animation: seal-draw 600ms var(--ease-out) both;
}
.seal-sigil path:nth-child(2) { animation-delay: 120ms; }
.seal-sigil path:nth-child(3) { animation-delay: 200ms; }
.seal-sigil path:nth-child(4) { animation-delay: 280ms; }
@keyframes seal-draw { to { stroke-dashoffset: 0; } }
@keyframes seal-fade { to { opacity: 0; } }

/* ==========================================================================
   7. ENCHANTED READING — the realm dims, blocks light one after another
   ========================================================================== */
#reading-layer {
  position: absolute;
  inset: 0;
  z-index: var(--z-w-read);
  pointer-events: none;
  background:
    radial-gradient(58% 52% at 50% 46%, transparent 0 44%, var(--veil-dark-core) 100%);
  opacity: 0;
  transition: opacity 900ms var(--ease-soft);
}
.app.is-reading #reading-layer { opacity: 0.46; }

.app.is-reading #editor-pane .prose > * {
  opacity: 0.35;
  transition: opacity 600ms var(--ease-soft), text-shadow 600ms var(--ease-soft);
}
.app.is-reading #editor-pane .prose > .is-lit {
  opacity: 1;
  text-shadow: 0 0 30px var(--veil-text-glow), 0 0 60px var(--veil-text-glow), 0 1px 2px var(--veil-shade);
}
.app.is-reading #editor-pane #note-editor { caret-color: transparent; }
/* the compass wears the progress of the reading */
.app.is-reading .compass__progress { opacity: 1; }

/* ==========================================================================
   8. THE COMPASS + RADIAL MENU
   Seven items on an arc from due west (182deg) to due north (270deg) at
   --compass-radius. The arc is quarter-turn on purpose: the centre sits in the
   bottom-right corner, so anything east or south of it would leave the
   viewport. Each item is placed with rotate -> translate -> un-rotate (no
   trigonometry, no JS), and its label rides the same angle 62px further out,
   which is what keeps neighbouring labels from ever touching.
   ========================================================================== */
#compass {
  position: fixed;
  right: var(--compass-inset);
  bottom: var(--compass-inset);
  z-index: var(--z-popover);
  display: none;
  width: var(--compass-size);
  height: var(--compass-size);
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--veil-lumen) 24%, transparent);
  border-radius: 50%;
  background: color-mix(in srgb, var(--veil-dark-core) 58%, transparent);
  backdrop-filter: blur(12px);
  color: var(--veil-lumen);
  cursor: pointer;
  box-shadow: 0 8px 28px var(--veil-pit), 0 0 26px var(--veil-glow);
  transition: transform 220ms var(--ease-out), box-shadow 220ms var(--ease-out);
}
.app.world-mode #compass { display: grid; place-items: center; }
#compass:hover { transform: rotate(-8deg) scale(1.05); }
#compass:active { transform: scale(0.96); }
#compass[aria-expanded="true"] { transform: rotate(-14deg); }
.compass__rose {
  width: 34px;
  height: 34px;
  fill: currentColor;
  stroke: none;
  filter: drop-shadow(0 0 8px var(--veil-glow));
}
/* ---------- reading progress ------------------------------------------------
   A ring that FILLS, clockwise from due north, as the blocks light one after
   another: js/realm.js writes --read-progress (0..1) on #compass. It is not a
   spinner — a spinner says "waiting", and nothing here is waiting; the reader is
   being read to, and the ring is how much of the note is left. Drawn as a conic
   gradient masked to a 2px annulus, so it is one element and no repaints.
   REVIEW FIX (c): this used to rotate forever and ignore the value. */
.compass__progress {
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  opacity: 0;
  transition: opacity var(--dur-quick) var(--ease-out);
  background: conic-gradient(from -90deg,
    var(--veil-lumen) 0 calc(var(--read-progress, 0) * 360deg),
    color-mix(in srgb, var(--veil-lumen) 16%, transparent) 0);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
}

/* ---------- the Thread's pip (spec §2.3) ------------------------------------
   A single dot on the compass ring, north-east of the rose, saying whether the
   line of light is holding. Four states and no more:
     synced   a steady light
     pending  a slow breath, because something is still travelling
     offline  dim, because nothing is travelling at all
     error    amber, because the writer should look at the Ledger

   It is NOT a control and must never behave like one: pointer-events:none, so
   the compass keeps its whole 56px hit target, and z-indexed BELOW the reading
   ring so a note being read to is never obscured by a dot. It is absolutely
   positioned against #compass, which is position:fixed, so it cannot affect
   the drag ring or the rose's own transforms either.
   The label lives on #compass's title attribute, written by js/app.js. */
.thread-pip {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: var(--veil-lumen);
  box-shadow: 0 0 6px var(--veil-glow), 0 0 2px var(--veil-pit);
  transition: opacity var(--dur-quick) var(--ease-out), background var(--dur-quick) var(--ease-out);
}
.thread-pip[data-state="synced"]  { opacity: 0.92; }
.thread-pip[data-state="pending"] { opacity: 0.92; animation: thread-breathe 2.4s var(--ease-soft, ease-in-out) infinite; }
.thread-pip[data-state="offline"] { opacity: 0.34; box-shadow: none; }
.thread-pip[data-state="error"]   { opacity: 1; background: var(--accent); box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 60%, transparent); }
.thread-pip[data-state="off"]     { opacity: 0; }

@keyframes thread-breathe {
  0%, 100% { opacity: 0.32; transform: scale(0.82); }
  50%      { opacity: 1;    transform: scale(1.12); }
}

#compass-menu {
  position: fixed;
  right: calc(var(--compass-inset) + var(--compass-size) / 2);
  bottom: calc(var(--compass-inset) + var(--compass-size) / 2);
  z-index: var(--z-popover);
  width: 0;
  height: 0;
}
.compass-item {
  --r: var(--compass-radius);
  --lr: 62px;
  position: absolute;
  left: 0;
  top: 0;
  width: 46px;
  height: 46px;
  padding: 0;
  border: none;
  background: none;
  color: var(--veil-lumen);
  cursor: pointer;
  transform: rotate(var(--a)) translate(var(--r)) rotate(calc(-1 * var(--a))) translate(-50%, -50%);
  opacity: 0;
  animation: compass-in 300ms var(--ease-out) both;
}
/* the arc: 182deg -> 270deg. Round 4b added "New elsewhere…" as the second
   item, so the sweep is now EIGHT steps of 12.571deg rather than seven of
   14.67deg — the arc itself never moves, only how finely it is divided. */
.compass-item:nth-child(1) { --a: 182deg;     animation-delay: 0ms; }
.compass-item:nth-child(2) { --a: 194.571deg; animation-delay: 30ms; }
.compass-item:nth-child(3) { --a: 207.143deg; animation-delay: 60ms; }
.compass-item:nth-child(4) { --a: 219.714deg; animation-delay: 90ms; }
.compass-item:nth-child(5) { --a: 232.286deg; animation-delay: 120ms; }
.compass-item:nth-child(6) { --a: 244.857deg; animation-delay: 150ms; }
.compass-item:nth-child(7) { --a: 257.429deg; animation-delay: 180ms; }
.compass-item:nth-child(8) { --a: 270deg;     animation-delay: 210ms; }

@keyframes compass-in {
  from { opacity: 0; scale: 0.82; }
  to   { opacity: 1; scale: 1; }
}

.compass-item__glyph {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--veil-lumen) 20%, transparent);
  background: color-mix(in srgb, var(--veil-dark-core) 54%, transparent);
  backdrop-filter: blur(10px);
  box-shadow: 0 6px 20px var(--veil-pit);
  transition: background 180ms var(--ease-out), transform 160ms var(--ease-out),
              box-shadow 180ms var(--ease-out);
}
.compass-item__glyph .icon { font-size: 1.06rem; }
.compass-item:hover .compass-item__glyph,
.compass-item:focus-visible .compass-item__glyph {
  background: color-mix(in srgb, var(--veil-lumen) 22%, transparent);
  box-shadow: 0 6px 22px var(--veil-pit), 0 0 26px var(--veil-glow);
  transform: scale(1.08);
}
.compass-item:active .compass-item__glyph { transform: scale(0.95); }

.compass-item__label {
  position: absolute;
  left: 50%;
  top: 50%;
  font-family: var(--font-ui);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--veil-lumen);
  text-shadow: 0 1px 3px var(--veil-pit), 0 0 18px var(--veil-glow);
  transform: rotate(var(--a)) translate(var(--lr)) rotate(calc(-1 * var(--a))) translate(-50%, -50%);
  opacity: 0.78;
  transition: opacity 160ms var(--ease-out);
}
.compass-item:hover .compass-item__label,
.compass-item:focus-visible .compass-item__label { opacity: 1; }

/* theme item mirrors the chrome button's sun/moon swap */
.compass-item--theme .icon--moon { display: none; }
html[data-theme="hearthlight"] .compass-item--theme .icon--sun  { display: none; }
html[data-theme="hearthlight"] .compass-item--theme .icon--moon { display: inline; }

/* ==========================================================================
   9. THE REALMS PANEL
   Eight places, not eight boxes: every card carries its own realm's light as a
   soft gradient, so the grid reads as a map.
   ========================================================================== */
.overlay--realms { align-items: center; }
.realms {
  position: relative;
  width: min(1120px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
  padding: var(--space-7) var(--space-6) var(--space-6);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--bg-raised) 92%, transparent);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lift);
  backdrop-filter: blur(18px);
}
.realms__head { margin-bottom: var(--space-5); text-align: center; }
.realms__title {
  margin: 0 0 var(--space-1);
  font-size: clamp(1.7rem, 3vw, 2.3rem);
}
.realms__sub {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.95rem;
}
.realms__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
}

.realm-card {
  position: relative;
  display: grid;
  grid-template-rows: auto auto auto auto 1fr auto;
  gap: var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--bg);
  overflow: hidden;
  isolation: isolate;
  transition: transform 200ms var(--ease-out), box-shadow 200ms var(--ease-out),
              border-color 200ms var(--ease-out);
}
.realm-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(120% 78% at 50% -12%, var(--card-glow, var(--accent-glow)), transparent 72%),
    linear-gradient(180deg, color-mix(in srgb, var(--card-deep, var(--bg-sunken)) 26%, transparent), transparent 58%);
  opacity: 1;
}
.realm-card:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--card-glow, var(--accent-glow)) 70%, var(--line));
  box-shadow: var(--shadow-lift);
}
.realm-card.is-current { border-color: var(--accent); }

/* each realm's own light (art values: these are the scenes' palettes) */
.realm-card[data-realm="tree"]      { --card-glow: rgba(247, 196, 212, 0.34); --card-deep: #2A1F45; }
.realm-card[data-realm="canals"]    { --card-glow: rgba(255, 172, 112, 0.38); --card-deep: #1A1026; }
.realm-card[data-realm="lake"]      { --card-glow: rgba(200, 218, 248, 0.30); --card-deep: #1B2C52; }
.realm-card[data-realm="pines"]     { --card-glow: rgba(160, 206, 232, 0.28); --card-deep: #12303A; }
.realm-card[data-realm="shrine"]    { --card-glow: rgba(160, 226, 212, 0.30); --card-deep: #4F6375; }
.realm-card[data-realm="council"]   { --card-glow: rgba(255, 216, 150, 0.34); --card-deep: #6B1F22; }
.realm-card[data-realm="maples"]    { --card-glow: rgba(255, 146, 54, 0.42); --card-deep: #4A1A12; }
.realm-card[data-realm="library"]   { --card-glow: rgba(255, 198, 108, 0.32); --card-deep: #1A1009; }

/* a small window into the realm: its own night, with its own light in it */
.realm-card__glyph {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: var(--card-deep);
  color: color-mix(in srgb, var(--card-glow) 72%, #FFF7E9);
  box-shadow: inset 0 0 14px color-mix(in srgb, var(--card-glow) 80%, transparent);
}
.vessel-glyph {
  width: 26px;
  height: 26px;
  display: block;
  fill: currentColor;
  stroke: none;
}
.realm-card__glyph .vessel-glyph {
  filter: drop-shadow(0 0 8px var(--card-glow));
}
.realm-card__name {
  margin: 0;
  font-size: 1.18rem;
  line-height: 1.18;
}
.realm-card__vessel {
  margin: 0;
  font-family: var(--font-body);
  font-style: italic;
  font-size: 0.88rem;
  color: var(--ink-soft);
}
.realm-card__vessel b { font-weight: 600; font-style: normal; color: var(--ink); }
.realm-card__counts {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-family: var(--font-ui);
  font-size: 0.74rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.realm-card__dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink-faint);
  opacity: 0.7;
}
.realm-card__notebooks {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
  align-content: start;
}
.realm-card__notebooks:empty { min-height: var(--space-4); }
.realm-card__notebooks button {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 32px;
  padding: var(--space-1) var(--space-2);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: 500 0.86rem/1.3 var(--font-ui);
  text-align: left;
  cursor: pointer;
  transition: background 160ms var(--ease-out);
}
.realm-card__notebooks button:hover { background: color-mix(in srgb, var(--card-glow) 52%, transparent); }
/* ROUND 4b (wayfinding §4): the per-notebook <select> is gone. A realm is a
   place, and a place is chosen on the ring — so this row now carries a quiet
   "Send to…" button that opens it, wearing the card's own clothes. */
.realm-card__send {
  width: 100%;
  min-height: 30px;
  margin: 2px 0 0;
  padding: 3px var(--space-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--bg-raised) 64%, transparent);
  color: var(--ink-faint);
  font: 500 0.76rem/1.3 var(--font-ui);
  cursor: pointer;
  appearance: none;
  transition: border-color 160ms var(--ease-out), color 160ms var(--ease-out),
              background 160ms var(--ease-out);
}
.realm-card__send:hover,
.realm-card__send:focus-visible {
  border-color: color-mix(in srgb, var(--card-glow) 70%, var(--line));
  background: color-mix(in srgb, var(--card-glow) 34%, var(--bg-raised));
  color: var(--ink);
}
.realm-card__notebooks li {
  display: grid;
  gap: 2px;
}

.realm-card__new {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 36px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg-raised) 70%, transparent);
  color: var(--ink-soft);
  font: 500 0.78rem/1 var(--font-ui);
  cursor: pointer;
  transition: background 160ms var(--ease-out), color 160ms var(--ease-out),
              transform 140ms var(--ease-out);
}
.realm-card__new:hover {
  background: color-mix(in srgb, var(--card-glow) 60%, var(--bg-raised));
  color: var(--ink);
}
.realm-card__new:active { transform: scale(0.97); }

/* ==========================================================================
  10. THE LEDGER DRAWER
  Outside world mode #ledger is `display: contents` — the sidebar, list and
  divider stay direct children of .workspace's grid and every round-1/2 rule
  (including the <=900px drawer behaviour in app.css) is untouched. In world
  mode it becomes a drawer over the realm, and app.js adds .ledger-open.
   ========================================================================== */
#ledger { display: contents; }
#btn-ledger-close { display: none; }

.app.world-mode #ledger {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  position: fixed;
  top: var(--chrome-h);
  bottom: 0;
  left: 0;
  width: var(--ledger-w);
  z-index: var(--z-overlay);
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  border-right: 1px solid var(--line);
  box-shadow: var(--shadow-lift);
  backdrop-filter: blur(18px);
  transform: translateX(-102%);
  visibility: hidden;
}
/* The slide is gated on html.theme-ready — the same hook app.css uses to keep
   the theme cross-fade off the first paint. Without it, entering world mode on
   boot animates the drawer OUT from under the chrome, and the user watches a
   Ledger they never opened leave the room. */
html.theme-ready .app.world-mode #ledger {
  transition: transform 420ms var(--ease-drawer, cubic-bezier(.32, .72, 0, 1)),
              visibility 0s linear 420ms;
}
html .app.world-mode.ledger-open #ledger {
  transform: translateX(0);
  visibility: visible;
  transition-delay: 0s, 0s;
}
/* the veil behind the drawer */
.app.world-mode.ledger-open::before {
  content: "";
  position: fixed;
  inset: var(--chrome-h) 0 0 0;
  z-index: calc(var(--z-overlay) - 1);
  background: var(--bg-veil);
  animation: ledger-veil 300ms var(--ease-out) both;
}
@keyframes ledger-veil { from { opacity: 0; } to { opacity: 1; } }

/* The close control is a HANDLE on the drawer's edge, not a button dropped into
   the note list's top row — that corner already belongs to the search field. */
.app.world-mode #btn-ledger-close {
  display: grid;
  position: absolute;
  right: -19px;
  top: 50%;
  z-index: 2;
  width: 38px;
  height: 38px;
  transform: translateY(-50%);
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--bg-raised);
  box-shadow: var(--shadow-soft);
  color: var(--ink-soft);
}
.app.world-mode #btn-ledger-close:hover { color: var(--ink); }
.app.world-mode #btn-ledger-close:active { transform: translateY(-50%) scale(0.94); }
.app.world-mode #ledger #pane-divider { display: none; }
.app.world-mode #ledger #note-list-pane { border-right: none; }

/* ==========================================================================
  11. HUSH  (the former focus mode, spec §11)
  The world stays; everything that could be clicked steps out of the frame.
   ========================================================================== */
/* `opacity: 0` alone still leaves every vessel and the compass in the tab order:
   hush took them out of the picture and left them in the keyboard's way, so Tab
   walked thirty-six invisible buttons. `visibility: hidden` is what actually
   removes a control — and because it is a discrete property it can still be
   transitioned, with the DELAY carrying the fade: going into hush it flips only
   after the opacity has reached 0 (delay = --dur-slow), coming out it flips
   immediately (delay 0) so the fade-up is visible from its first frame. */
.app.world-mode #compass,
.app.world-mode #vessels { visibility: visible; }
/* the compass keeps its own transform/box-shadow transition (§10) — the fade
   and the visibility flip are appended to it, never in place of it */
.app.world-mode #compass {
  transition: transform 220ms var(--ease-out), box-shadow 220ms var(--ease-out),
              opacity var(--dur-slow) var(--ease-soft), visibility 0s linear 0s;
}
.app.world-mode #vessels {
  transition: opacity var(--dur-slow) var(--ease-soft), visibility 0s linear 0s;
}
.app.world-mode.hush #compass-menu,
.app.world-mode.hush #vessels {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}
/* THE COMPASS SURVIVES HUSH. It is the only control the world has; hidden, a
   hushed sitting had no door out (a saved focus mode booted straight into
   this). It rests dim, brightens under the hand, and a click lifts the hush
   (js/realm.js onCompassClick → hooks.exitHush). */
.app.world-mode.hush #compass { opacity: 0.34; }
.app.world-mode.hush #compass:hover,
.app.world-mode.hush #compass:focus-visible { opacity: 1; }
.app.world-mode.hush #compass {
  transition: transform 220ms var(--ease-out), box-shadow 220ms var(--ease-out),
              opacity var(--dur-slow) var(--ease-soft),
              visibility 0s linear var(--dur-slow);
}
.app.world-mode.hush #vessels {
  transition: opacity var(--dur-slow) var(--ease-soft),
              visibility 0s linear var(--dur-slow);
}
/* §2 already rests the chrome at 0 with its controls pointer-dead, and reveals
   it only for :focus-within or an open Ledger — which is exactly what hush
   wants too, so hush no longer says anything about the bar at all. */
/* hush hides the footer outright — there is no text to fail to read at zero —
   and brings it back at FULL strength, never at a contrast-eating 0.8. */
.app.world-mode.hush #editor-footer { opacity: 0; }
.app.world-mode.hush #editor-pane:hover #editor-footer,
.app.world-mode.hush #editor-pane:focus-within #editor-footer { opacity: 1; }

/* ==========================================================================
  12. THE ROUND-1/2 OVERLAYS, OVER THE WORLD
  They all keep working; they just stop looking like furniture in a room that
  no longer exists. Glass instead of board, and the scrim does the separating.
   ========================================================================== */
.app.world-mode ~ .overlay .modal,
.app.world-mode ~ .overlay .palette,
.app.world-mode ~ .popover .popover__body { backdrop-filter: blur(16px); }

/* Toasts move to the bottom CENTRE in the world. The round-1 region sits in
   the bottom-right corner at --z-toast, which in the world is exactly where the
   compass lives — so every toast ("now lives in The Pines", "Hush. …touch the
   compass") covered the one control it was talking about. */
body:has(.app.world-mode) .toast-region {
  right: auto;
  left: 50%;
  bottom: calc(var(--space-4) + 6px);
  transform: translateX(-50%);
  align-items: center;
}

/* ==========================================================================
  13. RESPONSIVE
  Under 1100px the compass arc would run out of room, so it tightens; under
  900px the Ledger becomes a full-width drawer and the menu shortens its reach.
   ========================================================================== */
@media (max-width: 1100px) {
  :root { --compass-radius: 196px; }
  .compass-item { width: 42px; height: 42px; --lr: 56px; }
  .compass-item__glyph { width: 42px; height: 42px; }
}

@media (max-width: 900px) {
  :root { --compass-radius: 168px; --ledger-w: min(420px, 92vw); }
  .app.world-mode #ledger {
    grid-template-columns: 1fr;
    width: var(--ledger-w);
  }
  /* app.css makes #sidebar a fixed off-canvas drawer at this width; inside the
     Ledger that would fight the drawer it already lives in. Put it back in
     flow and let the Ledger be the only thing that slides. */
  .app.world-mode #ledger #sidebar {
    position: static;
    width: auto;
    transform: none;
    box-shadow: none;
  }
  .app.world-mode #ledger { overflow-y: auto; }
  .app.world-mode #editor-pane { position: relative; inset: auto; transform: none; }
  .realms__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  :root { --compass-radius: 138px; --compass-inset: 18px; }
  .compass-item { width: 38px; height: 38px; --lr: 48px; }
  .compass-item__glyph { width: 38px; height: 38px; }
  .compass-item__label { font-size: 0.56rem; letter-spacing: 0.06em; }
  .realms__grid { grid-template-columns: 1fr; }
  .app.world-mode #editor-pane .prose { padding: var(--space-5) var(--space-4) var(--space-6); }
  /* CROSS-FIX P2. At phone widths the row IS the viewport, so a blob that
     reaches 6% past it can only be clipped — and a clipped blur is a hard
     vertical edge down the side of the screen. The row's own padding-inline
     (app.css drops it to --space-4 here) already carries the blob past the
     first and last glyph, so the horizontal inset goes to 0: same plateau
     under the text, nothing outside the box, no overflow to clip. */
  .app.world-mode #editor-pane .editor__head::after,
  .app.world-mode #editor-pane #editor-footer::after { inset: -26px 0; }
}

/* ==========================================================================
  14. REDUCED MOTION
  Not "less world" — "a still world". Every scene, veil, vessel and panel stays
  exactly where it is and reads exactly the same; nothing travels, breathes,
  sweeps or flares. The opening ritual becomes a fade, the portal a crossfade.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .realm-scene { transition: opacity 240ms linear, visibility 0s linear 240ms; transform: none; }
  .realm-scene.is-current { transform: none; }
  #realm-scenes::after { transition: none; }

  .veil-haze { animation: none; opacity: 1; }
  #sheen-layer { display: none; }

  /* the calligraphy. js/atmosphere.js already refuses to track the caret at all
     under reduced motion (it drops the nib and hands the native caret back), so
     in practice there is nothing here to still. These three rules are the
     look's own guarantee, independent of the engine's gate: nothing of the
     calligraphy may ever move. The breathing is stopped with the SAME selector
     §3 uses — atmosphere.css's own reduced-motion rule for the nib is (1,1,0)
     and cannot reach .is-wisp.is-idle — the same trap as the Ledger transition
     at the end of this block. */
  .app.world-mode #caret-layer .quill-caret.is-wisp.is-idle { animation: none; }
  .app.world-mode #caret-layer .caret-trail { display: none; }
  .app.world-mode #ink-layer .bloom.is-mote { animation: none; display: none; }

  .vessel,
  .vessel.is-active,
  .vessel.is-born,
  .vessel.is-opening { animation: none; transform: none; filter: none; }
  .vessel .vessel-glyph,
  .vessel__glyph { filter: drop-shadow(0 0 10px var(--veil-glow)); }
  .vessel:hover,
  .vessel:focus-visible { transform: none; filter: brightness(1.25); }
  /* the title appears, it does not slide up into place */
  .vessel__label,
  .vessel:hover .vessel__label,
  .vessel:focus-visible .vessel__label {
    transform: translate(-50%, 0);
    transition: none;
  }

  .portal-ring,
  #portal-layer.is-travelling .portal-ring { animation: none; opacity: 0; }
  #world.is-travelling .realm-scene.is-current { transform: none; }

  .ritual__flare,
  .ritual__ray { display: none; }
  .ritual__dark { animation: ritual-dark 400ms linear both; }
  .seal-sigil { animation: seal-fade 400ms linear 200ms both; }
  .seal-sigil path,
  .seal-sigil circle { animation: none; stroke-dashoffset: 0; }

  #compass:hover,
  #compass:active,
  #compass[aria-expanded="true"] { transform: none; }
  .rush { animation: none; opacity: 0; }
  .compass-item { animation: none; opacity: 1; }
  /* spec §2.3: "Reduced motion: no breathing." The pip still SAYS
     pending - it just says it by being lit rather than by moving. */
  .thread-pip[data-state="pending"] { animation: none; opacity: 0.7; }
  .compass-item:hover .compass-item__glyph,
  .compass-item:focus-visible .compass-item__glyph,
  .compass-item:active .compass-item__glyph { transform: none; }

  /* REVIEW FIX (Important 1): this used to be `.app.world-mode #ledger` (1,2,0),
     which cannot beat the `html.theme-ready .app.world-mode #ledger` (1,3,1)
     rule that owns the transition — so the drawer kept sliding for 420ms under
     reduced motion. It must carry the SAME selector to win on source order.
     Every other override in this block was re-audited for the same trap: the
     only other rules in this file with an html-prefixed selector are the
     ledger-open state (which is instant either way) and the theme icon swap
     (no motion), so this was the one. */
  html.theme-ready .app.world-mode #ledger { transition: none; }
  .app.world-mode.ledger-open::before { animation: none; }
  .realm-card:hover { transform: none; }
  .app.is-reading #editor-pane .prose > * { transition: none; }
}

/* ==========================================================================
  15. WAYFINDING — the landmark, the Shelf, the realm ring, the carried vessel
  Round 4b. Three new surfaces, all of the compass menu's family: a painted
  ground the colour of the realm's own veil, a soft gold rim light along the
  top edge, serif titles, and the writing in --veil-text so it is cream over a
  dark realm and ink over a bright one without a single extra rule.

  --veil-core / --veil-alpha / --veil-text / --veil-pit already flip with the
  veil mode (§3), so everything below is written once and reads correctly in
  both — and in both THEMES, since those tokens are themselves theme-bound.
   ========================================================================== */

/* local clothes, so the three surfaces cannot drift apart */
.shelf,
.ring {
  --wf-ground: color-mix(in srgb, var(--veil-core) 86%, transparent);
  --wf-rim:    color-mix(in srgb, var(--veil-text) 20%, transparent);
  --wf-gold:   color-mix(in srgb, var(--accent-soft) 38%, transparent);
  --wf-lift:   0 18px 48px var(--veil-pit),
               0 0 34px color-mix(in srgb, var(--veil-glow) 46%, transparent),
               inset 0 1px 0 var(--wf-gold);
}

/* --------------------------------------------------------------------------
   15.1 THE LANDMARK
   One button per realm, inside #vessels (so it shares the vessels' z-order and
   hit-testing), standing at the --ax/--ay js/logic.js gives it. It breathes,
   it holds a halo, and its label rises INWARD — away from whichever edge of
   the window it is nearest — so a landmark at x:5 can never write off-screen.
   -------------------------------------------------------------------------- */
.landmark {
  --lm-size: 58px;
  position: absolute;
  left: calc(var(--ax, 50) * 1%);
  top: calc(var(--ay, 50) * 1%);
  width: var(--lm-size);
  height: var(--lm-size);
  margin: calc(var(--lm-size) / -2);
  padding: 0;
  border: none;
  background: none;
  color: var(--veil-lumen);
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 240ms var(--ease-out), filter 240ms var(--ease-out);
}
.landmark__glyph {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  fill: currentColor;
  stroke: none;
  filter: drop-shadow(0 1px 3px var(--veil-pit))
          drop-shadow(0 0 12px var(--veil-glow))
          drop-shadow(0 0 32px var(--veil-glow));
}
/* the breathing halo — slow enough to read as "alive", never as "loading" */
.landmark__halo {
  position: absolute;
  inset: -46%;
  border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--veil-glow) 92%, transparent) 0%,
    color-mix(in srgb, var(--veil-glow) 34%, transparent) 44%,
    transparent 72%);
  opacity: 0.55;
  pointer-events: none;
  animation: lm-breathe 6.5s var(--ease-in-out) infinite;
}
/* REVIEW FIX (POLISH 11b): the hover lift is applied to the GLYPH and its
   halo, never to the whole button — the label is a child, and scaling it by
   1.08 about the landmark's centre moved it up to 25px sideways AFTER
   placeLandmarkLabel() had measured it into the free band, which on the
   World-Tree pushed its left edge 6px off the window. */
.landmark__glyph { transition: transform 240ms var(--ease-out); }
.landmark:hover .landmark__glyph,
.landmark:focus-visible .landmark__glyph { transform: scale(1.08); }
.landmark:active .landmark__glyph { transform: scale(0.97); }
.landmark:hover .landmark__halo,
.landmark:focus-visible .landmark__halo { opacity: 0.86; }
/* while the Shelf is open the landmark is lit, and STEADY: the panel beside it
   is the thing being read, and a breathing light next to it only competes */
.landmark.is-open .landmark__halo { animation: none; opacity: 0.95; }
.landmark.is-open .landmark__glyph {
  filter: brightness(1.22) drop-shadow(0 0 16px var(--veil-glow)) drop-shadow(0 0 40px var(--veil-glow));
}
.landmark.is-dimmed { opacity: 0.14; transform: scale(0.92); }

@keyframes lm-breathe {
  0%, 100% { opacity: 0.44; transform: scale(0.94); }
  50%      { opacity: 0.78; transform: scale(1.06); }
}

.landmark__label {
  position: absolute;
  top: calc(100% + 10px);
  max-width: 260px;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--veil-text) 20%, transparent);
  background: color-mix(in srgb, var(--veil-core) 74%, transparent);
  backdrop-filter: blur(10px);
  font: 500 0.74rem/1.25 var(--font-ui);
  letter-spacing: 0.03em;
  white-space: nowrap;
  color: var(--veil-text);
  text-shadow: 0 1px 2px var(--veil-pit);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
/* js/realm.js MEASURES the horizontal offset (placeLandmarkLabel): the label
   lies OUTWARD, toward the edge the landmark stands nearest, inside the band of
   painting that is free of the words — and drops under the glyph when that side
   is too thin to hold it. Only the vertical offset is CSS's. */
.landmark__label { transform: translate(0, 4px); text-align: left; }
.landmark__label.is-under { text-align: center; }
/* a landmark low in the frame (the canals' quay, the library's desk) puts its
   label ABOVE itself, or it would be written off the bottom of the window */
.landmark.is-low .landmark__label { top: auto; bottom: calc(100% + 10px); }
.landmark:hover .landmark__label,
.landmark:focus-visible .landmark__label,
.landmark.is-open .landmark__label { opacity: 1; transform: translate(0, 0); }

/* the once-per-install whisper: a caption that drifts up beside the landmark
   and is gone. Never a tooltip, never a box you have to dismiss. */
/* js/realm.js MEASURES this one into place (placeWhisper): it is laid outward,
   toward the edge the landmark stands nearest, because the words begin only a
   few percent inboard of the landmark itself. left/top are written in px. */
.landmark__whisper {
  position: absolute;
  left: 0;
  top: 0;
  max-width: 14rem;
  font: italic 400 1rem/1.4 var(--font-display);
  color: var(--veil-lumen);
  text-shadow: 0 1px 4px var(--veil-pit), 0 0 12px var(--veil-pit), 0 0 26px var(--veil-glow);
  pointer-events: none;
  animation: lm-whisper 9s var(--ease-out) both;
}

@keyframes lm-whisper {
  0%   { opacity: 0; translate: 0 calc(10px * var(--motion-ok)); }
  12%  { opacity: 1; translate: 0 0; }
  78%  { opacity: 1; translate: 0 calc(-8px * var(--motion-ok)); }
  100% { opacity: 0; translate: 0 calc(-18px * var(--motion-ok)); }
}

/* REVIEW FIX (POLISH 10) — THE SKY STEPS BACK FROM THE WORDS.
   On a phone the words fill the frame, and the realms' anchors (drawn for a
   wide window) put vessels straight over the title. js/realm.js marks whatever
   is standing inside the veil's own box while a note is open on a narrow
   screen; those step back and stop taking a pointer, and come forward again
   the moment the note closes or the Shelf or the ring is opened. Wide windows
   never do this — there the anchors already sit clear of the veil. */
.vessel.is-behind-words,
.landmark.is-behind-words {
  opacity: 0.25;
  pointer-events: none;
}
.vessel.is-behind-words .vessel__label,
.landmark.is-behind-words .landmark__label { opacity: 0; }

/* --------------------------------------------------------------------------
   15.2 THE SHELF
   #shelf is the whole viewport and takes no pointer; only .shelf__panel does,
   and js/realm.js positions that panel beside the landmark (and out of the
   middle third while a note is open). Below 720px it stops being a panel and
   becomes a bottom sheet — at 390px that is the only honest shape for it.
   -------------------------------------------------------------------------- */
#shelf {
  position: fixed;
  inset: 0;
  z-index: var(--z-shelf);
  pointer-events: none;
}
.app:not(.world-mode) #shelf { display: none; }

.shelf__panel {
  position: absolute;
  /* narrow enough that a band beside the words can actually hold it: at 1440
     the text runs 373..1030, which leaves 359px of clear painting on either
     side. See positionShelf() in js/realm.js for the placement it feeds. */
  width: min(340px, 30vw);
  max-height: min(620px, 78vh);
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: var(--space-5) var(--space-4) var(--space-3);
  border: 1px solid var(--wf-rim);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(130% 70% at 14% 0%, color-mix(in srgb, var(--veil-glow) 22%, transparent), transparent 64%),
    var(--wf-ground);
  backdrop-filter: blur(18px);
  box-shadow: var(--wf-lift);
  color: var(--veil-text);
  pointer-events: auto;
  animation: shelf-in 260ms var(--ease-out) both;
}
@keyframes shelf-in {
  from { opacity: 0; scale: 0.97; translate: calc(-8px * var(--motion-ok)) 0; }
  to   { opacity: 1; scale: 1; translate: 0 0; }
}
.shelf.is-right .shelf__panel { animation-name: shelf-in-right; }
@keyframes shelf-in-right {
  from { opacity: 0; scale: 0.97; translate: calc(8px * var(--motion-ok)) 0; }
  to   { opacity: 1; scale: 1; translate: 0 0; }
}

.shelf__close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: color-mix(in srgb, var(--veil-text) 62%, transparent);
  cursor: pointer;
  transition: background 160ms var(--ease-out), color 160ms var(--ease-out),
              border-color 160ms var(--ease-out);
}
.shelf__close:hover,
.shelf__close:focus-visible {
  background: color-mix(in srgb, var(--veil-text) 12%, transparent);
  border-color: var(--wf-rim);
  color: var(--veil-text);
}
.shelf__close .icon { width: 15px; height: 15px; }

.shelf__head { padding-right: var(--space-6); }
.shelf__title {
  margin: 0 0 2px;
  font-family: var(--font-display);
  font-size: 1.32rem;
  line-height: 1.16;
  color: var(--veil-text);
}
.shelf__vessel {
  margin: 0 0 var(--space-1);
  font: italic 400 0.86rem/1.3 var(--font-body);
  color: color-mix(in srgb, var(--veil-text) 74%, transparent);
}
.shelf__vessel b { font-style: normal; font-weight: 600; color: var(--veil-text); }
.shelf__counts {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font: 500 0.7rem/1 var(--font-ui);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--veil-text) 58%, transparent);
}
.shelf__dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.8;
}

.shelf__body {
  overflow-y: auto;
  /* the panel narrows itself into whatever band is free beside the words
     (positionShelf), so a row must never be able to push it wider — a
     horizontal scrollbar inside a floating panel is a panel that is the wrong
     size, not a panel you were meant to scroll sideways */
  overflow-x: hidden;
  margin: var(--space-3) calc(var(--space-2) * -1) var(--space-2);
  padding: 0 var(--space-2);
  display: grid;
  gap: var(--space-4);
  align-content: start;
  scrollbar-width: thin;
}
.shelf__legend {
  margin: 0 0 var(--space-2);
  font: 600 0.66rem/1 var(--font-ui);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--veil-text) 50%, transparent);
}
.shelf__books,
.shelf-group__list {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.shelf-empty {
  margin: 0;
  font: italic 400 0.86rem/1.4 var(--font-body);
  color: color-mix(in srgb, var(--veil-text) 54%, transparent);
}

/* a row is one target plus one quiet verb. The verb only appears when the row
   is being used — hovered, or holding focus — so a list of twelve notes is a
   list of twelve titles, not twenty-four controls. */
.shelf-book,
.shelf-note {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  border-radius: var(--radius-sm);
}
.shelf-book__open,
.shelf-note__open {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-width: 0;
  min-height: 34px;
  padding: var(--space-1) var(--space-2);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--veil-text);
  font: 500 0.88rem/1.3 var(--font-ui);
  text-align: left;
  cursor: pointer;
  transition: background 150ms var(--ease-out);
}
.shelf-book__open:hover,
.shelf-book__open:focus-visible,
.shelf-note__open:hover,
.shelf-note__open:focus-visible { background: color-mix(in srgb, var(--veil-text) 10%, transparent); }
.shelf-book__open .icon { width: 15px; height: 15px; flex: none; opacity: 0.78; }
.shelf-book__name,
.shelf-note__title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shelf-book__count {
  flex: none;
  font: 500 0.72rem/1 var(--font-ui);
  color: color-mix(in srgb, var(--veil-text) 52%, transparent);
  font-variant-numeric: tabular-nums;
}
.shelf-note__when {
  flex: none;
  font: 400 0.7rem/1 var(--font-ui);
  color: color-mix(in srgb, var(--veil-text) 48%, transparent);
}
.shelf-send {
  flex: none;
  padding: 3px var(--space-2);
  border: 1px solid transparent;
  border-radius: 999px;
  background: none;
  color: color-mix(in srgb, var(--veil-text) 62%, transparent);
  font: 500 0.68rem/1.4 var(--font-ui);
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: pointer;
  opacity: 0;
  transition: opacity 150ms var(--ease-out), color 150ms var(--ease-out),
              border-color 150ms var(--ease-out), background 150ms var(--ease-out);
}
.shelf-book:hover .shelf-send,
.shelf-note:hover .shelf-send,
.shelf-send:focus-visible,
.shelf-book:focus-within .shelf-send,
.shelf-note:focus-within .shelf-send { opacity: 1; }
.shelf-send:hover,
.shelf-send:focus-visible {
  border-color: var(--wf-gold);
  background: color-mix(in srgb, var(--accent-soft) 14%, transparent);
  color: var(--veil-text);
}

.shelf-group + .shelf-group { margin-top: var(--space-3); }
.shelf-group__name {
  margin: 0 0 var(--space-1);
  padding-left: var(--space-2);
  font: italic 400 0.8rem/1.3 var(--font-display);
  color: color-mix(in srgb, var(--veil-text) 66%, transparent);
}

.shelf__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid color-mix(in srgb, var(--veil-text) 12%, transparent);
}
.shelf__act {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 34px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--wf-rim);
  border-radius: 999px;
  background: color-mix(in srgb, var(--veil-text) 6%, transparent);
  color: var(--veil-text);
  font: 500 0.78rem/1 var(--font-ui);
  cursor: pointer;
  transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out),
              transform 140ms var(--ease-out);
}
.shelf__act .icon { width: 14px; height: 14px; opacity: 0.8; }
.shelf__act:hover,
.shelf__act:focus-visible {
  border-color: var(--wf-gold);
  background: color-mix(in srgb, var(--accent-soft) 16%, transparent);
}
.shelf__act:active { transform: scale(0.97); }

/* The compass stands in the bottom-right corner, which is exactly where a
   bottom sheet puts its footer buttons — so while the sheet is up the rose
   steps aside. The sheet carries its own close, its own Esc and its own two
   actions, so nothing is lost while it is open. */
.app.shelf-sheet #compass,
.app.shelf-sheet #compass-menu { opacity: 0; pointer-events: none; }

/* the bottom sheet. js/realm.js drops its inline left/top at this width, so
   these four edges are the only thing placing the panel. */
.shelf.is-sheet .shelf__panel {
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  width: 100%;
  max-height: 76vh;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border-bottom: none;
  /* The sheet is full-bleed, but its ROWS are not: on a 1024px window (where
     the sheet is now also the fallback when nothing fits beside the words) a
     title on the left and a timestamp a metre away on the right is not a row,
     it is two columns of unrelated text. The content keeps a readable measure
     and centres itself; on a phone the max() collapses to the plain padding. */
  padding-inline: max(var(--space-4), calc((100% - 640px) / 2));
  padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
  animation-name: shelf-sheet-in;
}
@keyframes shelf-sheet-in {
  from { opacity: 0; translate: 0 calc(24px * var(--motion-ok)); }
  to   { opacity: 1; translate: 0 0; }
}

/* --------------------------------------------------------------------------
   15.3 THE REALM RING
   Eight glyphs on a circle around a centre js/realm.js clamps to the window,
   the hovered or focused one named in the middle. Same placement trick as the
   compass menu — rotate, translate, un-rotate — so there is no trigonometry
   anywhere and the glyphs never tilt.
   -------------------------------------------------------------------------- */
#realm-ring {
  position: fixed;
  inset: 0;
  z-index: var(--z-ring);
  --r: 120px;
}
.app:not(.world-mode) #realm-ring { display: none; }
.ring__scrim {
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 60% at var(--cx, 50%) var(--cy, 50%),
    color-mix(in srgb, var(--veil-pit) 30%, transparent),
    color-mix(in srgb, var(--veil-pit) 60%, transparent));
  backdrop-filter: blur(3px);
  animation: ring-scrim 240ms var(--ease-out) both;
}
@keyframes ring-scrim { from { opacity: 0; } to { opacity: 1; } }

.ring__wheel {
  position: absolute;
  left: var(--cx, 50%);
  top: var(--cy, 50%);
  width: 0;
  height: 0;
}
/* the ring's own thin line, so the eight read as one wheel and not as eight
   loose buttons floating over a painting */
.ring__wheel::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  margin: calc(var(--r) * -1);
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--veil-text) 18%, transparent);
  box-shadow: inset 0 0 40px color-mix(in srgb, var(--veil-glow) 22%, transparent);
  pointer-events: none;
}

.ring__centre {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 1.32);
  transform: translate(-50%, -50%);
  text-align: center;
  pointer-events: none;
}
/* REVIEW FIX (the Daylight pass): the ring wrote in --veil-lumen, "the realm's
   light", on a ground of --veil-core — which under a LIGHT veil is pale light
   on cream, and all but unreadable on the Mountain Shrine at noon. It uses
   --veil-text instead, exactly as the Shelf does: identical under a dark veil
   (the token IS the realm's light there) and ink under a bright one. */
.ring__hint {
  margin: 0 0 4px;
  font: 600 0.62rem/1.2 var(--font-ui);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--veil-text) 68%, transparent);
  text-shadow: 0 1px 3px var(--veil-pit);
}
.ring__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(0.98rem, 2.1vw, 1.3rem);
  line-height: 1.2;
  color: var(--veil-text);
  text-shadow: 0 0 22px var(--veil-text-glow), 0 1px 4px var(--veil-pit);
}

.ring-realm {
  position: absolute;
  left: 0;
  top: 0;
  width: 54px;
  height: 54px;
  padding: 0;
  border: none;
  background: none;
  color: var(--veil-text);
  cursor: pointer;
  transform: rotate(var(--a)) translate(var(--r)) rotate(calc(-1 * var(--a))) translate(-50%, -50%);
  animation: ring-in 300ms var(--ease-out) both;
}
.ring-realm:nth-of-type(1) { animation-delay: 0ms; }
.ring-realm:nth-of-type(2) { animation-delay: 26ms; }
.ring-realm:nth-of-type(3) { animation-delay: 52ms; }
.ring-realm:nth-of-type(4) { animation-delay: 78ms; }
.ring-realm:nth-of-type(5) { animation-delay: 104ms; }
.ring-realm:nth-of-type(6) { animation-delay: 130ms; }
.ring-realm:nth-of-type(7) { animation-delay: 156ms; }
.ring-realm:nth-of-type(8) { animation-delay: 182ms; }
@keyframes ring-in {
  from { opacity: 0; scale: 0.7; }
  to   { opacity: 1; scale: 1; }
}

.ring-realm__disc {
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--veil-text) 22%, transparent);
  background: color-mix(in srgb, var(--veil-core) 62%, transparent);
  backdrop-filter: blur(10px);
  box-shadow: 0 8px 24px var(--veil-pit);
  transition: background 170ms var(--ease-out), transform 160ms var(--ease-out),
              box-shadow 170ms var(--ease-out), border-color 170ms var(--ease-out);
}
.ring-realm__glyph {
  width: 30px;
  height: 30px;
  fill: currentColor;
  stroke: none;
  filter: drop-shadow(0 0 10px var(--veil-text-glow));
}
/* the realm you are standing in wears the gold rim, so the ring always says
   where you are before it asks where you want to be */
.ring-realm.is-current .ring-realm__disc {
  border-color: var(--wf-gold);
  box-shadow: 0 8px 24px var(--veil-pit), 0 0 20px color-mix(in srgb, var(--accent-soft) 34%, transparent);
}
.ring-realm:hover .ring-realm__disc,
.ring-realm:focus-visible .ring-realm__disc,
.ring-realm.is-over .ring-realm__disc,
.ring-realm.is-target .ring-realm__disc {
  background: color-mix(in srgb, var(--veil-text) 14%, var(--veil-core));
  border-color: color-mix(in srgb, var(--veil-text) 52%, transparent);
  box-shadow: 0 8px 26px var(--veil-pit), 0 0 30px var(--veil-glow);
  transform: scale(1.1);
}
.ring-realm.is-over .ring-realm__disc { transform: scale(1.2); }
.ring-realm:active .ring-realm__disc { transform: scale(0.96); }

/* a ring opened BY A DRAG is quiet: the hand is busy, and nothing was tabbed to */
.ring.is-quiet .ring__hint { opacity: 0.82; }

/* the compass glows while it is the ring's centre */
#compass.is-target {
  border-color: color-mix(in srgb, var(--accent-soft) 38%, transparent);
  box-shadow: 0 8px 28px var(--veil-pit), 0 0 40px var(--veil-glow);
}

/* --------------------------------------------------------------------------
   15.4 THE CARRIED VESSEL
   A vessel being dragged follows the pointer on --drag-x/--drag-y and rises
   above the ring it is being dropped into. `is-springing` is the way back when
   it is let go over nothing; `is-sent` is the streak toward a glyph.
   -------------------------------------------------------------------------- */
.app.is-dragging #vessels { z-index: var(--z-carried); }
.app.is-dragging .vessel:not(.is-lifted) { opacity: 0.4; }
/* nothing else in the world may take a pointer while one is carrying a light */
.app.is-dragging .landmark { pointer-events: none; }

.vessel.is-lifted {
  z-index: 2;
  /* a carried vessel is a pointer gesture end to end — no keyboard ever
     reaches this state — and Chrome's focus ring drawn around it mid-flight
     reads as a stray box floating over the ring */
  outline: none;
  translate: var(--drag-x, 0) var(--drag-y, 0);
  scale: 1.25;
  animation: none;
  filter: brightness(1.3);
  cursor: grabbing;
  transition: none;
}
.vessel.is-lifted .vessel__glyph {
  filter: drop-shadow(0 3px 8px var(--veil-pit))
          drop-shadow(0 0 20px var(--veil-glow))
          drop-shadow(0 0 50px var(--veil-glow));
}
.vessel.is-lifted .vessel__label { opacity: 1; transform: translate(-50%, 0); }

.vessel.is-springing {
  animation: none;
  translate: 0 0;
  transition: translate 340ms var(--ease-out), scale 340ms var(--ease-out),
              filter 340ms var(--ease-out);
}
.vessel.is-sent {
  z-index: 2;
  animation: none;
  translate: var(--drag-x, 0) var(--drag-y, 0);
  scale: 0.34;
  opacity: 0;
  filter: brightness(2);
  transition: translate 440ms var(--ease-in-out), scale 440ms var(--ease-in-out),
              opacity 440ms var(--ease-out), filter 440ms var(--ease-out);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   15.5 RESPONSIVE
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .shelf__panel { width: min(340px, 40vw); }
  .ring-realm,
  .ring-realm__disc { width: 48px; height: 48px; }
  .ring-realm__glyph { width: 26px; height: 26px; }
}
@media (max-width: 720px) {
  .landmark { --lm-size: 52px; }
  .landmark__label { max-width: 62vw; white-space: normal; }
  .landmark__whisper { max-width: 11rem; font-size: 0.9rem; }
  .ring-realm,
  .ring-realm__disc { width: 44px; height: 44px; }
  .ring-realm__glyph { width: 24px; height: 24px; }
  .ring__hint { font-size: 0.56rem; letter-spacing: 0.12em; }
}
@media (max-width: 430px) {
  .landmark { --lm-size: 46px; }
  .shelf.is-sheet .shelf__panel { max-height: 74vh; padding-inline: var(--space-3); }
  .shelf__title { font-size: 1.16rem; }
  .shelf__foot { flex-direction: column; align-items: stretch; }
  .shelf__act { justify-content: center; }
  /* every row's verb is permanent on a touch screen — there is no hover to
     reveal it with, and a control you cannot summon is a control you do not
     have */
  .shelf-send { opacity: 1; border-color: var(--wf-rim); }
}

/* --------------------------------------------------------------------------
   15.6 REDUCED MOTION — a still wayfinding. Everything still appears, nothing
   travels: no breathing halo, no orbit-in, no drift, no spring. A carried
   vessel still follows the pointer (that is the gesture itself, not decoration)
   but it arrives instead of easing.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .landmark__halo,
  .landmark.is-open .landmark__halo { animation: none; opacity: 0.72; transform: none; }
  .landmark:hover .landmark__glyph,
  .landmark:focus-visible .landmark__glyph,
  .landmark:active .landmark__glyph { transform: none; transition: none; }
  .landmark__label,
  .landmark:hover .landmark__label,
  .landmark:focus-visible .landmark__label,
  .landmark.is-open .landmark__label { transform: none; transition: none; }
  .landmark__whisper { animation: none; opacity: 1; }

  .shelf__panel,
  .shelf.is-right .shelf__panel,
  .shelf.is-sheet .shelf__panel { animation: none; }
  .shelf__act:active { transform: none; }

  .ring__scrim { animation: none; }
  .ring-realm { animation: none; opacity: 1; }
  .ring-realm:hover .ring-realm__disc,
  .ring-realm:focus-visible .ring-realm__disc,
  .ring-realm.is-over .ring-realm__disc,
  .ring-realm.is-target .ring-realm__disc,
  .ring-realm:active .ring-realm__disc { transform: none; }
  .ring-realm.is-over .ring-realm__disc { background: color-mix(in srgb, var(--veil-text) 22%, var(--veil-core)); }

  .vessel.is-springing { transition: none; }
  .vessel.is-sent { transition: opacity 200ms linear; translate: 0 0; scale: 1; }
}
