/* ── QCT Terminal CSS — Phosphor Aesthetic ── */

/* ── Global: disable text selection on interactive/display elements ────── */
body, button, .tarot-card, .term-btn, .term-radio, .rank-badge,
.card-name, .card-meta, .card-score, .card-score-label,
.orientation-badge, .ghost-pos-label, .panel-header, .spread-signal,
.cstat-row, .log-line, #canvas-focus-text, #intention-bar,
.card-materializing, .ec-btn,
#card-stage, #card-canvas, .ghost-card {
  user-select: none;
}

/* iOS Safari: suppress text selection and long-press callout on card areas */
#card-stage, #card-canvas, .ghost-card, .tarot-card {
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

:root {
  --phosphor: hsl(150, 84%, 66%);
  --phosphor-dim: hsl(150, 63%, 43%);
  --phosphor-faint: hsl(150, 60%, 12%);
  --phosphor-glow: hsla(150, 84%, 66%, 0.15);
  --phosphor-glow-strong: hsla(150, 84%, 66%, 0.35);
  --phosphor-glow-faint: hsla(150, 84%, 66%, 0.07);
  --bg: hsl(150, 22%, 2%);
  --bg-panel: hsl(150, 22%, 3%);
  --bg-row-alt: rgba(var(--lift), 0.05);
  --amber: #ffb300;

  /* Riverstone. Not grey: a neutral stone on a phosphor screen reads as a hole
     in the picture. These carry the same 150deg hue as everything else at very
     low saturation, so a stone looks lit by the same tube as the rest of the UI
     while still reading as rock rather than as glass. */
  --rune-stone-hi:  hsl(150, 14%, 21%);
  --rune-stone-mid: hsl(150, 16%, 13%);
  --rune-stone-lo:  hsl(150, 20%, 6%);
  --rune-sheen:     hsla(150, 30%, 70%, 0.10);
  --rune-cut:       hsl(150, 24%, 4%);

  /* The face a card is printed on, behind the art. Not --bg-panel: a panel is
     chrome and this is stock. */
  --card-ground:    #080f0a;

  /* The warm weight of prose: the photosensitivity notice, read once and
     carefully over a full-screen shade, and the legal callouts, which sit in
     an amber wash and take their body colour from it rather than from the
     page. Warm where --prose-ink is neutral, which is the whole distinction. */
  --ack-ink:        #d8d2c2;

  /* Prose. The long-form pages -- the landing's body copy, /how-it-works, the
     legal documents -- are read rather than scanned, and they have always set
     themselves in a near-neutral off-white instead of the phosphor. That is
     deliberate: a paragraph glowing green is a readout, and these are
     sentences. The neutral carries a trace of the tube's hue so it still looks
     lit by the same light.

     Three weights, because the pages use three: body, a brighter lead, and a
     dimmer aside. They were six hardcoded hex values across four templates. */
  --prose-ink:    #c3cac6;
  --prose-ink-hi: #d5dcd8;
  --prose-ink-lo: #9aa3a0;
  /* Barely there, and meant to be: enough bloom that a paragraph belongs on a
     tube without becoming a readout. On paper ink does not bloom at all. */
  --prose-glow:   rgba(210, 225, 216, 0.12);
  --red-alert: #ff3c3c;
  --scanline-opacity: 0.22;

  /* Halo. The colour a glow is made of, which is not the same question as the
     colour a letter is made of -- it only looks like the same question while
     the ground is black. On a pale ground a halo in the ink's own hue is a
     smudge; radiance there has to come from saturation in a *different* hue.
     So every shadow and drop-shadow in this file names a halo token rather
     than the ink token, and light mode redefines the halo alone.

     In dark mode these are aliases and nothing renders differently. That is
     the point: the indirection is verifiable precisely because it changes
     nothing. */
  --halo: var(--phosphor);
  --halo-amber: var(--amber);
  --halo-alert: var(--red-alert);

  /* Shade and lift. A translucent black darkens a black screen barely at all
     and a pale page a great deal, so these two flip sign between themes: what
     was a lift on the tube is a band on the page, and what was a hole is a
     tint. They are RGB triplets rather than colours because the alpha is the
     part each site chose for itself -- a 0.05 row stripe and a 0.85 vignette
     edge are the same ink at different strengths, and a token per strength
     would be forty tokens. */
  --shade: 0, 0, 0;
  --lift: 255, 255, 255;

  /* The scanline stripe is the one shade that inverts outright. Dark stripes
     on a dark tube read as a raster; on a pale page they read as a barcode.
     Light mode makes the stripe the pale one, which is light coming through
     laid paper. The rhythm and --scanline-opacity are unchanged either way.
     The vignette does not invert with it: a pale vignette on a pale page has
     nothing to darken, and the tube's curvature goes with it. It is made of
     --scrim below rather than of --shade, which this comment used to say. */
  --scanline-ink: 0, 0, 0;

  /* The ink a full-bleed darkening is made of, separately from --shade.

     --shade is a drop shadow's: small, close under a panel, gone within a few
     pixels of it. This is for the things that cover the screen instead -- the
     vignette, and the scrims behind the sampling overlay, the refocus prompt,
     the card modal, the sidebar and the photosensitivity notice. Whatever hue
     one of those carries it imposes on every colour beneath it, across the
     whole screen, which is a different job from a shadow and wants a
     different answer. Black on the tube, as it always was. */
  --scrim: 0, 0, 0;

  /* How a thing that emits light sits on the surface behind it. `screen` is
     the operation for light landing on a dark ground -- it can only ever
     brighten, which is what makes a phosphor block, a caret and a scan canvas
     read as glowing rather than as painted. On a pale ground it can only ever
     brighten toward white, so the same three objects dissolve into the page.
     `multiply` is its mirror: ink that can only darken. It also degrades the
     right way, because a multiply against a transparent backdrop leaves the
     source alone, and the source is already the ink. */
  --blend-emit: screen;

  /* The declarative half of the <meta name="color-scheme"> in _pwa_head.html.
     The meta covers the window before this file arrives; this is what holds
     once it has. */
  color-scheme: dark;

  /* Amber, in the two jobs the token cannot do at once. --amber is the ink;
     --amber-rgb is the same pigment at partial strength for washes, borders
     and strokes; --halo-amber-rgb is what an amber glow is *made* of. On
     black all three are the same gold. On celadon the ink and the washes go
     bronze so they can be seen, while the glow stays gold so it can still
     read as light rather than as a brown stain. */
  --amber-rgb: 255, 179, 0;
  --halo-amber-rgb: 255, 179, 0;

  /* The alert channel, split the same way and for the same reason. */
  --alert-rgb: 255, 60, 60;
  --halo-alert-rgb: 255, 60, 60;

  /* The ground at partial strength: the scrims that let a panel sit over the
     screen without the screen showing through it. Hardcoded to the tube's
     near-black, which is the one literal in this file that was never a colour
     choice at all -- it was --bg, written out because rgba() needs channels.
     Now it is --bg again, and it follows the theme. */
  --bg-rgb: 3, 10, 6;
}

/* ── Yang: the light theme ───────────────────────────────────────────────
   Not dark mode with the lights on. The dark is not a colour scheme, it is
   emitted light -- green phosphor on a black tube, with every level of
   emphasis carried by how hard a thing glows. Inverting the numbers inherits
   none of that. So this inverts the physics instead: light *transmitted*
   through a pale page rather than emitted off it, the green moving from the
   figure to the ground, and emphasis carried by ink density with a chromatic
   aura standing in for the glow.

   Green does not leave the product here. It changes role, and by area it is
   more dominant on this page than it ever was on the tube.

   What is NOT in this block: --phosphor, --phosphor-dim, --phosphor-faint,
   the three --phosphor-glow-* aura tokens, --bg, --bg-panel and the three
   --icon-* tokens. Those are written as inline styles on :root by the theme
   boot script, because they follow the visitor's hue and intensity, and an
   inline style on :root beats every rule in this file. Their light values are
   in _theme_boot.html and nowhere else. Adding one here would be silently
   dead. */
:root[data-theme="light"] {
  /* The halo. Follows the aura the boot script derives, rather than naming a
     colour: the aura is held 45 degrees off the visitor's ink so it can never
     collide with it, and a constant here would lose that the moment anybody
     moved the slider. */
  --halo: var(--phosphor-glow-strong);

  /* Amber is the one channel that does NOT invert.

     Everything else in this block obeys the rule the theme is built on: on
     the tube a thing is made of light, on the page it is made of ink. Amber
     was following it too, and every readable answer was a mustard -- because
     a dark yellow is a mustard, and there is no dark *bright* yellow to be
     had. Three attempts down that road (bronze, gold, yellow at the contrast
     ceiling) each traded more of the colour away for legibility and none of
     them looked like amber.

     So amber stops being ink and stays light: near enough the hue and the
     saturation the tube glows at, taken well down in lightness because
     #ffb300 at full strength sits too close to a pale page to have a body.
     The halo is NOT taken down with it -- it stays the tube's own #ffb300 by
     inheriting :root -- so the colour has a darker core inside a brighter
     gilt, and the glow bleeding out of the deep fill is the gradient the
     wordmark's OS is actually made of. That is the one arrangement here that
     reads as lit rather than as painted, and it is why the core is set this
     deep: a lighter core closes the gap to the halo, the bleed stops being
     visible, and the letters flatten into a yellow that leans orange.

     Saturation comes down with the lightness, but only part way, and the
     margin between "part way" and "all the way" is the whole colour. Full
     saturation at a dark lightness is not amber on paper, it is fire -- the
     letters read as hot orange and the eye stops seeing metal. Take the
     saturation too far the other way and it goes chalky: 45% was tried and
     read as dust on the page rather than leaf under it. 70% is the band where
     it is still gold and not yet flame.

     Measured against the default celadon: 3.44 for the core, and 2.39 between
     core and halo. The core was 43% until 2026-09-29 (2.65 and 1.74); the
     celadon was taken down three points then to stop the page glaring, which
     would have left it at 2.50, under the 3:1 that large text such as the
     subtitle needs, so the core came down to 36% with it. The halo is untouched,
     so the gap to it -- the bleed -- is wider than before, not narrower. Both are low on purpose -- this is a colour for fills,
     marks and the wordmark, and the narrow core-to-halo gap is what makes the
     bleed look continuous rather than like an outline.

     A gradient ramped down each row of the wordmark was built here and taken
     out: background-clip: text over the per-row spans gave real leaf banding,
     and it looked like a cheap chrome effect at any size the mark is actually
     shown at. The bevel the ASCII already has is the only one it needs.

     3.44 still sits under 4.5, so amber set as *small* text is marginal -- the status-bar
     values, .config-val.amber, the LOGOUT link, #tab-signup, .auth-status.info
     and .legal-callout b. That is on purpose and on record: those six are
     labels and accents at small sizes, and the alternative is the mustard
     above. An outline under the glyph was tried as a fix and taken out again
     -- at 120px it read as gold leaf, but the edge is what killed the
     gradient, and everywhere smaller it read as weight. */
  --amber: hsl(41, 70%, 36%);
  --amber-rgb: 156, 115, 28;
  /* --halo-amber and --halo-amber-rgb are deliberately absent: they inherit
     the tube's #ffb300, which is what keeps the glow brighter than the core. */


  /* Garnet. 6.52 against celadon, against the dark theme's 5.76 -- the alert
     channel is the one place light mode is marginally louder than dark. */
  --red-alert: hsl(0, 65%, 38%);
  --alert-rgb: 160, 34, 34;
  --halo-alert: hsl(0, 85%, 58%);
  --halo-alert-rgb: 239, 57, 57;

  /* Shade and lift, both flipped.

     The shade is a warm grey-green rather than black. It carries alphas from
     0.4 to 0.88 that were chosen against a black ground, where near-opaque
     black over black is invisible; the same alphas over celadon in true black
     would punch holes in the page. A mid-tone in the ground's own family
     shades at every one of those strengths without any of them becoming a
     hole, and it keeps the drop shadows reading as paper shadows.

     The lift becomes the ink -- and it has to be the ink the visitor actually
     chose, not a constant. It was written here as a literal, which quietly
     froze it at the aubergine the light theme was first drafted in: every
     row stripe stayed violet however the hue slider moved. --ink-rgb comes
     from the boot script, so it follows. */
  --shade: 103, 112, 81;
  --lift: var(--ink-rgb);

  /* The one shade that inverts outright. A dark stripe over a pale page is a
     barcode; a pale one is light coming through laid paper. */
  --scanline-ink: 255, 255, 255;
  --blend-emit: multiply;
  color-scheme: light;

  /* Near-neutral, and deliberately *less* chromatic than the page it darkens.
     --shade is a warm grey-green, which is right for a shadow cast on paper
     and wrong for a full-bleed darkening: at 0.85 it took the vignette's rim
     to a chroma of 28 against the page's own 12, so the edges read as an
     olive wash that every colour underneath had to argue with.

     The overlay scrims were that same bug somewhere easier to see, and they
     kept --shade for a while after the vignette had given it up. Measured
     over the celadon page: the sampling screen at 0.82 painted
     rgb(127,135,107) -- chroma 16 at hue 120 -- on a page that is itself only
     chroma 6, so a sheet of dim glass came out three times greener than the
     thing it was covering. The card modal at 0.72 and the refocus prompt at
     0.88 were the same colour at other strengths. This holds every one of
     them under chroma 4.

     The two per cent of warmth that is left keeps a dead-neutral grey from
     reading cool against a yellow-green ground; it is not enough to be a
     hue. */
  --scrim: 109, 108, 105;

  /* The ground at partial strength, for the scrims. */
  --bg-rgb: 229, 232, 217;

  /* Riverstone, turned over. The ordering is unchanged -- hi is still the lit
     face and lo still the shaded one -- but a stone on a page is a pale pebble
     rather than a dark one, and it takes the ground's warm hue for the same
     reason the dark stones take the tube's: a stone lit by a different light
     than the rest of the picture reads as a hole in it. */
  --rune-stone-hi:  hsl(72, 12%, 82%);
  --rune-stone-mid: hsl(72, 10%, 72%);
  --rune-stone-lo:  hsl(72, 13%, 58%);
  --rune-sheen:     hsla(60, 40%, 100%, 0.55);
  --rune-cut:       hsl(72, 14%, 34%);

  /* Card stock, and the notice that is read over a full-screen shade. A step
     above the panels so a card still reads as a card, but no longer paper-white:
     it was 96% (L* 97), the brightest surface in the theme, and the question
     box is made of it. Taken to 92% with the ground on 2026-09-29. */
  --card-ground: hsl(66, 22%, 92%);
  --ack-ink: hsl(60, 10%, 12%);

  /* Prose, turned over. Each of the three is matched to the contrast its dark
     counterpart measures against the tube -- 12.18, 14.57 and 7.86 -- so a
     paragraph carries the same weight on paper that it does on the screen.
     The trace of hue goes cool, because the ink it sits beside is now blue;
     in dark it leans green for the same reason. It is 7% saturation, which is
     a temperature rather than a colour. */
  --prose-ink:    hsl(220, 7%, 17%);
  --prose-ink-hi: hsl(220, 7%, 11%);
  --prose-ink-lo: hsl(220, 7%, 29%);
  --prose-glow:   transparent;
}


* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 20px;
  overflow: hidden;
  cursor: none;
}

a { text-decoration: none; }

/* ── Custom cursor ──────────────────────────────────────────────────── */

/* Three panels keep the operating system's cursors and hide the phosphor block
   while the pointer is over them: the archive sidebar (the HISTORY list) and
   the oracle sidebar here, and #settings-panel from terminal.js -- which fades
   #cursor on mousemove rather than matching :hover, because the panel slides in
   over whatever the pointer was already on top of.

   Anything on this list has to be on the exclusion list below as well, or the
   panel ends up with no cursor of either kind. */
body:has(#left-panel:hover) #cursor,
body:has(#detail-oracle:hover) #cursor { display: none; }
/* The consent gate wears the phosphor block like the rest of the product
   (owner, 2026-10-04). For a day it hid the block and was excluded from the
   suppression below, meant for a phone, where there is no block anyway: on a
   desktop that left the gate with no cursor at all, since body's
   `cursor: none` is inherited by everything that declares no cursor of its
   own -- only the boxes and the button showed the system pointer. */

/* Suppress the system cursor everywhere the phosphor block is the pointer.

   `cursor: none` on body, above, states only a default -- and it lost. Every
   button, link, checkbox, summary and drag handle in the product declares a
   `pointer`, `text` or `grab` of its own, and two of them declare it in an
   inline style attribute. The OS pointer therefore came back on exactly the
   surfaces carrying the most chrome: the landing page, the sign-in and sign-up
   forms, the consent gate, every menu. Beating all of that is a policy rather
   than a default, and that is what earns the `!important` here -- nothing
   weaker than an important author declaration reaches an inline style.

   The three exception panels are excluded wholesale rather than re-granted a
   cursor apiece. Excluding them leaves their own `pointer` / `text` /
   `ew-resize` rules working through the ordinary cascade, so there is no second
   copy of that list living here to be kept in step with them.

   Scoped to the width at which #cursor exists at all. Below 768px the block is
   hidden (see the mobile section) and the platform's cursor is the only one
   left: a touch device draws neither, but a narrow desktop window would
   otherwise be left with no pointer whatsoever.

   `.legal-page` opts the legal documents out entirely. They hide #cursor on
   purpose so that their text can be selected and copied, which is worth more on
   a document than the terminal's own pointer is -- see legal_base.html.

   A browser too old for a complex selector inside :not() drops this rule and
   nothing else, which leaves the system cursors exactly where they were rather
   than breaking anything. */
@media (min-width: 768px) {
  body:not(.legal-page) :not(
    #left-panel, #left-panel *,
    #settings-panel, #settings-panel *,
    #detail-oracle, #detail-oracle *
  ) { cursor: none !important; }
}


#cursor {
  position: fixed;
  width: 10px;
  height: 18px;
  background: var(--phosphor);
  pointer-events: none;
  z-index: 9999;
  mix-blend-mode: var(--blend-emit);
  transition: opacity 0.1s, width 0.1s, height 0.1s, clip-path 0.1s;
  box-shadow: 0 0 8px var(--halo), 0 0 20px var(--phosphor-glow-strong);
}

/* 4-way arrow cursor for canvas pan mode.
   clip-path lives on ::before so drop-shadow on the parent can bleed outside. */
#cursor.cursor-move {
  width: 24px;
  height: 24px;
  background: transparent;
  box-shadow: none;
  transform: translate(-7px, -3px);
  filter: drop-shadow(0 0 8px var(--halo)) drop-shadow(0 0 20px var(--phosphor-glow-strong));
}
#cursor.cursor-move::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--phosphor);
  /* Equal 30% wingspan on all 4 arrowheads, 16% shaft */
  clip-path: polygon(
    50% 0%,
    65% 25%, 58% 25%, 58% 42%,
    75% 42%, 100% 50%, 75% 58%,
    58% 58%, 58% 75%, 65% 75%,
    50% 100%,
    35% 75%, 42% 75%, 42% 58%,
    25% 58%, 0% 50%, 25% 42%,
    42% 42%, 42% 25%, 35% 25%
  );
}

/* ── CRT screen wrapper ─────────────────────────────────────────────── */

#screen {
  position: relative;
  width: 100vw;
  /* The window itself: html and body are height: 100%, which is the box a
     position: fixed element measures against. Not 100dvh: in the installed app on
     Android with three-button navigation, dvh came out taller than the
     window, so the screen ran past the window's foot and the public pages lost their last
     lines and footer below it (owner's screenshot, 2026-10-12). The app's
     pages hid it, their foot being the fixed bottom bar. */
  height: 100%;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  box-sizing: border-box;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Scanlines */
#screen::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 3px,
    rgba(var(--scanline-ink), var(--scanline-opacity)) 3px,
    rgba(var(--scanline-ink), var(--scanline-opacity)) 4px
  );
  pointer-events: none;
  z-index: 100;
}

html.scanlines-off #screen::before { display: none !important; }
/* The standalone Mind Machine is a flat field of static: no tube curvature at
   its edges. Its overlay sits at 101, under the rail, and the vignette above
   it at the same level (grimoire-entrainment.js sets mm-playing). */
html.mm-playing #screen::after { display: none !important; }
html.sweep-off .glitch-line { display: none !important; }

/* Vignette.

   Faded out of the bands at the top and the foot, in both themes. Every page
   keeps its controls there -- the header, the reading's title bar, the phone
   header, the footer, the oracle's composer -- whatever element draws them,
   and the vignette is strongest exactly at those edges. On the tube it took
   the lit ASK square from #ffb300 to rgb(108,76,1) (11.3:1 down to 2.6:1) and
   the title line from 8.5:1 to 5.6:1; on the page it greyed the paper under
   the gear. Lifting the chrome above it does not help -- the gear was already
   above it -- because a control's ground is the screen, and the screen is what
   the vignette darkens. The mask protects all of them at once, and a new page
   inherits it without being told. The sides keep the full curvature, which is
   where it reads as the tube and not as dimmed controls. */
#screen::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at center,
    transparent 55%,
    rgba(var(--scrim), 0.5) 85%,
    rgba(var(--scrim), 0.85) 100%
  );
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, transparent 72px, #000 150px,
    #000 calc(100% - 110px), transparent calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(to bottom,
    transparent 0, transparent 72px, #000 150px,
    #000 calc(100% - 110px), transparent calc(100% - 40px), transparent 100%);
  pointer-events: none;
  z-index: 101;
}

/* The vignette in yang: the same idea with the page's physics. A page lit
   from behind falls off gently toward its edges; it does not go grey. At the
   tube's 0.85 it took the ground under the top-right corner from
   rgb(236,238,226) to rgb(150,150,144). So the strength is a sixth of the
   tube's at the rim, and the falloff starts later. The band mask above
   applies here as well. */
:root[data-theme="light"] #screen::after {
  background: radial-gradient(ellipse at center,
    transparent 60%,
    rgba(var(--scrim), 0.06) 84%,
    rgba(var(--scrim), 0.14) 100%
  );
}

/* Phosphor bloom */
#bloom {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 40%,
    var(--wash-faint) 0%,
    transparent 70%
  );
  pointer-events: none;
  z-index: 99;
}

/* Noise grain */
#noise {
  position: absolute;
  inset: 0;
  opacity: 0.018;
  pointer-events: none;
  z-index: 98;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
  background-size: 128px;
}

/* Glitch sweep line.
   Travels on transform rather than top. Both read identically -- a 1px rule
   crossing the screen over eight seconds -- but `top` is a layout property, so
   the old version made the browser re-lay-out and re-style the page on every
   frame it was on screen, sixty times a second for as long as the sweep was
   enabled. Measured over six seconds on a 600-line log: 360 layouts, 360 style
   recalculations and 547ms of main-thread time, against 0 and 10ms here.
   transform and opacity are both compositor properties, so between them the
   whole animation now runs off the main thread. */
.glitch-line {
  position: absolute;
  left: 0; right: 0;
  /* The keyframes no longer set `top`, and without this the element would fall
     back to its static position inside #screen's flex column. */
  top: 0;
  height: 1px;
  background: var(--phosphor);
  opacity: 0;
  pointer-events: none;
  z-index: 102;
  animation: glitch-scan 8s linear infinite;
}

@keyframes glitch-scan {
  0%   { transform: translateY(-2px); opacity: 0; }
  2%   { opacity: 0.3; }
  98%  { transform: translateY(100vh); opacity: 0.15; }
  100% { transform: translateY(100vh); opacity: 0; }
}

/* ── Header ─────────────────────────────────────────────────────────── */

#header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 16px;
  border-bottom: 1px solid var(--phosphor-dim);
  flex-shrink: 0;
  background: var(--bg-panel);
  box-shadow: 0 1px 12px var(--phosphor-glow);
  z-index: 10;
}

#header .logo {
  font-family: 'VT323', monospace;
  font-size: 39px;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--halo), 0 0 30px var(--phosphor-glow-strong);
  letter-spacing: 2px;
  white-space: nowrap;
}

#header .logo span {
  color: var(--amber);
  text-shadow: 0 0 10px var(--halo-amber), 0 0 30px rgba(var(--halo-amber-rgb), 0.3);
}

.header-title {
  font-size: 15px;
  color: var(--phosphor-dim);
  letter-spacing: 2px;
  text-transform: uppercase;
}

.status-row {
  display: flex;
  gap: 24px;
  align-items: center;
}

.status-item {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
}

.status-label {
  font-size: 14px;
  color: var(--phosphor-dim);
  letter-spacing: 2px;
  text-transform: uppercase;
}

.status-value {
  font-size: 20px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
}

.status-value.amber { color: var(--amber); text-shadow: 0 0 6px var(--halo-amber); }
.status-value.alert { color: var(--red-alert); text-shadow: 0 0 6px var(--halo-alert); }

.user-email {
  font-size: 13px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}

.user-links {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
}

.user-link {
  font-size: 13px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  text-decoration: none;
  text-transform: uppercase;
  transition: color 0.15s, text-shadow 0.15s;
}

.user-link:hover {
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
}

.user-link.amber { color: var(--amber); }
.user-link.amber:hover { color: var(--amber); text-shadow: 0 0 6px var(--halo-amber); }

.user-link-sep {
  font-size: 12.5px;
  color: var(--phosphor-faint);
}

.pulse-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--phosphor);
  box-shadow: 0 0 8px var(--halo);
  animation: pulse 1.4s ease-in-out infinite;
  margin-right: 4px;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.2; }
}

/* ── Main layout ─────────────────────────────────────────────────────── */

#main {
  display: grid;
  flex: 1;
  overflow: hidden;
  gap: 0;
  min-height: 0;
}

/* ── Panel system ────────────────────────────────────────────────────── */

#left-panel {
  position: relative;
  z-index: 102;
  cursor: default;
  border-right: 1px solid var(--phosphor-faint);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--bg-panel);
  box-shadow: inset 0 0 24px var(--phosphor-glow-faint);
  min-height: 0;
}

#center-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 0;
}

#right-panel {
  border-left: 1px solid var(--phosphor-faint);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  min-height: 0;
}

.panel-header {
  padding: 8px 12px;
  border-bottom: 1px solid var(--phosphor-faint);
  font-size: 14px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  text-transform: uppercase;
  flex-shrink: 0;
  user-select: text;
}


/* Spread header: title + inline signal stats */
.spread-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.spread-signal {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

/* Add breathing room before each label (except the first) */
.spread-signal .signal-label + .bar-value {
  margin-right: 10px;
}

.spread-signal .signal-label {
  font-size: 13px;
  color: var(--phosphor-faint);
  letter-spacing: 1px;
}

.spread-signal .bar-value {
  font-size: 15px;
  letter-spacing: 1px;
  color: var(--phosphor);
}


/* ── Config / form elements ──────────────────────────────────────────── */

.config-section {
  padding: 10px 12px;
  border-bottom: 1px solid var(--phosphor-faint);
}

.config-title {
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  margin-bottom: 8px;
  text-transform: uppercase;
}

.config-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 6px;
}

.config-key {
  font-size: 16px;
  color: var(--phosphor-dim);
}

.config-val {
  font-size: 18px;
  color: var(--phosphor);
  text-shadow: 0 0 4px var(--halo);
}

.config-val.amber { color: var(--amber); text-shadow: 0 0 4px var(--halo-amber); }

/* Range slider */
input[type=range] {
  -webkit-appearance: none;
  width: 100%;
  height: 2px;
  background: var(--phosphor-faint);
  outline: none;
  margin: 4px 0 8px;
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 10px;
  height: 10px;
  background: var(--phosphor);
  box-shadow: 0 0 6px var(--halo);
  cursor: pointer;
}


/* Terminal radio buttons */
.term-radio-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 10px;
}

.term-radio {
  padding: 4px 8px;
  font-size: 18px;
  color: var(--phosphor-dim);
  border: 1px solid var(--phosphor-faint);
  cursor: pointer;
  transition: all 0.1s;
}

.term-radio:hover {
  border-color: var(--phosphor-dim);
  color: var(--phosphor);
}

.term-radio.active {
  border-color: var(--phosphor);
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  background: var(--wash);
}


/* Toggle switch */
.toggle-wrap {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.toggle-label { font-size: 16px; color: var(--phosphor-dim); }

.toggle {
  width: 32px;
  height: 14px;
  background: var(--phosphor-faint);
  border: 1px solid var(--phosphor-dim);
  position: relative;
  cursor: pointer;
}

.toggle.on { background: var(--phosphor-faint); border-color: var(--phosphor); }

.toggle::after {
  content: '';
  position: absolute;
  top: 1px; left: 1px;
  width: 10px; height: 10px;
  background: var(--phosphor-dim);
  transition: left 0.15s, background 0.15s;
}

.toggle.on::after {
  left: 19px;
  background: var(--phosphor);
  box-shadow: 0 0 6px var(--halo);
}

/* Buttons */
.term-btn {
  display: block;
  width: calc(100% - 24px);
  margin: 8px 12px;
  padding: 10px;
  background: transparent;
  border: 1px solid var(--phosphor);
  color: var(--phosphor);
  font-family: 'VT323', monospace;
  font-size: 30px;
  letter-spacing: 3px;
  cursor: pointer;
  text-shadow: 0 0 8px var(--halo);
  box-shadow: 0 0 10px var(--phosphor-glow), inset 0 0 10px var(--phosphor-glow);
  transition: all 0.15s;
  text-align: center;
}

.term-btn:hover {
  background: var(--wash);
  box-shadow: 0 0 20px var(--phosphor-glow-strong), inset 0 0 20px var(--phosphor-glow);
  text-shadow: 0 0 16px var(--halo);
}

.term-btn:active { transform: scale(0.97); }

.term-btn.amber-btn {
  border-color: var(--amber);
  color: var(--amber);
  text-shadow: 0 0 8px var(--halo-amber);
  box-shadow: 0 0 10px rgba(var(--halo-amber-rgb), 0.2), inset 0 0 10px rgba(var(--halo-amber-rgb), 0.1);
}

.term-btn.amber-btn:hover {
  background: rgba(var(--amber-rgb), 0.1);
  box-shadow: 0 0 20px rgba(var(--halo-amber-rgb), 0.4), inset 0 0 20px rgba(var(--halo-amber-rgb), 0.15);
}

.term-btn.sm {
  font-size: 24px;
  padding: 6px;
  margin: 4px 12px;
}

/* ── Card stage (pan/zoom viewport) ─────────────────────────────────── */

#card-stage {
  flex: 1;
  position: relative;
  overflow: hidden;
  cursor: none;
  min-height: 0;
  background: transparent;
}

#card-stage:active { cursor: grabbing; }


/* The infinite canvas inside the viewport */
#card-canvas {
  position: absolute;
  transform-origin: 0 0;
  /* size and transform set by JS */
}

/* Default (non-positioned) layout: flex row of cards */
#card-canvas.layout-default {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  /* Room for the clarifier badge that sits outside each card's right edge (a
     5px margin and a 28px badge), with air before the next card. Was 24px,
     which put the badge on the neighbour. */
  gap: 64px;
  padding: 40px;
}
/* A row of rune stones (The Norns) sits closer. A drawn stone's clarifier
   badge is inside its tile's corner ("Runes on the canvas"), not off its side,
   so the room kept for it is not needed -- and a stone narrower than its tile
   already has air either side. Owner, 2026-09-25: too far apart at 64;
   2026-09-26: a little too close at 8. */
#card-canvas.layout-default:has(> .tarot-card.is-rune) { gap: 20px; }

.tarot-card {
  position: relative;
  width: 110px;
  transition: opacity 1.5s ease;
  flex-shrink: 0;
}

/* Positioned layouts: cards are absolute within the canvas */
#card-canvas.layout-positioned .tarot-card {
  position: absolute;
  width: 120px;
  /* The stage draws its own cursor, so a system pointer here paints a second
     one on top of it. Cards are surface, not controls — the buttons that sit
     on them set their own pointer and still look clickable. */
  cursor: none;
}

/* The meter used to hang below the card, where it would overlap neighbours in
   a positioned spread — hence the old hover-only treatment. It sits inside the
   card now, so it simply shows. */

/* Hovered card rises above all siblings so its meta isn't clipped -- with a
   real pointer only. A touch screen applies :hover to whatever was tapped, and
   applies it before the click lands, so a buried card reported itself as
   already on top: grRaiseIfCovered() saw nothing to lift and the first tap
   opened its meaning instead of bringing it forward. On touch the lift is the
   deliberate first tap, and the z-index that tap sets is what counts. */
@media (hover: hover) {
  #card-canvas.layout-positioned .tarot-card:hover {
    z-index: 1000 !important;
  }
}

/* Fixed draw/navigate button at bottom of screen */
#draw-nav {
  position: fixed;
  bottom: 36px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
}

#draw-nav .term-btn {
  width: auto;
  margin: 0;
  padding: 8px 32px;
  font-size: 30px;
  letter-spacing: 3px;
  white-space: nowrap;
}

/* Zoom widget overlay */
#canvas-focus-text {
  position: absolute;
  top: 120px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  pointer-events: none;
  font-family: 'VT323', monospace;
  /* 10vw put the steps at 39px on a phone, which left the title above them no
     room to be larger and still fit the width. Only phones move: past 880px
     the clamp is pinned to 66px exactly as before. */
  font-size: clamp(22px, 7.5vw, 66px);
  color: var(--phosphor-dim);
  letter-spacing: 4px;
  text-shadow: 0 0 10px var(--phosphor-glow);
  white-space: normal;
  text-align: center;
  width: max-content;
  max-width: 90vw;
  opacity: 0;
  transition: opacity 0.6s ease;
  background: rgba(var(--scrim), 0.78);
  padding: 6px clamp(14px, 4vw, 28px);
}

/* The instruction alone never said what had just taken over the screen. The
   name goes above it, larger and brighter, so the first full-screen sequence
   reads as a feature rather than a fault. */
#canvas-focus-text .focus-title {
  display: block;
  font-size: 1.35em;
  /* VT323 caps sit high in the box with almost no internal leading, so a title
     line set tight lands directly on the steps below it and the two read as one
     run of text. The leading and the margin are what hold them apart. */
  line-height: 1.15;
  margin-bottom: 0.3em;
  /* In em, not px: on a phone the title is the widest line in the block, and a
     fixed 8px track pushed it past 90vw and wrapped it into the steps. */
  letter-spacing: 0.14em;
  /* Letter-spacing pads the right edge only; half of it back keeps the word
     optically centred over the steps. */
  text-indent: 0.14em;
  /* Two words, and breaking them is what made this look broken. */
  white-space: nowrap;
  color: var(--phosphor);
  text-shadow: 0 0 18px var(--phosphor-glow-strong);
}

#canvas-focus-text .focus-steps {
  display: block;
  /* The steps wrap to two or three lines on a phone; they need leading of their
     own or the wrapped lines collide the same way. */
  line-height: 1.2;
}

#card-stage.entrainment-active #canvas-focus-text {
  opacity: 1;
  transition: opacity 0.4s ease;
}

/* Three seconds from the moment the overlay opens, and then gone for that
   activation. It has been both other things: a one-shot fade that was over
   before a hesitant reader looked for it, and then a fade on *stillness* that
   came back on every pointer move -- which meant a reader settling in kept
   resetting the clock and the caption sat there through the whole approach.
   The second was the worse of the two, because a caption that will not leave
   argues with what it is asking for. See grShowFocusText(). */
#card-stage.entrainment-active #canvas-focus-text.is-faded,
#gr-entrainment-overlay.entrainment-active #canvas-focus-text.is-faded {
  opacity: 0;
}

/* Intention word flash */
#entrainment-word {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 25;
  pointer-events: none;
  font-family: 'VT323', monospace;
  font-size: 80px;
  color: var(--phosphor);
  letter-spacing: 6px;
  text-shadow: 0 0 24px var(--phosphor-glow-strong);
  white-space: nowrap;
  opacity: 0;
}

@keyframes wordFlash {
  0%   { opacity: 0; }
  15%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes mirrorReveal {
  0%   { transform: translate(-50%, -50%) scaleX(0);  opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) scaleX(-1); opacity: 1; }
}

@keyframes mirrorCollapse {
  0%   { transform: translate(-50%, -50%) scaleX(-1);  filter: none;                    opacity: 1; }
  60%  { transform: translate(-50%, -50%) scale(0.08); filter: blur(3px) brightness(5); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(0);    filter: blur(0)   brightness(9); opacity: 0; }
}

@keyframes wordCollapse {
  0%   { transform: translate(-50%, -50%) scale(1);    filter: none;                    opacity: 1; }
  60%  { transform: translate(-50%, -50%) scale(0.12); filter: blur(2px) brightness(4); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(0);    filter: blur(0)   brightness(9); opacity: 1; }
}

/* Procedural sigil canvas — centered, hidden until JS shows it */
#entrainment-sigil {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 26;
  pointer-events: none;
  opacity: 0;
  filter: drop-shadow(0 0 10px var(--halo));
}

#zoom-widget {
  position: absolute;
  bottom: 12px;
  left: 12px;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-faint);
  padding: 4px;
}

#zoom-widget button {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 27px;
  width: 28px;
  height: 28px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

#zoom-widget button:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
  background: var(--wash);
}

.tarot-card .card-frame {
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 0 15px var(--phosphor-glow), 0 0 40px var(--phosphor-glow-faint);
  background: var(--card-ground);
  overflow: hidden;
  aspect-ratio: 2 / 3.5;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* <picture> is only a wrapper for the format negotiation -- AVIF for the ~95%
   of browsers that read it, WebP for the rest. It must not become a layout box
   of its own, or the img's percentage sizing would resolve against it instead
   of the frame. */
.card-frame picture {
  display: block;
  width: 100%;
  height: 100%;
}

.card-frame img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: brightness(0.9) contrast(1.1);
}

.card-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    var(--wash-faint) 0%,
    transparent 40%,
    transparent 60%,
    var(--wash-faint) 100%
  );
  pointer-events: none;
}

/* The vignette is for an empty frame. Source art ranges from 0.44 to 0.61 in
   aspect against a fixed 2/3.5 frame, so every drawn card letterboxes or
   pillarboxes by a different amount — and the gradient painted over the top and
   bottom 40% lands on those bars as well as the art, drawing a bright edge
   exactly where the picture stops. Without it the bars are just card
   background and disappear. */
.card-with-image .card-frame::after { display: none; }

.card-with-image {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 0 15px var(--phosphor-glow), 0 0 40px var(--phosphor-glow-faint);
  background: var(--card-ground);
  overflow: hidden;
}

/* Suppress inner card-frame border — the outer wrapper owns it */
.card-with-image .card-frame {
  border: none;
  box-shadow: none;
}

/* The rank and orientation badges each reach 7px past their border into the
   card, so 3px of padding put them straight on top of the number and the name.
   11px clears the overlap with 4px of air; the extra weight is on the badge
   side only, so the artwork stays where it was. */
/* Fixed to two lines so every card is the same height. Names wrap at one line
   or two -- KNIGHT OF PENTACLES is the longest at 19 characters, and the longest
   single word across both corpora is 9 (PENTACLES, PRIESTESS), so nothing can
   overflow a line and nothing reaches a third. Without this the border grew
   under the two-line names and cards sat at different heights in a spread.

   A corpus with a longer word than the card is wide would need this revisited. */
.card-image-name {
  font-family: 'VT323', monospace;
  font-size: 14px;
  line-height: 1.15;
  /* Phosphor by default; .tarot-card.inverted turns it amber, so the colour is
     what tells a reversed card apart at a glance. */
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  letter-spacing: 2px;
  text-align: center;
  width: 100%;
  /* border-box is global, so this is two lines plus the padding. Fixed at two
     so every card in a spread is the same height whether its name wraps or
     not -- KNIGHT OF PENTACLES is the longest at 19 characters. */
  height: calc(2 * 1.15em + 2px + 6px);
  padding: 6px 0 2px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 0;
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
}

.card-placeholder .roman {
  font-size: 28px;
  color: var(--amber);
  text-shadow: 0 0 10px var(--halo-amber);
}

/* No height reservation here. Text-only cards live inside .card-frame, which
   carries aspect-ratio 2/3.5, so their outer box is already fixed and never
   varied -- reserving a second line would only risk overflowing it. */
.card-placeholder .card-name {
  font-size: 14px;
  line-height: 1.15;
  letter-spacing: 2px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  text-align: center;
  padding: 0 6px;
}

/* A TEXT card's Voice, under its name (spread-view.js, _placeholderHtml):
   flex:none so it keeps its height inside the fixed 2/3.5 frame. */
.card-placeholder:not(.is-rune) > .card-meta {
  flex: none;
  margin-top: 2px;
}

.card-placeholder .sigil {
  font-size: 32px;
  opacity: 0.6;
}

.card-placeholder .divider {
  width: 60%;
  height: 1px;
  background: var(--phosphor-faint);
  margin: 2px 0;
}

/* ── Runes ────────────────────────────────────────────────────────────────
   A rune is not a card, and the two differences that matter are the shape of
   the thing and the mark on it.

   Shape: a stave is carved on a stone, a tile or a slip of wood, all of which
   are near-square. The 2/3.5 card frame is a playing-card proportion and
   letterboxes a rune into a tall empty box.

   Mark: the glyph IS the symbol here, not a decoration beside the name, so it
   leads and it is large. Noto Sans Runic is a clean humanist sans and will not
   sit quietly next to VT323 -- it needs its own size, weight and letter-spacing
   rather than inheriting the terminal face's. The glow is the terminal's, so
   it still belongs to the screen. */
.card-placeholder.is-rune {
  gap: 10px;
}

.card-placeholder.is-rune .sigil {
  font-family: 'Noto Sans Runic', 'Segoe UI Historic', serif;
  font-size: 78px;
  line-height: 1;
  opacity: 1;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--phosphor-glow), 0 0 26px var(--phosphor-glow-faint);
  /* The Runic block is drawn on a tall narrow body; a little tracking stops
     the wider staves (Dagaz, Ingwaz, Jera) from crowding their own frame. */
  letter-spacing: 2px;
}

.card-placeholder.is-rune .card-name {
  font-size: 13px;
  letter-spacing: 3px;
}

/* The stave's phoneme, under the name -- a rune is a letter first. */
.card-placeholder.is-rune .rune-phoneme {
  font-size: 12.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  opacity: 0.75;
}

/* ── Merkstav ─────────────────────────────────────────────────────────────
   Glyph rotation is a new rule, not an extension of the inverted card rule
   above: that one turns the card *image* 180deg and leaves the text
   placeholder alone, which is why an inverted text card only goes amber.

   A rune has no image to turn. The mark itself is what fell, so the mark is
   what rotates -- and it rotates to the angle it actually landed at, which the
   server passes in as --rune-angle. The amber colouring from the inverted rule
   still applies and still carries the meaning; this adds the geometry.

   Symmetric staves never get .inverted, so they never rotate to 180 -- but
   they do still carry their own landing angle, because where a rune came to
   rest is true of it whether or not merkstav can be read from it. */
/* ── The stone ────────────────────────────────────────────────────────────
   A rune is handled, not dealt. The card frame is therefore switched off
   entirely for a rune rather than restyled -- but the element stays, because
   the ripple canvas, the ghost-scan overlay, the tutorial and every layout
   calculation are anchored to .card-frame, and the frame keeps its border and
   background exactly as a card's does. Only its proportion changes. */

/* The glow belongs to the stone's own silhouette, not to a box around it, so
   it is a drop-shadow on the SVG rather than a box-shadow on the frame. */
/* The stone takes the square share of the tile and the name sits under it, so
   the frame is a little taller than wide rather than square. min-height:0 is
   what lets the SVG actually shrink inside a flex column instead of forcing
   the label out of the box. */
.tarot-card.is-rune .card-frame { aspect-ratio: 1 / 1.22; }

.card-placeholder.is-stone {
  gap: 4px;
  padding: 2px 0 6px;
  justify-content: flex-start;
}

/* The voice meter sits under the name, as it does beneath an illustrated card.
   flex:none so it keeps its height and the stone gives up space instead. */
.card-placeholder.is-stone .card-meta {
  flex: none;
  width: 100%;
  margin-top: 2px;
}

.card-placeholder.is-stone .rune-stone {
  flex: 1 1 auto;
  min-height: 0;
  aspect-ratio: 1 / 1;
  width: auto;
  height: auto;
  margin: 0 auto;
}

.rune-stone {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  filter: drop-shadow(0 0 10px var(--phosphor-glow))
          drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
}

/* The whole stone turns, not just the mark on it. A stave that fell merkstav
   was turned over as an object; rotating the glyph alone inside a fixed
   silhouette would say the stone stayed put and the carving moved. */
.rune-stone-turn {
  transform: rotate(var(--rune-angle, 0deg));
  /* Explicit rather than relying on the initial value: engines have disagreed
     about what an SVG element's transform-origin is relative to, and getting
     it wrong swings the stone about a corner instead of its centre. */
  transform-box: view-box;
  transform-origin: 50px 50px;
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}

.rune-stone-body {
  stroke: var(--phosphor-faint);
  stroke-width: 0.8;
}

/* Volume. A copy of this stone's own outline, nudged up-left and clipped back
   inside it, so the highlight follows this silhouette instead of laying the
   same generic ellipse over all twenty-four. */
.rune-stone-sheen {
  fill: none;
  stroke: var(--rune-sheen);
  stroke-width: 2.4;
}

/* The incision, in two passes. The dark copy sits down-right of the lit one,
   which is the shadow a groove casts when the light is up-left -- the same
   direction the sheen assumes. Printed marks have no shadow; cut ones do. */
/* No font-size and no dominant-baseline here: both are set per glyph on the
   element, from the ink metrics in rune-stone.js. A shared font-size would make
   Kaunan half the height of Isa, and dominant-baseline centres the em box
   rather than the ink, which is not the same box for these staves. */
.rune-stone-cut,
.rune-stone-mark {
  font-family: 'Noto Sans Runic', 'Segoe UI Historic', serif;
  text-anchor: middle;
}

/* The groove's shadow: the cut copy sits down-right of the lit one by a
   constant, because the depth of a carving does not scale with the size of the
   mark in it. */
.rune-stone-cut {
  fill: var(--rune-cut);
  transform: translate(1.1px, 1.1px);
}

.rune-stone-mark {
  fill: var(--phosphor);
  filter: drop-shadow(0 0 4px var(--phosphor-glow-strong));
}

/* Amber is how an inverted draw already reads everywhere else; merkstav keeps
   that language rather than inventing a second one. */
.tarot-card.inverted .rune-stone-mark {
  fill: var(--amber);
  filter: drop-shadow(0 0 5px rgba(var(--halo-amber-rgb), 0.35));
}

/* An undrawn slot: the outline of a stone that is there but not yet turned
   over. Dashed and unfilled so the draw reads as a stone arriving rather than
   as one silhouette morphing into another -- the shape cannot match, because
   which stave is under it is exactly what is not yet known. */
.rune-stone-ghost {
  filter: none;
}

.rune-stone-ghost path {
  fill: none;
  stroke: var(--phosphor-faint);
  stroke-width: 1;
  stroke-dasharray: 4 5;
  opacity: 0.85;
}

.ghost-card.card-active .rune-stone-ghost path {
  stroke: var(--phosphor-dim);
  opacity: 1;
}

.ghost-card.draw-ready .rune-stone-ghost path {
  stroke: var(--phosphor);
  stroke-dasharray: none;
}

/* The stone fills the frame, so the label and the hint sit over it. */
.tarot-card.is-rune .ghost-pos-label,
.tarot-card.is-rune .gr-touch-hint {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 3;
}

.tarot-card.is-rune .ghost-pos-label { top: 50%; transform: translateY(-50%); }
.tarot-card.is-rune .gr-touch-hint   { bottom: 6%; }

/* Retained for the text-only style, where there is no stone to turn and the
   bare glyph is the whole card. */
.tarot-card.is-rune .card-placeholder .sigil {
  transform: rotate(var(--rune-angle, 0deg));
  transform-origin: 50% 50%;
  transition: transform 220ms ease-out;
}

/* Inside the card, under the art — not a caption hanging beneath it. The
   padding matches the name block above so the art sits centred between them. */
.card-meta {
  width: 100%;
  padding: 1px 6px 5px;
  text-align: center;
}

.card-score {
  font-size: 12.5px;
  color: var(--amber);
  text-shadow: 0 0 5px var(--halo-amber);
}

.card-score-label {
  font-size: 11.5px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
}

/* Selected card */
.tarot-card.selected .card-frame,
.tarot-card.selected .card-with-image {
  border-color: var(--phosphor);
  box-shadow: 0 0 25px var(--phosphor-glow-strong), 0 0 60px var(--phosphor-glow);
}

/* Inverted card */
.tarot-card.inverted .card-frame {
  border-color: var(--amber);
  box-shadow: 0 0 15px rgba(var(--halo-amber-rgb), 0.25), 0 0 40px rgba(var(--halo-amber-rgb), 0.07);
  filter: brightness(0.9);
}

.tarot-card.inverted .card-with-image {
  border-color: var(--amber);
  box-shadow: 0 0 15px rgba(var(--halo-amber-rgb), 0.25), 0 0 40px rgba(var(--halo-amber-rgb), 0.07);
}

.tarot-card.inverted .card-with-image .card-frame img {
  transform: rotate(180deg);
}

.tarot-card.inverted .card-placeholder .card-name,
.tarot-card.inverted .card-image-name {
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
}

/* Position badge above card */
.rank-badge {
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg);
  border: 1px solid var(--phosphor);
  padding: 1px 6px;
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  white-space: nowrap;
  z-index: 10;
}

.tarot-card:not(.selected) .rank-badge {
  border-color: var(--phosphor-dim);
  color: var(--phosphor-dim);
  text-shadow: none;
}

/* Reversed: amber, matching the card's border, its name and the REVERSED badge
   below — so every piece of chrome on the card agrees about the orientation.
   Placed after the :not(.selected) dimmer above rather than beside the other
   .inverted rules: both selectors weigh the same, so this only wins by coming
   later. */
.tarot-card.inverted .rank-badge {
  border-color: var(--amber);
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
}

/* Orientation badge */
/* The exact mirror of .rank-badge, and a sibling of it: the position cuts the
   top edge, the orientation cuts the bottom. It must live on .tarot-card rather
   than in .card-meta -- every shipped layout is positioned, where .card-meta is
   display:none until hover and then becomes its own bordered panel below the
   card. A badge inside it straddles that panel's edge, not the card's.

   .tarot-card's box is the card itself in those layouts, so bottom:-8px against
   rank-badge's top:-8px is a true reflection. */
.orientation-badge {
  position: absolute;
  bottom: -8px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg);
  font-size: 11.5px;
  letter-spacing: 1px;
  padding: 1px 5px;
  border: 1px solid;
  white-space: nowrap;
  z-index: 10;
}

/* Fallback flow layout: .card-meta is in normal flow there, so .tarot-card's
   bottom sits under it. Anchor to the card's own lower edge instead. */
#card-canvas:not(.layout-positioned) .orientation-badge {
  bottom: auto;
  top: 100%;
  transform: translate(-50%, -100%);
  margin-top: 8px;
}

/* Retained for /canvas and result.html, which still label both states. The
   grimoire card face emits the badge only for a reversal. */
.orientation-badge.upright {
  color: var(--phosphor-dim);
  border-color: var(--phosphor-faint);
}

.orientation-badge.inverted {
  color: var(--amber);
  border-color: var(--amber);
  text-shadow: 0 0 4px var(--halo-amber);
}


/* ── Terminal log (right panel) ──────────────────────────────────────── */

#log-output {
  flex: 1;
  overflow-y: auto;
  padding: 10px 12px;
  font-size: 16px;
  line-height: 1.6;
  min-height: 0;
}


.log-line { margin-bottom: 1px; }
.log-line .ts { color: var(--phosphor-faint); margin-right: 6px; }
.log-line .info      { color: var(--phosphor-dim); }
.log-line .data      { color: var(--phosphor); }
.log-line .highlight { color: var(--amber); text-shadow: 0 0 4px var(--halo-amber); }
.log-line .alert     { color: var(--red-alert); }
.log-line .dim       { color: var(--phosphor-faint); }

/* Blink cursor in log */
.blink {
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}


/* ── Footer ──────────────────────────────────────────────────────────── */

#footer {
  border-top: 1px solid var(--phosphor-faint);
  padding: 4px 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: var(--bg-panel);
  flex-shrink: 0;
  z-index: 10;
}

.footer-left { display: flex; gap: 20px; }

/* The shared footer (_footer.html) in three columns: the copyright at the
   left edge, the links centred on the screen rather than on what is left of
   it, and BETA and the photon stream at the right. Equal outer columns are
   what put the middle one at the true centre. The landing keeps its own
   two-part footer, which is why this is a class and not #footer. */
#footer.app-footer {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 20px;
}
/* The copyright as the landing sets it (login.html, #footer .footer-left). */
.app-footer .footer-copy { justify-self: start; align-items: center; min-width: 0; }
.app-footer .footer-copyright {
  font-size: 15px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
  white-space: nowrap;
}
/* The links are set in the header's type: the same 15px, and the header's
   3px tracking. At 1px they were the same size and read larger -- dense caps
   look bigger than spaced ones -- so the two bands did not match. */
.app-footer .footer-links {
  justify-self: center;
  display: flex;
  align-items: center;
  font-size: 15px;
  color: var(--phosphor-dim);
  letter-spacing: 3px;
  white-space: nowrap;
}
.app-footer .footer-sep { margin: 0 9px; letter-spacing: 0; }
.app-footer .footer-links a { color: inherit; }
.app-footer .footer-links a:hover { color: var(--phosphor); text-shadow: 0 0 8px var(--phosphor-glow); }
.app-footer .stage-badge {
  border: none; padding: 0; font: inherit; letter-spacing: inherit; opacity: 0.75;
}
/* BETA and the photon stream share the right-hand column (_footer.html). */
.app-footer .footer-status {
  justify-self: end;
  min-width: 0;
  display: flex;
  align-items: center;
  font-size: 15px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  white-space: nowrap;
}

/* The photon stream in the footer, at the footer's size. The run of bytes
   keeps its fixed width so the column never shifts as it slides. */
.app-footer .qrng-ticker { font-size: 15px; letter-spacing: 1px; text-transform: uppercase; }
.app-footer .qrng-ticker .qrng-ticker-bytes { min-width: 165px; margin-left: 10px; }
/* The footer is set bold; the run of bytes is not. It is the measurement
   rather than chrome, and at regular weight the hex pairs stay crisp and read
   as data beside the label's bold caps. */
.app-footer .qrng-ticker .qrng-ticker-bytes { font-weight: normal; }

/* Narrower screens give up detail before anything collides, and the footer
   measures that for itself (footer.js) rather than at breakpoints
   measured by hand, which every new link made wrong. First the run of bytes
   goes (the label and its dot still say the source is live). If the line
   still will not fit, .fit-wrap makes two rows on purpose: the links across
   the top, centred and free to break between items, and the copyright and
   the photon stream at the two ends beneath. On the member pages the footer
   is gone below 768px; elsewhere it stays, in smaller type. */
#footer.app-footer.fit-wrap {
  grid-template-columns: 1fr 1fr;
  grid-template-areas: "links links" "copy status";
  row-gap: 2px;
  padding-top: 5px;
  padding-bottom: 5px;
}
.app-footer.fit-wrap .footer-links  { grid-area: links; flex-wrap: wrap; justify-content: center; max-width: 100%; row-gap: 0; }
.app-footer.fit-wrap .footer-copy   { grid-area: copy; }
.app-footer.fit-wrap .footer-status { grid-area: status; }
/* Narrower still (a phone), one centred column: links, then the copyright,
   then BETA and the stream. */
#footer.app-footer.fit-stack {
  grid-template-columns: 1fr;
  grid-template-areas: "links" "copy" "status";
  justify-items: center;
}
.app-footer.fit-stack .footer-copy,
.app-footer.fit-stack .footer-status { justify-self: center; }

/* footer.js: what it sets aside, and separators left at a line's end. */
#footer .fit-dropped { display: none !important; }
#footer .fit-sep-off { visibility: hidden; }

/* ABOUT (_footer.html, footer.js): a button in the links' own face, and the
   menu it opens rising above it, in the panel style of the app's other
   pop-ups. While it is open the footer rises above the rail (102), as the
   KEYS bubble's footer did. */
.ft-menu { position: relative; display: inline-flex; }
.ft-menu-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}
.ft-menu-btn:hover,
.ft-menu-btn:focus-visible,
.ft-menu.open .ft-menu-btn { color: var(--phosphor); outline: none; text-shadow: 0 0 8px var(--phosphor-glow); }
/* A small filled triangle: at this size a stroked chevron blurred into a
   stray mark. Points up, the way the menu opens; down while it is open. */
.ft-menu-caret {
  width: 9px;
  height: 6px;
  fill: currentColor;
  transition: transform 0.15s;
}
.ft-menu.open .ft-menu-caret { transform: rotate(180deg); }
/* The menu itself, in the legal pages' table-of-contents language: a
   // ABOUT title over numbered entries, each with one line saying what is
   behind it. A row lights from its left edge on hover or focus; the page
   you are on keeps the edge lit. A notch points down at ABOUT, and stays on
   it when footer.js nudges the menu sideways to keep it on a phone's screen
   (--ft-dx). */
.ft-menu-list {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 14px);
  transform: translateX(calc(-50% + var(--ft-dx, 0px)));
  z-index: 200;
  display: flex;
  flex-direction: column;
  width: 288px;
  padding: 12px 0 8px;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-faint);
  box-shadow: 0 -8px 24px rgba(var(--shade), 0.5), 0 0 14px var(--phosphor-glow-faint);
  font-weight: normal;
  letter-spacing: 0;
  white-space: normal;
  text-align: left;
  animation: ftMenuIn 0.14s ease-out;
}
.ft-menu-list[hidden] { display: none; }
@keyframes ftMenuIn {
  from { opacity: 0; transform: translateX(calc(-50% + var(--ft-dx, 0px))) translateY(4px); }
  to   { opacity: 1; transform: translateX(calc(-50% + var(--ft-dx, 0px))); }
}
.ft-menu-list::after {
  content: '';
  position: absolute;
  left: calc(50% - var(--ft-dx, 0px));
  bottom: -6px;
  width: 10px;
  height: 10px;
  margin-left: -5px;
  background: var(--bg-panel);
  border-right: 1px solid var(--phosphor-faint);
  border-bottom: 1px solid var(--phosphor-faint);
  transform: rotate(45deg);
}
.ft-menu-title {
  padding: 0 18px 9px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--phosphor-faint);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}
.ft-menu-list a {
  display: grid;
  grid-template-columns: 26px 1fr;
  grid-template-areas: "num label" ". desc";
  row-gap: 1px;
  padding: 8px 18px 8px 16px;
  border-left: 2px solid transparent;
  font-family: 'Share Tech Mono', monospace;
  color: var(--phosphor-dim);
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.ft-menu-num {
  grid-area: num;
  font-size: 12px;
  line-height: 18px;
  color: color-mix(in srgb, var(--amber) 70%, transparent);
}
.ft-menu-label {
  grid-area: label;
  font-size: 14px;
  line-height: 18px;
  letter-spacing: 2px;
  color: var(--phosphor);
}
.ft-menu-desc {
  grid-area: desc;
  font-size: 12px;
  line-height: 16px;
  color: var(--phosphor-dim);
}
.ft-menu-list a:hover,
.ft-menu-list a:focus-visible {
  outline: none;
  background: color-mix(in srgb, var(--phosphor) 7%, transparent);
  border-left-color: var(--phosphor);
}
.ft-menu-list a:hover .ft-menu-label,
.ft-menu-list a:focus-visible .ft-menu-label { text-shadow: 0 0 8px var(--phosphor-glow); }
.ft-menu-list a:hover .ft-menu-num,
.ft-menu-list a:focus-visible .ft-menu-num { color: var(--amber); }
.ft-menu-list a[aria-current="page"] { border-left-color: var(--phosphor-dim); }
/* The footer's links glow as a whole on hover (#footer a:hover); here only
   the label does. */
#footer .ft-menu-list a,
#footer .ft-menu-list a:hover { text-shadow: none; }
#footer:has(.ft-menu.open) { z-index: 103; }

/* The cookie (_cookie_icon.html): opens the cookie bar. Line art in the
   footer's ink, beside PRIVACY. */
.ft-cookie {
  display: inline-flex;
  align-items: center;
  padding: 0;
  margin-left: 10px;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
}
.ft-cookie[hidden] { display: none; }
.ft-cookie svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2;
}
.ft-cookie svg circle { fill: currentColor; stroke: none; }
.ft-cookie:hover,
.ft-cookie:focus-visible { color: var(--phosphor); outline: none; filter: drop-shadow(0 0 4px var(--phosphor-glow)); }

/* Legal links in the footer (TERMS / PRIVACY) — inherit the footer's dim tone
   and brighten on hover, on both the authenticated shell and the public pages. */
#footer a { color: inherit; }
#footer a:hover { color: var(--phosphor); text-shadow: 0 0 8px var(--phosphor-glow); }

/* Secondary-text ink, not --phosphor-faint. Faint is the token for rules and
   hairlines: it measured 1.3-1.6:1 on the tube and 1.3:1 on the page, and the
   links in this line (how it works, terms, privacy) are ones a visitor has to
   be able to find. Still the quietest text on the screen, but text. */
.footer-right {
  font-size: 15px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
}

/* The header's text is set at the footer's size, so the two bands that frame
   the screen read as one system. The header keeps its wider tracking -- that
   is its register -- and takes only the size: 14px had it reading a step
   smaller than the footer it mirrors. The reading's title bar is the
   grimoire's header (it hides #header and draws its own), so it is named
   alongside it. */
#detail-header-bar,
#header .header-title { font-size: 15px; }

/* The text of the header and footer is bold, in both themes. The chrome sits
   at the edges in small, widely tracked caps, and at regular weight it read
   too dim on the page and on the tube alike. Share Tech Mono and VT323 ship
   one weight, so this is the browser's synthesised bold -- a controlled
   one-pixel thickening at these sizes, which is what is wanted.

   The reading's own title bar is the grimoire's header (it hides #header and
   draws its own), so it is named here too. */
#header,
#mobile-header,
#detail-header-bar,
#footer { font-weight: bold; }


/* ── Release-stage badge ──────────────────────────────────────────────
   One token with two homes: bracketed in the landing header, where it sets
   expectations before anyone signs up, and inline in the footer beside the
   rest of the system metadata. Amber rather than phosphor — it is a caveat
   about the software, not a reading. */
.stage-badge {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12.5px;
  letter-spacing: 2px;
  color: var(--amber);
  border: 1px solid rgba(var(--amber-rgb), 0.4);
  padding: 3px 9px;
  white-space: nowrap;
}
/* A button, which opens _beta_modal.html: the badge is where the reader
   asks what "beta" means. The face stays the badge's. */
button.stage-badge {
  background: transparent;
  cursor: pointer;
  transition: color 0.15s, text-shadow 0.15s, opacity 0.15s;
}
button.stage-badge:hover,
button.stage-badge:focus-visible,
#footer button.stage-badge:hover,
#footer button.stage-badge:focus-visible {
  opacity: 1;
  text-shadow: 0 0 8px var(--amber);
}

/* In the footer it is one word among many: the metadata line sets the scale,
   the amber alone marks it out. */
.footer-right .stage-badge {
  border: none;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  opacity: 0.75;
}

/* ── Loading overlay ─────────────────────────────────────────────────── */

#loading-overlay {
  position: absolute;
  inset: 0;
  background: rgba(var(--bg-rgb), 0.88);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  z-index: 50;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s;
}

#loading-overlay.active {
  opacity: 1;
  pointer-events: all;
}

.loading-text {
  font-family: 'VT323', monospace;
  font-size: 33px;
  color: var(--phosphor);
  text-shadow: 0 0 12px var(--halo);
  letter-spacing: 4px;
}

.loading-bar-wrap {
  width: 240px;
  height: 3px;
  background: var(--phosphor-faint);
  overflow: hidden;
}

.loading-bar-inner {
  height: 100%;
  background: var(--phosphor);
  box-shadow: 0 0 8px var(--halo);
  animation: loading-sweep 1.8s ease-in-out infinite;
}

@keyframes loading-sweep {
  0%   { width: 0%;   margin-left: 0%; }
  50%  { width: 60%;  margin-left: 20%; }
  100% { width: 0%;   margin-left: 100%; }
}

/* ── Settings panel ──────────────────────────────────────────────────── */

/* Restore system cursors inside settings panel.

   One of the three panels the suppression policy at the top of this file
   excludes, which is what lets these plain declarations still win. */
#settings-panel, #settings-panel * { cursor: default; }
#settings-panel button,
#settings-panel label,
#settings-panel a,
#settings-panel [onclick]          { cursor: pointer; }
#settings-panel input[type="range"]  { cursor: ew-resize; }
#settings-panel input[type="text"],
#settings-panel input[type="number"],
#settings-panel input[type="password"] { cursor: text; }

#settings-panel {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 320px;
  background: var(--bg-panel);
  border-left: 1px solid var(--phosphor-dim);
  box-shadow: -4px 0 24px var(--phosphor-glow);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.25s ease-in-out;
}

#settings-panel.open {
  transform: translateX(0);
}

.settings-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 14px;
  border-bottom: 1px solid var(--phosphor-dim);
  font-size: 14px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  text-transform: uppercase;
  flex-shrink: 0;
}

.settings-header button {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 18px;
  cursor: pointer;
  padding: 1px 6px;
}

.settings-header button:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
}

.settings-body {
  flex: 1;
  overflow-y: auto;
  padding: 0;
  /* A column so the deletion strip can claim the foot of the panel with
     margin-top:auto. Sections keep their own height: flex items shrink by
     default, which would squash them the moment the list overflows. */
  display: flex;
  flex-direction: column;
}
.settings-body > * { flex-shrink: 0; }


/* Hue colour picker. input.hue-slider, not .hue-slider: the generic
   input[type=range] rule above outranks a bare class, and its flat
   phosphor-faint track was painted over this rainbow -- the colour picker
   never showed its colours (found 2026-10-04). */
input.hue-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 10px;
  border-radius: 2px;
  background: linear-gradient(to right,
    hsl(0,100%,62%), hsl(30,100%,62%), hsl(60,100%,62%), hsl(90,100%,62%),
    hsl(120,100%,62%), hsl(150,100%,62%), hsl(180,100%,62%), hsl(210,100%,62%),
    hsl(240,100%,62%), hsl(270,100%,62%), hsl(300,100%,62%), hsl(330,100%,62%), hsl(360,100%,62%)
  );
  cursor: pointer;
  outline: none;
  border: none;
}
.hue-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid #111;
  cursor: pointer;
  box-shadow: 0 0 4px rgba(var(--shade), 0.6);
}
.hue-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid #111;
  cursor: pointer;
}

#hue-swatch {
  width: 22px;
  height: 22px;
  border-radius: 2px;
  border: 1px solid var(--phosphor-dim);
  flex-shrink: 0;
  background: hsl(150, 100%, 62%);
}

.settings-section-header {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  color: var(--phosphor);
  font-size: 13px;
  letter-spacing: 3px;
  padding: 7px 14px 6px;
  border-top: 1px solid var(--phosphor-dim);
  border-bottom: 1px solid var(--phosphor-dim);
  margin: 0;
  text-shadow: 0 0 6px var(--phosphor-glow);
}

.settings-section-icon {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
  opacity: 0.85;
  vertical-align: middle;
}


.settings-body .term-btn {
  width: 100%;
  margin: 8px 0;
  font-size: 24px;
}

/* ── Settings, laid out (owner, 2026-10-04) ──────────────────────────────
   One setting to a row: the label on the left, the control on the right,
   every row at least 48px, a faint rule between rows to read across by.
   Section headers slim -- a 24px icon, one rule -- with the room going to
   the controls instead. Knobs 40px (terminal.js), segmented buttons and
   number boxes tall enough to hit; the Mind Machine's technical knobs behind
   ADVANCED. Full width on a phone, where 320px left a useless strip of page
   showing beside it. */
.settings-header { padding: 6px 6px 6px 16px; }
.settings-header button {
  width: 44px;
  height: 44px;
  padding: 0;
  font-size: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.settings-header .settings-section-icon { width: 28px; height: 28px; }
.settings-section-header {
  padding: 10px 16px;
  border-bottom: none;
}
.settings-section-header .settings-section-icon { width: 24px; height: 24px; }
.set-section + .set-section { margin-top: 8px; }
.set-rows { padding: 4px 16px 12px; }
.set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 48px;
}
.set-row + .set-row,
.set-rows > .set-row + .ec-hint,
.set-advanced .set-row { border-top: 1px solid var(--phosphor-faint); }
.set-row-stack {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 12px 0;
}
.set-row-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.set-row-stack .hue-slider { flex: none; width: 100%; height: 14px; }
.set-ctl { display: flex; align-items: center; gap: 10px; }
.set-seg { display: flex; gap: 6px; }
.set-seg-full .ec-btn { flex: 1 1 0; }
#settings-panel .ec-btn { height: 36px; line-height: 34px; padding: 0 14px; }
#settings-panel .ec-val-input { width: 64px; height: 32px; font-size: 16px; }
.set-note { color: var(--amber); padding: 10px 0; text-align: center; }
.set-row-end { justify-content: flex-end; }
#oracle-mode-hint { padding: 0; }
.set-advanced { border-top: 1px solid var(--phosphor-faint); }
.set-advanced summary {
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 48px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.set-advanced summary::-webkit-details-marker { display: none; }
.set-advanced-mark { color: var(--phosphor-dim); font-size: 16px; transition: transform 0.2s ease; }
.set-advanced[open] .set-advanced-mark { transform: rotate(90deg); }
/* Settings' way into the ACCOUNT modal, under the address. */
.settings-body .term-btn.set-account-open { margin: 4px 0 0; width: 100%; }
/* Who is signed in, in amber at the right of ACCOUNT's header, unlabeled.
   The address can be long: it gives way rather than pushing the header past
   the panel. */
.set-signed-in-email {
  margin-left: auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--amber);
  font-size: 15px;
  letter-spacing: 0;
  text-shadow: 0 0 6px rgba(var(--halo-amber-rgb), 0.4);
}
/* The display name's box carries its SAVE inside it, at the right: the box
   keeps that room clear (padding-right), so the text never runs under it.
   Dim until the box differs from what is saved. */
.set-text-wrap { position: relative; }
.set-text-wrap .set-text { padding-right: 72px; }
.set-text-save {
  position: absolute;
  top: 50%;
  right: 5px;
  transform: translateY(-50%);
  height: 30px;
  padding: 0 10px;
  background: none;
  border: 1px solid var(--phosphor);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 2px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.set-text-save:disabled { border-color: var(--phosphor-faint); color: var(--phosphor-dim); opacity: 0.6; cursor: default; }
.set-text-save:not(:disabled):active { background: var(--wash); }
.set-text-save.is-saved { border-color: var(--phosphor-faint); color: var(--phosphor); opacity: 1; }
/* The display name's box, in the settings' own ink. */
.set-text {
  width: 100%;
  box-sizing: border-box;
  height: 40px;
  padding: 0 10px;
  background: var(--bg);
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;
  letter-spacing: 1px;
  outline: none;
}
.set-text:focus { border-color: var(--phosphor); box-shadow: 0 0 8px var(--phosphor-glow); }
.set-text::placeholder { color: var(--phosphor-dim); opacity: 0.7; letter-spacing: 0.5px; }
a.user-email { color: inherit; text-decoration: none; cursor: pointer; }
a.user-email:hover { color: var(--phosphor); }
/* YIN / YANG at the head of DISPLAY, larger than the on/off switches and
   unlike them: a 76x36 track the taijitu rolls across at 32px, the two words
   either side, the one in force lit. The shared switch is the landing
   header's (_theme_toggle.html); these rules scale it here only. */
.set-row-yy { justify-content: center; gap: 16px; min-height: 68px; }
.yy-word {
  background: none;
  border: none;
  padding: 8px 4px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  cursor: pointer;
  transition: color 0.2s, text-shadow 0.2s;
  -webkit-tap-highlight-color: transparent;
}
:root:not([data-theme="light"]) .yy-word-yin,
:root[data-theme="light"] .yy-word-yang {
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}
#settings-panel .yy-toggle { margin-right: 0; }
#settings-panel .yy-track { width: 76px; height: 36px; }
#settings-panel .yy-knob { top: 1px; left: 1px; width: 32px; height: 32px; }
:root[data-theme="light"] #settings-panel .yy-knob { transform: translateX(40px) rotate(180deg); }

@media (max-width: 767px) {
  #settings-panel { width: 100%; border-left: none; }
}
@media (hover: none) {
  .hue-slider::-webkit-slider-thumb { width: 22px; height: 22px; }
  .hue-slider::-moz-range-thumb     { width: 22px; height: 22px; }
}

/* ── The ACCOUNT modal ─────────────────────────────────────────────────
   Opened from the reader's row, the header's address, and MANAGE ACCOUNT in
   Settings (base.html, #account-modal). LOG OUT in the header; sections
   under hairlines (HANDLE, PLAN, SIGN-IN), each a label over its value with
   a small action at the right; then a foot of two text links: EXPORT HISTORY
   left, DELETE ACCOUNT in amber right. */
#account-modal .card-modal-panel { max-width: 440px; }
/* Who is signed in, in amber under the title, unlabeled. A long address gives
   way rather than widening the panel. */
.acct-email {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--amber);
  font-size: 15px;
  letter-spacing: 0;
  text-shadow: 0 0 6px rgba(var(--halo-amber-rgb), 0.4);
}
.acct-body { display: flex; flex-direction: column; padding-top: 4px; }
/* Each section -- HANDLE, PLAN, SIGN-IN -- under a hairline, its label
   over it in the same small key, the same space above and below each. */
.acct-body .set-row-stack { padding: 0; }
.acct-body .acct-sec {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px 0;
  border-top: 1px solid var(--phosphor-faint);
}
.acct-body .acct-sec:first-child { border-top: none; padding-top: 4px; }
.acct-body .acct-sec .config-key { font-size: 13px; letter-spacing: 2.5px; }
.acct-body .term-btn { width: 100%; margin: 0; }
/* The modal's small buttons -- LOG OUT, CHANGE PLAN, CHANGE: the size and
   face of AUTO-GEN and SAVE in the handle's box, so the body is a list to
   read rather than a column of slabs. Only the forms' own buttons stay full. */
.term-btn.sm.acct-compact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  flex-shrink: 0;
  margin: 0;
  height: 30px;
  padding: 0 14px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 2px;
  text-decoration: none;
  text-shadow: none;
  box-shadow: none;
}
.acct-compact:hover { box-shadow: 0 0 10px var(--phosphor-glow); text-shadow: 0 0 6px var(--halo); }
.acct-compact:disabled { border-color: var(--phosphor-faint); color: var(--phosphor-dim); opacity: 0.6; cursor: default; }
/* LOG OUT, under the address in the header: the button used most here, so
   it is the first thing under the name -- small, and centered with it. */
#account-modal .cm-head .acct-logout { margin: 14px auto 0; color: var(--phosphor-dim); border-color: var(--phosphor-dim); }
#account-modal .cm-head .acct-logout:hover { color: var(--phosphor); border-color: var(--phosphor); }
#account-modal .cm-head { padding-bottom: 18px; }
/* A label at the left, its action at the right: EMAIL / CHANGE. */
.acct-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 30px;
}
.acct-row-label { font-size: 14px; letter-spacing: 1.5px; color: var(--phosphor); }
.acct-signin { gap: 8px; }
.acct-signin .acct-sec-title { margin-bottom: 2px; }
/* PLAN: the plan large, its date under it, CHANGE PLAN at the right; CANCEL
   as a quiet link under them (billing.js, billingRender). */
.acct-plan-body { display: flex; flex-direction: column; gap: 10px; }
.acct-plan-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.acct-plan-now { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.acct-plan-name {
  font-family: 'VT323', monospace;
  font-size: 26px;
  line-height: 1;
  letter-spacing: 2px;
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}
.acct-plan-when { font-size: 11.5px; letter-spacing: 1.5px; color: var(--phosphor-dim); }
.acct-plan-sched { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.acct-plan-sched .ec-hint { margin: 0; padding: 0; }
.acct-plan-body .ec-hint { margin: 0; padding: 0; }
.acct-plan-cancel { align-self: flex-start; }
.acct-founder-link { align-self: flex-start; color: var(--amber); }
.acct-founder-badge {
  display: inline-block; vertical-align: 4px; margin-left: 6px; padding: 1px 7px;
  font-family: 'Share Tech Mono', monospace; font-size: 10.5px; letter-spacing: 2px;
  color: var(--amber); border: 1px solid rgba(var(--amber-rgb), 0.5);
  background: rgba(var(--amber-rgb), 0.1); text-shadow: none;
}
.acct-body .ec-hint.acct-past-due { color: var(--amber); line-height: 1.5; }
/* CLAIM FOUNDER PRICING's form. */
.founder-panel { width: min(440px, 100%); }
.founder-form { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.founder-lore { font-style: italic; color: var(--amber); }
.founder-panel .cm-meaning-text a { color: var(--phosphor); text-decoration: underline dotted; text-underline-offset: 3px; }
.founder-panel .cm-meaning-text a:hover { text-shadow: 0 0 6px var(--halo); }
.founder-form .term-btn { width: 100%; margin: 6px 0 0; }
.founder-form .ec-hint:empty { display: none; }
/* The allowance meter: ORACLE CREDITS  62 / 200, a bar, the renewal date. */
.acct-credits { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.acct-credits[hidden] { display: none; }
.acct-credits-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.acct-body .acct-credits .config-key { font-size: 11.5px; letter-spacing: 2px; }
.acct-credits-n { font-size: 13px; letter-spacing: 1px; color: var(--phosphor-dim); }
.acct-credits-n b { font-weight: normal; color: var(--phosphor); font-size: 15px; text-shadow: 0 0 6px var(--halo); }
.acct-credits-bar { height: 4px; background: var(--phosphor-faint); overflow: hidden; }
.acct-credits-bar span { display: block; height: 100%; background: var(--phosphor); box-shadow: 0 0 8px var(--phosphor-glow-strong); }
.acct-credits-when { font-size: 11px; letter-spacing: 1.5px; color: var(--phosphor-dim); }
/* HANDLE: AUTO-GEN and SAVE side by side inside the box, at its right; the
   box keeps their room clear, so the text never runs under them. A long
   handle scrolls in what is left. Under the box, the hint: the rules, the
   next allowed change, or what went wrong. */
.acct-handle-btns {
  position: absolute;
  top: 50%;
  right: 5px;
  transform: translateY(-50%);
  display: flex;
  gap: 5px;
}
.acct-handle-btns .set-text-save { position: static; transform: none; padding: 0 8px; letter-spacing: 1px; }
.acct-handle-wrap .set-text { padding-right: 148px; }
/* Read as a footnote to the box: small, close under it, set back. */
.acct-body .ec-hint.acct-handle-hint {
  display: block;
  padding: 2px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  letter-spacing: 0.5px;
  opacity: 0.85;
}
.acct-body .ec-hint.acct-handle-hint.err { opacity: 1; }
.acct-body .ec-hint.err { color: var(--red-alert); }
/* CHANGE EMAIL, and the form it opens in place of itself. The buttons' own
   display would otherwise outrank the hidden attribute that swaps them. */
.acct-body [hidden] { display: none !important; }
.acct-email-form { display: flex; flex-direction: column; gap: 10px; padding: 8px 0 4px; }
.acct-email-actions { display: flex; gap: 10px; }
.acct-body .acct-email-actions .term-btn { flex: 1 1 0; }
.acct-email-status { padding: 6px 0 0; margin: 0; }
/* CHANGE PASSWORD opens the same way, under the email's row. */
.acct-body .ec-hint.ok { color: var(--phosphor); }
/* "Forgot it?": a quiet way out under the form, not a third button. */
.acct-forgot {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 2px 0;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.5px;
  color: var(--phosphor-dim);
  border-bottom: 1px dotted var(--phosphor-dim);
  cursor: pointer;
}
.acct-forgot:hover, .acct-forgot:focus-visible { color: var(--phosphor); border-bottom-color: var(--phosphor); }
.acct-email-status:empty { display: none; }
/* The foot: EXPORT HISTORY at the left, DELETE ACCOUNT in amber at the
   right, as text links rather than buttons (owner, 2026-10-10), so neither
   reads as the modal's main action. Set apart from the rows above. */
.acct-foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding-top: 18px;
  border-top: 1px solid var(--phosphor-faint);
}
.acct-link {
  background: none;
  border: none;
  border-bottom: 1px dotted currentColor;
  padding: 2px 0;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
  cursor: pointer;
}
.acct-link:hover, .acct-link:focus-visible { color: var(--phosphor); }
.acct-link:disabled { cursor: default; opacity: 0.7; }
.acct-link.acct-danger-link { color: var(--amber); opacity: 0.8; }
.acct-link.acct-danger-link:hover, .acct-link.acct-danger-link:focus-visible {
  opacity: 1;
  text-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.5);
}

/* The handle's welcome note: the handle large, in amber, under its title. */
.hn-handle {
  display: block;
  color: var(--amber);
  font-size: 26px;
  letter-spacing: 2px;
  text-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.45);
  overflow-wrap: anywhere;
}
.hn-actions { display: flex; justify-content: center; gap: 12px; margin-top: 8px; }
.hn-actions .term-btn { width: auto; padding: 6px 28px; }
.hn-status { text-align: center; color: var(--red-alert); }
.hn-status:empty { display: none; }

/* ── Home page specific ──────────────────────────────────────────────── */

.home-logo {
  font-family: 'VT323', monospace;
  font-size: 120px;
  color: var(--phosphor);
  text-shadow: 0 0 20px var(--halo), 0 0 60px var(--phosphor-glow-strong);
  letter-spacing: 8px;
  line-height: 1;
}

.home-logo span:not(.home-logo-psi) { color: var(--amber); text-shadow: 0 0 20px var(--halo-amber); }

.home-logo-psi {
  display: inline-block;
  animation: glitch-psi 2s linear infinite;
}

@keyframes glitch-psi {
  0% {
    text-shadow: -10px 0 #ff0000, 10px 0 #00ffff;
  }
  100% {
    text-shadow: -10px 0 #ff0000, 10px 0 #00ffff;
  }
}

.home-tagline {
  font-size: 16px;
  color: var(--phosphor-dim);
  letter-spacing: 3px;
  text-transform: uppercase;
  margin-top: 8px;
}

.home-sigil {
  font-family: 'VT323', monospace;
  font-size: 42px;
  color: var(--phosphor-faint);
  margin: 20px 0;
  line-height: 1.4;
  text-align: center;
  letter-spacing: 4px;
}


.intent-textarea {
  width: 100%;
  max-width: 480px;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 18px;
  padding: 8px;
  outline: none;
  resize: none;
  height: 96px;
  cursor: none;
  margin-top: 8px;
  /* The same caret the landing console types with: phosphor rather than the
     browser's black bar, and a block where the browser supports caret-shape
     (CSS UI 4 — elsewhere it is ignored and the thin phosphor caret stands, so
     the fallback is a colour change rather than a broken state). */
  caret-color: var(--phosphor);
  caret-shape: block;
}

.intent-textarea:focus { border-color: var(--phosphor-dim); }

/* ── The block caret in the question field ────────────────────────────────
   On a desktop the field is focused on arrival, so the native caret is already
   there and caret-shape: block above makes it a block. On a phone it is not:
   both pages deliberately decline to focus it, because raising the keyboard
   would bury the hero before anyone has read it -- and iOS refuses programmatic
   focus outside a gesture anyway. The result was a question box with no cursor
   in it at all on exactly the devices where the page has the least room to
   explain itself.

   So this stands in for the caret precisely when there is no caret: while the
   field is empty and unfocused. Empty is what makes a fixed overlay honest --
   the caret would be at position 0, which is where this sits -- and unfocused
   is what keeps it from ever appearing beside the real one. Focus the field and
   it gives way; type and .has-text retires it for good.

   It sits over the placeholder's first glyph, which is where a native block
   caret sits too. */
.field-caret {
  position: absolute;
  /* Every value below the position is #cursor's, verbatim: the same 10x18
     block, the same two-layer glow, the same screen blend. It stands in for a
     caret in a terminal whose pointer is already a block of phosphor, so it
     should be that block and not a dimmer cousin of it -- the old single
     8px --phosphor-glow shadow read as a much fainter object beside the mouse.
     The one difference is the blink, which is what makes it a caret. */
  width: 10px;
  height: 18px;
  background: var(--phosphor);
  box-shadow: 0 0 8px var(--halo), 0 0 20px var(--phosphor-glow-strong);
  mix-blend-mode: var(--blend-emit);
  animation: blink 1s step-end infinite;
  pointer-events: none;
}

/* The real caret is there, or there is text for it to sit in. Either way this
   has nothing left to stand in for. */
.lp-question-wrap:focus-within .field-caret,
.lp-question-wrap.has-text .field-caret { display: none; }


/* ── Ghost-card scan ─────────────────────────────────────────────────────
   What runs inside an empty slot while it waits to be drawn: card silhouettes
   and a sigil surfacing through static, neither ever resolving. Driven by
   ghost-scan.js, which is handed a .card-frame and injects these layers into
   it. Shared by the guest landing and the signed-in canvas, and applied only
   to the active slot — every other ghost stays an empty frame. */

/* Solid phosphor masked by each silhouette PNG's alpha, so the ghost follows
   the visitor's terminal hue instead of being baked to one colour. */
.ghost-scan-sil {
  position: absolute;
  inset: 0;
  background: var(--phosphor);
  opacity: 0;
  pointer-events: none;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
}
/* On paper the silhouette is a negative, as it is on the tube: ghost-scan.js
   (silMask) hands light mode a solid layer and the card's mask, and this cuts
   the one from the other, so the ink fills the paper and the linework is left
   the page's colour. Dark mode gets the mask alone, and nothing to composite. */
[data-theme="light"] .ghost-scan-sil {
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

.ghost-scan-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.5;
  pointer-events: none;
  mix-blend-mode: var(--blend-emit);
}

/* The scan canvas is held at half opacity so its grain stays a suggestion. An
   armed slot is a different job: there the ripples are the only thing telling
   the visitor to tap, so the canvas is allowed more of its own brightness.
   .card-active is what both the landing page and the app set on the slot that
   is waiting to be spent. */
.card-active .ghost-scan-canvas {
  opacity: 0.72;
}

/* The sigil: vectors over the scan canvas (ghost-scan.js), so it stays crisp
   at any zoom and pixel density. Held at the canvas's opacity and blended as
   it is, with the glow the canvas drew as shadowBlur. */
.ghost-scan-sigil {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  visibility: hidden;
  opacity: 0.5;
  mix-blend-mode: var(--blend-emit);
  color: var(--phosphor);
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 2.5px var(--phosphor));
}
.ghost-scan-sigil .is-fill { fill: currentColor; stroke: none; }
.card-active .ghost-scan-sigil { opacity: 0.72; }
/* On paper the mark is a mandala (sigilForTheme, terminal.js): four or five
   times the strokes of the sigil, much of it right where the amber position
   name sits. At the armed slot's full weight it muddied the name, so it stays
   a watermark there. */
:root[data-theme="light"] .card-active .ghost-scan-sigil { opacity: 0.38; }

/* Sampling: the field thickens and the frame stops inviting a click. */
.card-frame.ghost-scan-focusing {
  animation: none !important;
  border-color: var(--amber);
}

/* The label names the slot, so it stays above the scan. */
.ghost-card .ghost-pos-label { position: relative; z-index: 3; }

/* ── Rune ghost slots ──
   A stone waiting to be drawn is its dashed outline, not a square tile: no
   frame, no ground, no box glow (owner, 2026-09-26). The scan behind it -- the
   grain, the passing staves, the tap ripples -- fades out in the shape of that
   stone, whole to the dashed line and gone a little beyond it. --stone-mask is
   the slot's own stone, blurred (rune-stone.js, ghostMask), in a box half as
   wide again as the stone's, centred -- the canvases below are that box. The
   sigil is its own vector layer (.ghost-scan-sigil) outside the mask: it is
   wider than the stone and must show whole. The active slot's flash moves from the frame's border
   to the stone's outline. */
.ghost-card.is-rune .card-frame,
.ghost-card.is-rune.card-active .card-frame,
.ghost-card.is-rune.draw-ready .card-frame,
.ghost-card.is-rune .card-frame.ghost-scan-focusing {
  border-color: transparent;
  background: none;
  box-shadow: none;
  animation: none;
}
.ghost-card.is-rune .card-frame::after { display: none; }
/* Nothing clips the slot: with no frame drawn there is no edge to keep inside,
   and a clip at the tile's sides cut the rings off in a straight line where a
   wide stone's fade had not yet finished. */
.ghost-card.is-rune .card-frame { overflow: visible; }
.ghost-card.is-rune .ghost-scan-canvas,
.ghost-card.is-rune .gr-ripple-canvas {
  /* Larger than the frame -- a square half as wide again, centred -- so the
     fade finishes before the canvas's edge, even on Freestyle's square tile
     where the stone stands almost the tile's full height. */
  inset: auto;
  left: -25%;
  top: 50%;
  width: 150%;
  height: auto;
  aspect-ratio: 1 / 1;
  transform: translateY(-50%);
  -webkit-mask-image: var(--stone-mask);
          mask-image: var(--stone-mask);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
.ghost-card.is-rune.card-active .rune-stone-ghost {
  animation: rune-ghost-flash 0.5s linear infinite;
}
.ghost-card.is-rune.card-active .rune-stone-ghost path {
  animation: rune-ghost-stroke 0.5s linear infinite;
}
.ghost-card.is-rune.draw-ready .rune-stone-ghost,
.ghost-card.is-rune.draw-ready .rune-stone-ghost path,
.ghost-card.is-rune .ghost-scan-focusing .rune-stone-ghost,
.ghost-card.is-rune .ghost-scan-focusing .rune-stone-ghost path { animation: none; }
.ghost-card.is-rune.draw-ready .rune-stone-ghost {
  filter: drop-shadow(0 0 6px var(--halo)) drop-shadow(0 0 16px var(--phosphor-glow));
}
.ghost-card.is-rune .ghost-scan-focusing .rune-stone-ghost path { stroke: var(--amber); }
/* The card slot's flash, phosphor to amber and back, on the outline. The glow
   is on the SVG, not the path: engines differ on CSS filters on SVG children. */
@keyframes rune-ghost-stroke {
  0%, 100% { stroke: var(--phosphor); }
  50%      { stroke: var(--amber); }
}
@keyframes rune-ghost-flash {
  0%, 100% { filter: drop-shadow(0 0 4px var(--halo)) drop-shadow(0 0 12px var(--phosphor-glow)); }
  50%      { filter: drop-shadow(0 0 4px var(--halo-amber)) drop-shadow(0 0 12px rgba(var(--halo-amber-rgb), 0.25)); }
}


/* ── QRNG ticker (header) ────────────────────────────────────────────────
   Sits where the page title used to. A green dot is cheap talk, so the actual
   bytes scroll: real quantum output from a shared server-side buffer, the same
   stream that picks the cards. See wyrdos/ui/static/entropy-ticker.js. */

.qrng-ticker {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  white-space: nowrap;
  user-select: none;
}

.qrng-ticker .qrng-ticker-state {
  color: var(--phosphor);
  text-shadow: 0 0 5px var(--phosphor-glow-strong);
}

.qrng-ticker .qrng-ticker-bytes {
  /* Amber against the phosphor label: the run is the measurement, not chrome
     describing it, and the colour break is what stops six hex pairs reading as
     a continuation of the words beside them. */
  color: var(--amber);
  opacity: 0.85;
  /* Fixed width so a shorter run never shifts the header under it.
     6 bytes as "xx " is 18 characters at ~8.4px each. */
  min-width: 155px;
  text-align: left;
  /* Wider than the 7px that separates the rest of the row: the label names the
     source and the run is the evidence for it, so they read as two things
     rather than one long string. */
  margin-left: 12px;
}

/* Unverified reads as unverified: no glow, no claim. */
.qrng-ticker.is-checking { opacity: 0.6; }
.qrng-ticker.is-checking .qrng-ticker-state { color: var(--phosphor-dim); text-shadow: none; }

/* Offline is not cosmetic here — nothing stands in for the source, so draws fail. */
.qrng-ticker.is-offline .pulse-dot { background: var(--amber); animation: none; }
.qrng-ticker.is-offline .qrng-ticker-state { color: var(--amber); text-shadow: 0 0 6px var(--halo-amber); }

@media (max-width: 900px) {
  .qrng-ticker .qrng-ticker-bytes { display: none; }
}


/* ── Stats block (result page) ───────────────────────────────────────── */

.stats-block {
  display: none;
  margin-top: 8px;
  padding: 8px;
  border: 1px solid var(--phosphor-faint);
  background: var(--phosphor-faint);
  font-size: 15px;
  color: var(--phosphor-dim);
  line-height: 1.7;
}

.stats-block.visible { display: block; }

.stats-block span.val {
  color: var(--amber);
  text-shadow: 0 0 4px var(--halo-amber);
}

.stats-block .warning { color: var(--red-alert); }

/* ── Result summary panel ────────────────────────────────────────────── */

.summary-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 4px;
  font-size: 16px;
}

.summary-key { color: var(--phosphor-dim); }
.summary-val { color: var(--phosphor); text-shadow: 0 0 4px var(--halo); }
.summary-val.amber { color: var(--amber); text-shadow: 0 0 4px var(--halo-amber); }

/* ── Error alert ─────────────────────────────────────────────────────── */

#error-alert {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--bg-panel);
  border: 2px solid var(--red-alert);
  box-shadow: 0 0 30px rgba(var(--halo-alert-rgb), 0.4);
  padding: 24px 32px;
  z-index: 300;
  max-width: 480px;
  text-align: center;
  display: none;
}

#error-alert.visible { display: block; }

#error-alert .error-title {
  font-family: 'VT323', monospace;
  font-size: 36px;
  color: var(--red-alert);
  text-shadow: 0 0 10px var(--halo-alert);
  letter-spacing: 3px;
  margin-bottom: 10px;
}

#error-alert .error-msg {
  font-size: 18px;
  color: var(--phosphor-dim);
  line-height: 1.6;
}

/* ── Oracle Card Artifact ──────────────────────────────────────────────── */

#oracle-card {
  position: absolute;
  min-width: 280px;
  min-height: 120px;
  width: 420px;
  background: rgba(var(--bg-rgb), 0.85);
  border: 1px solid var(--phosphor);
  box-shadow: 0 0 18px var(--phosphor-glow-strong), inset 0 0 12px var(--phosphor-glow-faint);
  display: flex;
  flex-direction: column;
  z-index: 80;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}


#oracle-card-header {
  display: flex;
  user-select: none;
  align-items: center;
  justify-content: space-between;
  padding: 6px 10px;
  border-bottom: 1px solid var(--phosphor-faint);
  background: var(--bg-panel);
  cursor: grab;
  flex-shrink: 0;
}

#oracle-card-header:active { cursor: grabbing; }

#oracle-card-title {
  font-family: 'VT323', monospace;
  font-size: 27px;
  letter-spacing: 3px;
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}

#oracle-card-sigil {
  font-size: 21px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
}

.oracle-card-controls {
  display: flex;
  gap: 6px;
}

.oracle-ctrl-btn {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 21px;
  width: 22px;
  height: 22px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.1s, color 0.1s;
}

.oracle-ctrl-btn:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
}

#oracle-card-body {
  flex: 1;
  user-select: text;
  overflow-y: auto;
  padding: 14px 16px;
  font-size: 15px;
  line-height: 1.8;
  color: var(--phosphor-dim);
  min-height: 0;
  /* Fixed watermark: luminosity-blends the rune to phosphor theme colour;
   * dark gradient overlay dims it. background-attachment:scroll (default)
   * keeps it fixed while text scrolls. */
  background-image:
    linear-gradient(rgba(var(--bg-rgb), 0.92) 0%, rgba(var(--bg-rgb), 0.92) 100%),
    url('/static/img/pyramid.gif');
  background-color: var(--bg-panel);
  background-blend-mode: multiply, normal;
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}

/* Oracle card toolbar */
#oracle-card-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 4px 20px 4px 8px; /* right padding clears the resize handle */
  border-top: 1px solid var(--phosphor-faint);
  flex-shrink: 0;
  gap: 4px;
}
.oracle-tool-right {
  display: flex;
  gap: 4px;
}
.oracle-tool-btn {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 22px;
  width: 26px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.1s, color 0.1s;
  line-height: 1;
  cursor: none;
}
.oracle-tool-btn:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
}

#oracle-card-body.oracle-visible {
  color: var(--phosphor);
}

#oracle-text {
  white-space: pre-wrap;
}

/* The position-and-symbol line above each block of a concise reading, and the
   SUMMARY line. Marked by renderOracleText() -- the stored reading is plain
   text and stays that way.

   Weight and brightness together, because neither is enough on its own here.
   Only the 400 face of Share Tech Mono is self-hosted, so `bold` is whatever
   the browser synthesises from it, and under the phosphor glow a synthesised
   700 smears rather than reads. Brightness is also how a real terminal did
   bold: the body of a reading is --phosphor-dim, and a header standing at full
   phosphor is the contrast the eye actually catches. The letter-spacing keeps
   the faux-bold from closing up. */
.oracle-head {
  font-weight: 700;
  color: var(--phosphor);
  letter-spacing: 0.04em;
  text-shadow: 0 0 6px var(--phosphor-glow);
}
/* A rune header's rune, upright, after the header (renderOracleText,
   terminal.js). Drawn here, not written into the text, so copying the reading
   copies only the oracle's words. Twice the header's size, in phosphor; line-
   height 0 so the larger glyph does not open the header's line. */
.oracle-head[data-rune]::after {
  content: attr(data-rune);
  margin-left: 0.35em;
  font-family: 'Noto Sans Runic', 'Segoe UI Historic', serif;
  font-size: 2em;
  line-height: 0;
  vertical-align: -0.12em;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow);
}

/* The divider between the reading's opening paragraph and the blocks beneath
   it. The oracle emits the words alone on a line -- THE CARDS, THE RUNES --
   and the rule is drawn here: a model asked for dashes writes them unevenly,
   and half of one is on screen while the line is still streaming.

   inline-block rather than block because the pane is pre-wrap text and a block
   would collapse the newline around it; width:100% then takes the rule the
   full measure regardless of how short the label is. */
.oracle-section {
  display: inline-block;
  width: 100%;
  font-weight: 700;
  color: var(--phosphor);
  letter-spacing: 0.18em;
  text-shadow: 0 0 6px var(--phosphor-glow);
  border-bottom: 1px solid var(--phosphor-dim);
  padding-bottom: 3px;
  opacity: 0.9;
}


#oracle-card-cursor {
  display: inline-block;
  width: 8px;
  height: 13px;
  background: var(--phosphor);
  vertical-align: middle;
  animation: oracle-blink 0.8s step-end infinite;
  margin-left: 2px;
}

@keyframes oracle-blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}

#oracle-card-resize {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 14px;
  height: 14px;
  cursor: se-resize;
  color: var(--phosphor-faint);
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
}

#oracle-card.collapsed #oracle-card-body,
#oracle-card.collapsed #oracle-card-toolbar,
#oracle-card.collapsed #oracle-card-resize {
  display: none;
}

#oracle-card-divider {
  border: none;
  border-top: 1px solid var(--phosphor-faint);
  margin: 0 16px 10px;
  flex-shrink: 0;
}

#oracle-card.collapsed #oracle-card-divider { display: none; }

/* Entrance animation */
@keyframes oracle-appear {
  from { opacity: 0; transform: scaleY(0.4); }
  to   { opacity: 1; transform: scaleY(1); }
}
#oracle-card.oracle-animate { animation: oracle-appear 0.35s ease-out; }

/* Suppress system cursor inside oracle card */
#oracle-card, #oracle-card * { cursor: none; }


/* ── Canvas page: intention bar ──────────────────────────────────────── */

#intention-bar {
  padding: 6px 16px;
  border-bottom: 1px solid var(--phosphor-faint);
  background: var(--bg-panel);
  flex-shrink: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 16px;
}

.intention-label {
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-faint);
  white-space: nowrap;
  flex-shrink: 0;
}

#intention-text {
  color: var(--phosphor-dim);
  font-style: italic;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


/* ── Canvas page: ghost (undrawn) card slots ─────────────────────────── */

.ghost-card .card-frame {
  border-color: var(--phosphor-faint);
  box-shadow: none;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ghost-pos-label {
  font-family: 'VT323', monospace;
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-faint);
  text-align: center;
  padding: 0 6px;
  line-height: 1.3;
}

.card-active .ghost-pos-label {
  color: var(--amber);
  text-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.55);
  font-size: 16px;
}

/* Active ghost card sits above drawn cards (important for overlapping positions
   like Celtic Cross pos 0/1 which share the same coordinates).
   !important because the cards carry inline z-indexes that a stylesheet rule
   cannot otherwise beat: initCanvas() stacks the Celtic Cross's Present at 5
   under Challenge at 6, and a drawn card that was the active slot keeps 10
   (grReplaceGhostCard). Without it the Present's ghost sat under Challenge's
   while it was the one to draw, and a drawn Present sat over Challenge's.
   1001, not 10: a drawn card tapped forward over another drawn card climbs
   one above the highest beneath it (grRaiseIfCovered), and in a custom spread
   that card may also lie partly over the slot being drawn. And a hovered
   card rises to 1000 on a desktop (#card-canvas ... :hover, near the top of
   this file), which must not lift it over the slot either. The canvas is
   transformed, so it is its own stacking context and nothing here reaches
   past it.
   Not on a Freestyle table, whose ghost is stacked by its own inline z-index
   (grimoire-freestyle.js) above cards already down. */
#card-stage:not(.fs-table) #card-canvas.layout-positioned .tarot-card.ghost-card.card-active {
  z-index: 1001 !important;
}

/* The two rules below carry #card-stage to outweigh
   `#card-stage:not(.fs-table) #card-canvas > * { pointer-events: auto }`
   (2 ids beat 1). From 2026-10-03 to 2026-10-20 they lost to it, so a phone's
   tap on the Celtic Cross's center landed on the waiting Challenge slot, which
   draws nothing, while the Present was the one to draw -- and, once it was
   drawn, on the dimmed Present, opening its meaning, while Challenge was. */

/* Non-active ghost cards are visual placeholders only — pass touches through
   so they never block the active card in overlapping positions */
#card-stage #card-canvas.layout-positioned .tarot-card.ghost-card:not(.card-active) {
  pointer-events: none;
}

/* Drawn card under an active ghost at the same position — dim and block no
   touches until the ghost is drawn (e.g. THE PRESENT while CHALLENGE is pending) */
#card-stage #card-canvas.layout-positioned .tarot-card[data-drawn="true"].ghost-overlap {
  opacity: 0.18;
  pointer-events: none;
  transition: opacity 0.4s ease;
}

/* Active card slot: flashing phosphor↔amber border with glow */
.card-active .card-frame,
.card-active .card-with-image {
  border-color: var(--phosphor);
  animation: active-card-flash 0.5s linear infinite;
}

@keyframes active-card-flash {
  0%   {
    border-color: var(--phosphor);
    box-shadow: 0 0 12px var(--halo), 0 0 30px var(--phosphor-glow), 0 0 60px var(--phosphor-glow-faint);
  }
  50%  {
    border-color: var(--amber);
    box-shadow: 0 0 12px var(--halo-amber), 0 0 30px rgba(var(--halo-amber-rgb), 0.25), 0 0 60px rgba(var(--halo-amber-rgb), 0.1);
  }
  100% {
    border-color: var(--phosphor);
    box-shadow: 0 0 12px var(--halo), 0 0 30px var(--phosphor-glow), 0 0 60px var(--phosphor-glow-faint);
  }
}


/* Draw button — below active card, outside card bounds */
.card-draw-wrap {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 10px;
  white-space: nowrap;
  z-index: 5;
}

.card-active .card-draw-wrap { display: block; }

.card-draw-btn {
  width: 100% !important;
  margin: 0 !important;
  padding: 6px 20px !important;
  font-size: 18px !important;
  letter-spacing: 3px !important;
  position: relative;
  overflow: hidden;
}

/* Charging fill overlay */
.card-draw-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--phosphor);
  transform: scaleX(0);
  transform-origin: left;
  opacity: 0.22;
  pointer-events: none;
}

@keyframes drawCharge {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.card-draw-btn.draw-charging::after {
  animation: drawCharge 1s linear forwards;
}

/* Charged: the slot brightens and is ready for the click. */
.ghost-card.draw-ready .card-frame {
  animation: none;
  border-color: var(--phosphor);
  box-shadow: 0 0 18px var(--halo), 0 0 46px var(--phosphor-glow);
}

.card-draw-btn.draw-ready {
  box-shadow: 0 0 14px var(--halo), 0 0 4px var(--halo);
  color: var(--bg) !important;
  background: var(--phosphor) !important;
}


/* ── Entrainment: static canvas overlay ──────────────────────────────── */

/* The background static's frame: fixed, exactly the viewport, clipping.
   makeNoiseField draws the canvas up to one cell past the viewport's edge, so
   its specks land on whole device pixels -- "overflow is the box's to clip".
   Fixed to the viewport, the canvas had no box clipping it, and Chrome on
   Android, finding content past the screen's edge, widened the layout to
   take it in; the canvas re-measured the wider window and overshot again,
   until the layout stood at Chrome's 980px fallback and the page was shown
   zoomed out to 0.44 with the desktop styles in force (found on the sign-in
   callback with a diagnostic, 2026-10-04). The frame is the clipping box. */
.bg-static-frame {
  position: fixed;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
}
#bg-static-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: 0.07;
  image-rendering: pixelated;
}

#static-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 15;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.5s ease;
  image-rendering: pixelated;
  filter: blur(1.5px);
  filter: blur(0.6px);
}

/* Card quantum materialization — a flickering resolve out of static.
 *
 * This is the moment the whole draw has been building toward, and at 0.8s it
 * was over before it registered: the card was simply there, with a flicker on
 * the way in. It runs for 1.9s now, and the extra time is spent where it can be
 * seen -- the arrival holds longer at each failed attempt, and the card settles
 * rather than snapping to its final state.
 *
 * Stops are spaced to land about 57ms apart whatever the duration is, which
 * is the flicker rate the original 0.8s version had. Stretching a fixed stop
 * list over a longer run instead would have
 * slowed each swing to a ~120ms pulse, which reads as a lamp with a loose
 * connection rather than a signal resolving. Density is what keeps it fast
 * while the whole takes longer, and the amplitude decays across the run so
 * the fight eases into a settle rather than stopping dead.
 *
 * The signal is struggling to resolve and keeps nearly failing. So the early
 * frames swing wide: almost gone, then nearly there, then gone again. It comes
 * in slightly oversized and out of register, the way a tube locks a picture,
 * and the last third is a settle rather than more flicker -- a flicker that
 * continues to the end reads as a fault rather than as an arrival.
 *
 * Scale is animated through a wrapper transform rather than on .tarot-card
 * itself: Celtic Cross rotates some of its cards, and a scale here would
 * overwrite that rotation. See .card-materializing below. */
@keyframes quantumMaterialize {
  0.0%   { opacity: 0;    filter: blur(20px) brightness(5.5) saturate(0); }
  4.0%   { opacity: 0.79; filter: blur(7.3px) brightness(2.85) saturate(0.03); }
  8.0%   { opacity: 0.13; filter: blur(13.8px) brightness(4.67) saturate(0.07); }
  12.0%  { opacity: 0.81; filter: blur(6.5px) brightness(2.65) saturate(0.11); }
  16.0%  { opacity: 0.22; filter: blur(12.3px) brightness(4.26) saturate(0.15); }
  20.0%  { opacity: 0.84; filter: blur(5.7px) brightness(2.46) saturate(0.2); }
  24.0%  { opacity: 0.31; filter: blur(10.8px) brightness(3.86) saturate(0.24); }
  28.0%  { opacity: 0.86; filter: blur(5.0px) brightness(2.27) saturate(0.29); }
  32.0%  { opacity: 0.4 ; filter: blur(9.3px) brightness(3.48) saturate(0.34); }
  36.0%  { opacity: 0.88; filter: blur(4.3px) brightness(2.09) saturate(0.39); }
  40.0%  { opacity: 0.49; filter: blur(7.9px) brightness(3.11) saturate(0.43); }
  44.0%  { opacity: 0.9 ; filter: blur(3.6px) brightness(1.92) saturate(0.48); }
  48.0%  { opacity: 0.57; filter: blur(6.6px) brightness(2.75) saturate(0.53); }
  52.0%  { opacity: 0.92; filter: blur(3.0px) brightness(1.76) saturate(0.57); }
  56.0%  { opacity: 0.65; filter: blur(5.4px) brightness(2.42) saturate(0.62); }
  60.0%  { opacity: 0.94; filter: blur(2.4px) brightness(1.6) saturate(0.66); }
  64.0%  { opacity: 0.72; filter: blur(4.2px) brightness(2.11) saturate(0.71); }
  68.0%  { opacity: 0.95; filter: blur(1.8px) brightness(1.46) saturate(0.75); }
  72.0%  { opacity: 0.79; filter: blur(3.1px) brightness(1.82) saturate(0.79); }
  76.0%  { opacity: 0.97; filter: blur(1.3px) brightness(1.33) saturate(0.83); }
  80.0%  { opacity: 0.85; filter: blur(2.1px) brightness(1.56) saturate(0.86); }
  84.0%  { opacity: 0.98; filter: blur(0.8px) brightness(1.21) saturate(0.9); }
  88.0%  { opacity: 0.9 ; filter: blur(1.3px) brightness(1.33) saturate(0.93); }
  92.0%  { opacity: 0.99; filter: blur(0.4px) brightness(1.11) saturate(0.96); }
  96.0%  { opacity: 0.94; filter: blur(0.6px) brightness(1.15) saturate(0.98); }
  100.0% { opacity: 1;    filter: none; }
}

/* The picture locking in: a shade too large and a hair off-centre while the
   signal is still fighting, easing to true as it settles. Small numbers on
   purpose -- at this size anything more reads as a card being thrown at the
   screen rather than resolving on it. */
@keyframes quantumSettle {
  0%   { transform: scale(1.085) translate(2px, -2px); }
  22%  { transform: scale(1.055) translate(-2px, 1px); }
  44%  { transform: scale(1.04)  translate(1px, 1px); }
  62%  { transform: scale(1.022) translate(-1px, 0); }
  80%  { transform: scale(1.008) translate(0, 0); }
  100% { transform: none; }
}

.card-materializing {
  animation: quantumMaterialize 1.4s ease-out forwards;
}

/* The settle rides on the frame, not the card: a Celtic Cross card carries a
   rotate() in its own style attribute, and animating transform on the card
   would replace it and stand the card upright mid-draw. */
.card-materializing .card-frame,
.card-materializing .card-with-image {
  animation: quantumSettle 1.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

/* Reduced motion gets the card, not the performance -- but still through an
   animation, because both callers wait for animationend to take the class off
   again. A rule of `animation: none` would leave that class on the card. */
@media (prefers-reduced-motion: reduce) {
  .card-materializing {
    animation: quantumMaterialize 0.01s linear forwards;
  }
  .card-materializing .card-frame,
  .card-materializing .card-with-image {
    animation: none;
  }
}

/* Fade active card out when mouse is still during entrainment */
#card-stage.entrainment-active .card-active,
#card-stage.entrainment-active .card-draw-wrap {
  transition: opacity 0.6s ease;
}
#card-stage.entrainment-active.entrainment-still .card-active,
#card-stage.entrainment-active.entrainment-still .card-draw-wrap {
  opacity: 0 !important;
  transition: opacity 1.2s ease;
}

/* Hide all non-active cards while entrainment is running */
#card-stage.entrainment-active .tarot-card:not(.card-active) {
  opacity: 0 !important;
  transition: opacity 1.5s ease;
}

/* Restore non-active cards quickly when entrainment fades out */
#card-stage:not(.entrainment-active) .tarot-card:not(.card-active) {
  transition: opacity 0.8s ease;
}

/* ── Entrainment: side-panel controls ────────────────────────────────── */

.ec-hint {
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
  padding-top: 5px;
}

.ec-unit {
  font-family: 'Share Tech Mono', monospace;
  font-size: 15px;
  color: var(--phosphor-dim);
  white-space: nowrap;
}

.ec-btn {
  background: transparent;
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: 'VT323', monospace;
  font-size: 21px;
  letter-spacing: 1px;
  padding: 0 6px;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.1s;
  height: 20px;
  line-height: 20px;
}

.ec-btn:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
  text-shadow: 0 0 4px var(--halo);
}

.ec-btn.active {
  border-color: var(--phosphor);
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  background: var(--wash);
}

.ec-btn.ec-muted {
  border-color: var(--amber);
  color: var(--amber);
  text-shadow: 0 0 5px var(--halo-amber);
}

/* Toggle switch */
.ec-toggle {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
  /* Anchors the visually hidden checkbox below, so it cannot escape and push a
     scrollbar out of some distant positioned ancestor. */
  position: relative;
}

/* The checkbox was display:none, which does more than hide it: a display:none
   control is removed from the accessibility tree and from the tab order, so
   these switches could not be labelled, could not be reached by keyboard, and
   were announced by nothing. Hidden visually instead -- out of flow, so the
   track does not move a pixel -- which leaves it focusable, announceable, and
   still driving `.ec-toggle input:checked + .ec-toggle-track` below. */
.ec-toggle input[type="checkbox"] {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

/* The switch is now reachable by keyboard, so it has to show where focus is --
   on the track, since the input itself is invisible. Same outline the Mind
   Machine switch on the workspace uses. :focus-visible, so a mouse click never
   draws it. */
.ec-toggle input[type="checkbox"]:focus-visible + .ec-toggle-track {
  outline: 1px solid var(--phosphor-dim);
  outline-offset: 2px;
}
/* OFF is a dark track with a dim knob, ON a lit track with a dark knob. OFF
   used to be the whole track in --phosphor-dim, which sits close enough to
   lit --phosphor that the two states had to be told apart by the knob's side
   alone. The OFF track is now mostly ground, and the knob takes the dim ink so
   it still shows against it. Mixed toward --bg rather than named, so it holds
   for every hue the slider gives and on paper, where it pales instead. */
.ec-toggle-track {
  position: relative;
  width: 34px;
  height: 16px;
  background: color-mix(in srgb, var(--phosphor-dim) 30%, var(--bg));
  border-radius: 8px;
  transition: background 0.2s;
}
.ec-toggle input:checked + .ec-toggle-track {
  background: var(--phosphor);
  box-shadow: 0 0 6px var(--halo);
}
.ec-toggle-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  background: var(--phosphor-dim);
  border-radius: 50%;
  transition: left 0.2s, background 0.2s;
}
.ec-toggle input:checked + .ec-toggle-track .ec-toggle-knob {
  left: 20px;
  background: var(--bg);
}

/* Editable Hz value fields */
.ec-val-input {
  width: 46px;
  background: var(--bg);
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 15px;
  padding: 1px 3px;
  outline: none;
  height: 18px;
  text-align: right;
  -moz-appearance: textfield;
}
.ec-val-input::-webkit-inner-spin-button,
.ec-val-input::-webkit-outer-spin-button { -webkit-appearance: none; }
.ec-val-input:focus { border-color: var(--phosphor); }


/* ── Autofill: the one colour the browser picks for us ───────────────────
   A remembered email or password arrives wearing the browser's own tint --
   Chromium's #e8f0fe, a pale blue -- and it is the only surface in the app
   whose colour was never chosen here. It was always wrong; it is merely
   obvious now that the light ink is rose, because a blue field on a rose page
   reads as a different product rather than as a filled-in box.

   background-color cannot fix it. The UA sets that declaration !important, so
   an author rule loses outright no matter how specific. An inset shadow wide
   enough to cover the field paints *over* it instead, which is why this is a
   box-shadow and not the obvious property. -webkit-text-fill-color is needed
   for the same reason -- `color` is overridden too -- and caret-color because
   a rose caret in a field the browser thinks is blue is otherwise missed.

   Firefox does not use a background at all; it tints with a filter, so it
   needs its own rule and its own answer. The two selectors cannot be listed
   together: a browser that does not recognise one drops the entire rule, and
   :-webkit-autofill and :autofill are not universally recognised.

   var(--bg) rather than a literal because every input in the app is on it --
   .auth-field input, .ec-val-input, .intent-textarea -- so the filled state
   matches the empty one on both themes without a second set of values. */
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
  -webkit-box-shadow: inset 0 0 0 1000px var(--bg);
          box-shadow: inset 0 0 0 1000px var(--bg);
  -webkit-text-fill-color: var(--phosphor);
  caret-color: var(--phosphor);
}

input:autofill,
textarea:autofill,
select:autofill {
  filter: none;
}

/* The credential fields are the only inputs whose focus state carries a
   box-shadow of its own, and box-shadow is a single property: the rule above
   would be replaced by the glow rather than joined to it, and the blue would
   come back the moment the field was focused. Both values, in the order the
   two rules would otherwise fight over. The selector outruns
   `.auth-field input:focus` on specificity, so it does not depend on this
   file loading before login.html and auth_base.html -- which it does not. */
.auth-field input:-webkit-autofill:focus {
  -webkit-box-shadow: inset 0 0 0 1000px var(--bg), 0 0 6px var(--phosphor-glow);
          box-shadow: inset 0 0 0 1000px var(--bg), 0 0 6px var(--phosphor-glow);
}

/* Speaker mute button */
.ec-speaker {
  background: transparent;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
  width: 26px;
  height: 20px;
  position: relative;
  padding: 0;
  transition: opacity 0.15s;
}
.ec-speaker::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 16px;
  height: 14px;
  background: var(--phosphor);
  box-shadow: 0 0 6px var(--halo);
  clip-path: polygon(0% 22%, 44% 22%, 100% 0%, 100% 100%, 44% 78%, 0% 78%);
}
.ec-speaker:hover { opacity: 0.75; }
.ec-speaker.ec-muted { opacity: 0.3; }


/* ── Canvas page: refocus modal ──────────────────────────────────────── */

#refocus-modal {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(var(--bg-rgb), 0.75);
}

#refocus-modal-inner {
  max-width: 400px;
  padding: 20px 24px;
  border: 1px solid var(--amber);
  background: var(--bg-panel);
  box-shadow: 0 0 24px rgba(var(--halo-amber-rgb), 0.2);
  text-align: center;
}


/* ── Canvas page: right-panel stats ─────────────────────────────────── */

#canvas-stats {
  border-top: 1px solid var(--phosphor-faint);
  padding: 8px 12px;
  flex-shrink: 0;
  font-size: 15px;
}

.cstat-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 3px;
}

.cstat-k { color: var(--phosphor-faint); letter-spacing: 1px; }
.cstat-v { color: var(--phosphor); text-shadow: 0 0 4px var(--halo); }
.cstat-v.amber { color: var(--amber); text-shadow: 0 0 4px var(--halo-amber); }


/* ═══════════════════════════════════════════════════════════════════════════
   GRIMOIRE — Reading Room (two-panel layout)
   ══════════════════════════════════════════════════════════════════════════ */

#archive-list {
  flex: 1;
  overflow-y: auto;
  min-height: 0;
}

/* --phosphor-faint is a hairline colour -- 12% lightness, sat against a 3%
   panel -- and it earns that everywhere else in this file as a border. As text
   it was barely a shade off the background. --phosphor-dim is the panel's
   secondary register, the one .archive-entry-date and .sidebar-section-header
   already read in, which leaves this quieter than the full-phosphor intention
   above it without disappearing. The divider work is done by the border, which
   stays faint because a rule is what --phosphor-faint is for. */
.archive-month {
  padding: 5px 12px 2px;
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  border-top: 1px solid var(--phosphor-faint);
  margin-top: 4px;
  text-transform: uppercase;
}

.archive-month:first-child {
  margin-top: 0;
  border-top: none;
}

.archive-entry {
  padding: 7px 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--phosphor-faint);
  transition: background 0.1s;
}

.archive-entry:hover {
  background: var(--wash-faint);
}

.archive-entry.active {
  background: var(--wash);
  border-left: 2px solid var(--phosphor);
  padding-left: 10px;
}

.sidebar-section-header {
  padding: 14px 12px 4px;
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-transform: uppercase;
}

.archive-entry-intention {
  font-size: 12.5px;
  color: var(--phosphor);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 2px;
}

.archive-entry-date {
  font-size: 11.5px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
}

.archive-entry--unfinished .archive-entry-intention {
  color: var(--amber);
}
.archive-entry--unfinished .archive-entry-date {
  color: color-mix(in srgb, var(--amber) 60%, transparent);
}

/* ── Archive meatball (overflow) menu ─────────────────────────────────────── */

/* Anchor for absolutely-positioned meatball; pad right to keep text clear of button */
.archive-entry {
  position: relative;
  padding-right: 30px;
}

/* Clickable body — block, fills entry width minus the right padding */
.archive-entry-body {
  cursor: pointer;
}

/* Date sits below the title, right-aligned */
.archive-entry-date {
  text-align: right;
}

/* Three-dot button — absolutely positioned at the right-centre of the entry */
.archive-meatball {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--phosphor);
  font-size: 18px;
  letter-spacing: -3px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  opacity: 0;
  transition: opacity 0.15s, background 0.15s, color 0.15s;
  padding: 0;
  font-family: 'Share Tech Mono', monospace;
}

.archive-entry:hover .archive-meatball,
.archive-entry.aom-open .archive-meatball {
  opacity: 1;
}

.archive-meatball:hover,
.archive-entry.aom-open .archive-meatball {
  color: var(--phosphor);
  background: var(--wash);
}

/* On touch devices hide the meatball — long-press triggers the menu instead */
@media (hover: none) {
  .archive-meatball { display: none; }
}

/* Singleton context menus — positioned fixed via JS to escape sidebar overflow */
/* Sized for the archive page, which has room the rail never did. The menu is
   fixed-position, so it is not constrained by whatever surface opened it. */
#archive-ctx-menu,
#archive-ctx-confirm {
  position: fixed;
  z-index: 400;
  background: var(--bg);
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 6px 20px rgba(var(--shade), 0.65);
  min-width: 168px;
  display: none;
}

#archive-ctx-menu.actx-visible,
#archive-ctx-confirm.actx-visible {
  display: block;
}

.archive-overflow-btn {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  text-align: left;
  padding: 11px 14px;
  cursor: pointer;
  transition: color 0.1s, background 0.1s;
}

.archive-overflow-btn:last-child {
  border-bottom: none;
}

.archive-overflow-btn:hover {
  color: var(--phosphor);
  background: var(--wash-faint);
}

.archive-overflow-danger:hover {
  color: var(--red-alert);
  background: rgba(var(--alert-rgb), 0.06);
}

.archive-overflow-confirm-label {
  padding: 10px 14px 7px;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-transform: uppercase;
  border-bottom: 1px solid var(--phosphor-faint);
}

/* Inline rename input — replaces the intention text in-place */
.archive-rename-input {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12.5px;
  outline: none;
  width: 100%;
  padding: 0 0 1px;
  margin-bottom: 2px;
  caret-color: var(--phosphor);
  min-width: 0;
}

/* Delete fade-collapse animation */
.archive-entry.ae-deleting {
  opacity: 0;
  max-height: 0 !important;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  border-bottom-width: 0 !important;
  overflow: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease,
              max-height 0.22s ease 0.05s,
              padding 0.18s ease,
              border-bottom-width 0.18s ease;
}

#grimoire-workspace {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 0;
  position: relative;
}

/* The gear is drawn (_icon_settings.html) rather than masked out of a PNG, so
   the button is just a box and the icon takes its ink from `color`. */
/* Fixed to the window, not a page region: it is on every app page now
   (_settings_button.html), and the archive scrolls. 18px in from the right
   rather than 8, so a browser's overlay scrollbar does not run over it. */
#ws-settings-btn {
  position: fixed;
  top: 6px;
  right: 18px;
  padding: 3px;
  z-index: 103;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--phosphor);
  cursor: pointer;
}
#ws-settings-btn .gear-icon { width: 100%; height: 100%; display: block; }
#ws-settings-btn:hover { filter: brightness(1.15); }

/* The phone header's gear, at the size its neighbours take. */
.mob-icon-btn .gear-icon { width: 24px; height: 24px; display: block; }

/* ── Workspace settings panels (display / mind machine) ──────────────── */

#ws-display-settings,
#ws-mindmachine-settings {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  overflow-y: auto;
}

.ws-settings-body {
  padding: 20px 24px;
  flex: 1;
  overflow-y: auto;
}

.ws-settings-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px 32px;
}

.ws-settings-block {
  display: flex;
  flex-direction: column;
}

.ws-settings-toggles {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 32px;
}

.ws-toggle-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 200px;
}

/* Generic range slider for workspace settings */
.ws-range {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 6px;
  border-radius: 2px;
  background: var(--phosphor-faint);
  cursor: pointer;
  outline: none;
  border: none;
}
.ws-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--phosphor);
  border: none;
  cursor: pointer;
  box-shadow: 0 0 6px var(--phosphor-glow);
}
.ws-range::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--phosphor);
  border: none;
  cursor: pointer;
}

/* Active sidebar nav state */
.sidebar-nav-item.sb-nav-active {
  background: var(--wash);
  color: var(--phosphor);
}

/* The hero and the new-revelation form share this screen: the wordmark centres
   in the space it has, the form sits under it, and the pair scroll together.
   This used to centre a single hero and clip anything taller -- with the form
   below, `overflow: hidden` would have cut it off at the fold.
   `text-align: center` moved to .home-hero: inherited from here it centred the
   form's labels and the question the visitor types. */
#ws-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow-y: auto;
  min-height: 0;
}

/* Holds the workspace while a ?r= arrival fetches its reading, so the page
   shows what it is doing instead of the screen it is about to replace. */
#ws-loading {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

.ws-loading-line {
  font-size: 13px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}

/* Three dots, cycling: the one sign, app-wide, that something is loading
   (owner, 2026-10-06). Put it after the word: SAVING<span class="loading-dots"
   aria-hidden="true"></span>. Text content rather than opacity so it reads as
   a terminal working rather than as something pulsing at the reader. The
   hidden ::before holds the width of all three dots, so neither the word nor
   the button around it shifts as they cycle. */
.loading-dots {
  position: relative;
  display: inline-block;
  text-align: left;
}
.loading-dots::before { content: "..."; visibility: hidden; }
.loading-dots::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  animation: loading-dots 1.2s steps(4, end) infinite;
}

@keyframes loading-dots {
  0%   { content: ""; }
  25%  { content: "."; }
  50%  { content: ".."; }
  75%  { content: "..."; }
  100% { content: ""; }
}

@media (prefers-reduced-motion: reduce) {
  .loading-dots::after { content: "..."; animation: none; }
}

/* ── Home hero ───────────────────────────────────────────────────── */

.home-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* The hero and the form centre as a pair in the fold: `margin-top: auto`
     here and `margin-bottom: auto` on #nr-panel split the leftover space
     between them. Auto margins rather than `justify-content: center`, because
     they collapse to nothing when the pair is taller than the fold -- the top
     stays reachable and #ws-empty simply scrolls.

     They were anchored to the top for a while, because the leftover space is
     what absorbs any change in height: the options picker once opened inline
     and the whole block jumped half its height upward in a single frame. The
     picker is a modal now, and the one line that still comes and goes --
     #gr-begin-status -- keeps its room reserved, so nothing here changes
     height at rest. Anything that grows the form inline will bring the jump
     back. */
  margin-top: auto;
  /* Tighter than the 20px it used when this screen held nothing else: the hero
     now shares the fold with the form, and every pixel it spends above pushes
     BEGIN REVELATION below the fold on a laptop. */
  gap: 16px;
  padding: 24px 20px 16px;
  flex-shrink: 0;
}

/* One size across the product. This used to be sized for a workspace panel --
   clamp(8px, 1.4vw, 12px) -- against the landing hero's clamp(7px, 2.1vw, 17px),
   which meant a visitor met the brand mark at one size on the way in and a
   smaller one every session after. It is the same mark; it gets the same
   clamp, and login.html no longer overrides it. */
.home-ascii-logo {
  /* Share Tech Mono answers for the spaces only; every drawing glyph comes from
     Psibyl Glyphs. See fonts.css for why the second face has to be named. */
  font-family: 'Share Tech Mono', 'Psibyl Glyphs', monospace;
  font-size: clamp(7px, 2.1vw, 17px);
  line-height: 1.2;
  color: var(--phosphor);
  text-shadow:
    0 0 10px var(--phosphor-glow-strong),
    0 0 30px var(--phosphor-glow);
  white-space: pre;
  letter-spacing: 0;
  user-select: none;
  margin: 0;
}

.logo-os {
  color: var(--amber);
  text-shadow: 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3);
}

/* The Psibyl mark (_psibyl_mark.html): the P at the full .home-ascii-logo size,
   SIBYL beside it at 22/27 of that, and SIBYL's underline brought up level with
   the foot of the P. 22/27 is what puts both ends on the P: from SIBYL's top to
   the middle of the underline's last row is 7.5 rows less the 0.9em lift, 8.1
   of its own em, against the P's 5.5 rows, 6.6 of its em -- so the tops meet
   and the two bottom strokes run as one line.

   Sized to be exactly as wide as QUANTUM REVELATION TERMINAL beneath it, not
   by viewport: the mark is 22.8 of its own em wide, measured, and the subtitle
   is a fixed 24px VT323 on a desktop and 23px on a phone. The desktop pair
   is the 19.75px / 34px pair it replaced, taken down 30% together: VT323 only
   draws whole pixel sizes, so the line is 24px, and the mark is set to 14.2px,
   which matches it within 1% at 1x and 2x -- 13.825px, the straight 70%, came
   out 3% short of it. 13.5px matches the 23px line at 2x and 3x, which is
   every phone. Each page switches where its own subtitle
   does -- 767px for the Grimoire's .home-subtitle, 560px for the landing's
   .lp-subtitle -- so the two stay the same width through the change. */
.psibyl-mark {
  --mark-f: 14.2px;
  display: flex;
  align-items: flex-start;
}
/* On a phone the Grimoire's empty state drops the hero altogether -- the mark
   and, since 2026-10-04 (owner), QUANTUM REVELATION TERMINAL too: the screen is
   too short to spend on either, and the header above already carries the mark
   (.mob-logo). The form stands in the middle of the screen (owner,
   2026-10-04): with the hero gone it takes both auto margins itself, which
   split the room above and below it -- and collapse to nothing once the form
   and RECENT are taller than the screen, so it then starts at the top and
   scrolls, as before. A small gap in place of its leading <br>. */
@media (max-width: 767px) {
  .home-hero { display: none; }
  #nr-panel { padding-top: 16px; margin-top: auto; }
  #nr-panel > br:first-child { display: none; }
}
@media (max-width: 560px) { .lp-hero   .psibyl-mark { --mark-f: 13.5px; } }
/* Left-aligned whatever the hero around it does: both heroes centre their text,
   and a <pre> inherits that, which centres each row within its block. The rows
   are a drawing, not text -- centred, the shorter letter rows slide right,
   away from the P, as soon as the underline rows are longer than they are.

   Row height. The glyph face draws its blocks and rules 1.188em tall (2433 of
   2048 units, -512 to 1921), not the 1.2em .home-ascii-logo gives every line,
   so at 1.2 each row stood a hair taller than what was drawn in it: a seam
   through every block at every row boundary, and a break in every double rule
   crossing one. At 1.188 they tile exactly. Where round() is available the row
   goes a step further, to a whole pixel rounded down, so the glyphs overlap by
   a fraction instead and every row lands on the pixel grid the same way; a
   fractional row snaps differently from one row to the next, which reads as
   the lines wobbling. */
.psibyl-mark .home-ascii-logo {
  /* The glyph face alone, spaces included. Behind Share Tech Mono, as
     .home-ascii-logo sets it, every space came from Share Tech Mono at 0.54em
     against the blocks' 0.602em, so each space pulled the rest of its row about
     a pixel left and no two rows with different numbers of spaces agreed on
     where a column was: the S's and I's first rows sat left of the rows under
     them, and B, Y and L drifted row by row. See fonts.css. */
  font-family: 'Psibyl Glyphs', monospace;
  font-size: var(--mark-f);
  text-align: left;
  line-height: 1.188;
  --row: 1.188em;
}
.psibyl-mark .psibyl-rest {
  font-size: calc(var(--mark-f) * 22 / 27);
  /* Gives back the space the lifted underline left below it. */
  margin-bottom: calc(-0.75 * var(--row));
  /* And the five cells its rows run past the L before .psibyl-ul draws them
     back under the P, so the box ends where the ink does and the mark centres
     on what is visible. */
  margin-right: calc(-5 * 0.602em);
}
@supports (width: round(down, 1.5px, 1px)) {
  .psibyl-mark .home-ascii-logo {
    --row: round(down, calc(var(--mark-f) * 1.188), var(--row-step, 1px));
    line-height: var(--row);
  }
  .psibyl-mark .psibyl-rest {
    --row: round(down, calc(var(--mark-f) * 22 / 27 * 1.188), var(--row-step, 1px));
  }
}
/* The small copy in the phone header (.mob-logo, base.html) -- and, the
   same copy at the same size, in a desktop header that shows the mark
   (.hdr-mark: the shared page, shared.html). It was sized to
   the 76px-wide text wordmark it replaced, and is 90px now -- 4px, up from
   3.4px (owner, 2026-10-04). At 3.4px a whole-pixel row is far too
   coarse -- the S-I-B-Y-L rows would lose a ninth of their height and the two
   bottom strokes would part -- so its rows round to device pixels instead,
   which keeps them on the grid just the same. Glows are scaled down with it.

   It does not glitch: at this size the shear reads as noise, not as the
   mark breaking up. The Grimoire's driver fires every .glitch-block on the
   page, this copy's included, so the animation is refused here instead. */
/* The desktop header's row for it: every header that shows the mark (owner,
   2026-10-06) -- base.html, auth_base.html, legal_base.html, shared.html. */
#header .hdr-mark { display: flex; align-items: center; min-height: 39px; }
#header .hdr-mark a { display: block; line-height: 0; }
:is(.mob-logo, .hdr-mark) .psibyl-mark { --mark-f: 4px; --row-step: 1px; }
/* Flush with the foot of the P. The lift below is right for the large mark,
   where the rows round to whole pixels and the two bottom strokes meet; at
   this size the rows round to device pixels, every row comes out shorter than
   drawn, and the underline stood 1.5-2px above the P's foot. Dropped by the
   1.5px that closes it: measured ink to ink, exactly flush at 3x, and half a
   pixel short at 2x, where no drop lands both device rows at once. */
:is(.mob-logo, .hdr-mark) .psibyl-ul { top: calc(-0.75 * var(--row) + 1.5px); }
:is(.mob-logo, .hdr-mark) .glitch-block { animation: none !important; }
@media (min-resolution: 2dppx) { :is(.mob-logo, .hdr-mark) .psibyl-mark { --row-step: 0.5px; } }
@media (min-resolution: 3dppx) { :is(.mob-logo, .hdr-mark) .psibyl-mark { --row-step: 0.33333px; } }
:is(.mob-logo, .hdr-mark) .psibyl-mark .home-ascii-logo {
  text-shadow: 0 0 3px var(--phosphor-glow-strong), 0 0 8px var(--phosphor-glow);
}
:is(.mob-logo, .hdr-mark) .psibyl-mark .logo-os {
  text-shadow: 0 0 3px var(--halo-amber), 0 0 7px rgba(var(--halo-amber-rgb), 0.3);
}
/* The underline is lifted three quarters of a row toward SIBYL. The 22/27 in
   .psibyl-rest assumes exactly that: from SIBYL's top to the middle of the
   underline's last row is 7.5 rows less the 0.75 lift, 6.75 rows, against the
   P's 5.5 rows to the middle of its last, and 5.5 / 6.75 is 22/27.

   It also reaches back under the P toward its stem: the two rows carry five
   more cells than SIBYL is wide, and the whole line is drawn five cells to the
   left, so its right end stays under the L. 0.602em is one cell of the glyph
   face (1233 of 2048 units). Five leaves a clear gap before the stem's shadow
   -- about 16px on a desktop, 9px on a phone; a sixth closes it to a few
   pixels and the two glows run together. */
.psibyl-ul { position: relative; top: calc(-0.75 * var(--row)); left: calc(-5 * 0.602em); }

/* The mark's three glitches, one per part, named by each span's data-glitch.
   None has a 100% frame: with the fill held forwards, an absent end frame
   resolves to the element's own resting style, so each part settles back to
   exactly what it wears at rest, on any page and in either theme, whatever the
   page's own glitch-burst does to the sigils.
     P      -- the red/cyan shear, then its phosphor glow.
     S, I   -- amber through the burst, the colour of the letters they join, with
               the amber halo under the shear in every frame: without it the same
               amber read darker beside BYL, which keep their glow. Then a snap
               back to phosphor.
     underline -- phosphor through the burst, then a snap back to amber.
   B, Y and L do not glitch. Neither does the header's small copy at all. */
@keyframes psibyl-glitch {
  0%   { text-shadow: -6px 0 #ff3c3c,  6px 0 #3cffef; }
  30%  { text-shadow:  7px 0 #ff3c3c, -5px 0 #3cffef; }
  60%  { text-shadow: -8px 0 #ff3c3c,  8px 0 #3cffef; }
  80%  { text-shadow:  4px 0 #3cffef, -3px 0 #ff3c3c; }
}
@keyframes psibyl-si-glitch {
  0%   { color: var(--amber); text-shadow: -6px 0 #ff3c3c,  6px 0 #3cffef, 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3); }
  30%  { color: var(--amber); text-shadow:  7px 0 #ff3c3c, -5px 0 #3cffef, 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3); }
  60%  { color: var(--amber); text-shadow: -8px 0 #ff3c3c,  8px 0 #3cffef, 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3); }
  80%  { color: var(--amber); text-shadow:  4px 0 #3cffef, -3px 0 #ff3c3c, 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3); }
  99%  { color: var(--amber); text-shadow: 0 0 10px var(--halo-amber), 0 0 24px rgba(var(--halo-amber-rgb), 0.3); }
}
@keyframes psibyl-ul-glitch {
  0%   { color: var(--phosphor); text-shadow: -6px 0 #ff3c3c,  6px 0 #3cffef; }
  30%  { color: var(--phosphor); text-shadow:  7px 0 #ff3c3c, -5px 0 #3cffef; }
  60%  { color: var(--phosphor); text-shadow: -8px 0 #ff3c3c,  8px 0 #3cffef; }
  80%  { color: var(--phosphor); text-shadow:  4px 0 #3cffef, -3px 0 #ff3c3c; }
  99%  { color: var(--phosphor); text-shadow: 0 0 14px var(--phosphor), 0 0 42px var(--phosphor-glow-strong); }
}

/* Matched to .lp-subtitle on the landing, which is the same line under the same
   mark: VT323 rather than the inherited Share Tech Mono, and 24px rather than
   13px. No margin, unlike the landing's -- .home-hero spaces its children with
   gap, and a margin here would be added to that rather than replacing it. */
.home-subtitle {
  font-family: 'VT323', monospace;
  font-size: 24px;
  line-height: 1.25;
  letter-spacing: 2.1px;
  color: var(--amber);
  text-shadow: 0 0 12px var(--halo-amber);
}

/* The sigil is the question box's hint text now, not a hero element: it lies
   over the empty field and clears on the first keystroke. Smaller than the 20px
   it had on the hero, because it has to sit inside the field rather than own a
   band of the page. */
.nr-question-wrap {
  position: relative;
  width: 100%;
  max-width: 480px;
  align-self: center;
}

/* A textarea is inline-level, so the line box it sits in leaves ~5px of
   descender space beneath it -- enough that an overlay pinned to `bottom: 0`
   hangs below the field it is meant to sit in.

   Transparent, the way .nr-begin-btn is: the shared .intent-textarea paints
   var(--bg), a shade darker than the var(--bg-panel) it sits on, so the field
   read as a flat rectangle punched into the screen -- the grain, the bloom and
   the vignette all pass over it, and it was the one patch of the hero where the
   ground behind them did not continue. Now nothing is painted and the static
   runs through. appearance:none because iOS Safari supplies a ground of its own
   to a textarea that declares none. */
.nr-question-wrap .intent-textarea {
  display: block;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
}

.home-sigil {
  font-family: 'Share Tech Mono', monospace;
  font-size: 15px;
  line-height: 1.55;
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--phosphor-glow-strong);
  animation: sigilPulse 4s ease-in-out infinite;
  margin: 0;
  user-select: none;
}

.nr-question-wrap .home-sigil,
.nr-question-wrap .nr-sigil {
  position: absolute;
  /* Centred on the field. Emphatically NOT a flex container: _startSigilAnim()
     wraps every glyph in its own span, and as flex items those spans laid
     themselves out one per line -- the grid unrolled into a single column
     running down the page. A plain <pre> keeps its own line breaks.
     The +4px offsets half the 8px top margin the textarea carries, which is
     inside this wrapper and would otherwise pull the sigil up by that much.
     Never a click target and never announced: the caret belongs to the field
     underneath it, and the label above already names it. */
  top: calc(50% + 4px);
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  transition: opacity 0.2s ease;
}

/* Anything typed and the hint is gone, the way a placeholder goes. The pulse has
   to be called off with it: sigilPulse animates opacity, and a running animation
   outranks a plain declaration -- leave it on and the hint fades to 0.85 and
   sits there over the words being typed. */
.nr-question-wrap.has-text .home-sigil,
.nr-question-wrap.has-text .nr-sigil {
  animation: none;
  opacity: 0;
}

/* The hint is in two registers now, and they do not compete: the sigil is the
   mark, centred and set well back, while the words sit at the caret where the
   answer will go. Dimmer than typed input, so the field still reads as empty. */
.nr-question-wrap .intent-textarea::placeholder {
  color: var(--phosphor-dim);
  opacity: 0.65;
}

/* ── The brand sigil, drawn in characters ─────────────────────────────────
   The favicon -- three staves and one crossing -- on a character grid, in the
   same alphabet as the wordmark. Two rules follow, one per surface; both obey
   the same two constraints, which are worth stating once.

   FAMILY. The glyph face alone, 'Psibyl Glyphs' (fonts.css), spaces
   included, as the wordmark's rules have it: no other face the page loads has
   a block glyph, and a space from any other face is a different width, which
   puts every row's columns somewhere else.

   ROWS. A whole number of pixels, and no taller than the glyph. The face
   draws a block 1.188em tall (-0.25em to 0.938em), so a row of 1.2em -- what
   these rules had, copied from the wordmark before the wordmark was fixed --
   stood a hair taller than what was drawn in it: a dark seam across every
   stave at every row boundary, plain at 1x. A fractional row also snaps
   differently from one row to the next. So the font-size is chosen to make a
   whole-pixel row that the glyph overhangs slightly, and line-height is that
   row in pixels.

   COLUMNS. A block is 0.6em wide, and at these sizes the browser puts each
   one on a whole pixel (the advance rounds), so a block narrower than its
   column leaves a sliver of background at every column boundary: vertical
   lines down each stave. The font-size is chosen so 0.6em is just over a whole
   pixel, and the block covers its column.

   GRID. Then the cell is 0.6em by its row, close to 1:2, so art that is to
   come out SQUARE is drawn twice as wide as it is tall. Both marks below are
   W = 2H, and both come out square: columns x 0.6em across, rows x the row
   down. */

/* In the question field: hint text, so it sits well back -- dim phosphor at
   half opacity. 24 columns of 5px and 12 rows of 10px: 120px on a side, the
   height the wordmark stands at on the landing page.

   No pulse of its own: .glitch-block hands this one to the glitch driver in
   grimoire.html, which writes style.animation directly, so an animation here
   would only be overwritten by the first burst. Dimming is opacity, not glow,
   for the same reason -- glitch-burst ends on `text-shadow: none` and fills
   forwards, so any resting shadow is gone the moment the mark first breaks. */
.nr-sigil {
  font-family: 'Psibyl Glyphs', monospace;
  /* 0.6em is 5.1px over a 5px column; the block is 10.1px tall in a 10px row. */
  font-size: 8.5px;
  line-height: 10px;
  letter-spacing: 0;
  white-space: pre;
  color: var(--phosphor-dim);
  opacity: 0.5;
  text-shadow: 0 0 6px var(--phosphor-glow);
  margin: 0;
  user-select: none;
}

/* In the archive sidebar, where it replaced a 52px PNG. 14 columns of 4px and
   7 rows of 8px: 56px on a side.

   Half-blocks (▀▄) rather than the field's plain █: at 52px a whole-cell grid
   is 14 x 7, too coarse for three staves and a crossing to survive -- they
   merge into a filled square, the same failure the favicon's own comment
   describes at 16px. Splitting each row in two buys a 14 x 14 raster out of the
   same 7 lines of text, and since the cell is 2:1 those half-cells come out
   square. Full phosphor and the wordmark's glow: this is the brand mark on the
   panel, not a hint, and it carries no .glitch-block -- it holds steady while the
   wordmark breaks. */
.brand-sigil {
  /* Named rather than left to `monospace`: no face the page loads otherwise has
     a half-block, so the device drew this mark with whatever it had, and a
     phone's substitute put the staves out of true. See fonts.css. */
  font-family: 'Psibyl Glyphs', monospace;
  /* 0.6em is 4.05px over a 4px column; the block is 8.02px tall in an 8px row.
     At 6.2px and 1.2 the rows parted at every boundary and the columns did
     too: the block was 3.72px in a column rounded up to 4. */
  font-size: 6.75px;
  line-height: 8px;
  letter-spacing: 0;
  white-space: pre;
  flex-shrink: 0;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--phosphor-glow-strong), 0 0 30px var(--phosphor-glow);
  margin: 0;
  user-select: none;
}

@keyframes sigilPulse {
  0%, 100% { opacity: 0.85; text-shadow: 0 0 6px var(--phosphor-glow); }
  50%       { opacity: 1;    text-shadow: 0 0 16px var(--phosphor-glow-strong), 0 0 32px var(--phosphor-glow); }
}

.sc-flash {
  color: var(--phosphor);
  text-shadow: 0 0 14px var(--halo);
  opacity: 0.4;
}

/* #nr-panel was #ws-new-reading, a workspace state you reached by pressing
   BEGIN NEW REVELATION. It is a section of the hero screen now, so it neither
   grows to fill a screen nor scrolls on its own -- #ws-empty does the
   scrolling for both halves. */
#nr-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  align-items: center;
  flex-shrink: 0;
  margin-bottom: auto;   /* centres with .home-hero; see there */
}

#nr-flow {
  padding: 0 24px 32px;
  margin-top: -4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

#nr-panel .nr-block {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  min-width: 280px;
  width: 100%;
  max-width: 480px;
  margin-top: 20px;
}

#nr-panel .nr-block:first-child { margin-top: 0; }

#nr-panel .term-radio-group {
  display: flex;
  flex-direction: row;
  align-self: stretch;
  gap: 4px;
  /* The options under TYPE are not a fixed set — another revelation type brings
     its own layouts and its own decks, and the deck list grows on its own. A row
     that cannot wrap runs off the panel instead. */
  flex-wrap: wrap;
}

#nr-panel .term-radio {
  /* Grow to share the row, but never shrink below the words: an option that
     shrinks breaks "RIDER-WAITE-SMITH" across two lines and drops a SOON tag
     onto its own. Wrapping the group to a second line is the better failure,
     and flex-shrink:0 is what chooses it. */
  flex: 1 0 auto;
  white-space: nowrap;
  text-align: center;
  /* 6px, not 8: the three layouts need 407px of the 414 the row has, and at 8px
     they needed 419 and wrapped. A fourth layout will wrap anyway — that is the
     designed failure, and it costs a line rather than a broken word. */
  padding: 7px 6px;
}

#nr-panel .nr-begin-btn {
  width: 100%;
}

/* The question is the one field the visitor actually fills, and a four-line box
   asked for a sentence. Ten lines at the 21px line box this face gives — six
   more than the shared .intent-textarea height, which the landing console and
   home page still use at their own sizes. It briefly scaled with the viewport,
   when the hero above it still carried the sigil and the two together pushed
   BEGIN below the fold; the sigil moving in here gave the height back. Kept in
   step with the rows attribute in grimoire.html, which is what shows if this
   rule is ever dropped. */
#gr-intention {
  height: 222px;
}

.nr-label {
  font-size: 13px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  margin-bottom: 10px;
}

#gr-begin-status {
  margin-top: 10px;
  /* One line held open even when empty, so a status arriving does not grow
     the centred form and nudge everything above it. */
  min-height: 1.3em;
  font-size: 12px;
  color: var(--phosphor-dim);
  letter-spacing: 1px;
  text-align: center;
}

/* ── The settings: tradition control, chips, picker ───────────────────────
   DRAW_OPTIONS_PLAN.md. The chips name what will be drawn; the spread and the
   deck open the picker modal, the style turns in place. */

.nr-chips {
  display: flex;
  align-items: stretch;
  gap: 4px;
  /* No rule above them. The question and the settings are one act -- what you
     are asking and what it will be drawn with -- and a line between them made
     the settings read as a separate section rather than the second half of the
     same form. The spacing is the separation. */
  padding-top: 14px;
}

/* Phosphor: the chips are the settled state of the reading, and phosphor is
   what the terminal says a settled value in. */
/* The form runs on the terminal's own cursor, like the rest of the workspace.
   Its controls each asked for a system cursor of their own -- pointer on the
   chips, the radios and BEGIN, not-allowed on the disabled and coming-soon
   ones -- which put an arrow or a slashed circle on screen beside the phosphor
   block. Scoped to the panel and carried by an id, so it outranks all of them
   while .term-radio keeps its pointer everywhere else it is used. Disabled
   still reads as disabled: that is what the dimmed opacity is for. */
#nr-panel, #nr-panel * { cursor: none; }

.nr-chip {
  /* Equal shares of the line, whatever the words: flex-basis 0 divides the
     width rather than the leftovers, so four buttons are four equal buttons and
     not one long deck name beside three short ones. */
  flex: 1 1 0;
  min-width: 0;
  background: transparent;
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  letter-spacing: 1px;
  line-height: 1.3;
  padding: 6px 4px;
  text-align: center;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.nr-chip:hover {
  border-color: var(--phosphor);
  background: var(--wash-faint);
  text-shadow: 0 0 8px var(--halo);
}

.nr-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.nr-chip-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Equal shares while the names fit; past that, the spread's and the deck's
   chips grow to their names. A spread's name can be its positions
   (PAST·PRESENT·FUTURE), which a third of the line cannot hold, and it must not
   take the deck's name from it in doing so. Each is capped so the three always
   fit the panel -- 220px of spread and 120px of deck, the style's short word
   in what remains -- and a longer name than that truncates. On a phone there
   is no room to give, and every chip truncates as before. */
@media (min-width: 481px) {
  .nr-chip[data-group="g-layout"],
  .nr-chip[data-group="g-corpus"] { min-width: max-content; }
  .nr-chip[data-group="g-layout"] .nr-chip-text { max-width: 220px; }
  .nr-chip[data-group="g-corpus"] .nr-chip-text { max-width: 120px; }
}
/* ▾ opens the picker, ↻ turns to the next style. Quiet: the name is the
   point, the mark only says what a press will do. */
.nr-chip-mark {
  flex: 0 0 auto;
  font-size: 12px;
  opacity: 0.7;
}
.nr-chip[disabled] .nr-chip-mark { display: none; }

/* A setting with a single answer. It is still the readout for something real,
   so it keeps its place and its text; it simply stops offering a way in, and
   the hover states that promised one are taken back. */
.nr-chip[disabled] {
  cursor: default;
  opacity: 0.72;
}

.nr-chip[disabled]:hover {
  border-color: var(--phosphor-dim);
  background: transparent;
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
}

.nr-begin-btn {
  background: transparent;
  border: 1px solid var(--phosphor);
  color: var(--phosphor);
  font-family: 'VT323', monospace;
  font-size: 24px;
  letter-spacing: 4px;
  cursor: pointer;
  text-shadow: 0 0 8px var(--halo);
  box-shadow: 0 0 10px var(--phosphor-glow), inset 0 0 10px var(--phosphor-glow);
  padding: 10px 28px;
  transition: all 0.15s;
}

.nr-begin-btn:hover { background: var(--wash); }
.nr-begin-btn:disabled { opacity: 0.45; }
/* Waiting on a question. Looks as it did when it was disabled, but still takes
   the press, so it can say what it is waiting for (grBeginReading). */
.nr-begin-btn.is-unready { opacity: 0.45; }
.nr-begin-btn.is-unready:hover { background: transparent; }

/* The button spends the question, and it comes alive the moment there is one to
   spend -- but it sits below the field, where someone typing is not looking. So
   it announces itself the way the oracle icon does when a reading is ready to
   channel, and that flash is a swing of channel, not of brightness: it crosses
   from phosphor to amber and back on a 0.7s ease-in-out.

   oracleIconFlash reaches amber the long way round, because it is tinting a PNG
   -- sepia() lands the image near 38deg and --icon-hue-rotate (n - 38) carries
   it the rest of the way to the visitor's phosphor, so the peak's
   --icon-amber-hue is really "stop at 46deg", which is amber. Text needs none
   of that machinery and names the two colours outright. The amber is fixed
   while the phosphor is the visitor's, which is the same asymmetry the whole
   terminal runs on: the second channel is a constant, and what it interrupts is
   theirs.

   Infinite, like the oracle's: it is an unread notice, and it stands until it is
   answered. Two things answer it, both in grimoire.js -- emptying the question
   box (grNrSyncBegin drops the class and re-arms) and pressing the button
   (grBeginReading disarms it for good). Note that the class is added rather
   than re-added on each keystroke: classList.add on an element that already
   carries it is a no-op, so the swing runs unbroken while a question is typed
   instead of snapping back to frame one on every character. */
@keyframes beginReady {
  0%, 100% {
    background: transparent;
    border-color: var(--phosphor);
    color: var(--phosphor);
    text-shadow: 0 0 8px var(--halo);
    box-shadow: 0 0 10px var(--phosphor-glow), inset 0 0 10px var(--phosphor-glow);
  }
  50% {
    background: rgba(var(--amber-rgb), 0.10);
    border-color: var(--amber);
    color: var(--amber);
    text-shadow: 0 0 12px var(--halo-amber), 0 0 28px rgba(var(--halo-amber-rgb), 0.45);
    box-shadow: 0 0 22px rgba(var(--halo-amber-rgb), 0.40), inset 0 0 16px rgba(var(--halo-amber-rgb), 0.25);
  }
}

.nr-begin-btn.begin-ready {
  animation: beginReady 0.7s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .nr-begin-btn.begin-ready { animation: none; }
}

#ws-reading-detail {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 0;
}

#detail-content-row {
  flex: 1;
  display: flex;
  flex-direction: row;
  overflow: hidden;
  min-height: 0;
  position: relative;
}

#detail-card-area {
  flex: 1;
  width: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
  min-height: 0;
}

#detail-oracle {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  cursor: default;
  width: 400px;
  min-width: 92px;
  border-left: 1px solid var(--phosphor-faint);
  overflow-y: auto;
  /* Frosted, and it has to stay frosted -- the pane sits over the spread and
     the cards showing through it is the point. But blur alone does not make a
     backdrop quiet: it moves colour around without taking any away, so a
     saturated card under a 13px blur becomes a soft field of exactly the same
     red, and phosphor text over that is competing with it for the eye.

     So the backdrop is desaturated and dimmed before the scrim goes over it.
     saturate() is what dulls -- it is the vividness rather than the lightness
     that was fighting the text -- and brightness() takes the level down to
     where the type sits clearly above it. The scrim then does the rest, at more
     than twice its old weight.

     All three are needed. Scrim alone, taken high enough to hide a bright card,
     stops being glass and becomes a panel; dulling first means the glass can
     stay glass. The light theme redoes the same three values in its own
     direction -- there the backdrop is lifted toward the paper, not dimmed --
     at the foot of this file. */
  background-color: rgba(var(--bg-rgb), 0.6);
  backdrop-filter: blur(13px) saturate(0.35) brightness(0.5);
  -webkit-backdrop-filter: blur(13px) saturate(0.35) brightness(0.5);
  box-shadow: inset 0 0 24px var(--phosphor-glow-faint), -4px 0 20px rgba(var(--shade), 0.4);
  transition: width 0.35s ease;
}

#draw-controls {
  flex-shrink: 0;
}

#oracle-resize-handle {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: ew-resize;
  z-index: 10;
}

#oracle-resize-handle:hover,
#detail-oracle.resizing #oracle-resize-handle {
  background: linear-gradient(to right, var(--phosphor-faint), transparent);
}

#detail-oracle.resizing {
  transition: none !important;
  user-select: none;
}

.oracle-header-btn {
  background: transparent;
  border: none;
  color: var(--phosphor-dim);
  cursor: pointer;
  padding: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.7;
  transition: opacity 0.15s, color 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.oracle-header-btn:hover { opacity: 1; color: var(--phosphor); }
#oracle-copy-btn.copied { color: var(--amber); opacity: 1; }
/* Every copy button: amber when it copied, red when it could not
   (psibylCopyFlash, terminal.js). */
.oracle-header-btn.copied { color: var(--amber); opacity: 1; }
.oracle-header-btn.copy-failed { color: var(--red-alert); opacity: 1; }

/* The phone sheet's grab mark; drawn only in the 767px block below. */
.sheet-grip { display: none; }

#detail-oracle .panel-header {
  cursor: pointer;
  user-select: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-right: 12px;
  position: relative;
}

/* Detail header — amber status label for unfinished readings */
.title-status-amber {
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
}

/* Oracle icon flashing when READY FOR ORACLE.

   Both stops come from the theme boot now. The peak used to be written here as
   brightness * 1.33, hue-rotate(8deg), saturate(8) -- three numbers that are
   right on the tube and wrong on paper, where the ink is dark rather than
   bright and 8deg is reached by walking the wheel backwards through green. The
   arithmetic and the reasoning for all three live next to the curves they
   belong to, in _theme_boot.html; the fallbacks here are dark mode's, for the
   one case where the boot script has not run. */
@keyframes oracleIconFlash {
  0%, 100% {
    filter: grayscale(1) brightness(var(--icon-brightness, 1.5)) sepia(1)
            hue-rotate(var(--icon-hue-rotate, 112deg))
            saturate(var(--icon-saturate, 2.2));
  }
  50% {
    filter: grayscale(1) brightness(var(--icon-amber-brightness, 1.995)) sepia(1)
            hue-rotate(var(--icon-amber-hue, 8deg))
            saturate(var(--icon-amber-saturate, 8));
  }
}
#detail-oracle.oracle-ready .panel-header-icon,
#detail-oracle.oracle-ready #oracle-panel-strip .strip-icon {
  animation: oracleIconFlash 0.7s ease-in-out infinite;
}

#oracle-panel-strip .strip-icon {
  width: 75px;
  height: 75px;
}

/* While the oracle waits, the strip's arrow is put away: the flashing icon
   is the way in (grExpandOracleByHand). */
#detail-oracle.oracle-ready #oracle-panel-strip .strip-expand-btn { visibility: hidden; }
/* Under the flashing icon: the price, and what is left (grOracleQuote). */
.oracle-strip-cost {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  margin-top: 6px;
  font-family: 'Share Tech Mono', monospace;
  text-align: center;
  line-height: 1.2;
}
.oracle-strip-cost[hidden],
#detail-oracle:not(.oracle-ready) .oracle-strip-cost { display: none; }
.oracle-strip-cost .osc-price {
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
}
.oracle-strip-cost .osc-left { font-size: 10.5px; letter-spacing: 1px; color: var(--phosphor-dim); }
.oracle-strip-cost.is-short .osc-left { color: var(--amber); text-shadow: 0 0 6px rgba(var(--halo-amber-rgb), 0.4); }
/* The phone's floating channel icon carries the same, under it, while the
   icon is there. */
.gr-channel-cost { margin-top: 2px; }
#draw-controls:not(.has-channel) .gr-channel-cost { display: none; }

/* --oracle-fs is the pane's reading size, set on #oracle-panel-content by the
   header's − / + (grOracleFontAdj). Everything the pane shows sizes from it --
   the reading, and the log of clarifiers and follow-ups beneath it, whose
   smaller chrome keeps its proportion as a fraction of it -- so one press
   moves the whole pane, including entries added after the press. */
#oracle-stream-text, #oracle-static-text {
  user-select: text;
  font-size: var(--oracle-fs, 16px);
  color: var(--phosphor-dim);
  line-height: 1.75;
  white-space: pre-wrap;
}

#oracle-stream-text {
  color: var(--phosphor);
}

/* The oracle wait indicator (oracle-wait.js), shared by the canvas and the guest
   landing. It stands in the pane where the reading will appear, so it is sized
   like the reading but coloured amber — the terminal's second channel, used for
   what the machine is doing rather than what it has said. The reading arrives in
   phosphor, so the swap reads as an arrival. */
.oracle-wait {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5em;
  /* Inherited, not pinned: the landing's oracle pane steps 16 / 15 / 14px down
     the breakpoints, and the indicator has to sit at the size of the sentence
     that will replace it or the swap reads as a jump. */
  font-size: inherit;
  line-height: inherit;
  letter-spacing: 0.08em;
  color: var(--amber);
}

/* In the app the host is the stream *wrapper*, which inherits panel chrome
   sizing rather than the reading's, so match #oracle-stream-text explicitly. */
#oracle-stream-area .oracle-wait {
  font-size: var(--oracle-fs, 16px);
  line-height: 1.75;
}

.oracle-wait-glyph {
  font-style: normal;
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
  /* The glyphs are drawn from several scripts and are not one width; a fixed box
     keeps the word beside it from jittering left and right as they cycle. */
  display: inline-block;
  width: 1.2em;
  text-align: center;
}

.oracle-wait-word {
  font-weight: normal;
}

#detail-header-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Room for the gear, which sits over this bar's right end (absolutely
     placed, z 103), so the title wraps before it rather than running under
     it. The photon readout that used to sit beside it is in the footer. */
  padding-right: 56px;
}

.detail-meta {
  font-size: 18px;
  font-weight: bold;
  font-style: italic;
  color: var(--amber);
  letter-spacing: 1px;
  padding: 6px 14px 8px;
  border-bottom: 1px solid var(--phosphor-faint);
  white-space: normal;
  overflow: visible;
  text-align: center;
  flex-shrink: 0;
  user-select: text;
}


/* ── Grimoire: full-window entrainment overlay ──────────────────────── */

#gr-entrainment-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  /* The static runs up to a cell past the edge (makeNoiseField, measure). */
  overflow: hidden;
  background: var(--bg);
  cursor: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 1.5s ease;
}

#gr-entrainment-overlay.entrainment-active {
  opacity: 1;
  pointer-events: auto;
}

/* Standalone MM play: sit below sidebar (z-index 102) so sidebar stays reachable */
#gr-entrainment-overlay.mm-standalone {
  z-index: 101;
}

/* Hide draw-specific elements in standalone MM mode */
#gr-entrainment-overlay.mm-standalone #canvas-focus-text,
#gr-entrainment-overlay.mm-standalone #entrainment-word,
#gr-entrainment-overlay.mm-standalone #entrainment-sigil {
  display: none !important;
}

#gr-entrainment-overlay.entrainment-active #canvas-focus-text {
  opacity: 1;
  transition: opacity 0.4s ease;
}

#gr-entrainment-overlay #static-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 1;
  image-rendering: pixelated;
  filter: blur(0.6px);
}


/* ── Grimoire: entrainment fullscreen button ─────────────────────────── */

#gr-fullscreen-btn {
  position: absolute;
  bottom: 24px;
  right: 24px;
  z-index: 30;
  width: 56px;
  height: 56px;
  background: var(--bg);
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.2s, color 0.2s, box-shadow 0.2s;
  /* Four-corner fullscreen icon via box-shadow inset */
}

#gr-fullscreen-btn:hover {
  border-color: var(--phosphor);
  color: var(--phosphor);
  box-shadow: 0 0 12px var(--phosphor-glow);
}

/* Corner-bracket icon drawn with pseudo-elements */
#gr-fullscreen-btn::before,
#gr-fullscreen-btn::after {
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
  border-color: currentColor;
  border-style: solid;
}

#gr-fullscreen-btn::before {
  top: 8px;
  left: 8px;
  border-width: 2px 0 0 2px;
}

#gr-fullscreen-btn::after {
  bottom: 8px;
  right: 8px;
  border-width: 0 2px 2px 0;
}

/* Exit-fullscreen state: inward-pointing corners */
#gr-fullscreen-btn.is-fullscreen::before {
  top: 12px;
  left: 12px;
  border-width: 0 2px 2px 0;
}

#gr-fullscreen-btn.is-fullscreen::after {
  bottom: 12px;
  right: 12px;
  border-width: 2px 0 0 2px;
}

/* ── Mobile header ───────────────────────────────────────────────── */

#mobile-header { display: none; }

/* ── Footer on mobile ──────────────────────────────────────────────── */
/* The member pages' footer gives way to the bottom bar on a phone. Every
   other page keeps the shared footer (_footer.html), in smaller type. */
@media (max-width: 767px) {
  #footer { display: none !important; }
  #footer.site-footer:not(.footer-member) { display: grid !important; padding: 4px 12px; }
  .site-footer .footer-copyright,
  .site-footer .footer-links,
  .site-footer .footer-status,
  .site-footer .qrng-ticker { font-size: 11px; letter-spacing: 1px; }
  .site-footer .footer-sep { margin: 0 5px; }
  .site-footer .ft-cookie { margin-left: 6px; }
  .site-footer .ft-cookie svg { width: 12px; height: 12px; }
}

/* ── Mobile FOCUS/DRAW button ──────────────────────────────────────── */

/* On mobile: hide in-card draw buttons (use bottom bar instead) */
@media (max-width: 767px) {
  .card-draw-wrap  { display: none !important; }

  /* Slim fixed-height UI elements to maximise card stage space */
  .panel-header   { padding: 5px 10px !important; }

  .detail-meta { padding: 2px 10px 3px !important; }
  /* The ghost card is the draw target on both contexts — hide the bottom bar's
     draw button.
     Zero out draw-controls padding/border so the empty container is invisible;
     it gains its appearance back only when the CHANNEL ORACLE button is visible. */
  #gr-draw-btn   { display: none !important; }
  #draw-controls { padding: 0 !important; border-bottom: none !important; }

  /* The oracle icon, once every card is down, floats on the cloth (owner,
     2026-10-04). It used to stand in a bar of its own below the stage -- a
     rule above it, 24px of foot, the height taken from the stage -- and read
     as a toolbar. Now the bar is laid over the foot of the stage, with no
     rule and no ground, so the icon is part of the canvas: the stage keeps
     its full height, and the cards are fitted above the icon instead
     (visibleCardBand, terminal.js), as they are above the oracle sheet. It
     stands on the bottom bar's row, so a raised bar lifts it with it; the
     bottom bar's lip already keeps it off the device's own strip. The sheet
     appears only once the icon is tapped.

     Carried by a class because an empty bar must take no room at all: for
     the length of a draw it is not there, and the icon appears only once
     every card is down. */
  #ws-reading-detail { position: relative; }
  #draw-controls.has-channel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: var(--tb-row-now, 0px);
    padding: 0 0 8px !important;
    border-top: none !important;
    background: none;
    /* No z-index: one would make the bar a layer of its own, and the icon's
       screen blend (--blend-emit) would mix with that empty layer instead of
       the cloth -- its black ground showing as a box. Later in the page than
       the stage, it paints over it without one. */
    pointer-events: none;
    transition: bottom 0.45s ease;
  }
  #draw-controls.has-channel .channel-oracle-btn { pointer-events: auto; }
}

/* On desktop: hide the bottom-bar draw/channel controls — the card itself is
   the draw target on both contexts now. */
@media (min-width: 768px) {
  #draw-controls { display: none !important; }
}

/* Mobile FOCUS/DRAW button container - visible only on mobile */
#mobile-draw-container {
  display: none !important;
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
}

/* Show mobile button only on mobile screens */
@media (max-width: 767px) {
  #mobile-draw-container { display: block !important; }
}

/* Larger button for mobile - increased touch target size */
#mobile-draw-container .mobile-draw-btn {
  width: auto !important;
  margin: 0 !important;
  padding: 22px 56px !important;
  font-size: 48px !important;
  letter-spacing: 4px !important;
  white-space: nowrap;
  border-radius: 8px;
  background: var(--bg) !important;
  border-color: var(--phosphor) !important;
  color: var(--phosphor) !important;
  text-shadow: 0 0 12px var(--phosphor-glow) !important;
  box-shadow: 0 0 14px var(--phosphor-glow) !important;
}

/* Active mobile button state (entrainment ready) */
.mobile-draw-btn.draw-ready {
  box-shadow: 0 0 14px var(--halo), 0 0 4px var(--halo);
  color: var(--bg) !important;
  background: var(--phosphor) !important;
}

/* CHANNEL ORACLE button — flashes phosphor/amber after last card is drawn */
@keyframes oracleFlash {
  0%   { background: var(--phosphor); color: var(--bg); border-color: var(--phosphor); box-shadow: 0 0 14px var(--halo); }
  50%  { background: var(--amber);    color: var(--bg); border-color: var(--amber);    box-shadow: 0 0 14px var(--halo-amber); }
  100% { background: var(--phosphor); color: var(--bg); border-color: var(--phosphor); box-shadow: 0 0 14px var(--halo); }
}

/* The oracle mark, not a slab of words. A full-width phosphor button read as a
   form control on a screen that has none, and spent 52px of card height saying
   what the icon says silently. The button is only the tap target now — 64px of
   icon plus its padding clears the 44px minimum comfortably. */
.channel-oracle-btn {
  display: block;
  width: auto !important;
  margin: 5px auto 7px !important;
  padding: 2px !important;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  animation: none !important;
  line-height: 0;
  cursor: pointer;
}

.channel-oracle-icon {
  display: block;
  width: 64px;
  height: 64px;
  /* The phosphor treatment every panel icon gets, and the flash the oracle panel
     uses when it is ready — one signal, one animation, wherever it appears. */
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
  animation: oracleIconFlash 0.7s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .channel-oracle-icon { animation: none; }
}

/* ── Ghost card: ripple canvas + tutorial hint (mobile touch-draw) ────── */

/* Full-bleed canvas overlay inside .card-frame — draws ripple rings */
.gr-ripple-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
}

/* Tutorial "HOLD · RELEASE TO DRAW" hint — fades in during tutorial playback */
.gr-touch-hint {
  position: absolute;
  bottom: 14%;
  left: 0;
  right: 0;
  padding: 0 8px;
  text-align: center;
  /* Share Tech Mono, not VT323: at 11px the pixel face's small x-height
     blurs, and this is read at a glance mid-gesture (owner, 2026-10-03). It
     matches the coach for moving the spread (#view-coach).
     Sized against the card, not the viewport. The card is 120px wide in canvas
     coordinates and the whole canvas is scaled up from there, so a vw-based
     size would scale twice and the longest line would run off the frame. The
     face is ~45% wider than VT323: at 11px on a half-pixel track a line has
     the 104px the padding leaves, about 16 characters -- the hint's own line
     breaks are chosen to stay under it (grimoire-cards.js). */
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.5px;
  color: var(--phosphor-dim);
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  transition: opacity 0.5s ease;
  line-height: 1.35;
}

.gr-touch-hint.visible {
  opacity: 0.85;
}

/* Responsive label helpers */
@media (min-width: 768px) { .mobile-label { display: none; } }
@media (max-width: 767px) { .desktop-label { display: none; } }

/* Tutorial hint is mobile-only */
@media (min-width: 768px) {
  .gr-touch-hint { display: none; }
}

/* Oracle streaming header */
.oracle-stream-header {
  position: relative;
  font-family: 'VT323', monospace;
  font-size: 30px;
  color: var(--phosphor);
  letter-spacing: 4px;
  text-shadow: 0 0 12px var(--phosphor-glow);
  white-space: nowrap;
  text-align: center;
  padding: 8px 0 4px;
}

.oracle-stream-header .streaming-indicator {
  display: inline-block;
  width: 4px;
  height: 24px;
  background: var(--phosphor);
  margin-left: 8px;
  animation: blink 1s step-end infinite;
}

/* Oracle streaming container (mobile) */
.oracle-stream-container {
  display: none;
  position: absolute;
  top: 120px;
  left: 50%;
  transform: translateX(-50%);
  width: 90%;
  max-width: 600px;
  z-index: 25;
  text-align: center;
}

.oracle-stream-container.visible {
  display: block;
}

.oracle-stream-text {
  margin-top: 8px;
  font-size: 18px;
  line-height: 1.6;
  color: var(--phosphor-dim);
  text-shadow: 0 0 4px var(--phosphor-glow);
  background-image: linear-gradient(rgba(var(--bg-rgb), 0.92) 0%, rgba(var(--bg-rgb), 0.92) 100%), url('/static/img/pyramid.gif');
  background-color: var(--bg-panel);
  background-blend-mode: multiply, normal;
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
  padding: 12px;
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 0 12px var(--phosphor-glow);
  min-height: 150px;
  overflow-y: auto;
}

.oracle-stream-cursor {
  display: none;
  animation: blink 0.8s step-end infinite;
  font-size: 20px;
  vertical-align: middle;
  color: var(--phosphor);
  background: var(--phosphor);
}

.oracle-stream-container.visible .oracle-stream-cursor {
  display: inline-block;
}

/* ── Mobile sidebar drawer ─────────────────────────────────────────── */

#gr-sidebar-backdrop   {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 199;
  background: rgba(var(--scrim), 0.55);
}

@media (max-width: 767px) {
  #header        { display: none; }
  #cursor        { display: none !important; }

  /* No rule beneath it and no ground of its own (owner, 2026-10-04): the
     header stands on the screen's own static, as the page under it does,
     rather than as a bar laid across the top. */
  #mobile-header {
    display: flex;
    align-items: center;
    padding: 0 4px;
    height: 44px;
    flex-shrink: 0;
    background: transparent;
    z-index: 10;
  }

  .mob-left, .mob-right { flex: 1; }
  .mob-right             { text-align: right; }

  /* The mark itself, small (.mob-logo .psibyl-mark). It replaced a VT323
     PSI/BYL text wordmark, whose `.mob-logo span` rule is gone with it: it
     would have painted every span in the mark amber, P, S and I included. */
  .mob-logo {
    display: flex;
    justify-content: center;
    align-items: center;
  }

  #main {
    grid-template-columns: 1fr !important;
  }

  #left-panel {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    width: 260px;
    z-index: 200;
    transform: translateX(-100%);
    transition: transform 0.25s ease;
  }

  #main.sidebar-open #left-panel {
    transform: translateX(0);
    box-shadow: 4px 0 24px rgba(var(--shade), 0.7);
  }

  #zoom-widget { display: none; }

  /* Mobile icon buttons (hamburger, gear, wand) */
  .mob-icon-btn {
    background: transparent;
    border: none;
    color: var(--phosphor);
    cursor: pointer;
    padding: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-shadow: 0 0 8px var(--halo);
    filter: drop-shadow(0 0 4px var(--halo));
    -webkit-tap-highlight-color: transparent;
    line-height: 1;
  }

  /* Image icons inside mobile top-bar buttons — constrain to fit the 44px header */
  .mob-icon-btn .strip-icon {
    width: 24px;
    height: 24px;
    object-fit: contain;
  }

  /* The menu digram (_icon_menu.html): greater yang, two whole lines, on the
     light theme; greater yin, two broken lines, on the dark one -- the icon
     follows the theme's own yin and yang. */
  .menu-digram { display: block; }
  .menu-digram .dg-yang { display: none; }
  :root[data-theme="light"] .menu-digram .dg-yang { display: inline; }
  :root[data-theme="light"] .menu-digram .dg-yin  { display: none; }

  /* New Revelation's plus, at the size the gear beside it takes. */
  .mob-icon-btn .nav-plus {
    width: 24px;
    height: 24px;
  }

  /* Hide desktop reading header on mobile — buttons are in mobile top bar */
  #detail-header-bar { display: none !important; }

  /* Intent/question bar — legible size, word-wrap instead of truncate, and the
     oracle sheet's gesture mirrored: a pull on its bottom edge opens it to the
     whole question or closes it away entirely.
     
     The sizing lives here rather than with the other mobile rules above because
     this block already owns the bar's mobile appearance and does it with
     !important — an `overflow: visible !important` here is what let a long
     question paint straight over the cards once the bar was given a height. */
  .detail-meta {
    padding: 5px 10px 12px !important;   /* the last 12px is the handle's room */
    font-size: 16px !important;
    color: var(--phosphor) !important;
    white-space: normal !important;
    text-overflow: unset !important;
    /* Clip and scroll, so what the bar cannot show it keeps to itself. */
    overflow-x: hidden !important;
    overflow-y: auto !important;
    overscroll-behavior: contain;
    height: var(--intent-h, auto);
    max-height: 50vh;
    /* The grab handle, drawn rather than added to the markup, and with the
       default background-attachment so it stays put while the text scrolls. */
    background-image: linear-gradient(var(--phosphor-faint), var(--phosphor-faint));
    background-repeat: no-repeat;
    background-size: 34px 3px;
    background-position: center calc(100% - 4px);
    cursor: grab;
  }
  .detail-meta.intent-dragging { cursor: grabbing; }

  /* Closed means closed. overflow clips at the padding box, not the content box,
     so a bar squeezed to its minimum still paints a line of the question through
     its own bottom padding — the padding that exists to keep the grab handle off
     the text. Hiding the ink is what actually empties the bar, and it keeps the
     strip tall enough to press. */
  .detail-meta.intent-closed { color: transparent !important; }
}

/* ── The stage under a finger ─────────────────────────────────────────
   One finger pans the canvas and two pinch it, on every spread and at every
   width (bindCanvasControls, terminal.js), so the page itself must neither
   scroll nor zoom under them. */
#card-stage { touch-action: none; }

/* Phone only: the recentre button, top right of the stage -- the corner the
   Mind Machine switch leaves free, since that is hidden on a phone. Drawn like
   the zoom widget's buttons, which do this job on a larger screen.

   Under the oracle sheet (z-index 40), not over it: a sheet raised to the top
   covers the stage, and the button belongs to the stage (owner, 2026-10-03).
   It was 50, and stood on the sheet's header. Still above the cards, which
   paint at the canvas's own level. Freestyle's Notes sheet covers it the
   same way, from the other side (#fs-notes, 70). */
#mob-recentre, .view-coach { display: none; }
@media (max-width: 767px) {
  #mob-recentre {
    display: flex;
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 30;
    width: 36px;
    height: 36px;
    align-items: center;
    justify-content: center;
    padding: 0;
    /* The glyph alone, straight on the cloth (owner, 2026-10-03): no panel,
       no frame. The 36px stays as the target. Round, with a border that is
       there but clear, so the one time it is drawn -- the flash below, which
       lights the border and a glow -- it rings the glyph rather than boxing
       it. The halo keeps it legible over a card. */
    background: none;
    border: 1px solid transparent;
    border-radius: 50%;
    color: var(--phosphor);
    text-shadow: 0 0 4px var(--bg), 0 0 8px var(--phosphor-glow);
    font-family: 'VT323', monospace;
    font-size: 27px;
    line-height: 1;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }
  #mob-recentre:active { color: var(--phosphor); text-shadow: 0 0 10px var(--phosphor-glow-strong); }
  /* The coach's closing step points at it (grimoire.js, grViewCoachStep): the
     flashing-button swing every waiting control uses (beginReady), here a
     ring round the glyph. Still for anyone who has asked for less motion,
     and amber instead, so it is still singled out. */
  #mob-recentre.vc-flash { animation: beginReady 0.7s ease-in-out infinite; }
  @media (prefers-reduced-motion: reduce) {
    #mob-recentre.vc-flash { animation: none; color: var(--amber); border-color: var(--amber); }
  }

  /* Two coaches share these rules by their class, .view-coach: #view-coach for
     moving a finished spread, #fs-coach for moving cards on a Freestyle table
     (grimoire-freestyle.js, fsCoach*). */
  /* The coach for moving a finished spread (grimoire.js, grViewCoach*):
     pinch, drag, twist, each done on the spread itself. A card centred in the
     band the cards have (placed by the script), at the stage's level so a
     raised sheet covers it as it covers the stage's buttons. It takes no
     touches but its buttons: the gesture it asks for starts on the spread
     beneath it and has to reach the stage. */
  .view-coach {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    position: absolute;
    left: 50%;
    top: var(--vc-top, 50%);
    transform: translate(-50%, -50%);
    /* Sized to be read at arm's length (owner, 2026-10-03): 16px lines, 14px
       buttons, a 180px illustration. The longest line, TAP ⊙ TO RESET THE
       VIEW, still fits on one at 320px wide. */
    width: min(330px, calc(100% - 24px));
    padding: 14px 16px 12px;
    z-index: 30;
    background: rgba(var(--bg-rgb), 0.95);
    border: 1px solid var(--phosphor-faint);
    box-shadow: 0 0 18px var(--phosphor-glow);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.45s ease;
  }
  .view-coach[hidden] { display: none; }
  .view-coach.is-shown { opacity: 1; }
  .view-coach button { pointer-events: auto; }
  .vc-count {
    align-self: flex-end;
    font-size: 12px;
    letter-spacing: 2px;
    color: var(--phosphor-dim);
    margin-bottom: -12px;
  }
  .vc-art { width: 180px; height: 108px; display: none; overflow: visible; }
  .vc-text {
    display: none;
    font-family: 'Share Tech Mono', monospace;
    font-size: 16px;
    letter-spacing: 2px;
    color: var(--phosphor);
    text-shadow: 0 0 6px var(--phosphor-glow);
    text-align: center;
  }
  .view-coach[data-step="1"] .vc-art-1, .view-coach[data-step="1"] .vc-text-1,
  .view-coach[data-step="2"] .vc-art-2, .view-coach[data-step="2"] .vc-text-2,
  .view-coach[data-step="3"] .vc-art-3, .view-coach[data-step="3"] .vc-text-3,
  .view-coach[data-step="4"] .vc-art-4, .view-coach[data-step="4"] .vc-text-4 { display: block; }
  .view-coach[data-step="4"] .vc-count,
  .view-coach[data-step="4"] .vc-skip { visibility: hidden; }
  /* A step done: its line lights, then the next one comes. */
  .view-coach.is-done .vc-text { color: var(--amber); text-shadow: 0 0 8px var(--halo-amber); }
  .vc-actions { display: flex; justify-content: space-between; width: 100%; margin-top: 4px; }
  .vc-actions button {
    background: none;
    border: 1px solid transparent;
    color: var(--phosphor-dim);
    font-family: inherit;
    font-size: 14px;
    letter-spacing: 2px;
    padding: 8px 12px;
    min-height: 44px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }
  .vc-actions .vc-next { color: var(--phosphor); border-color: var(--phosphor-faint); }
  .vc-actions button:active { color: var(--phosphor); border-color: var(--phosphor); }

  /* The illustrations. The fingertip is the ghost card's phantom touch
     (GhostRipple, grimoire-cards.js) drawn in SVG: a lit dot in a soft
     pulsing ring, with a ripple where it lands. The card outline does what
     the gesture does to the spread. One 3s loop: land, move, hold, lift,
     and the card eases back while no finger is on it. */
  .vc-art .vc-card {
    fill: var(--wash-faint, transparent);
    stroke: var(--phosphor-dim);
    stroke-width: 1.2;
    transform-box: view-box;
    transform-origin: 70px 42px;
  }
  .vc-art .vc-dot   { fill: var(--phosphor); filter: drop-shadow(0 0 3px var(--phosphor-glow-strong)); }
  .vc-art .vc-halo  { fill: none; stroke: var(--phosphor); stroke-width: 1.4;
                      animation: vcHalo 0.9s ease-in-out infinite alternate; }
  .vc-art .vc-ripple { fill: none; stroke: var(--phosphor); stroke-width: 1;
                       transform-box: fill-box; transform-origin: center;
                       animation: vcRipple 3s ease-out infinite; }
  .vc-art .vc-tip   { transform-box: view-box; transform-origin: 70px 42px; }

  .vc-pinch-a     { animation: vcPinchA 3s ease-in-out infinite; }
  .vc-pinch-b     { animation: vcPinchB 3s ease-in-out infinite; }
  .vc-card-pinch  { animation: vcCardPinch 3s ease-in-out infinite; }
  .vc-drag        { animation: vcDrag 3s ease-in-out infinite; }
  .vc-card-drag   { animation: vcCardDrag 3s ease-in-out infinite; }
  .vc-twist-a,
  .vc-twist-b     { animation: vcTwist 3s ease-in-out infinite; }
  .vc-card-twist  { animation: vcCardTwist 3s ease-in-out infinite; }
  .vc-card-home   { animation: vcCardHome 3s ease-in-out infinite; }
  /* Freestyle's coach (#fs-coach): a held card lifts and lights; a lit card
     is the one picked up, which moves, and wears its turn handle, which a
     fingertip carries round the card's centre as the card turns. */
  .vc-art .vc-card-lit { stroke: var(--phosphor); filter: drop-shadow(0 0 3px var(--phosphor-glow-strong)); }
  .vc-hold        { animation: vcHold 3s ease-in-out infinite; }
  .vc-card-lift   { animation: vcCardLift 3s ease-in-out infinite; }
  .vc-art .vc-turn { transform-box: view-box; transform-origin: 70px 42px;
                     animation: vcCardTwist 3s ease-in-out infinite; }
  .vc-art .vc-handle circle { fill: var(--bg-panel); stroke: var(--phosphor); stroke-width: 1.1; }
  .vc-art .vc-handle path   { fill: none; stroke: var(--phosphor); stroke-width: 1.1;
                              stroke-linecap: round; stroke-linejoin: round; }

  @keyframes vcHalo   { from { opacity: 0.25; } to { opacity: 0.6; } }
  @keyframes vcRipple {
    0%, 3%  { opacity: 0;   transform: scale(1); }
    5%      { opacity: 0.8; transform: scale(1); }
    30%     { opacity: 0;   transform: scale(3.4); }
    100%    { opacity: 0;   transform: scale(3.4); }
  }
  /* Fingers: absent, land (to 10%), move (12-58%), hold, lift (72-80%). */
  @keyframes vcPinchA {
    0%, 4%   { opacity: 0; transform: translate(0, 0); }
    10%, 12% { opacity: 1; transform: translate(0, 0); }
    58%, 72% { opacity: 1; transform: translate(-12px, 9px); }
    80%, 100%{ opacity: 0; transform: translate(-12px, 9px); }
  }
  @keyframes vcPinchB {
    0%, 4%   { opacity: 0; transform: translate(0, 0); }
    10%, 12% { opacity: 1; transform: translate(0, 0); }
    58%, 72% { opacity: 1; transform: translate(12px, -9px); }
    80%, 100%{ opacity: 0; transform: translate(12px, -9px); }
  }
  @keyframes vcCardPinch {
    0%, 12%  { transform: scale(0.75); }
    58%, 80% { transform: scale(1.25); }
    96%, 100%{ transform: scale(0.75); }
  }
  @keyframes vcDrag {
    0%, 4%   { opacity: 0; transform: translate(-32px, 0); }
    10%, 12% { opacity: 1; transform: translate(-32px, 0); }
    58%, 72% { opacity: 1; transform: translate(32px, 0); }
    80%, 100%{ opacity: 0; transform: translate(32px, 0); }
  }
  @keyframes vcCardDrag {
    0%, 12%  { transform: translate(-32px, 0); }
    58%, 80% { transform: translate(32px, 0); }
    96%, 100%{ transform: translate(-32px, 0); }
  }
  @keyframes vcTwist {
    0%, 4%   { opacity: 0; transform: rotate(0deg); }
    10%, 12% { opacity: 1; transform: rotate(0deg); }
    58%, 72% { opacity: 1; transform: rotate(60deg); }
    80%, 100%{ opacity: 0; transform: rotate(60deg); }
  }
  @keyframes vcCardTwist {
    0%, 12%  { transform: rotate(0deg); }
    58%, 80% { transform: rotate(60deg); }
    96%, 100%{ transform: rotate(0deg); }
  }
  /* The closing note: a card off-centre, turned and enlarged, settling back
     upright and home -- what the recentre button does. */
  @keyframes vcCardHome {
    0%       { transform: translate(20px, -4px) rotate(40deg) scale(1.2); opacity: 0; }
    10%, 20% { transform: translate(20px, -4px) rotate(40deg) scale(1.2); opacity: 1; }
    60%, 85% { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
    95%, 100%{ transform: translate(0, 0) rotate(0deg) scale(1); opacity: 0; }
  }
  /* A finger lands and stays; at the hold's length the card lifts, and lies
     down again once the finger has gone. */
  @keyframes vcHold {
    0%, 4%   { opacity: 0; }
    10%, 72% { opacity: 1; }
    80%, 100%{ opacity: 0; }
  }
  @keyframes vcCardLift {
    0%, 40%  { transform: scale(1);    stroke: var(--phosphor-dim); }
    48%, 84% { transform: scale(1.12); stroke: var(--phosphor); }
    96%, 100%{ transform: scale(1);    stroke: var(--phosphor-dim); }
  }
  /* The coach for resizing the oracle (grimoire.js, grSheetCoach*): a child
     of the sheet, standing above its top line and centred on the ring, so it
     rides with the sheet as it moves. Inside the sheet, under the ring, once
     the sheet stands too high for it to fit above (.is-inside). Above the
     sheet's own content, which it covers while it is up. */
  .sheet-coach {
    top: auto;
    bottom: calc(100% + 24px);
    transform: translateX(-50%);
    z-index: 3;
  }
  .sheet-coach.is-inside { bottom: auto; top: 26px; }
  .sc-sub {
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    letter-spacing: 1.5px;
    color: var(--phosphor-dim);
    text-align: center;
    margin-top: -4px;
  }
  .sc-actions { justify-content: flex-end; }
  .vc-art .sc-panel { fill: var(--bg-panel); stroke: none; }
  .vc-art .sc-bar   { fill: var(--phosphor-faint); stroke: none; }
  .vc-art .sc-edge  { stroke: var(--phosphor-dim); stroke-width: 1.2; }
  .vc-art .sc-ring circle { fill: var(--bg-panel); stroke: var(--phosphor); stroke-width: 1.1; }
  .vc-art .sc-ring path   { fill: none; stroke: var(--phosphor); stroke-width: 1.2;
                            stroke-linecap: round; stroke-linejoin: round; }
  .vc-art .sc-sheet { animation: scSheet 3.6s ease-in-out infinite; }
  .vc-art .sc-tip   { animation: scTip 3.6s ease-in-out infinite; }
  /* The bar the coach points at, lit while it is up: the waiting control's
     swing (beginReady) on the header, and its ring. Amber and still for less
     motion. */
  #detail-oracle.sheet-coached .sheet-grip { border-radius: 50%; animation: beginReady 0.7s ease-in-out infinite; }
  /* On the header, laid over it rather than on it: the swing passes through a
     clear ground, and the header must stay opaque over the reading. */
  #detail-oracle.sheet-coached .panel-header::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px solid transparent;
    pointer-events: none;
    animation: beginReady 0.7s ease-in-out infinite;
  }
  /* The sheet rises over the card (to 40%), holds, and falls back (to 75%). */
  @keyframes scSheet {
    0%, 12%  { transform: translateY(8px); }
    38%, 50% { transform: translateY(-26px); }
    74%, 100%{ transform: translateY(8px); }
  }
  /* The fingertip lands on the ring, rides it up and down, and lifts. */
  @keyframes scTip {
    0%, 4%   { opacity: 0; }
    10%, 78% { opacity: 1; }
    86%, 100%{ opacity: 0; }
  }

  /* The coach for the bottom bar's grip (grimoire.js, grNavCoach*): fixed
     just above the grip, over the oracle sheet if it is up there. The grip's
     sign glows while it is up: the waiting control's swing (beginReady). */
  .nav-coach {
    position: fixed;
    top: auto;
    bottom: var(--nc-bottom, 72px);
    transform: translateX(-50%);
    z-index: 60;
  }
  html.tb-coached .tb-sigil { border-radius: 50%; animation: beginReady 0.7s ease-in-out infinite; }
  .vc-art .nc-sigil > *, .vc-art .nc-key {
    fill: none;
    stroke: var(--phosphor);
    stroke-width: 1.1;
    vector-effect: non-scaling-stroke;
  }
  .vc-art .nc-sigil { filter: drop-shadow(0 0 3px var(--phosphor-glow-strong)); }
  .vc-art .nc-tip  { animation: ncTip 3.4s ease-in-out infinite; }
  .vc-art .nc-tip .vc-ripple { animation-duration: 3.4s; }
  .vc-art .nc-key  { transform-box: fill-box; transform-origin: center;
                     animation: ncKey 3.4s ease-out infinite; }
  .vc-art .nc-key-b { animation-delay: 0.08s; }
  .vc-art .nc-key-c { animation-delay: 0.16s; }
  /* A fingertip lands on the sign (to 10%) and lifts (to 26%). */
  @keyframes ncTip {
    0%, 4%   { opacity: 0; }
    10%, 20% { opacity: 1; }
    26%, 100%{ opacity: 0; }
  }
  /* Then the keys rise out of the sign, stand, and fade for the next loop. */
  @keyframes ncKey {
    0%, 22%  { opacity: 0; transform: translateY(26px) scale(0.4); }
    40%, 84% { opacity: 1; transform: translateY(0) scale(1); }
    96%, 100%{ opacity: 0; transform: translateY(0) scale(1); }
  }

  @media (prefers-reduced-motion: reduce) {
    html.tb-coached .tb-sigil { animation: none; color: var(--amber); }
    .vc-art .nc-tip { opacity: 0; }
    #detail-oracle.sheet-coached .sheet-grip { animation: none; color: var(--amber); }
    #detail-oracle.sheet-coached .panel-header::after { animation: none; border-color: var(--amber); }
    .vc-art .sc-sheet { transform: translateY(-26px); }
    .vc-art * { animation: none !important; }
    .vc-pinch-a     { transform: translate(-12px, 9px); }
    .vc-pinch-b     { transform: translate(12px, -9px); }
    .vc-card-pinch  { transform: scale(1.25); }
    .vc-drag, .vc-card-drag { transform: translate(32px, 0); }
    .vc-twist-a, .vc-twist-b, .vc-card-twist, .vc-art .vc-turn { transform: rotate(60deg); }
    .vc-card-lift   { transform: scale(1.12); stroke: var(--phosphor); }
    .vc-art .vc-ripple { opacity: 0; }
    .vc-art .vc-halo { opacity: 0.45; }
  }
}

/* Workspace settings button is redundant on mobile — settings lives in the top bar */
@media (max-width: 767px) {
  #ws-settings-btn { display: none !important; }
}

/* Oracle pane: expand width is set via inline style by grExpandOracle() (desktop only) */
#detail-oracle.oracle-expanded {
  min-width: 260px;
}

/* Oracle pane on mobile: a sheet over the cards, not a row beneath them.
   Fixed rather than in flow, because past a point the sheet has to be able to
   cover the cards — a flex row can only ever squeeze them. Its height is one
   custom property, which is what the drag on its header moves. */
@media (max-width: 767px) {
  #detail-content-row {
    flex-direction: column;
  }
  #detail-oracle {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    /* !important because the desktop width is restored as an inline style on
       load (initOracleResize in grimoire.js) and set again by grExpandOracle,
       neither of which knows about the viewport. Inline beats a plain rule, so
       a phone inherited whatever width the oracle was last dragged to on a
       desktop -- left:0 with an explicit width put the right edge off-screen,
       and `position: fixed` means the page will not scroll to reach it.
       Left as a CSS override rather than a guard in the JS so that a rotation
       into landscape still hands the saved width back, with no reload. */
    width: auto !important;
    height: var(--oracle-h, 45vh);
    max-height: none;
    border-left: none;
    border-top: 1px solid var(--phosphor-faint);
    flex-shrink: 0;
    z-index: 40;
    /* The panel itself never scrolls: the header is pinned by being a flex item
       that does not grow, and the reading scrolls inside the body below it. */
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transition: height 0.25s ease;
  }

  /* A drag must track the finger, not ease behind it. */
  #detail-oracle.sheet-dragging { transition: none; }

  #oracle-panel-content {
    overflow: hidden;
  }

  #detail-oracle .panel-header {
    flex: 0 0 auto;
    /* Opaque: the reading scrolls underneath it. */
    background: var(--bg-panel);
    /* The vertical drag belongs to the sheet, so the browser must not spend it
       on scrolling the page first. */
    touch-action: none;
    cursor: grab;
  }
  #detail-oracle.sheet-dragging .panel-header { cursor: grabbing; }

  /* The grab affordance: the header is a handle, and nothing else on this
     screen is, so it has to say so. A small ring astride the sheet's top
     line, centred on it: the caret up above the line, the caret down below
     it, each pointing the way the sheet goes (owner, 2026-10-03). It was a
     34px bar inside the header, which said "handle" without saying which way.
     30px across: its lower half is the 15px above the header's title, which
     starts 24px down. Filled with the panel's ground so the line does not run through it. It
     drags the sheet like the header (grSheetInit), and its hit area is wider
     than the ring, as the clarifier badge's is.

     For half of it to stand above the sheet, the sheet must not clip: the
     pane is overflow:visible below, and the clipping that keeps the reading
     inside its column is #oracle-panel-content's, which it always was. */
  #detail-oracle .sheet-grip,
  #fs-notes .sheet-grip {
    display: block;
    position: absolute;
    top: 0;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 30px;
    height: 30px;
    color: var(--phosphor-dim);
    z-index: 2;
    touch-action: none;
    cursor: grab;
    /* Behind the app header once the sheet nears the top, rather than over
       the mark: grSheetSetHeight() sets how much of its top is past the
       header's foot. A polygon rather than inset() so the sides and foot can
       stand 14px out, past the 12px touch area. Eased as the sheet's height
       is, and not while dragging. */
    clip-path: polygon(-14px var(--grip-clip, 0px), calc(100% + 14px) var(--grip-clip, 0px),
                       calc(100% + 14px) calc(100% + 14px), -14px calc(100% + 14px));
    transition: clip-path 0.25s ease;
  }
  #detail-oracle.sheet-dragging .sheet-grip,
  #fs-notes.sheet-dragging .sheet-grip { transition: none; }
  #detail-oracle .sheet-grip::before,
  #fs-notes .sheet-grip::before {
    content: '';
    position: absolute;
    inset: -12px;
  }
  #detail-oracle .sheet-grip svg,
  #fs-notes .sheet-grip svg { display: block; width: 100%; height: 100%; }
  #detail-oracle .sheet-grip circle,
  #fs-notes .sheet-grip circle { fill: var(--bg-panel); }
  #detail-oracle.sheet-dragging .sheet-grip,
  #fs-notes.sheet-dragging .sheet-grip { color: var(--phosphor); cursor: grabbing; }

  #oracle-section-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
}

@media (max-width: 600px) {
  #gr-fullscreen-btn {
    width: 64px;
    height: 64px;
    bottom: 20px;
    right: 20px;
  }
}

/* ── Sidebar brand area (Open WebUI-inspired) ──────────────────────── */
/* Top of sidebar with logo and collapse button */
.sidebar-brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px 16px;
  gap: 8px;
  background: var(--bg-panel);
}

/* ── Nav rail: the action, then the places ───────────────────────────────── */

/* The primary action, above the rule and drawn as a button. It is not a
   destination and should not have to be found among them. */
.sidebar-action {
  padding: 6px 8px 10px;
  border-bottom: 1px solid var(--phosphor-faint);
}

/* Reverse video: a solid phosphor ground with the background colour on top.
   This is how a terminal emphasises, and it is the reason this does not need
   to move to be found. Nothing else in the rail is filled, so the eye lands on
   it first at any width.

   Not a flashing or pulsing button, deliberately. It is permanent chrome seen
   on every load, so motion that reads as alive on the first visit reads as
   nagging by the twentieth -- and this product gates its own flicker behind a
   photosensitivity acknowledgement, which would be a strange thing to do while
   blinking at somebody in the navigation. The bloom below is presence without
   movement. */
.nav-primary {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 10px 10px;
  background: var(--phosphor);
  border: 1px solid var(--phosphor);
  color: var(--bg);
  cursor: pointer;
  text-align: left;
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 2px;
  text-transform: uppercase;
  text-shadow: none;
  box-shadow: 0 0 14px var(--phosphor-glow);
  transition: box-shadow 0.15s, filter 0.15s;
}

.nav-primary:hover {
  filter: brightness(1.12);
  box-shadow: 0 0 22px var(--phosphor-glow-strong);
}

.nav-primary:active { filter: brightness(0.95); }

/* A plus: the one mark that says "make one" without a legend, and the only
   thing the button needs beyond its words. It replaced both the caret and the
   wand icon -- the icon was a raster mark being silhouetted against a bright
   fill to survive it, which is a lot of work for a decoration sitting next to
   the words "New Revelation". */
.nav-plus {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

/* Collapsed, the same idea at icon scale: the one filled cell in the strip. */
.strip-icon-btn.strip-primary {
  background: var(--phosphor);
  box-shadow: 0 0 14px var(--phosphor-glow);
}

.strip-icon-btn.strip-primary:hover {
  background: var(--phosphor);
  filter: brightness(1.12);
  box-shadow: 0 0 22px var(--phosphor-glow-strong);
}

/* Collapsed, at the same size the archive mark takes in the strip, so the two
   cells read as a matched pair rather than as two unrelated buttons. */
.strip-icon-btn.strip-primary .nav-plus {
  width: 32px;
  height: 32px;
  color: var(--bg);
}

.sidebar-nav-section {
  padding: 8px 8px;
  border-bottom: 1px solid var(--phosphor-faint);
}

/* A place. Inline SVG rather than a raster icon, so it takes the phosphor
   directly instead of through a filter chain. */
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  color: var(--phosphor-dim);
  text-decoration: none;
  letter-spacing: 2px;
  text-transform: uppercase;
  font-size: 13px;
  transition: color 0.15s, background 0.15s;
}

.nav-item:hover {
  color: var(--phosphor);
  background: rgba(var(--lift), 0.03);
}

/* Sized to sit level with the raster icons beside it, by eye rather than by
   box. The PNGs are 36px in the collapsed strip and 22px expanded, but they
   carry internal padding and this does not -- so matching the boxes made the
   mark read larger than its neighbour. A tenth off each evens them out. */
.nav-icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

.strip-icon-btn .nav-icon {
  width: 32px;
  height: 32px;
}

/* Where you are, expanded: a rule down the left edge of the row. The row runs
   the width of the rail, so the rule sits on the rail's own edge -- which is
   what makes it read as a marker rather than as a line drawn near something. */
.nav-item.nav-current {
  color: var(--phosphor);
  background: var(--wash-faint);
  box-shadow: inset 2px 0 0 var(--halo);
}

.nav-item.nav-current .nav-icon {
  filter: drop-shadow(0 0 5px var(--phosphor-glow-strong));
}

/* The collapsed strip's items are anchors as well as buttons now. */
.strip-icon-btn {
  color: var(--phosphor-dim);
  text-decoration: none;
}

.strip-icon-btn:hover { color: var(--phosphor); }

.sidebar-nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--phosphor-dim);
  cursor: pointer;
  padding: 6px 8px;
  text-align: left;
  letter-spacing: 1px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12.5px;
  transition: color 0.15s, background 0.15s;
}

.sidebar-nav-item:hover {
  color: var(--phosphor);
  background: rgba(var(--lift), 0.03);
}

.sidebar-nav-icon {
  width: 36px;
  height: auto;
  flex-shrink: 0;
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
}

.sidebar-nav-label {
  font-size: 14px;
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* Sidebar brand collapse button (shows sidebar icon on hover) */
.sidebar-brand-collapse-btn {
  background: transparent;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.15s, transform 0.15s;
}

.sidebar-brand-collapse-btn:hover {
  opacity: 1;
  transform: scale(1.1);
}

.sidebar-brand-collapse-btn:active {
  transform: scale(0.95);
}

/* Sidebar icon shown in the collapse button */
/* The two arrows that work the archive panel: ◀ in the expanded header to shut
   it, ▶ on the collapsed strip to open it again. One rule for both, because
   they are one control in two states and any drift between them would show as
   the panel opened and closed. Both were images -- icon_sidebar.png needing the
   five-filter chain to be tinted into the visitor's phosphor, which a character
   takes for free -- and neither said which way the panel would go.

   Both sit at rest rather than appearing on hover. The strip's used to fade in,
   which asked the visitor to discover a control by hovering the one thing on a
   narrow strip; there is nothing else there to mistake it for.

   The colour is the visitor's own phosphor, not a fixed hue, so these track the
   hue setting the way the rest of the terminal does.

   font-family is the load-bearing line here, not decoration. ◀ and ▶ are a
   mirrored pair in any one face, but these two spans sit in different font
   contexts: the strip's is a span in a div and inherits body's Share Tech Mono,
   while the header's is inside a <button>, and a button does not inherit
   font-family -- no rule in this file gives bare buttons one, so it was falling
   through to the UA's proportional default. Share Tech Mono carries neither
   triangle (nor any of the box-drawing set), so the two arrows were resolving
   to two different fallback faces and drawing two different triangles. Naming
   the stack here lands both in the same one. */
.sidebar-collapse-arrow,
.strip-expand-arrow {
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1;
  color: var(--phosphor-dim);
  opacity: 0.75;
  transition: opacity 0.15s, color 0.15s;
}

.sidebar-brand-collapse-btn:hover .sidebar-collapse-arrow,
.strip-logo-row:hover .strip-expand-arrow {
  opacity: 1;
  color: var(--phosphor);
}

/* Brand logo and text - always visible when expanded */
.sidebar-logo {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.brand-logo {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  opacity: 0.85;
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
  transition: opacity 0.15s;
}

/* Show sidebar icon when hovering over the logo area (positioned absolutely to avoid layout shift) */
.sidebar-logo {
  position: relative;
}

/* Sidebar icon - hidden by default but takes up no space */
.brand-logo[data-icon="sidebar"] {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}


.brand-logo:hover { opacity: 1; }

.brand-text {
  font-family: 'VT323', monospace;
  font-size: 38px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  letter-spacing: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.brand-text-highlight {
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
}


.brand-text {
  font-family: 'VT323', monospace;
  font-size: 38px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  letter-spacing: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.brand-text-highlight {
  color: var(--amber);
  text-shadow: 0 0 6px var(--halo-amber);
}

/* ── Sidebar collapse system ─────────────────────────────────────── */


/* Smooth transition for left panel width via #main grid */
/* The rail's width is a rule, not an inline style, so the resting state can be
   settled before the first paint -- see the pre-paint script in base.html.
   Collapsed is the default; .rail-expanded is set only when this browser has
   asked for it. */
#main {
  transition: grid-template-columns 0.3s ease;
  grid-template-columns: 110px 1fr;
}

/* Expanded, the rail is as wide as the reader has dragged it. --rail-w is set
   on <html> by the pre-paint script in base.html from what this browser last
   chose, and live by the handle in nav-rail.js; unset, it is the default. 240px
   by default: wide enough for NEW REVELATION to stay on one line (it needs
   ~222px), with a little air. */
:root { --rail-w: 240px; }
html.rail-expanded #main { grid-template-columns: var(--rail-w) 1fr; }

/* While dragging, the column follows the pointer rather than easing after it,
   and nothing underneath gets selected on the way. */
html.rail-resizing #main { transition: none; }
html.rail-resizing,
html.rail-resizing * { cursor: col-resize !important; user-select: none; -webkit-user-select: none; }

/* The handle: the rail's right edge, expanded and on a desktop only. Invisible
   until it is under the pointer or focused, then a rule of phosphor. */
#rail-resize {
  display: none;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 6px;
  z-index: 5;
  cursor: col-resize;
  background: transparent;
  touch-action: none;
  transition: background 0.15s, box-shadow 0.15s;
}
html.rail-expanded #rail-resize { display: block; }
#rail-resize:hover,
#rail-resize:focus-visible,
html.rail-resizing #rail-resize {
  outline: none;
  background: var(--phosphor-faint);
  box-shadow: 0 0 8px var(--phosphor-glow);
}

/* The drawer owns the width below the breakpoint; see the mobile block. */
@media (max-width: 767px) {
  #main,
  html.rail-expanded #main { grid-template-columns: 1fr; }
  #rail-resize,
  html.rail-expanded #rail-resize { display: none; }
}

/* Left panel: overflow:hidden so strip clips cleanly during transition */
#left-panel { overflow: hidden; }

/* Left panel wrapper for normal content */
#left-panel-content {
  display: flex;
  flex-direction: column;
  flex: 1;
  overflow: hidden;
  min-height: 0;
}

/* Collapsed left panel. 110px rather than the oracle's 92: the sigil is 52px
   and centred to line up with the 36px icons below it, and the expand arrow
   hangs in the right margin. At 92 that margin was 20px, of which the arrow ate
   8 of padding and 8 of glyph -- the arrow sat on the mark. 110 leaves ~12px of
   air between them while keeping the mark centred, which an arrow laid out in
   flow beside it would not: that would shove the sigil off the axis the icons
   below stand on. */
/* Collapsed is a fact about the document, not about this element.
   It used to be a class JS put on #left-panel at DOMContentLoaded, which meant
   every page load painted the expanded rail -- brand, labels and all -- inside
   a 110px column before the script ran and swapped it for the strip. Clicking
   a nav item is a navigation, so that flash happened on every click. The
   <html> class is set before the first paint by the script in base.html, and
   now decides the contents as well as the width. */
html:not(.rail-expanded) #left-panel {
  min-width: 110px;
  max-width: 110px;
}

html:not(.rail-expanded) #left-panel #left-panel-content { display: none; }

/* Oracle panel wrapper for normal content */
#oracle-panel-content {
  display: flex;
  flex-direction: column;
  flex: 1;
  overflow-y: auto;
  min-height: 0;
}

/* Desktop: the header stays in view. The pane used to scroll as a whole, so a
   long reading took the header -- its title, the text size, copy, collapse --
   up and out of sight with it. Now the pane is a column that does not scroll:
   the header holds its place, the reading scrolls in the body beneath it, and
   the question composer keeps to the foot, as on a phone (see the 767px block,
   "The panel itself never scrolls"). Collapsed to its strip, the pane is left
   as it was; while it is hidden, its inline display:none still wins. */
@media (min-width: 768px) {
  #detail-oracle:not(.panel-collapsed) {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  #detail-oracle:not(.panel-collapsed) #oracle-panel-content { overflow: hidden; }
  #detail-oracle .panel-header { flex: none; }
  #oracle-section-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }
  /* The column's last row, not sticky: in a clipping column a sticky box
     misbehaves, as the 767px block below found for the phone's sheet. */
  #detail-oracle #oracle-composer,
  #detail-oracle #oracle-complete { flex: none; position: static; margin-top: 0; }
}

/* Collapsed oracle panel */
#detail-oracle.panel-collapsed {
  width: 92px !important;
  min-width: 92px;
  overflow: hidden;
  /* Remove pyramid.gif background when collapsed - only show icon strip */
  background-image: none;
  background-color: var(--bg-panel);
}

#detail-oracle.panel-collapsed #oracle-panel-content { display: none; }

/* ── Panel strip (shown when sidebar is collapsed) ──────────────── */

/* Width comes from the panel, not from here: the left strip and the oracle's
   share this rule but no longer share a width. */
.panel-strip {
  display: none;
  flex-direction: column;
  align-items: center;
  width: 100%;
  flex: 1;
  padding: 8px 0;
  gap: 0;
}

.strip-logo-row {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 4px 0;
  width: 100%;
}

/* Everything but its position is shared with .sidebar-collapse-arrow, above.
   Pinned to the right of the strip, which puts it in the margin beside the
   centred sigil rather than over it -- see the width note on
   html:not(.rail-expanded) #left-panel for why that margin is the size it is. */
.strip-expand-arrow {
  position: absolute;
  right: 8px;
}


html:not(.rail-expanded) #left-panel #left-panel-strip { display: flex; }
#detail-oracle.panel-collapsed #oracle-panel-strip { display: flex; }

/* Strip expand icon button */
.strip-expand-btn {
  background: transparent;
  border: none;
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 15px;
  cursor: pointer;
  padding: 8px 0 4px;
  line-height: 1;
  -webkit-tap-highlight-color: transparent;
}

.strip-expand-btn:hover { color: var(--phosphor); }


/* Desktop-only panel collapse toggle button */
.panel-collapse-btn {
  background: transparent;
  border: none;
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 14px;
  cursor: pointer;
  padding: 2px 5px;
  line-height: 1;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s;
}

.panel-collapse-btn:hover { color: var(--phosphor); }

/* On mobile: never collapse to strip; always show content */
@media (max-width: 767px) {
  .panel-collapse-btn { display: none !important; }
  .panel-strip        { display: none !important; }
  #left-panel-content { display: flex !important; }
  #oracle-panel-content { display: flex !important; flex-direction: column; }
  /* Prevent collapse class from taking effect on mobile */
  html:not(.rail-expanded) #left-panel { min-width: unset; max-width: unset; }
  #detail-oracle.panel-collapsed { width: auto !important; min-width: unset; }
  /* Unclipped, so the grab mark can stand half above the sheet's top line
     (.sheet-grip, above). Nothing else reaches outside it: the reading is
     held in #oracle-panel-content, a flex column that clips on its own. */
  #detail-oracle,
  #detail-oracle.panel-collapsed { overflow: visible; }
}

/* And the mirror of it. The oracle header carried two collapse marks side by
   side: .panel-collapse-btn, which folds the whole pane away to its strip, and
   #oracle-collapse-icon, which folds only the reading inside it and is what a
   tap on the mobile sheet's header drives. Each answers one context. The pane
   control is already hidden on mobile above; this hides the sheet control on
   desktop, so either way the header carries one arrow rather than two an inch
   apart doing different things -- which read as one control drawn twice.

   Folding the body is the one with nothing to offer here. #detail-oracle is a
   fixed column, so hiding its body hands the cards no room back; the strip
   does. Hidden rather than deleted because three places in the scripts write
   to this element and two of them do it without a null check. */
@media (min-width: 768px) {
  #oracle-collapse-icon { display: none; }
}
/* And on a phone too, now (owner, 2026-10-04): the sheet's grab ring and its
   header tap fold and unfold it, so the arrow only restated them. */
@media (max-width: 767px) {
  #oracle-collapse-icon { display: none; }
}

/* The folded sheet on a phone -- its header alone (.sheet-folded, set by
   grSheetSetHeight). No rule under the header: folded, there is nothing
   beneath it to rule off. And no text-size buttons: there is no text showing
   to size. They come back with the reading. */
@media (max-width: 767px) {
  #detail-oracle.sheet-folded .panel-header { border-bottom-color: transparent; }
  #detail-oracle.sheet-folded .oracle-size-btn { display: none; }
}

/* Freestyle's Notes sheet, the same (owner, 2026-10-04): folded on a phone
   -- not .is-open, which fsNotesSheetSetHeight sets by height -- it draws no
   rule under its header and no text-size buttons, and it carries no fold
   arrow at all; its header tap and drag fold and unfold it. The copy button
   stays, as the oracle's does. */
@media (max-width: 767px) {
  #fs-notes-collapse-icon { display: none; }
  #fs-notes:not(.is-open) .fs-notes-head { border-bottom-color: transparent; }
  #fs-notes:not(.is-open) .notes-size-btn { display: none; }
  /* Typed into with the keyboard up: the sheet stands on the keyboard
     (grimoire-freestyle.js, fsNotesKbFit), over the bar it stands on else. */
  /* With the keyboard up the sheet stands on the keyboard; the bottom bar
     stays under it. The keyboard moves at its own pace, and easing the
     sheet's foot after it trailed.

     And a skirt: the sheet's own ground, carried down from its foot behind
     the keyboard. A keyboard measured a few pixels short, or one still
     sliding, left a strip between the two where the cards showed; now that
     strip is the sheet. Only while typing -- the rest of the time the skirt
     would cover the bar. */
  #fs-notes.kb-up,
  #main:has(#tabbar) #fs-notes.kb-up {
    bottom: var(--kb, 0px) !important;
    transition: height 0.25s ease !important;
  }
  #fs-notes.kb-up::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 100vh;
    /* Solid, where the sheet is frosted (a translucent ground over a blur):
       frosted, the cards would show through it blurred, which is the thing
       being covered. The page's own ground is what the frosted sheet reads
       as over the cloth. */
    background: var(--bg);
  }
}

/* ── Panel & strip icons ─────────────────────────────────────────── */

/*
 * Icons are desaturated then re-tinted to match the user's phosphor hue and
 * intensity.  Pipeline: grayscale → brightness boost → sepia base →
 * hue-rotate (--icon-hue-rotate) → saturate (--icon-saturate).
 * mix-blend-mode: screen removes the black icon background (black = 0 in screen).
 * Both CSS vars are set by applyHue() in terminal.js and the inline theme-init
 * script in base.html, so they update instantly when the user adjusts settings.
 */
.panel-header-icon,
.strip-icon {
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
}


/* Small icon in panel header title */
.panel-header-icon {
  width: 52px;
  height: auto;
  opacity: 0.85;
  vertical-align: middle;
  flex-shrink: 0;
}


/* Icon button in collapsed strip */
#left-panel-strip .strip-icon-btn {
  margin-top: 10px;
}

/* Full width of the strip, not shrink-to-icon.
   The button used to be exactly as wide as its glyph, which made the current
   marker a 32px rectangle hugging the icon with no air around it, and put its
   left-edge rule in the middle of the rail attached to nothing. A row that
   spans the rail gives the highlight somewhere to sit and puts the rule on the
   rail's own edge -- and it makes the tap target the whole width rather than
   the mark. */
.strip-icon-btn {
  position: relative;
  width: 100%;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 9px 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, color 0.15s;
  -webkit-tap-highlight-color: transparent;
}

.strip-icon-btn:hover { background: var(--wash-faint); }

/* Where you are, collapsed. The ground is softer than the expanded row's
   because there is no label to carry it -- the mark does, so it takes the
   phosphor and a glow, and the edge rule says which one. */
.strip-icon-btn.nav-current {
  background: var(--wash-faint);
  color: var(--phosphor);
}

.strip-icon-btn.nav-current::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 2px;
  height: 26px;
  background: var(--phosphor);
  box-shadow: 0 0 8px var(--halo);
}

.strip-icon-btn.nav-current .nav-icon {
  filter: drop-shadow(0 0 6px var(--phosphor-glow-strong));
}

.strip-icon {
  width: 36px;
  height: auto;
  display: block;
}


/* ── Scrollbars ──────────────────────────────────────────────────────────── */
/* The house scrollbar, everywhere (owner, 2026-09-26): the Oracle panel's is
   the reference -- a thin phosphor-dim thumb on no track, brightening under
   the cursor. It used to be opted into pane by pane, which left the rest to
   the browser's own grey bar and a handful of one-offs (the log, the
   settings, the card stage, the oracle's cards) each a little different.
   Now it is every scroller's, the page's own included; nothing need name
   itself here.

   Always on wherever there is something to scroll (owner, 2026-10-04): it
   used to stay invisible until the pointer was over its pane, and a bar
   that waits for the pointer hides the fact that there is more. The browser
   draws it only on a box that overflows, so it shows exactly when scrolling
   is there to be had.

   Two engines, both written: scrollbar-width/-color for Firefox and for
   Chromium since 121 (which then ignores the pseudo-elements), and the
   ::-webkit-scrollbar set for Safari and older Chromium. A touch screen's
   own bar is an overlay that fades between scrolls, so there the native bar
   is hidden and terminal.js draws one in its place (#drawn-scrollbars). */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--phosphor-dim) transparent;
}

::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--phosphor-dim);
  border-radius: 2px;
  transition: background 0.3s;
}
::-webkit-scrollbar-thumb:hover { background: var(--phosphor); }

/* The one exception (owner, 2026-10-04): the New Revelation screen
   (#ws-empty) shows its bar only while it is being scrolled -- named here by
   id, so its markup is untouched; any other box can join with
   data-scrollbar="transient". terminal.js sets .is-scrolling for the scroll
   and a beat after; with a mouse that is what lights the bar, and on a touch
   screen the drawn thumb waits for it too. */
#ws-empty, [data-scrollbar="transient"] { scrollbar-color: transparent transparent; }
#ws-empty.is-scrolling, [data-scrollbar="transient"].is-scrolling { scrollbar-color: var(--phosphor-dim) transparent; }
#ws-empty::-webkit-scrollbar-thumb,
[data-scrollbar="transient"]::-webkit-scrollbar-thumb { background: transparent; }
#ws-empty.is-scrolling::-webkit-scrollbar-thumb,
[data-scrollbar="transient"].is-scrolling::-webkit-scrollbar-thumb { background: var(--phosphor-dim); }

/* The touch screen's bar, drawn (terminal.js, "Scroll bars that stay"): the
   same thumb, on every box that scrolls, held on screen rather than faded.
   The native overlay is hidden so there are never two. */
@media not all and (hover: hover) and (pointer: fine) {
  * { scrollbar-width: none; }
  *::-webkit-scrollbar { display: none; }
}
#drawn-scrollbars {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 2147483000;
  overflow: hidden;
}
.drawn-thumb {
  position: absolute;
  left: 0;
  top: 0;
  width: 4px;
  border-radius: 2px;
  background: var(--phosphor-dim);
}
.drawn-thumb[hidden] { display: none; }

/* The Oracle's bar, always on while the pane is open (owner, 2026-10-03):
   there is nearly always more reading than pane, and a bar that waits for the
   pointer hides the fact. With a mouse it is the house bar held at its hover
   colour; on a touch screen, whose own bar is an overlay that fades between
   scrolls, the native one is hidden and grimoire.js draws #oracle-scrollbar
   in its place -- the same thin phosphor-dim thumb on no track. Shown only
   when the reading overflows. */
@media (hover: hover) and (pointer: fine) {
  #oracle-section-body,
  #oracle-panel-content,
  #detail-oracle { scrollbar-color: var(--phosphor-dim) transparent; }
  #oracle-section-body::-webkit-scrollbar-thumb,
  #oracle-panel-content::-webkit-scrollbar-thumb,
  #detail-oracle::-webkit-scrollbar-thumb { background: var(--phosphor-dim); }
  #oracle-section-body::-webkit-scrollbar-thumb:hover,
  #oracle-panel-content::-webkit-scrollbar-thumb:hover,
  #detail-oracle::-webkit-scrollbar-thumb:hover { background: var(--phosphor); }
  #oracle-scrollbar { display: none !important; }
}
@media not all and (hover: hover) and (pointer: fine) {
  #oracle-section-body { scrollbar-width: none; }
  #oracle-section-body::-webkit-scrollbar { display: none; }
  #oracle-panel-content { position: relative; }
}
.oracle-scrollbar {
  position: absolute;
  right: 2px;
  width: 4px;
  z-index: 3;
  pointer-events: none;
}
.oracle-scrollbar[hidden] { display: none; }
.oracle-scrollbar .osb-thumb {
  position: absolute;
  left: 0;
  right: 0;
  border-radius: 2px;
  background: var(--phosphor-dim);
}

/* ── Mind Machine sidebar player ─────────────────────────────────── */

/* The Mind Machine's own section, at the foot of the expanded panel and
   directly above the reader's. Padded as .sidebar-nav-section is, so its row
   lines up with the places above; margin-top:auto takes it to the foot. */
.sidebar-foot-section {
  margin-top: auto;
  padding: 8px;
  border-top: 1px solid var(--phosphor-faint);
  flex-shrink: 0;
}

/* A row like the rail's others (.nav-item): same padding, gap, type and left
   edge, so the icon stands in the column Archive's and Feedback's do and the
   label starts where theirs start. The play control rides the right end. */
#mm-sidebar-player {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  flex-shrink: 0;
}

/* A raster mark carries its own padding, so it is drawn at 22px -- as the
   rail's PNGs are expanded -- in the 20px slot the SVG icons take, and the
   label lines up with theirs. */
.mm-player-icon {
  width: 22px;
  height: 22px;
  margin: -1px;
  flex-shrink: 0;
  filter:
    grayscale(1)
    brightness(var(--icon-brightness, 1.5))
    sepia(1)
    hue-rotate(var(--icon-hue-rotate, 112deg))
    saturate(var(--icon-saturate, 2.2));
  mix-blend-mode: var(--blend-emit);
  opacity: 0.75;
}

.mm-player-label {
  flex: 1;
  font-size: 13px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mm-player-btn {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  padding: 2px 8px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
  flex-shrink: 0;
}

.mm-player-btn:hover {
  color: var(--phosphor);
  border-color: var(--phosphor-dim);
}

/* Icon flash when MM is playing — matches oracle icon flash */
#mm-strip-play-btn.mm-playing .strip-icon,
#mm-sidebar-player.mm-playing .mm-player-icon {
  animation: oracleIconFlash 0.7s ease-in-out infinite;
}

/* Strip already has the MM icon; hide the expanded player when collapsed */
/* Desktop only. On a phone the rail is a drawer showing the expanded panel
   whatever .rail-expanded says -- there is no strip -- so hiding the player
   there took the Mind Machine out of the only rail a phone has. */
@media (min-width: 768px) {
  html:not(.rail-expanded) #left-panel #mm-sidebar-player { display: none; }
}

/* ── User profile section in archive sidebar ──────────────────────── */

#archive-user-profile {
  margin-top: auto;
  padding: 8px 12px;
  border-top: 1px solid var(--phosphor-faint);
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex-shrink: 0;
}

.user-profile-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  min-width: 0;
}

.user-profile-circle {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--wash);
  border: 1px solid var(--phosphor-faint);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: all 0.15s;
  position: relative;
  z-index: 1;
}

.user-profile-row:hover .user-profile-circle {
  background: var(--wash);
  border-color: var(--phosphor);
  box-shadow: 0 0 8px var(--phosphor-glow);
}

/* The @ of a handle (owner, 2026-10-06), where the initial stood. Its own
   one-character face (fonts.css, 'Psibyl At'): neither VT323's @ nor Share
   Tech Mono's is legible at this size. */
.user-profile-initial {
  font-family: 'Psibyl At', 'DejaVu Sans', sans-serif;
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 4px var(--halo);
}

.user-profile-email {
  font-size: 13px;
  color: var(--phosphor-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}


/* Collapsed state - show only the circle */
html:not(.rail-expanded) #left-panel #archive-user-profile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

html:not(.rail-expanded) #left-panel .user-profile-row {
  justify-content: center;
}

html:not(.rail-expanded) #left-panel .user-profile-email {
  display: none;
}

/* The avatar is a control (it opens Settings at ACCOUNT); say so. */
html:not(.rail-expanded) #left-panel .user-profile-row { cursor: pointer; }


/* ── Signal strength meter ──────────────────────────────────────────────────
   Track runs -3..+3 with null at dead centre; the bar fills outward from that
   mark, so right of it is above the noise floor and left of it below. Band
   divisions are drawn faintly into the track so the fill position and the label
   beneath can never disagree. Phosphor only. See signal-meter.js. */

/* Boxed, with the caption inside an unbroken frame. The border is a dimmed
   amber rather than the full token: the null mark is amber too, and at equal
   brightness the frame would flatten the one mark on the meter that has to
   stand out. */

.sig-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin: 0 auto;
  padding: 0;
  width: 100%;
  /* No frame of its own. The box used to hang below the card and overlap its
     neighbours in a spread, so it needed an opaque background and a rule to
     read as a separate object. It sits inside the card now — the card's own
     border already contains it, and a second one boxed a box and cost height
     the card did not have to give. */
  user-select: none;
}

.sig-box-hd {
  white-space: nowrap;
  font-size: 10px;
  letter-spacing: 1.1px;
  color: color-mix(in srgb, var(--amber) 78%, transparent);
}

.sig-meter-track {
  position: relative;
  width: 100%;
  height: 6px;
  background: var(--phosphor-faint);
  border: 1px solid var(--phosphor-dim);
  border-color: color-mix(in srgb, var(--phosphor-dim) 55%, transparent);
  overflow: hidden;
  /* Band edges at -1.5 / -0.5 / +1.0 / +2.5 -> 25% / 41.67% / 66.67% / 91.67% */
  background-image: linear-gradient(
    to right,
    transparent 0 24.6%, var(--phosphor-faint) 24.6% 25.4%,
    transparent 25.4% 41.3%, var(--phosphor-faint) 41.3% 42.1%,
    transparent 42.1% 66.3%, var(--phosphor-faint) 66.3% 67.1%,
    transparent 67.1% 91.3%, var(--phosphor-faint) 91.3% 92.1%,
    transparent 92.1% 100%
  );
}

.sig-meter-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--phosphor);
  opacity: 0.85;
  box-shadow: 0 0 4px var(--phosphor-glow-strong);
}

/* Below the noise floor reads as recessive, not as an equal-and-opposite
   achievement. Same bar, less presence. */
.sig-meter-fill.is-below {
  background: var(--phosphor-dim);
  opacity: 0.7;
  box-shadow: none;
}

.sig-meter-null {
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 50%;
  width: 1px;
  margin-left: -0.5px;
  background: var(--amber);
  box-shadow: 0 0 4px var(--halo-amber);
}

.sig-meter-rising {
  transition: left 520ms cubic-bezier(0.16, 0.9, 0.3, 1),
              width 520ms cubic-bezier(0.16, 0.9, 0.3, 1);
}

/* Past what the track can draw. The number still tells the truth; this stops a
   pegged bar being read as an exact position. */
.sig-box.is-pegged .sig-meter-read span::after {
  content: '\2191';
  margin-left: 2px;
  font-size: 11.5px;
  opacity: 0.8;
}

.sig-meter-read {
  display: flex;
  align-items: baseline;
  gap: 5px;
  line-height: 1.2;
}

.sig-meter-read b {
  font-size: 11px;
  letter-spacing: 1.1px;
  color: var(--phosphor-dim);
  font-weight: normal;
}

.sig-meter-read span {
  font-size: 12.5px;
  color: var(--phosphor);
  text-shadow: 0 0 5px var(--phosphor-glow-strong);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .sig-meter-rising { transition: none; }
}


/* ── Mind Machine switch (card stage) ───────────────────────────────────────
   Anchored to #card-stage, the pan/zoom viewport, not to #card-canvas which
   moves inside it -- so the switch holds its corner while the spread is dragged
   around beneath it. Top-left; the stage's other controls are on the bottom
   edge, zoom at bottom-left and fullscreen at bottom-right. The feature defaults off, so this is what keeps it
   discoverable: a setting nobody can find is a setting that does not exist. */

.ws-mm {
  position: absolute;
  top: 8px;
  left: 10px;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 6px;
  background: none;
  border: none;
  font-family: inherit;
  /* Set in the header and footer's type -- 15px, bold, 3px tracking -- so the
     one control floating over the stage reads as part of the same chrome as
     the bands that frame it. It defaults off and has to be found, which is the
     other reason it is not set any smaller. */
  font-size: 15px;
  font-weight: bold;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  white-space: nowrap;
  /* #card-stage sets cursor:none for its own cursor; a control needs to look
     like one. */
  cursor: pointer;
}

/* Over the cards (owner, 2026-10-07): the switch keeps its z-index of 60 on
   every stage, a spread panned across the corner going under it. From
   2026-10-03 it sat beneath the cards (z-index 0 off a Freestyle table), as
   print on a cloth sits under a card laid on it, until the owner changed their
   mind. The canvas still takes no touches itself, only its cards do -- it is
   an invisible 1800-unit sheet -- so a touch between cards falls through to
   the stage, where the pan is handled. */
/* Gone once the reading has nothing left to draw: every card, and every
   clarifier it allows (grimoire-clarifiers.js, _grMindMachineSwitchSync). */
#card-stage.mm-spent .ws-mm { display: none; }
#card-stage:not(.fs-table) #card-canvas { pointer-events: none; }
#card-stage:not(.fs-table) #card-canvas > * { pointer-events: auto; }

/* "DRAW WITH" scopes the name: the sidebar has a standalone binaural player
   called the Mind Machine, and this switch governs something narrower -- whether
   the sequence runs as part of a draw. */
.ws-mm-label {
  line-height: 1.2;
}

/* The pill, scaled with the label it sits beside: 34x16 was drawn for 13px
   text and read undersized next to 15px bold. Here only -- the settings
   panel's toggle keeps its own size. */
.ws-mm .ec-toggle-track { width: 38px; height: 18px; border-radius: 9px; }
.ws-mm .ec-toggle-knob  { width: 14px; height: 14px; top: 2px; left: 2px; }

.ws-mm:hover .ws-mm-label,
.ws-mm:focus-visible .ws-mm-label { color: var(--phosphor); }
.ws-mm:focus-visible { outline: 1px solid var(--phosphor-dim); outline-offset: 2px; }

/* Drive the settings toggle's own checked appearance from this button's state.
   The .ec-toggle rules key off `input:checked +`, which needs a checkbox
   sibling; this control is a real button with role="switch", so the same two
   properties are mirrored here rather than the geometry being copied. Anything
   that changes the pill in settings changes it here too. */
.ws-mm.is-on .ec-toggle-track {
  background: var(--phosphor);
  /* Brighter than the settings toggle's checked state on purpose. Same shape
     and colour -- only the glow differs. That one sits in a lit panel; this one
     floats over the card stage, where 6px reads as dim and the switch has to
     announce that it is armed. */
  box-shadow: 0 0 10px var(--halo), 0 0 22px var(--phosphor-glow-strong);
}

.ws-mm.is-on .ec-toggle-knob { left: 22px; background: var(--bg); }

.ws-mm.is-on .ws-mm-label {
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo), 0 0 18px var(--phosphor-glow-strong);
}

@media (prefers-reduced-motion: reduce) {
  .ws-mm .ec-toggle-track,
  .ws-mm .ec-toggle-knob { transition: none; }
}

/* Off the phone's canvas (owner, 2026-10-04), left to Settings, where the
   same toggle lives. It was put here on 2026-10-03, at the stage's top left
   beside the recentre button, because a feature nobody can find does not
   exist; the owner's call now is that Settings is enough, and the row goes
   back to the cards -- visibleCardBand frames them from the stage's top once
   the switch is gone. Off by default, as ever (psibyl_entrainment, and the
   photosensitivity acknowledgement, both required: grimoire-entrainment.js);
   with it on, the ghost card's hint names the hold that starts it. Desktop
   keeps the switch. */
@media (max-width: 767px) {
  .ws-mm { display: none; }
}

/* ── Mind Machine photosensitivity gate ─────────────────────────────────────
   Shown once before the entrainment feature is first allowed to run. Sits above
   the entrainment overlay (z 500) so it is still legible if a stray activation
   path ever reaches it, and below the phosphor cursor. */
#mm-ack-overlay[hidden] { display: none !important; }
#mm-ack-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(var(--scrim), 0.88);
  backdrop-filter: blur(2px);
}
#mm-ack-box {
  max-width: 500px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  border: 1px solid var(--phosphor-dim);
  background: var(--bg-panel);
  box-shadow: 0 0 24px var(--phosphor-glow), inset 0 0 30px var(--phosphor-glow-faint);
  padding: 24px 28px 22px;
}
/* Calm, not alarming (owner, 2026-10-10): the panel's own phosphor, the
   reading voice of the app for the text, and START as the plain first action. */
#mm-ack-title {
  font-family: 'VT323', monospace;
  font-size: 28px;
  letter-spacing: 3px;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--phosphor-glow-strong);
  margin-bottom: 14px;
}
#mm-ack-box p {
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1.75;
  color: var(--prose-ink);
  margin: 0 0 10px;
}
#mm-ack-box .mm-ack-more { font-size: 12.5px; color: var(--prose-ink-lo); margin-top: 4px; }
#mm-ack-box a {
  color: var(--phosphor);
  border-bottom: 1px dotted var(--phosphor-dim);
  text-decoration: none;
}
#mm-ack-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}
.mm-ack-btn {
  width: 100%;
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'VT323', monospace;
  font-size: 22px;
  letter-spacing: 2px;
  padding: 8px 14px;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.12s;
}
.mm-ack-btn:hover {
  border-color: var(--phosphor-dim);
  color: var(--phosphor);
}
.mm-ack-btn.mm-ack-accept {
  border-color: var(--phosphor);
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
  box-shadow: 0 0 8px var(--phosphor-glow), inset 0 0 8px var(--phosphor-glow);
}
.mm-ack-btn.mm-ack-accept:hover {
  background: var(--wash);
  box-shadow: 0 0 16px var(--phosphor-glow-strong), inset 0 0 12px var(--phosphor-glow);
}

/* The gate is a document, not terminal chrome: its text must be readable and
   the pointer must be visible, both of which terminal.css suppresses globally. */
#mm-ack-overlay, #mm-ack-overlay * {
  cursor: auto;
  user-select: text;
  -webkit-user-select: text;
}

@media (max-width: 560px) {
  /* The same step-down login.html gives .lp-subtitle at this width. */
  .home-subtitle { font-size: 23px; letter-spacing: 2px; }

  #mm-ack-box   { padding: 18px; }
  #mm-ack-title { font-size: 24px; letter-spacing: 2px; }
  #mm-ack-box p { font-size: 13px; }
  .mm-ack-btn   { font-size: 18px; letter-spacing: 1px; }
}


/* ── Card meaning modal ──────────────────────────────────────────────────── */
/* Opened by tapping any drawn card. Content built by grCardModalHtml(). Sits
   above the scanlines but below the phosphor cursor (z 9999), so the terminal
   still points at it. */
.card-modal {
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.card-modal.open { display: flex; }

/* The desktop-only notice can be raised from inside another modal -- + NEW
   CUSTOM SPREAD in the spread picker -- and must sit over it. Every modal
   shares .card-modal's z-index, so without this the later one in the page
   wins, and the picker is later. OK returns to the picker, still open. */
#desktop-only-modal { z-index: 4001; }

.card-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(var(--scrim), 0.72);
}

/* The delete-account panel takes focus itself on opening, so its DELETE box
   does not raise a phone's keyboard (terminal.js, accountDeleteBegin); it
   is a landing place, not a control, and wears no focus ring. */
#acct-del-modal .card-modal-panel:focus { outline: none; }

.card-modal-panel {
  position: relative;
  z-index: 1;
  width: min(560px, 100%);
  max-height: min(82vh, 760px);
  overflow-y: auto;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 0 24px var(--phosphor-glow-strong),
              inset 0 0 60px var(--phosphor-glow-faint);
  padding: 20px 22px 26px;
  -webkit-overflow-scrolling: touch;
}

/* The meaning modal keeps its PREV / NEXT row in view: the panel is a column
   that does not scroll, and the body inside it is what scrolls. Its own id,
   not the shared class -- the clarifier and warning modals are one scroller. */
#card-modal .card-modal-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* The right-hand padding belongs to the body, not the panel: the body is the
     scroller, and inside the panel's padding its scrollbar sat against the
     text. Here the bar runs down the frame's right edge and the text keeps
     the same 22px of air it had. */
  padding-right: 0;
}
#card-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-right: 22px;
}

.cm-nav {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  /* No right-hand overhang: the panel has no right padding (#card-modal). */
  margin: 14px 0 -26px -22px;
  padding: 8px 12px;
  border-top: 1px solid var(--phosphor-faint);
}
.cm-nav[hidden] { display: none; }
.cm-nav-btn {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 2px;
  padding: 8px 12px;
  min-width: 92px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color 0.15s, border-color 0.15s;
}
.cm-nav-btn:hover:not(:disabled) {
  color: var(--phosphor);
  border-color: var(--phosphor-dim);
}
.cm-nav-btn:disabled { opacity: 0.3; cursor: default; }
.cm-nav-count {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}

.card-modal-close {
  position: absolute;
  top: 8px;
  right: 10px;
  background: transparent;
  border: none;
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
  transition: color 0.15s, text-shadow 0.15s;
}
.card-modal-close:hover {
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}

.cm-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  padding-right: 24px;
  padding-bottom: 12px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--phosphor-faint);
}
.cm-name {
  font-family: 'VT323', monospace;
  font-size: 30px;
  letter-spacing: 2px;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--halo), 0 0 30px var(--phosphor-glow-strong);
}
.cm-orient {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  /* A tag anywhere (IRREVERSIBLE, DESKTOP ONLY, a draft's date) is this pill;
     the meaning modal's orientation names its colour below. */
  color: var(--phosphor);
  background: var(--wash);
}
.cm-orient-up  { color: var(--phosphor); background: var(--wash); text-shadow: 0 0 6px var(--halo); }
.cm-orient-rev { color: var(--amber);    background: rgba(var(--amber-rgb), 0.14); text-shadow: 0 0 6px var(--halo-amber); }

.cm-position {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-bottom: 16px;
}
/* A card in a spread: its position leads the modal, labelled, set apart from
   the symbol below by a rule, with the clarifiers button beneath it. First in
   the panel, so it keeps clear of the close button as the head used to. */
.cm-position.is-position,
.cm-position:first-child {
  padding-right: 24px;
}
.cm-position.is-position {
  gap: 4px;
  padding-bottom: 14px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--phosphor-faint);
}
.cm-pos-label {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  opacity: 0.75;
}
.cm-position.is-position .cm-pos-name {
  font-size: 15px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
}
.cm-clar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.cm-clar-row .cm-clar-btn { margin: 0; }

/* Tags are pills; controls are square. Everything in the meaning modal that
   can be pressed keeps the terminal's square corners and outline, and every
   label -- how a symbol fell, its keywords, the clarifiers drawn -- is a soft
   rounded tint with no outline, so the two never read as each other (owner,
   2026-09-26: tags looked too much like buttons). */
.cm-tag,
.cm-orient,
.cm-kw {
  display: inline-flex;
  align-items: center;
  border: none;
  border-radius: 999px;
  padding: 2px 10px;
  line-height: 1.4;
  white-space: nowrap;
}
.cm-clar-tag {
  gap: 6px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--phosphor);
  background: var(--wash);
}
.cm-clar-tag-deck { color: var(--phosphor-dim); font-size: 10px; }

/* The symbol on one line: a rune's stave, its name with how it fell under it,
   and its voice to the right -- beside the name at every width, never under
   it; a long name wraps within its own column instead. Space and the next
   heading part it from what follows -- no rule. */
#card-modal-body .cm-ident {
  flex-wrap: nowrap;
  align-items: center;
  gap: 14px;
  padding: 0;
  margin-bottom: 20px;
  border-bottom: none;
}
.cm-ident-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
}
.cm-ident-main .cm-name { overflow-wrap: anywhere; }
.cm-ident .cm-voice { flex: 0 0 170px; margin: 0; }
.cm-ident .cm-voice .sig-box { max-width: none; }
/* The Voice's hint: a (?) at its top right, and a bubble saying what it is
   (grimoire.html, tpl-voice-help) on hover, or focus for a finger or a key.
   The bubble hangs below and to the left, into the modal rather than off it. */
.cm-ident .cm-voice { position: relative; }
.cm-voice-help { position: absolute; top: -7px; right: 0; z-index: 5; }
.cm-voice-help-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  padding: 0;
  border: 1px solid var(--phosphor-dim);
  border-radius: 50%;
  background: none;
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 10px;
  line-height: 1;
  cursor: help;
}
.cm-voice-help-btn:hover,
.cm-voice-help-btn:focus-visible { color: var(--phosphor); border-color: var(--phosphor); outline: none; }
.cm-voice-help-tip {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 300px;
  max-width: calc(100vw - 48px);
  padding: 10px 12px;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-dim);
  box-shadow: 0 6px 18px rgba(var(--shade), 0.45);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  line-height: 1.55;
  letter-spacing: 0.2px;
  text-align: left;
  white-space: normal;
  user-select: text;
}
/* A bridge over the gap under the (?), so a pointer on its way to the link
   does not leave the hover and close the bubble. */
.cm-voice-help-tip::before { content: ''; position: absolute; left: 0; right: 0; top: -8px; height: 8px; }
.cm-voice-help-tip p { margin: 0; }
.cm-voice-help-tip p + p { margin-top: 8px; }
.cm-voice-help-tip b { font-weight: normal; color: var(--amber); }
/* The five bands, quietest to loudest, spread across the bubble on one line. */
.cm-voice-help-scale {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2px 4px;
  font-size: 10px;
  letter-spacing: 0.5px;
  color: var(--phosphor-dim);
}
.cm-voice-help-more a { font-size: 10.5px; letter-spacing: 1.5px; color: var(--phosphor-dim); text-decoration: none; }
.cm-voice-help-more a:hover { color: var(--phosphor); }
.cm-voice-help:hover .cm-voice-help-tip,
.cm-voice-help:focus-within .cm-voice-help-tip { display: block; }
/* The clarifier modal's hint (grimoire-clarifiers.js, _grClarifierHelp): the
   same (?) and bubble, inline after the CLARIFIERS title. The title sits at the
   modal's left, so the bubble hangs from the (?)'s left edge, into the modal,
   and sheds the title's display type and glow. */
.cm-voice-help.clm-help {
  position: relative;
  top: auto;
  right: auto;
  display: inline-block;
  margin-left: 10px;
  vertical-align: 5px;
}
.clm-help .cm-voice-help-tip {
  left: -8px;
  right: auto;
  text-transform: none;
  text-shadow: none;
}
/* A phone's modal has no room for a 300px bubble hung from a (?) partway
   along the title: it ran off the modal's right edge. There the bubble hangs
   from the header instead, across its full width. */
@media (max-width: 767px) {
  /* The header, and so the bubble in it, above the cards of the tree below. */
  .clm-head { position: relative; z-index: 6; }
  .cm-voice-help.clm-help { position: static; }
  .clm-help .cm-voice-help-tip { left: 0; right: 0; width: auto; max-width: none; }
}
@media (max-width: 767px) {
  #card-modal-body .cm-ident { gap: 10px; }
  .cm-ident .cm-voice { flex-basis: 118px; }
  .cm-ident .cm-rune-ref { font-size: 40px; }
}
.cm-pos-name {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}
.cm-pos-meaning {
  font-size: 13px;
  line-height: 1.5;
  color: var(--phosphor-dim);
  opacity: 0.85;
}

.cm-section { margin-bottom: 18px; }
.cm-section-label {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--phosphor-dim);
  opacity: 0.7;
  margin-bottom: 8px;
}

.cm-assoc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px 18px;
}
.cm-assoc {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 4px;
  border-bottom: 1px dotted var(--phosphor-faint);
}
.cm-assoc-k {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  opacity: 0.7;
}
.cm-assoc-v {
  font-size: 13px;
  color: var(--phosphor);
  text-align: right;
}

.cm-kwrow {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
  opacity: 0.5;
}
.cm-kwrow.cm-active { opacity: 1; }
.cm-kwrow-label {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11.5px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  min-width: 68px;
  flex-shrink: 0;
}
.cm-kws { display: flex; flex-wrap: wrap; gap: 6px; }
.cm-kw {
  font-size: 13px;
  color: var(--phosphor);
  background: var(--wash);
}

.cm-meaning { margin-bottom: 14px; }
.cm-meaning.cm-dim { opacity: 0.55; }
.cm-meaning-label {
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  margin-bottom: 5px;
}
.cm-meaning.cm-active .cm-meaning-label {
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
}
.cm-meaning-text {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--phosphor);
}

.cm-symbols {
  font-size: 13px;
  line-height: 1.6;
  color: var(--phosphor-dim);
}

/* ── Account deletion modal ─────────────────────────────────────────────────
   The card modal in amber. Same panel, backdrop and ✕ as every other modal
   here; only the accent changes, because this is the one that does not undo. */
.card-modal-warn .card-modal-panel {
  border-color: var(--amber);
  box-shadow: 0 0 24px rgba(var(--halo-amber-rgb), 0.22),
              inset 0 0 60px rgba(var(--halo-amber-rgb), 0.05);
}
.card-modal-warn .cm-head   { border-bottom-color: rgba(var(--amber-rgb), 0.28); }
.card-modal-warn .cm-name   { color: var(--amber); text-shadow: 0 0 10px rgba(var(--halo-amber-rgb), 0.45); }
.card-modal-warn .cm-meaning-text b {
  color: var(--amber);
  font-weight: normal;
  text-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.3);
}
.card-modal-warn .cm-meaning-text a {
  color: var(--amber);
  border-bottom: 1px dotted var(--amber);
}
.acct-del-stop {
  border-left: 2px solid var(--amber);
  padding-left: 12px;
  color: var(--amber) !important;
  text-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.25);
}
.acct-del-input {
  width: 100%;
  height: 30px;
  text-align: left;
  border-color: var(--amber);
  color: var(--amber);
}
.acct-del-actions {
  display: flex;
  gap: 8px;
  margin-top: 4px;
}
.acct-del-actions .term-btn { flex: 1; margin: 0; }
/* Until the word is typed the confirm is inert, and has to look it: it is the
   one control in here that does something. */
.acct-del-go {
  border-color: var(--amber);
  color: var(--amber);
}
.acct-del-go:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  text-shadow: none;
  box-shadow: none;
}
.acct-del-error {
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: var(--amber);
  margin-top: 12px;
}
.acct-del-error[hidden] { display: none !important; }

/* The disclosure is a document to read and a field to type in, not terminal
   chrome; terminal.css suppresses both selection and the pointer globally. */
#acct-del-modal, #acct-del-modal * {
  cursor: auto;
  user-select: text;
  -webkit-user-select: text;
}
#acct-del-modal button { cursor: pointer; }

@media (max-width: 767px) {
  /* Every modal opens in the middle of the screen with its whole frame in
     view -- never as a sheet anchored to the foot with its sides cut off. The
     owner's call (2026-09-24): a modal that rises from the bottom edge reads
     as part of the page, and one with all four borders reads as what it is.
     A long one scrolls inside its frame. */
  .card-modal { align-items: center; padding: 16px; }
  .card-modal-panel {
    width: 100%;
    max-height: 82vh;
  }
  .cm-name { font-size: 26px; }
}


/* ── Runes: frame kept, stone inside it ───────────────────────────────────
   The frame stays. An earlier pass stripped it on the theory that a stone
   needs no box; on screen the box was doing real work -- it gives the slot an
   edge, carries the selected and merkstav states, and keeps a rune the same
   kind of object as a card in a spread. What follows is the small set of
   adjustments a stone needs *inside* that frame, not instead of it.

   Placed last in the file on purpose: several rules above re-draw frame
   furniture at the same specificity as these, and at equal specificity the
   last rule wins. Raising specificity instead would work today and lose again
   to the next rule someone adds.

   Superseded for a drawn stone on the canvas (2026-09-25, the owner's call):
   see "Runes on the canvas: the stone and nothing else". The frame is kept for
   an undrawn slot and wherever else a stone is shown framed. */

/* The vignette stays off, and this follows the existing rule rather than
   departing from it: .card-with-image already kills it, because it is depth
   for an *empty* frame and a frame with something in it is not empty. A stone
   is something in it. Left on, the top-and-bottom phosphor wash lands across
   the stone's own shoulders and fights the sheen that is already lighting it
   from up-left. */
.tarot-card.is-rune .card-frame::after {
  display: none;
}

/* Clearance under the position badge. .rank-badge is absolutely placed at
   top:-8px, so it straddles the frame's top edge and hangs a few pixels inside
   it. A card placeholder's own 10px of padding is what keeps a card name clear
   of it; the stone needs the same allowance. */
.card-placeholder.is-stone {
  padding-top: 11px;
}

/* The voice meter is narrower here. Full width suits a card, which is wider
   than it is busy; under a stone the meter was the widest thing on the tile and
   pulled the eye away from the mark, which is what the reader is actually
   looking at. */
.card-placeholder.is-stone .card-meta {
  width: 76%;
  padding-left: 0;
  padding-right: 0;
  margin: 1px auto 0;
}

/* Merkstav already turns the frame amber, the same as a reversed card. Carry it
   onto the stone's own edge too, so the state reads on the object and not only
   on the box around it. */
.tarot-card.is-rune.inverted .rune-stone-body {
  stroke: rgba(var(--amber-rgb), 0.35);
}

/* ══════════════════════════════════════════════════ ARCHIVE PAGE ══════════ */

/* A single column, not a grid.
   Load-bearing: a narrow grid cell forces the question to truncate and leaves
   no room for the spread, which is what made the rail's entry a 42-character
   slice. A chronological archive is scanned vertically anyway, so the column
   takes the width and caps rather than filling a wide display edge to edge. */
#archive-page {
  overflow-y: auto;
  min-height: 0;
  padding: 24px 32px 64px;
}

.arc-head {
  max-width: 1000px;
  margin: 0 auto 4px;
}

.arc-head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.arc-count {
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  white-space: nowrap;
}

.arc-search-wrap {
  position: relative;
  margin: 12px 0 2px;
}

/* The hint, drawn rather than typed: an icon and a word sharing one baseline,
   in the terminal's own colour. Out of the pointer tree so a click lands in
   the field beneath it, and gone the moment anything is typed -- which is what
   a placeholder does, and the only behaviour worth keeping from one. */
.arc-search-hint {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 7px;
  pointer-events: none;
  color: var(--phosphor-dim);
  font-size: 13px;
  letter-spacing: 1px;
  opacity: 0.75;
  transition: opacity 0.12s;
}

.arc-search-hint svg { display: block; }

/* Gone once something is typed, or when the page arrived with a query already
   in the box. Not on focus: a hint that leaves before the first keystroke
   takes the prompt away exactly when it is being read. */
.arc-search:not(:placeholder-shown) ~ .arc-search-hint { opacity: 0; }

.arc-search {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-faint);
  border-bottom-color: var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 1px;
  padding: 9px 11px 9px 32px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.arc-search::placeholder {
  color: var(--phosphor-dim);
  opacity: 0.7;
  letter-spacing: 1px;
}

.arc-search:focus {
  border-color: var(--phosphor-dim);
  box-shadow: 0 0 0 1px var(--phosphor-glow), inset 0 0 18px var(--phosphor-glow-faint);
}

/* The list dims while a new query is in the air, so a slow search reads as
   working rather than as an empty archive. */
#arc-list[aria-busy="true"] {
  opacity: 0.45;
  transition: opacity 0.12s;
}

.arc-filters {
  margin: 10px 0 4px;
}

.arc-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* Month headings run the width of the column and read as log section rules. */
.arc-month {
  max-width: 1000px;
  margin: 22px auto 6px;
  padding: 5px 12px 3px;
  font-size: 12.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  border-top: 1px solid var(--phosphor-faint);
  text-transform: uppercase;
  text-align: right;
}

.arc-month:first-child { margin-top: 6px; }

/* ── One entry ───────────────────────────────────────────────────────────── */

.ae {
  position: relative;
  max-width: 1000px;
  margin: 0 auto;
  border-bottom: 1px solid var(--phosphor-faint);
  transition: background 0.1s;
}

/* Alternate rows, barely. Charcoal against the near-black panel: enough to
   follow a row across to its date without the list reading as banded.
   Deliberately hue-neutral -- --bg and --bg-panel are rewritten at runtime with
   the reader's own phosphor hue, and a tinted stripe would drift with it.

   Applied by arcStripe() rather than :nth-child, because entries are
   interleaved with month headings and the filter hides rows: only the visible
   ones should alternate, and neither of those is expressible in a selector. */
.ae--alt { background: var(--bg-row-alt); }

/* After the stripe, so hover still reads on both kinds of row. */
.ae:hover { background: var(--wash-faint); }

.ae-body {
  display: block;
  padding: 12px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}

/* The headline. Two lines, clamped rather than sliced -- the rail cut the
   question at 42 characters, which decapitates most real questions.

   Amber and oblique, because it is the one thing on the entry that is the
   reader's own words rather than the system's: everything around it is the
   product describing the reading. It carried quotation marks for the same
   reason and they are gone -- at two lines clamped, a closing mark lands
   wherever the clamp happens to fall, and a quotation that opens and never
   closes reads as an error rather than as a quotation. The colour and the
   slant already say whose words these are. */
.ae-question {
  font-size: 16px;
  line-height: 1.45;
  padding-right: 36px;            /* clears the menu button */
  color: var(--amber);
  /* Synthesised oblique and synthesised bold: fonts.css ships Share Tech Mono
     at weight 400 only, so there is no italic and no bold file for the browser
     to reach for and it smears the 400 in both directions. That is fine here
     and would not be everywhere -- it is one short line at 16px, where the
     smear reads as emphasis; at 10px letter-spaced chrome it reads as mud. */
  font-style: italic;
  font-weight: 700;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── The symbol strip ────────────────────────────────────────────────────── */

/* The whole spread on one row, in draw order. Always one row, whatever the
   layout: that is what keeps entry heights uniform while the strip's length
   varies, and the varying length is the point -- it shows the size of a reading
   at a glance. Ten marks at 65px plus gaps is ~700px, which the column of
   .ae (max 1000px) takes with room to spare. */
.ae-strip {
  display: flex;
  align-items: flex-end;
  gap: 5px;
  margin-top: 9px;
  /* A Freestyle session has no fixed size, and a long one ran the row off the
     right of the page. Marks that do not fit wrap onto a second row, which is
     cut off: the strip stops at the table's width on a whole mark rather than
     halfway through one. One mark's height, so the entry's height is kept. */
  flex-wrap: wrap;
  height: 110px;
  overflow: hidden;
}

/* Every mark occupies the same box whatever it holds, so the row is even. */
.ae-mk {
  flex: 0 0 auto;
  width: 65px;
  height: 110px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ── A card ──────────────────────────────────────────────────────────────── */

/* The frame is a constant, the art is not. Decks differ in aspect (Rider-Waite
   is ~0.61, Soprafino ~0.44) and individual scans differ again, so sizing the
   border to the image would give a row of slightly different rectangles. The
   border is the fixed thing and the card is centred inside it -- the same
   contain-and-letterbox .card-frame does at full size. */
.ae-mk--card {
  border: 1px solid var(--phosphor-dim);
  background: var(--bg);
  overflow: hidden;
  box-sizing: border-box;
}

.ae-mk--card picture {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.ae-mk--card img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  margin: auto;
}

/* Reversed: amber, and the way it was drawn. Amber is how an inverted draw
   already reads everywhere else in the product. */
.ae-mk--card.ae-mk--inv {
  border-color: var(--amber);
}

.ae-mk--card.ae-mk--inv img,
.ae-mk--card.ae-mk--inv .ae-mk-glyph {
  transform: rotate(180deg);
}

.ae-mk--card.ae-mk--inv .ae-mk-glyph {
  color: var(--amber);
}

/* Drawn when a deck has no art for this card -- the text style. Not a hole. */
.ae-mk-glyph {
  font-size: 26px;
  line-height: 1;
  color: var(--phosphor);
  opacity: 0.8;
}

/* ── A stone ─────────────────────────────────────────────────────────────── */

/* A rune is not a card and is not framed like one: no rectangle, no fill, just
   the stone. The same outlines the workspace uses (rune-stone.js), one per
   codepoint, so a rune is the same stone here as it was when it was cast. */
.ae-mk--stone {
  border: none;
  background: none;
}

.ae-mk--stone .rune-stone {
  width: 100%;
  height: 100%;
}

/* Merkstav turns the whole stone, not just the mark on it, and colours the cut
   amber -- the same language .tarot-card.inverted uses at full size. The turn
   itself rides on --rune-angle, which drawnStone() writes onto the svg. */
.ae-mk--stone.ae-mk--inv .rune-stone-mark {
  fill: var(--amber);
}

/* Only ever shown where the strip is capped — see the mobile block below. */
.ae-strip-more {
  display: none;
  align-self: center;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}

.ae-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-top: 6px;
  padding-right: 36px;            /* clears the menu button */
  font-size: 12.5px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
}

/* The deck, then the spread. The deck is the more particular fact -- it says
   Soprafino or Rider-Waite, where the tradition could only say "tarot" -- so it
   leads and takes the brighter register. */
.ae-deck {
  color: var(--phosphor);
  text-transform: uppercase;
}

.ae-sep { opacity: 0.5; }

.ae-kind { text-transform: uppercase; }

/* Said in words. The rail turned an unfinished reading amber and left the
   reader to work out what amber meant. */
.ae-status {
  color: var(--amber);
  border: 1px solid color-mix(in srgb, var(--amber) 35%, transparent);
  border-radius: 2px;
  padding: 1px 6px;
  font-size: 11.5px;
}

.ae-foot {
  margin-top: 5px;
  font-size: 12.5px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
  text-align: right;
}

.ae--hidden { display: none; }

/* The reading currently open in the workspace, marked in the recents. */
.ae--open {
  background: var(--wash-faint);
  box-shadow: inset 2px 0 0 var(--halo);
}

.ae-deleting {
  opacity: 0;
  transform: translateX(-12px);
  transition: opacity 0.25s, transform 0.25s;
}

.ae-rename-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--phosphor-dim);
  color: var(--amber);
  font-family: inherit;
  font-size: 15px;
  font-style: italic;
  padding: 2px 4px;
}

/* ── Entry menu ──────────────────────────────────────────────────────────── */

.ae-menu {
  position: absolute;
  right: 6px;
  top: 10px;
  width: 34px;
  height: 34px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--phosphor);
  font-size: 24px;
  letter-spacing: -3px;
  font-family: 'Share Tech Mono', monospace;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  opacity: 0;
  transition: opacity 0.15s, background 0.15s;
  padding: 0;
}

.ae:hover .ae-menu,
.ae.aom-open .ae-menu { opacity: 1; }

.ae-menu:hover,
.ae.aom-open .ae-menu { background: var(--wash); }

/* There is width here, unlike the rail, so the actions do not have to hide
   behind a hover the touch reader never gets. */
@media (hover: none) {
  .ae-menu { opacity: 1; }
}

/* ── Empty state and paging ──────────────────────────────────────────────── */

.arc-empty {
  max-width: 1000px;
  margin: 24px auto;
  color: var(--phosphor-dim);
  font-size: 12.5px;
  letter-spacing: 1.5px;
  line-height: 2;
}

.arc-empty span { opacity: 0.55; font-size: 12px; }

#arc-more {
  max-width: 1000px;
  margin: 20px auto 0;
  text-align: center;
}

.arc-more-btn {
  background: transparent;
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 2px;
  padding: 8px 20px;
  cursor: pointer;
  transition: background 0.15s;
}

.arc-more-btn:hover:not(:disabled) { background: var(--wash); }
.arc-more-btn:disabled { opacity: 0.5; cursor: default; }

@media (max-width: 767px) {
  #archive-page { padding: 16px 16px 48px; }
  .ae-body { padding: 11px 8px; }
  .ae-question { font-size: 15px; padding-right: 40px; }
  .ae-meta { padding-right: 40px; }

  /* At 375px there is ~340px of entry, so ten marks would be ~30px each --
     below what dense line art reads at. Better to show six honestly and say
     how many are left than to render the whole spread as a smear.
     Keep STRIP_MOBILE_CAP in wyrdos/ui/helpers.py in step with the 6 here:
     it is what decides the "+N" the template renders. */
  .ae-mk { width: 44px; height: 74px; }
  .ae-strip { height: 74px; }
  .ae-mk--stone .rune-stone { width: 100%; height: 100%; }
  .ae-mk:nth-of-type(n+7) { display: none; }
  .ae-strip-more { display: block; }
  .ae-mk-glyph { font-size: 19px; }
}


/* ── Recent revelations, on the New Revelation screen ─────────────────────── */

/* What the rail's list was actually good for: getting back to the last day or
   two. Here it is on the screen you land on, at the size the archive renders,
   rather than beside you as a 42-character slice. */
.nr-recent {
  margin-top: 30px;
  border-top: 1px solid var(--phosphor-faint);
  padding-top: 10px;
}

.nr-recent-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 2px;
}

/* 14px, which is .panel-header's size. That is the point: this is a
   section heading, not chrome, and at the 10px the rest of
   the letter-spaced chrome uses it read as a caption on the entries below
   rather than as a label over them.

   The two move together. They are a heading and its action on one baseline,
   and a half-step between them would look like a mistake. */
.nr-recent-title {
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}

.nr-recent-all {
  font-size: 14px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-decoration: none;
  transition: color 0.15s;
}

.nr-recent-all:hover { color: var(--phosphor); }

/* The welcome, where RECENT will stand for a member with no readings yet
   (grimoire.html): the same rule and heading as RECENT, a line saying what to
   do, then the plan's numbers as three quiet tiles. */
.nr-welcome {
  margin-top: 30px;
  border-top: 1px solid var(--phosphor-faint);
  padding-top: 10px;
}
.nr-welcome-lede {
  margin: 6px 0 14px;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--prose-ink-lo);
}
.nr-welcome-facts { display: flex; gap: 8px; }
.nr-welcome-facts .nr-fact { flex: 1 1 0; min-width: 0; }
.nr-fact {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  padding: 12px 8px 10px;
  border: 1px solid var(--phosphor-faint);
  background: rgba(var(--lift), 0.02);
  text-align: center;
  text-decoration: none;
}
.nr-fact-n {
  font-family: 'VT323', monospace;
  font-size: 32px;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--phosphor-glow-strong);
}
.nr-fact-icon { height: 32px; display: flex; align-items: center; }
.nr-fact-icon svg { width: 26px; height: 26px; }
.nr-fact-l { font-size: 10.5px; letter-spacing: 1.5px; line-height: 1.4; color: var(--phosphor-dim); }
.nr-fact-link { transition: border-color 0.15s, background 0.15s; }
.nr-fact-link:hover { border-color: var(--phosphor-dim); background: var(--wash); }
.nr-fact-link:hover .nr-fact-l { color: var(--phosphor); }

/* A guest reading that came with the sign-in: one line, said once. */
#nr-panel .nr-claimed {
  flex-direction: row;
  align-items: center;
  gap: 10px;
  margin-top: 26px;
  padding: 10px 14px;
  border: 1px solid var(--phosphor-faint);
  background: var(--wash);
  font-size: 13px;
  letter-spacing: 0.5px;
  color: var(--phosphor);
}
#nr-panel .nr-claimed[hidden] { display: none; }
.nr-claimed-mark { color: var(--phosphor); text-shadow: 0 0 6px var(--phosphor-glow-strong); }
.nr-claimed a {
  margin-left: auto;
  font-size: 12px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  text-decoration: none;
  white-space: nowrap;
}
.nr-claimed a:hover { color: var(--phosphor); }
@media (max-width: 767px) {
  #nr-panel .nr-claimed { flex-wrap: wrap; row-gap: 4px; }
  .nr-claimed a { margin-left: 22px; flex-basis: 100%; }
}

/* The entry component is the archive's, so it arrives at archive width. Here
   it lives in a form column and takes the column instead. */
#nr-recent-list .ae { max-width: none; }
/* Nothing follows the last entry here, so its rule would only underline the
   end of the screen (owner, 2026-10-04). The archive's list keeps its rules. */
#nr-recent-list .ae:last-child { border-bottom: none; }

/* ── Compact entry ───────────────────────────────────────────────────────── */

/* One row: the question, and the date at the right. Five of these is a way
   back to the last few days, not a second archive -- the strip, the deck and
   the layout are not rendered at all (not hidden), so the page never asks the
   database for draws it will not draw. */
.ae--compact .ae-body {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 9px 4px;
}

/* One line here, not two. A recent reading is being recognised rather than
   read -- and the whole question is a click away in the archive. */
.ae--compact .ae-question {
  flex: 1 1 auto;
  min-width: 0;
  -webkit-line-clamp: 1;
  font-size: 14.5px;
}

.ae--compact .ae-foot {
  flex: 0 0 auto;
  margin-top: 0;
  white-space: nowrap;
}

/* ── The yin / yang toggle ────────────────────────────────────────────────
   A switch whose knob is a taijitu: it travels the track and rotates through
   180 degrees as it goes, because the symbol's symmetry is rotational rather
   than mirrored -- mirroring it produces a taijitu that is subtly wrong in a
   way people notice without being able to say why. The lit half ends up
   facing whichever end of the track the knob is resting at, so the glyph
   leans out toward the side it is on.

   Half, not lobe: the glyph's divider runs top to bottom, so its two halves
   are left and right while its two lobes are high and low. The rotation
   swings both, and it is the halves that the travel is about.

   No S-curve dividing the *track*. It was tried; it does not survive 20px --
   which is not in tension with the one inside the knob. Across 38px of track
   an S is a long shallow curve that reads as a wobble; across the knob's 14px
   disc it is two tight half-turns that read as the symbol they belong to. */
.yy-toggle {
  display: inline-flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  /* Leading the cluster and set apart from it. The gap is what does the work:
     without it this is a fourth button beside SIGN IN and JOIN (FREE), and a
     setting starts reading as a third call to action. */
  margin-right: 8px;
  line-height: 0;
}

.yy-track {
  display: inline-block;
  position: relative;
  width: 38px;
  height: 20px;
  border: 1px solid var(--phosphor-dim);
  border-radius: 999px;
  background: var(--bg-panel);
  box-shadow: inset 0 0 8px var(--phosphor-glow-faint);
  transition: border-color 0.2s, box-shadow 0.2s;
}

.yy-toggle:hover .yy-track {
  border-color: var(--phosphor);
  box-shadow: inset 0 0 10px var(--phosphor-glow);
}

.yy-toggle:focus-visible .yy-track {
  outline: 1px solid var(--phosphor);
  outline-offset: 2px;
}

.yy-knob {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 16px;
  height: 16px;
  transform: translateX(0) rotate(0deg);
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  filter: drop-shadow(0 0 4px var(--phosphor-glow-strong));
}

:root[data-theme="light"] .yy-knob {
  transform: translateX(18px) rotate(180deg);
}

/* The two halves are the theme's own ink and ground, so the glyph is made of
   the same two colours the page is. The rim keeps the ground half from
   dissolving into the track. */
.yy-ink, .yy-seed-ink       { fill: var(--phosphor); }
.yy-seed-ground             { fill: var(--bg); }
.yy-ground {
  fill: var(--bg);
  stroke: var(--phosphor-dim);
  stroke-width: 0.6;
}

@media (prefers-reduced-motion: reduce) {
  .yy-knob { transition: none; }
}

/* .header-right is tight on narrow screens, and the stage badge is the thing
   to drop first: it is a statement about the product, while this is a control
   somebody may need in order to read the page at all. */
@media (max-width: 560px) {
  .header-right .stage-badge { display: none; }
}


/* ── Icons: cutting the black out of them, on a pale page ────────────────
   Every icon_*.png in this app is a bright glyph on an OPAQUE BLACK
   background -- no alpha channel at all. What erased that black was
   `mix-blend-mode: screen`, and screen only erases it when something opaque
   is painted behind the icon *inside its own stacking context*. That is not
   a guarantee anything here makes: `.lp-console` sets opacity and transform,
   so it opens a stacking context and paints nothing into it, and the icon in
   the landing's oracle header had no backdrop to blend against. The raw
   black tile came through.

   On the tube that tile is black on a page that is already black, so the bug
   was invisible from the beginning and stays invisible. On celadon it is a
   dark square around the oracle's sigil. So the mask is scoped to light
   mode, and dark mode is left exactly as it was.

   That scoping is not tidiness, it is arithmetic. A luminance mask sets
   alpha from the PNG's own brightness, and the filter chain has *already*
   scaled the colour by that same brightness -- so together they render a
   pixel at L squared rather than at L. Measured on these six files, that
   costs icon_display two thirds of its weight and icon_sidebar
   nineteen twentieths: solid cores survive and every antialiased stroke
   thins out and breaks apart. In light mode the loss lands on ink over a
   pale ground and reads as a softer edge; on the tube it reads as a broken
   icon. Anything that changes here has to answer that squaring first.

   The real repair is an alpha channel in the six PNGs, which would let both
   the mask and the blend go away in both themes. That is a change to the
   assets rather than to the stylesheet.

   The four mask properties are the ones #ws-settings-btn has always used. */
:root[data-theme="light"] .settings-section-icon,
:root[data-theme="light"] .channel-oracle-icon,
:root[data-theme="light"] .sidebar-nav-icon,
:root[data-theme="light"] .brand-logo,
:root[data-theme="light"] .panel-header-icon,
:root[data-theme="light"] .strip-icon,
:root[data-theme="light"] .mm-player-icon {
  -webkit-mask-size: contain;          mask-size: contain;
  -webkit-mask-repeat: no-repeat;      mask-repeat: no-repeat;
  -webkit-mask-position: center;       mask-position: center;
  /* Luminance, not alpha. These files have no alpha to read. */
  -webkit-mask-source-type: luminance;
  -webkit-mask-mode: luminance;        mask-mode: luminance;
}

/* One rule per file, keyed off the src, because the classes above are shared
   across icons and CSS cannot read an element's own src into a url(). */
:root[data-theme="light"] img[src$="icon_oracle.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_oracle.png');
          mask-image: url('/static/img/icons/icon_oracle.png');
}
:root[data-theme="light"] img[src$="icon_settings.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_settings.png');
          mask-image: url('/static/img/icons/icon_settings.png');
}
:root[data-theme="light"] img[src$="icon_newreading.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_newreading.png');
          mask-image: url('/static/img/icons/icon_newreading.png');
}
:root[data-theme="light"] img[src$="icon_display.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_display.png');
          mask-image: url('/static/img/icons/icon_display.png');
}
:root[data-theme="light"] img[src$="icon_mindmachine.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_mindmachine.png');
          mask-image: url('/static/img/icons/icon_mindmachine.png');
}
:root[data-theme="light"] img[src$="icon_sidebar.png"] {
  -webkit-mask-image: url('/static/img/icons/icon_sidebar.png');
          mask-image: url('/static/img/icons/icon_sidebar.png');
}


/* The oracle pane's frosting, in transmitted light rather than emitted.

   Dark and light want the same thing -- a backdrop quiet enough to read over --
   by opposite means. On the tube the card behind is brighter than the pane, so
   it is dimmed; on the page it is ink, darker than the paper, so dimming it
   would drive it toward the text rather than away. It is lifted instead, and
   the pale scrim is carried a little heavier because paper hides less than
   black does. The dulling is the same either way: it was the vividness that
   made the text hard to read, and that is not a property of the theme. */
:root[data-theme="light"] #detail-oracle {
  background-color: rgba(var(--bg-rgb), 0.72);
  backdrop-filter: blur(13px) saturate(0.3) brightness(1.18);
  -webkit-backdrop-filter: blur(13px) saturate(0.3) brightness(1.18);
}


/* ── Clarifiers ──────────────────────────────────────────────
   The badge on each card, the clarifier modal and the meaning modal's
   clarifiers button and tags. Built by grimoire-clarifiers.js; design in
   notes/dev_notes/CLARIFIERS_PLAN.md. */

/* The badge: outside the card, clear of its right edge, and level with the
   card's name. It used to sit on the top-right corner, overlapping the edge,
   and a long position name (THE CHALLENGE, EXTERNAL INFLUENCES) ran under it.
   Off the card it can collide with nothing the card draws. --badge-y is set per
   card by grimoire-clarifiers.js from where that card's name sits; 8px is the
   fallback for a card whose name has not laid out yet. Dim with a `+` while
   the card can take a clarifier; solid with the count once it has some. */
.clarifier-badge {
  position: absolute;
  top: var(--badge-y, 8px);
  left: calc(100% + 5px);
  /* The icon alone, no square round it: 23px, which is the 22px the icon drew
     at inside the old square's padding, 5% larger. Its glows are the icon's
     own shape (drop-shadow), not a box's. */
  width: 23px;
  height: 23px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--phosphor-dim);
  cursor: pointer;
  z-index: 11;
  transition: color 0.15s, filter 0.15s, opacity 0.15s;
  /* A tap must not paint the browser's highlight over it: on a phone a square
     flashed where the icon was while the modal came up (owner, 2026-10-03).
     Nor the focus ring a tap leaves; keyboard focus keeps its glow below. */
  -webkit-tap-highlight-color: transparent;
}
.clarifier-badge:focus:not(:focus-visible) { outline: none; }
/* The glyph is small; the target is not. The pseudo-element widens what a
   finger can hit without widening what the eye sees -- 45px, over the 44px
   a touch target wants. */
.clarifier-badge::before {
  content: '';
  position: absolute;
  inset: -11px;
}
.clarifier-badge .clarifier-icon { width: 100%; height: 100%; display: block; }
.clarifier-badge .cli-front,
.clarifier-badge .cli-count-bg { fill: var(--bg); }
.clarifier-badge .cli-count-wrap { display: none; }
.clarifier-badge .cli-count {
  fill: currentColor;
  stroke: none;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  font-weight: bold;
}
.clarifier-badge.is-open { opacity: 0.75; }
.clarifier-badge.has-count {
  color: var(--phosphor);
  filter: drop-shadow(0 0 4px var(--phosphor-glow));
}
.clarifier-badge.has-count .cli-plus  { display: none; }
.clarifier-badge.has-count .cli-count-wrap { display: inline; }
.clarifier-badge:hover,
.clarifier-badge:focus-visible {
  opacity: 1;
  color: var(--phosphor);
  filter: drop-shadow(0 0 5px var(--phosphor-glow-strong));
  outline: none;
}

/* A card laid across another (the Celtic Cross's Challenge) is turned 90°
   clockwise about its centre, and the badge turns with it. Beside it would be
   off the end of a phone's screen -- that card is the widest thing in any
   spread -- so it sits just above the card's top edge on screen, near its
   right end, still clear of the card. In the card's own unrotated frame that
   is outside its left edge (on screen, above) and near its top (on screen,
   the right end); the badge then turns back so the digit stands upright. */
/* Any other angle a custom layout sets (45° steps, full circle): the badge
   stays on the card's own corner and is turned back so its digit stands
   upright on screen. The 90° rule below is more deliberate, and wins by
   coming later. */
.tarot-card[data-rot] > .clarifier-badge {
  transform: rotate(calc(-1 * var(--card-rot, 0deg)));
}
.tarot-card[data-rot="90"] > .clarifier-badge {
  top: 4px;
  left: -28px;
  transform: rotate(-90deg);
}

/* One pulse when the badge first appears, then still. */
@keyframes clarifierPulse {
  0%   { filter: drop-shadow(0 0 0 transparent); color: var(--phosphor); opacity: 1; }
  45%  { filter: drop-shadow(0 0 7px var(--phosphor-glow-strong)); color: var(--phosphor); opacity: 1; }
  100% { filter: drop-shadow(0 0 0 transparent); }
}
.clarifier-badge.clarifier-pulse { animation: clarifierPulse 1.3s ease-out 1; }

/* Opened from a meaning modal's CLARIFIERS button, the clarifier modal sits
   over it -- above the meaning modal, below one stacked from a face (4100). */
.clarifier-modal.over-meaning { z-index: 4050; }

/* The meaning modal's clarifiers button: under the position, the badge's mark
   beside the word. The mark shows its plus while there are none and the count
   once there are, as the badge does. */
.cm-clar-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  padding: 4px 10px 4px 8px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.cm-clar-btn:hover,
.cm-clar-btn:focus-visible {
  color: var(--phosphor);
  border-color: var(--phosphor-dim);
  background: var(--wash-faint);
  outline: none;
}
.cm-clar-btn.has-count { color: var(--phosphor); border-color: var(--phosphor-dim); }
.cm-clar-btn .clarifier-icon { width: 20px; height: 20px; display: block; flex: none; }
.cm-clar-btn .cli-front,
.cm-clar-btn .cli-count-bg { fill: var(--bg-panel); }
.cm-clar-btn .cli-count-wrap { display: none; }
.cm-clar-btn.has-count .cli-plus { display: none; }
.cm-clar-btn.has-count .cli-count-wrap { display: inline; }
.cm-clar-btn .cli-count {
  fill: currentColor;
  stroke: none;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  font-weight: bold;
}

/* With a mouse, the dim `+` waits for the card it belongs to: one on every
   symbol of a spread was clutter (owner, 2026-09-25). It shows while the
   pointer is on the card -- or on the badge itself, which is the card's child,
   so crossing to it keeps it up -- and while the card or the badge has
   keyboard focus. Keyboard focus only (:focus-visible): a click focuses the
   badge too, and :focus-within kept it up after the click until focus went
   somewhere else. The short delay before it goes forgives a pointer that
   slips off the edge. A badge with a count -- a clarifier has been drawn --
   always shows its number: that one says something without being asked.
   Touch screens keep it as it was; they have no hover to wait for.

   Once shown it stays for 5 seconds after the pointer leaves the card
   (.clar-linger, grimoire-clarifiers.js), so a pointer that drifts off on its
   way to the small mark still finds it there; and while it shows it swings
   phosphor to amber, as every waiting control does (owner, 2026-10-20). The
   swing is beginReady, on the mark's currentColor. That keyframe also paints
   a button's ground and box glow, which a bare icon must not show, and only
   an !important declaration outranks an animation -- hence those two. */
@media (hover: hover) and (pointer: fine) {
  #card-canvas .tarot-card > .clarifier-badge.is-open {
    opacity: 0;
    pointer-events: none;
    animation: none;
    transition: opacity 0.15s ease 0.2s;
  }
  #card-canvas .tarot-card:hover > .clarifier-badge.is-open,
  #card-canvas .tarot-card.clar-linger > .clarifier-badge.is-open,
  #card-canvas .tarot-card:focus-visible > .clarifier-badge.is-open,
  #card-canvas .tarot-card > .clarifier-badge.is-open:focus-visible {
    opacity: 1;
    pointer-events: auto;
    transition-delay: 0s;
    animation: beginReady 0.7s ease-in-out infinite;
    background: none !important;
    box-shadow: none !important;
  }
}
/* The first-time hint beside it (grimoire-clarifiers.js, _grClarifierPillShow):
   a pill joined to the `+` by a line, fixed to the viewport so the canvas's
   zoom and twist leave it legible, and placed there each frame by transform.
   Under every modal (4000 and up). Steady: the `+` it points at flashes. */
.clar-hint {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 3900;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}
.clar-hint.is-shown { opacity: 1; }
.clar-hint-pill {
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
  padding: 4px 12px;
  border: 1px solid var(--phosphor-dim);
  border-radius: 999px;
  background: var(--bg-panel);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  line-height: 1.3;
  letter-spacing: 2px;
  box-shadow: 0 0 10px var(--phosphor-glow);
}
.clar-hint-line {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  background: var(--phosphor-dim);
}
@media (prefers-reduced-motion: reduce) {
  .clar-hint { transition: none; }
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
  #card-canvas .tarot-card:hover > .clarifier-badge.is-open,
  #card-canvas .tarot-card.clar-linger > .clarifier-badge.is-open,
  #card-canvas .tarot-card:focus-visible > .clarifier-badge.is-open,
  #card-canvas .tarot-card > .clarifier-badge.is-open:focus-visible {
    animation: none;
    color: var(--phosphor);
  }
}

/* Touch screens draw the dim `+` beside every card that can still take a
   clarifier, and keep it there: with no hover to wait for, it was hidden
   outright for a while (2026-09-26), and the meaning modal's CLARIFIERS button
   was the only way in -- one tap too deep to be found (owner, 2026-10-03).
   Once the reading's clarifiers are all drawn the `+` goes, and only the cards
   that have one keep their badge, with its count (_grClarifierBadgeState). */

/* DRAW CLARIFIER with the Mind Machine on (grimoire-clarifiers.js): the
   sequence's overlay rises above the modals (every .card-modal is 4000-4100)
   and fades in over them at once, as it fades in over the canvas for a card.
   Lowered again as the draw goes, so the modals -- with the new clarifier --
   are on top while the static fades out beneath them. */
body.mm-clarifier #gr-entrainment-overlay { z-index: 4200; }

/* With the Mind Machine on, the button is held, and a held thumb drifts: the
   drift must not scroll the modal, since a scroll the browser starts cancels
   the touch and the hold with it. The modal still scrolls from anywhere
   else. */
.term-btn.clm-draw-btn {
  touch-action: none;
  /* Nor may a long press open the browser's menu over it, or select its
     label, as it did with the Mind Machine on (owner, 2026-10-03). The ghost
     card's hold is covered the same way (#card-stage, .ghost-card, at the top
     of this file); grimoire-clarifiers.js also refuses the contextmenu. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

.term-btn.clm-draw-btn.is-charging:not(:disabled) {
  /* Above the button's own flash (.term-btn.clm-flash:not(:disabled)), which
     it replaces while the finger is down. */
  background-image: linear-gradient(90deg, var(--phosphor-glow-strong) 50%, transparent 50%);
  background-size: 200% 100%;
  background-position: 100% 0;
  animation: clmCharge 1.05s linear forwards;
}
@keyframes clmCharge { to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) {
  .term-btn.clm-draw-btn.is-charging:not(:disabled) { animation: none; background-position: 0 0; }
}

/* The CLARIFIERS button's one pulse, the first time it appears on a touch
   device (grimoire-clarifiers.js, _grClarifierHintOnce). Held until the modal
   has opened, then twice round, then still. */
@keyframes cmClarPulse {
  0%, 100% { border-color: var(--phosphor-faint); box-shadow: 0 0 0 transparent; }
  50%      { border-color: var(--phosphor); color: var(--phosphor);
             box-shadow: 0 0 10px var(--phosphor-glow-strong); }
}
.cm-clar-btn.cm-clar-pulse { animation: cmClarPulse 1.1s ease-in-out 0.4s 2; }

/* The CLARIFIERS button flashes for as long as the reading can take another
   (owner, 2026-09-26) -- with the swing every waiting action uses (BEGIN,
   READ IT, + POSITION: beginReady); once its clarifiers are all drawn it is
   still. Comes after the one-time hint pulse,
   which it replaces while it runs. */
.cm-clar-btn.can-draw { animation: beginReady 0.7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .cm-clar-btn.can-draw { animation: none; }
}

/* The meaning modal opened from a face in the clarifier modal sits above it,
   so closing it returns the reader to the clarifiers. */
.card-modal.stacked { z-index: 4100; }

/* A face in the clarifier modal opens its meaning, as a card in the spread does. */
.clm-card.clm-opens { cursor: pointer; }
.clm-card.clm-opens:hover .card-frame,
.clm-card.clm-opens:hover .card-with-image,
.clm-card.clm-opens:focus-visible .card-frame,
.clm-card.clm-opens:focus-visible .card-with-image {
  box-shadow: 0 0 14px var(--phosphor-glow-strong);
}
.clm-card.clm-opens:focus-visible { outline: none; }

/* The modal: the meaning modal's frame, a little wider for the tree. */
.clarifier-modal .card-modal-panel { width: min(620px, 100%); }
.clm-head { justify-content: space-between; }
.clm-budget {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}
.clm-tree { display: flex; flex-direction: column; gap: 12px; }
.clm-parent,
.clm-row { display: flex; align-items: center; gap: 12px; }
.clm-row { padding-left: 22px; }
.clm-branch {
  font-family: 'Share Tech Mono', monospace;
  font-size: 15px;
  color: var(--phosphor-dim);
  white-space: pre;
  flex-shrink: 0;
}
/* The same card face the spread draws, at the modal's size. Out of the canvas
   it is in flow, so the width is all it needs. */
.clm-card.tarot-card { width: 70px; flex-shrink: 0; }
.clm-parent .clm-card.tarot-card { width: 88px; }
.clm-card .card-meta,
.clm-card .card-image-name { display: none; }
/* A rune here is only its mark in its stone: the name and sound are in the
   label beside it. */
.clm-card.is-rune .card-name,
.clm-card.is-rune .rune-phoneme,
.clm-card.is-rune .divider { display: none; }
/* ...and the stone stands on its own, without the card's frame around it.
   TEXT style has no stone, so its bare glyph keeps the frame. The hover glow
   moves onto the stone itself. */
.clm-card.is-rune .card-frame:has(.is-stone) {
  border: none;
  box-shadow: none;
  background: transparent;
  filter: none;
  aspect-ratio: auto;
  overflow: visible;
}
.clm-card.is-rune .card-frame:has(.is-stone) .card-placeholder.is-stone { padding: 0; }
.clm-card.clm-opens.is-rune:hover .card-frame:has(.is-stone),
.clm-card.clm-opens.is-rune:focus-visible .card-frame:has(.is-stone) { box-shadow: none; }
.clm-card.clm-opens.is-rune:hover .rune-stone,
.clm-card.clm-opens.is-rune:focus-visible .rune-stone {
  filter: drop-shadow(0 0 8px var(--phosphor-glow-strong));
}
.clm-ghost .card-frame {
  border: 1px dashed var(--phosphor-dim);
  background: transparent;
  box-shadow: none;
}
.clm-label { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.clm-pos,
.clm-slot {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}
.clm-sym {
  font-family: 'VT323', monospace;
  font-size: 22px;
  letter-spacing: 1px;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}
.clm-meta { font-size: 11.5px; letter-spacing: 1.5px; color: var(--phosphor-dim); }
.clm-meta.rev { color: var(--amber); }
.clm-deck { font-size: 10.5px; letter-spacing: 1.5px; color: var(--phosphor-dim); opacity: 0.8; }
.clm-dim { opacity: 0.4; }
.clm-sources { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 8px; }
.clm-source {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 1.5px;
  padding: 4px 8px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.clm-source:hover { color: var(--phosphor); }
.clm-source.selected {
  border-color: var(--phosphor);
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
}
.clm-source.locked { opacity: 0.5; cursor: not-allowed; }
.clm-left { opacity: 0.7; }
.clm-lock { color: var(--amber); }
/* DRAW CLARIFIER and READ IT are .term-btn.sm, the app's large terminal
   button: 24px VT323, stretched to the full width. In a modal of small
   labels that read as a banner. Here they are sized to their words. Two
   classes, to outrank .term-btn.sm (which also names two) -- one lost to it
   and changed nothing. */
.term-btn.clm-draw-btn,
.term-btn.clm-read-btn,
.term-btn.ol-read {
  display: inline-block;
  width: auto;
  align-self: flex-start;
  margin: 2px 0;
  padding: 2px 12px;
  font-size: 18px;
  letter-spacing: 2px;
  white-space: nowrap;
}

/* The modal's two actions swing the way BEGIN does (beginReady): outline and
   text move from phosphor to amber and back on a 0.7s ease-in-out, over at
   most a faint amber wash. A filled flash (oracleFlash) was tried here first
   and was too loud for a modal this small -- it read as an alarm rather than
   an invitation. Still for anyone who has asked for less motion, and still
   while a draw or a read is under way. */
.term-btn.clm-flash:not(:disabled) {
  animation: beginReady 0.7s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .term-btn.clm-flash:not(:disabled) { animation: none; }
}

/* READ IT sits in the row of the clarifier it reads, under its deck line. */
.clm-label .clm-read-btn { margin-top: 6px; }
.clm-unread {
  color: var(--amber);
  border: 1px solid var(--amber);
  font-size: 10px;
  padding: 0 4px;
  margin-left: 6px;
  letter-spacing: 1px;
}
.clm-note,
.clm-error {
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 1.5px;
  line-height: 1.7;
  color: var(--amber);
}

/* The note under the tree -- that the oracle reads clarifiers with the spread
   -- is about the whole modal rather than any one row, so it is centred, and
   it stands a line further off the last card (one 11px line at 1.7 is about
   19px) so it does not read as that card's caption. */
.clm-note {
  margin-top: 33px;
  text-align: center;
}

/* The clarifier modal's oracle action: READ IT, or a way into the composer. */
.clm-oracle { margin-top: 16px; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.clm-oracle .clm-note { margin-top: 0; }


/* ── The oracle pane as a log, and the follow-up composer ────────────────────
   Built by grimoire-followups.js; design in notes/dev_notes/CLARIFIERS_PLAN.md
   §4. The log continues the reading, so its answers wear the reading's own
   type; what the machine records around them (drawn, asked, unread) is set
   smaller, in the chrome's voice. */

#oracle-log {
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid var(--phosphor-faint);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ol-readwith {
  font-size: calc(var(--oracle-fs, 16px) * 0.72);
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  line-height: 1.6;
}
.ol-mark {
  font-size: calc(var(--oracle-fs, 16px) * 0.72);
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}
.ol-event {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-left: 1px solid var(--phosphor-faint);
  padding: 4px 8px;
  font-family: 'Share Tech Mono', monospace;
  font-size: calc(var(--oracle-fs, 16px) * 0.75);
  letter-spacing: 1px;
  line-height: 1.6;
  color: var(--phosphor);
  cursor: pointer;
  transition: border-color 0.15s;
}
.ol-event:hover,
.ol-event:focus-visible { border-left-color: var(--phosphor); outline: none; }
.ol-what.rev { color: var(--amber); }
.ol-unread {
  color: var(--amber);
  border: 1px solid var(--amber);
  font-size: calc(var(--oracle-fs, 16px) * 0.625);
  padding: 0 4px;
  margin-left: 4px;
}
.ol-read { align-self: flex-start; }
.ol-turn { display: flex; flex-direction: column; gap: 4px; }
/* The querent's own words, in amber: the composer they were typed into is
   amber, so a question keeps its colour from the field to the log, and it
   stands apart from the oracle's phosphor answer beneath it. */
.ol-q {
  font-size: calc(var(--oracle-fs, 16px) * 0.875);
  color: var(--amber);
  text-shadow: 0 0 6px rgba(var(--halo-amber-rgb), 0.35);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ol-also { font-size: calc(var(--oracle-fs, 16px) * 0.6875); letter-spacing: 1.5px; color: var(--amber); }
.ol-answer {
  user-select: text;
  font-size: var(--oracle-fs, 16px);
  color: var(--phosphor-dim);
  line-height: 1.75;
  white-space: pre-wrap;
  margin-top: 4px;
}
.ol-cursor { animation: blink 0.8s step-end infinite; color: var(--phosphor); }

/* Sticky to the foot of whichever box scrolls -- the pane on a desktop, its
   body on a phone -- and pushed to the bottom of a short pane by the auto
   margin, so the field never floats halfway up an empty column.

   On a desktop the pane (#detail-oracle) is what scrolls, and its content box
   only grows. That box's overflow:auto made it the sticky container all the
   same, and a container that never scrolls never lets anything stick -- the
   field sat at the end of the reading instead of at the foot of the pane. So
   the content box does not clip on a desktop. A phone keeps its own rules: the
   body scrolls inside a fixed column there, and the composer is simply the
   column's last row. */
@media (min-width: 768px) {
  #oracle-panel-content { overflow-y: visible; }
}
#oracle-composer {
  position: sticky;
  bottom: 0;
  margin-top: auto;
  z-index: 2;
  padding: 10px 14px 12px;
  border-top: 1px solid var(--phosphor-faint);
  background: var(--bg-panel);
}
/* The field is amber: the second channel, and the one live control in a pane
   of phosphor reading, so it is found without being looked for.

   One bordered box the width of the pane: the textarea is borderless inside
   it, and the footer (what is left, and ASK) sits inside the same border
   beneath the text, so nothing typed ever runs under the button. */
.oc-field {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--amber);
  box-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.18);
  cursor: text;
  transition: box-shadow 0.15s;
}
.oc-field:focus-within { box-shadow: 0 0 12px rgba(var(--halo-amber-rgb), 0.35); }

#oc-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  resize: none;
  /* Three lines of text: 14px at 1.4, and the padding. A fixed height: a
     question is capped at 150 characters, and the few that run to a fourth
     line scroll inside the field rather than growing it. */
  height: 67px;
  overflow-y: auto;
  padding: 8px 10px 2px;
  background: transparent;
  border: none;
  color: var(--amber);
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1.4;
  outline: none;
  /* The block caret, as the question field's: amber here, and a block where
     caret-shape is supported. */
  caret-color: var(--amber);
  caret-shape: block;
}
#oc-input::placeholder {
  color: var(--amber);
  opacity: 0.7;
  text-shadow: 0 0 6px var(--halo-amber);
}
#oc-input:disabled { opacity: 0.5; }
/* Two lines on a phone (owner, 2026-10-23): 14px at 1.4, and the padding.
   At three, the composer alone left a 320px-wide phone's sheet no room for
   the reading at its default height, and ran past its foot. */
@media (max-width: 767px) {
  #oc-input { height: 49px; }
}

/* The stand-in caret (.field-caret, defined with the question field) in
   amber, at this field's first glyph: its padding, and a block this font's
   height. Shown only while the field is empty and unfocused, exactly as on
   the hero. */
.oc-field .field-caret {
  left: 10px;
  top: 9px;
  width: 9px;
  height: 17px;
  background: var(--amber);
  box-shadow: 0 0 8px var(--halo-amber), 0 0 18px rgba(var(--halo-amber-rgb), 0.45);
}
.oc-field:focus-within .field-caret,
.oc-field.has-text .field-caret { display: none; }

.oc-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 6px 6px 10px;
}
.oc-left {
  font-size: 11px;
  letter-spacing: 1.5px;
  color: var(--amber);
  opacity: 0.8;
}

/* ASK: an up-arrow in a square, lit the way New Revelation is lit -- a filled
   cell with a glow -- in the field's amber. Outlined while there is nothing to
   send, filled once there is, so it says when it will do something. */
.oc-send {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  padding: 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--amber);
  color: var(--amber);
  box-shadow: 0 0 8px rgba(var(--halo-amber-rgb), 0.2);
  cursor: pointer;
  transition: box-shadow 0.15s, filter 0.15s, background 0.15s, color 0.15s;
}
.oc-send svg { width: 100%; height: 100%; display: block; }
.oc-send:not(:disabled) {
  background: var(--amber);
  color: var(--bg);
  box-shadow: 0 0 14px rgba(var(--halo-amber-rgb), 0.55);
}
.oc-send:not(:disabled):hover {
  filter: brightness(1.12);
  box-shadow: 0 0 22px rgba(var(--halo-amber-rgb), 0.75);
}
.oc-send:not(:disabled):active { filter: brightness(0.95); }
.oc-send:disabled { opacity: 0.9; cursor: default; }
.oc-also {
  display: none;
  margin-bottom: 6px;
  font-size: 11px;
  letter-spacing: 1.5px;
  color: var(--amber);
}
#oracle-composer.expanded .oc-also:not([hidden]) { display: block; }
/* Suggested questions: pills in the meaning modal's tag look (.cm-tag -- a
   soft rounded tint, no outline), but these press: a tap puts the question in
   the field. They wrap, since a question runs longer than a keyword. */
.oc-pills:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.oc-pill {
  border: none;
  border-radius: 999px;
  padding: 3px 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 0.5px;
  text-align: left;
  color: var(--phosphor);
  background: var(--wash);
  cursor: pointer;
  transition: box-shadow 0.15s;
}
.oc-pill:hover:not(:disabled),
.oc-pill:focus-visible { box-shadow: inset 0 0 0 1px var(--phosphor-dim); outline: none; }
.oc-pill:disabled { opacity: 0.45; cursor: default; }
/* On a phone, one line each, cut short with an ellipsis (owner, 2026-10-23):
   wrapped, three long questions stood 200px tall in a 45vh sheet, squeezed
   the reading to a sliver and pushed the field below the sheet's foot. The
   tap shows the whole question in the field. */
@media (max-width: 767px) {
  .oc-pill {
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
.oc-error { margin-top: 6px; font-size: 11px; letter-spacing: 1px; color: var(--amber); }
/* It takes the composer's place, so it keeps the composer's footing. */
#oracle-complete {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--bg-panel);
  margin-top: auto;
  padding: 12px 14px;
  border-top: 1px solid var(--phosphor-faint);
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-align: center;
}

/* On a phone the pane's content box clips (overflow: hidden) and its body
   scrolls, and a clipping box is still a sticky container -- so a sticky
   composer followed the sheet's visible foot upward as it was dragged down,
   until it sat over the oracle's own header. There it is simply the column's
   last row, and is clipped away with the rest when the sheet closes.

   After #oracle-complete's own rule, not before it: the two are the same
   specificity, so whichever comes later wins. Placed first, this lost, and on
   a phone the finished reading's notice stayed sticky -- dragged to its lowest,
   the sheet is only its header tall, the notice rode up over that header, and
   the header is the drag handle, so the sheet could not be pulled up again. */
@media (max-width: 767px) {
  #oracle-composer,
  #oracle-complete { position: static; }
}

/* ══ Custom layouts: /layouts and the builder ═════════════════════════════
   CUSTOM_LAYOUTS_PLAN.md. The builder is a design tool and nothing here is
   used by a reading -- a reading of a custom layout is the Grimoire's own
   canvas, unchanged. */

#layouts-page {
  overflow-y: auto;
  min-height: 0;
  padding: 24px 32px 64px;
}

.lo-lede {
  color: var(--prose-ink-lo);
  font-size: 13px;
  line-height: 1.7;
  letter-spacing: 0.3px;
  max-width: 76ch;
  margin: 10px 0 0;
}

.lo-section, .lo-locked {
  max-width: 1000px;
  margin: 32px auto 0;
}

/* A section's name, with a hairline running on from it: NEW SPREAD, YOUR
   SPREADS. Brighter than anything in the section, so the page reads in two
   blocks before it reads in items. */
.lo-section-title {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
  font-family: inherit;
  font-weight: normal;
  font-size: 12.5px;
  letter-spacing: 3px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
}
.lo-section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--phosphor-faint), transparent);
}
.lo-count {
  font-size: 11px;
  letter-spacing: 1px;
  padding: 1px 7px;
  color: var(--phosphor-dim);
  border: 1px solid var(--phosphor-faint);
  text-shadow: none;
}

/* NEW SPREAD: a row of tiles, each a starting shape -- BLANK, then the
   canonical spreads -- with its name and its tradition under it. */
.lo-starters {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: 10px;
}
.lo-starter {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 12px 8px 10px;
  border: 1px solid var(--phosphor-faint);
  background: rgba(var(--lift), 0.02);
  color: var(--phosphor);
  text-decoration: none;
  text-align: center;
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
.lo-starter:hover, .lo-starter:focus-visible {
  border-color: var(--phosphor);
  background: var(--wash);
  box-shadow: 0 0 12px var(--phosphor-glow);
}
.lo-starter-fig {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin-bottom: 4px;
}
.lo-starter-fig .lo-fig { width: 64px; height: 64px; }
.lo-starter-name { font-size: 12px; letter-spacing: 1.5px; line-height: 1.35; overflow-wrap: break-word; max-width: 100%; }
.lo-starter-kind { font-size: 10px; letter-spacing: 2px; color: var(--phosphor-dim); }
.lo-starter-blank { border-style: dashed; }
.lo-starter-blank .lo-starter-fig { color: var(--phosphor-dim); }
.lo-starter-blank .nav-plus { width: 28px; height: 28px; }
.lo-starter-blank:hover .lo-starter-fig { color: var(--phosphor); }

/* NEW SPREAD's two ways in: BLANK and TEMPLATE, side by side, a size up
   from the template tiles they lead to. */
.lo-new-choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 200px));
  gap: 12px;
}
.lo-new-choice { padding: 18px 10px 14px; font-family: inherit; cursor: pointer; }
.lo-new-choice .lo-starter-fig,
.lo-new-choice .lo-starter-fig .lo-fig { width: 72px; height: 72px; }
.lo-new-choice .lo-starter-name { font-size: 13px; letter-spacing: 2.5px; }
.lo-tpl-icon rect { stroke-width: 1.5; }

/* TEMPLATE's modal (layouts.html): the tradition switch, then CANONICAL and
   FEATURED, each a grid of the same tiles. The list scrolls inside the
   frame, so the frame stays whole in view. */
.tpl-panel { width: min(760px, 100%); }
.tpl-seg { justify-content: center; margin: 0 0 16px; }
.tpl-seg .ec-btn { height: 34px; line-height: 32px; padding: 0 18px; }
.tpl-body {
  max-height: min(60vh, 560px);
  overflow-y: auto;
  padding: 2px 2px 4px;
}
.tpl-group + .tpl-group { margin-top: 20px; }
.tpl-group-title { margin-bottom: 10px; font-size: 11.5px; }
.tpl-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.tpl-item[hidden], .tpl-group[hidden] { display: none; }
.lo-starter-credit {
  font-size: 10px;
  letter-spacing: 1px;
  color: var(--amber);
  overflow-wrap: anywhere;
}
button.lo-starter { font-family: inherit; cursor: pointer; }
.lo-starter.is-locked { opacity: 0.7; }
.lo-starter.is-locked:hover { opacity: 1; }
.lo-starter .paid-crown { display: inline-flex; vertical-align: -2px; }
.lo-starter .paid-crown-icon { width: 13px; height: 13px; }

/* A spread in miniature (_spread_figure.html). Cards are phosphor outlines;
   stones are stone, in the riverstone the Grimoire draws them in, so the two
   traditions differ at a glance. */
.lo-fig { display: block; overflow: visible; }
.lo-fig rect { stroke-width: 1.25; }
.lo-fig-card rect { fill: var(--phosphor-glow-faint); stroke: var(--phosphor-dim); }
.lo-fig-stone rect { fill: var(--rune-stone-mid); stroke: var(--prose-ink-lo); }
.lo-starter:hover .lo-fig-card rect,
.lo-card-fig:hover .lo-fig-card rect { stroke: var(--phosphor); }

/* The tradition, as a tag: tarot in phosphor, runes in stone. */
.lo-trad-tarot { color: var(--phosphor); }
.lo-trad-runes { color: var(--prose-ink); }
.lo-trad {
  padding: 1px 6px;
  border: 1px solid currentColor;
  border-color: color-mix(in srgb, currentColor 35%, transparent);
}

.lo-locked {
  border: 1px dashed var(--phosphor-faint);
  padding: 14px 16px;
  color: var(--prose-ink-lo);
  font-size: 13px;
  line-height: 1.6;
}
.lo-locked p { margin: 0 0 12px; }
.lo-locked p:last-child { margin-bottom: 0; }

.lo-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--phosphor-faint);
}

.lo-name {
  color: var(--phosphor);
  font-size: 17px;
  letter-spacing: 1.5px;
  text-decoration: none;
}
a.lo-name:hover { text-shadow: 0 0 8px var(--halo); }

.lo-meta {
  margin-top: 4px;
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}

.lo-desc {
  margin-top: 6px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--phosphor-dim);
}

.lo-actions { display: flex; gap: 6px; flex-shrink: 0; }

.lo-act, .lb-btn {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: inherit;
  font-size: 11.5px;
  letter-spacing: 2px;
  padding: 5px 9px;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}
.lo-act:hover, .lb-btn:hover:not(:disabled) {
  border-color: var(--phosphor);
  color: var(--phosphor);
  background: var(--wash);
}
.lb-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Over a card's corner in the builder: the turn icon, as in Freestyle, in
   place of the block cursor (layout-builder.js, lbRotCursor). */
body.lb-rot-hover #cursor { visibility: hidden !important; }
#lb-rot-cursor {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  transform: translate(-50%, -50%);
  color: var(--phosphor);
  filter: drop-shadow(0 0 4px var(--phosphor-glow-strong));
}
#lb-rot-cursor[hidden] { display: none; }
body.lb-rot-hover .lb-card { cursor: none; }

/* + POSITION when the builder opens: the glow BEGIN and READ IT use, until
   the first press of any button (layout-builder.js, lbInviteAdd). */
.lb-btn.lb-invite:not(:disabled) { animation: beginReady 0.7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .lb-btn.lb-invite:not(:disabled) { animation: none; }
}
.lb-btn.active { border-color: var(--phosphor); color: var(--phosphor); background: var(--wash); }
.lo-del.armed { border-color: var(--amber); color: var(--amber); }

.lo-row { flex-wrap: wrap; }

/* YOUR SPREADS, as cards (layouts.html): the shape and the name, the tags,
   then the actions along the foot -- EDIT the one lit, COPY beside it, DELETE
   apart at the right in amber. The Spread Manager's rows stay .lo-row alone. */
#lo-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
  gap: 14px;
}
#lo-list .arc-empty { margin: 0; grid-column: 1 / -1; }
#lo-list .lo-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--phosphor-faint);
  background: rgba(var(--lift), 0.02);
  transition: border-color 0.15s, box-shadow 0.15s;
}
#lo-list .lo-card:hover { border-color: color-mix(in srgb, var(--phosphor-dim) 60%, transparent); }
#lo-list .lo-card.is-featured { border-color: rgba(var(--amber-rgb), 0.45); }
.lo-card-body { display: flex; gap: 16px; align-items: flex-start; }
.lo-card-fig {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 84px;
  height: 84px;
  border: 1px solid var(--phosphor-faint);
  background: rgba(var(--shade), 0.25);
}
.lo-card-fig .lo-fig { width: 70px; height: 70px; }
.lo-card .lo-main { min-width: 0; flex: 1; }
.lo-card .lo-name {
  display: inline-block;
  font-size: 18px;
  letter-spacing: 1.5px;
  line-height: 1.3;
  overflow-wrap: anywhere;
  text-shadow: 0 0 6px var(--phosphor-glow-strong);
}
.lo-card .lo-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  font-size: 10.5px;
}
.lo-card .lo-desc { color: var(--prose-ink-lo); margin-top: 8px; }

/* SUGGEST FOR FEATURED, and its state (FEATURED_SPREADS_PLAN.md §5): the
   state as a badge, the actions as quiet text links, since they are the
   occasional thing to do here, not the everyday one. */
.lo-suggest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
}
.lo-badge {
  font-size: 10.5px;
  letter-spacing: 2px;
  padding: 2px 8px;
  color: var(--phosphor-dim);
  border: 1px solid var(--phosphor-faint);
}
.lo-badge-featured {
  color: var(--amber);
  border-color: rgba(var(--amber-rgb), 0.5);
  background: rgba(var(--amber-rgb), 0.1);
  text-shadow: 0 0 6px rgba(var(--halo-amber-rgb), 0.45);
}
.lo-suggest-note { font-size: 11.5px; letter-spacing: 0.5px; color: var(--amber); }
.lo-link {
  background: none;
  border: none;
  border-bottom: 1px dotted currentColor;
  padding: 1px 0;
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
  cursor: pointer;
}
.lo-link:hover, .lo-link:focus-visible { color: var(--phosphor); }
.lo-link:disabled { opacity: 0.5; cursor: default; }

.lo-card .lo-actions {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--phosphor-faint);
  gap: 8px;
}
.lo-card .lo-act-primary {
  border-color: var(--phosphor-dim);
  color: var(--phosphor);
  padding-left: 16px;
  padding-right: 16px;
}
.lo-card .lo-del {
  margin-left: auto;
  border-color: transparent;
  color: rgba(var(--amber-rgb), 0.8);
}
.lo-card .lo-del:hover, .lo-card .lo-del.armed {
  border-color: rgba(var(--amber-rgb), 0.5);
  color: var(--amber);
  background: rgba(var(--amber-rgb), 0.08);
}
.sp-suggest-panel { width: min(480px, 100%); }
.sp-suggest-panel .fb-check { margin-top: 12px; }
.sp-suggest-panel .fb-email-row { margin-top: 8px; }
.sp-suggest-panel .fb-email { width: 100%; }
.sp-suggest-panel .fb-actions { margin-top: 14px; }

/* The Spread Manager: admins only, below the admin's own spreads. */
.sm { margin-top: 40px; }
/* Founder claims (_founder_claims.html): the two handles, then the decision. */
.fc-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--phosphor-faint);
}
.fc-who { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; }
.fc-handle { font-size: 11px; letter-spacing: 2px; color: var(--phosphor-dim); }
.fc-handle b { font-weight: normal; font-size: 15px; letter-spacing: 1px; color: var(--phosphor); }
.fc-when { font-size: 11px; color: var(--phosphor-dim); }
.fc-row.is-decided { opacity: 0.75; }
.sm-sec { max-width: 1000px; margin: 18px auto 0; }
.sm-sec-h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--phosphor-dim);
  font-size: 12px;
  letter-spacing: 2.5px;
  color: var(--phosphor);
}
.sm-count { font-style: normal; color: var(--phosphor-dim); margin-left: 6px; }
.sm-empty { padding: 14px 0; }
.sm-row .lo-name + .cm-orient { margin-left: 8px; vertical-align: middle; }
.sm-ignored, .sm-removed { opacity: 0.7; }
.sm-meta-edit { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-items: center; }
.sm-meta-edit .lb-input { width: auto; font-size: 12px; padding: 4px 6px; }
.sm-meta-edit .sm-credit { width: 220px; }
#sm-ignored[hidden], #sm-removed[hidden] { display: none; }

/* ── The builder ─────────────────────────────────────────────────────────── */

#lb-page {
  display: grid;
  grid-template-columns: 1fr 340px;
  min-height: 0;
  min-width: 0;
}

#lb-work {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
}

.lb-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.lb-bar-actions { display: flex; align-items: center; gap: 6px; }
/* Wide on purpose: SAVE sits beside UNDO and REDO, and a slip of the pointer
   between them should land on nothing rather than on the other action. */
.lb-bar-sep {
  align-self: stretch;
  width: 1px;
  margin: 0 18px;
  background: var(--phosphor-faint);
}
.lb-bar-actions .lb-save-btn { border-color: var(--phosphor-dim); color: var(--phosphor); }
/* Larger than the builder's other buttons: these are what a spread is built
   with, and at .lb-btn's own size they were easy to miss. Wraps below the
   title where the canvas is too narrow for the row. */
.lb-bar { flex-wrap: wrap; }
.lb-bar-actions { flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-left: auto; }
.lb-bar-actions .lb-btn {
  font-size: 16px;
  letter-spacing: 2px;
  padding: 7px 13px;
}

#lb-stage {
  flex: 1;
  position: relative;
  overflow: hidden;
  touch-action: none;
  min-height: 0;
}

/* The canvas carries the pale grid: a line every cell, a stronger one every
   five. It is the same 1800x1500 the Grimoire lays spreads on. */
#lb-canvas {
  position: absolute;
  left: 0;
  top: 0;
  width: 1800px;
  height: 1500px;
  transform-origin: 0 0;
  border: 1px solid var(--phosphor-faint);
  background-color: var(--bg);
  background-image:
    linear-gradient(to right,  var(--phosphor-faint) 1px, transparent 1px),
    linear-gradient(to bottom, var(--phosphor-faint) 1px, transparent 1px),
    linear-gradient(to right,  var(--phosphor-glow-faint) 1px, transparent 1px),
    linear-gradient(to bottom, var(--phosphor-glow-faint) 1px, transparent 1px);
  background-size: 150px 150px, 150px 150px, 30px 30px, 30px 30px;
}
#lb-canvas.is-preview { background-image: none; }

/* A position's footprint: the whole drawn card with room for its tags, and
   the badge's square beside it -- what overlap is measured against. The box
   itself is not drawn; the card's outline and the badge square are. */
.lb-card {
  position: absolute;
  box-sizing: border-box;
  transform-origin: center center;
  cursor: grab;
  user-select: none;
}
.lb-card:active { cursor: grabbing; }

/* The solid outline is a whole drawn card -- the art, then the name strip and
   meter -- centred in its footprint, which is where a reading centres it.
   Left, top, width and height come from the script: they follow the
   tradition's card size and which side the badge is on. */
.lb-card-body {
  position: absolute;
  box-sizing: border-box;
  border: 1px solid var(--phosphor-dim);
  background: var(--card-ground);
  box-shadow: 0 0 12px var(--phosphor-glow);
}

/* The art: what the slot shows before the draw. */
.lb-card-art {
  position: relative;
  box-sizing: border-box;
  border-bottom: 1px dashed var(--phosphor-faint);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 10px 8px;
  text-align: center;
}

/* Which way is up: an empty card looks the same at 0 and 180 degrees. */
.lb-card-up {
  position: absolute;
  top: -1px;
  left: 50%;
  width: 18px;
  height: 3px;
  transform: translateX(-50%);
  background: var(--phosphor);
  box-shadow: 0 0 6px var(--halo);
}

.lb-card-n {
  font-family: 'VT323', monospace;
  font-size: 56px;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--halo);
}

.lb-card-label {
  font-size: 11px;
  letter-spacing: 1.5px;
  line-height: 1.4;
  color: var(--phosphor-dim);
  word-break: break-word;
}

.lb-card-notes {
  position: absolute;
  right: 6px;
  bottom: 4px;
  font-size: 13px;
  color: var(--phosphor-dim);
}

/* Where the clarifier badge will stand in a reading. Counted in the
   footprint, so a neighbour cannot sit on it. */
.lb-badge-slot {
  position: absolute;
  width: 23px;
  height: 23px;
  box-sizing: border-box;
  border: 1px dashed var(--phosphor-dim);
  color: var(--phosphor);
  font-size: 15px;
  line-height: 21px;
  text-align: center;
  text-shadow: 0 0 6px var(--halo);
}

/* Where the name strip and meter will be: two faint rules. */
.lb-card-strips {
  background:
    linear-gradient(var(--phosphor-faint), var(--phosphor-faint)) 12px 38% / calc(100% - 24px) 1px no-repeat,
    linear-gradient(var(--phosphor-faint), var(--phosphor-faint)) 12px 72% / calc(100% - 24px) 3px no-repeat;
}

.lb-card.is-selected .lb-card-body {
  border-color: var(--phosphor);
  box-shadow: 0 0 20px var(--phosphor-glow-strong), inset 0 0 12px var(--phosphor-glow);
}

.lb-card.is-invalid .lb-card-body,
.lb-card.is-invalid .lb-badge-slot { border-color: var(--red-alert, var(--amber)); }
.lb-card.is-invalid .lb-card-n { color: var(--red-alert, var(--amber)); text-shadow: none; }

/* Preview: each position as a reading will draw it. The card is the
   Grimoire's own markup (.tarot-card, .card-with-image, .rank-badge, the VOICE
   meter), so its rules lay it out; only its place in the footprint is set here
   -- the body centred, as a reading centres a drawn card on its point. */
.lb-card .lb-pv-card {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  cursor: inherit;
}
/* A drawn stave is the stone alone: no tag, name or meter, the stone as tall
   as the tile and centred in it, with the canvas's rim rather than its halo. */
.lb-card .lb-pv-stone {
  position: absolute;
  display: flex;
  justify-content: center;
}
.lb-card .lb-pv-stone .rune-stone {
  flex: none;
  width: auto;
  height: 100%;
  aspect-ratio: 1 / 1;
  filter: drop-shadow(0 0 2px var(--phosphor-glow))
          drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
}

#lb-mobile-note {
  position: absolute;
  left: 12px;
  right: 12px;
  top: 12px;
  z-index: 40;
  padding: 8px 10px;
  border: 1px dashed var(--phosphor-faint);
  background: var(--bg-panel);
  color: var(--phosphor-dim);
  font-size: 11.5px;
  letter-spacing: 1.5px;
  text-align: center;
}

/* ── The panel ── */

#lb-panel {
  border-left: 1px solid var(--phosphor-faint);
  background: var(--bg-panel);
  overflow-y: auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.lb-sec {
  padding: 12px 14px 14px;
  border-bottom: 1px solid var(--phosphor-faint);
}

.lb-sec-h {
  font-size: 11.5px;
  letter-spacing: 3px;
  color: var(--phosphor-dim);
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
}

.lb-field { display: block; margin-bottom: 10px; }

.lb-lab {
  display: flex;
  justify-content: space-between;
  font-size: 10.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  margin-bottom: 4px;
}
.lb-count { font-style: normal; opacity: 0.7; }

.lb-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  border-bottom-color: var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 0.5px;
  line-height: 1.5;
  padding: 7px 9px;
  outline: none;
  resize: vertical;
}
.lb-input:focus { border-color: var(--phosphor); }
.lb-input::placeholder { color: var(--phosphor-dim); opacity: 0.6; }

.lb-trad { display: flex; flex-wrap: wrap; gap: 6px; }

.lb-list { list-style: none; margin: 0; padding: 0; }

/* Tight on the left: the number, the label, the angle and four buttons share
   the panel's 340px, and the label is what needs the room. */
.lb-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 4px 6px 0;
  border: 1px solid transparent;
  border-bottom-color: var(--phosphor-faint);
  cursor: grab;
  font-size: 13px;
}
.lb-row:hover { background: var(--wash); }
.lb-row.is-selected { border-color: var(--phosphor-dim); }
.lb-row.is-invalid .lb-row-n { color: var(--red-alert, var(--amber)); }
.lb-row.is-dragging { opacity: 0.4; }
.lb-row.drop-before { box-shadow: inset 0 2px 0 var(--phosphor); }
.lb-row.drop-after  { box-shadow: inset 0 -2px 0 var(--phosphor); }

.lb-row-n {
  font-family: 'VT323', monospace;
  font-size: 22px;
  width: 20px;
  flex-shrink: 0;
  text-align: right;
  color: var(--phosphor);
}
.lb-row-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--phosphor); }
.lb-row-rot { font-size: 11px; color: var(--phosphor-dim); flex-shrink: 0; }
.lb-row-move { display: flex; gap: 2px; flex-shrink: 0; }
.lb-row-move button {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-size: 9px;
  width: 20px;
  height: 20px;
  padding: 0;
  cursor: pointer;
}
.lb-row-move button:disabled { opacity: 0.3; cursor: default; }
.lb-row-move button:hover:not(:disabled) { border-color: var(--phosphor); color: var(--phosphor); }
.lb-row-move .lb-row-del { margin-left: 4px; display: flex; align-items: center; justify-content: center; }
.lb-row-move .lb-row-del:hover { border-color: var(--amber) !important; color: var(--amber) !important; }

.lb-hint {
  margin-top: 8px;
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
  opacity: 0.8;
}

.lb-angle { display: flex; align-items: center; gap: 10px; }
.lb-angle .lb-btn { font-size: 16px; padding: 2px 10px; }
/* The angle is typed here as well as stepped: a small field for the number,
   the degree sign outside it so it is never typed or deleted by mistake. */
.lb-angle #lb-angle-val {
  width: 58px;
  padding: 2px 4px;
  text-align: center;
  font-size: 15px;
}
.lb-angle #lb-angle-val::placeholder { font-size: 12px; letter-spacing: 1px; }
.lb-angle-deg { margin-left: -8px; color: var(--phosphor); font-size: 15px; }

.lb-remove { margin-top: 4px; }
.lb-remove:hover:not(:disabled) { border-color: var(--amber) !important; color: var(--amber) !important; }

.lb-foot {
  margin-top: auto;
  padding: 10px 2px 16px;
  text-align: center;
}
.lb-foot .term-btn:disabled { opacity: 0.5; cursor: wait; }
.lb-cancel {
  font-size: 11.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  text-decoration: none;
}
.lb-cancel:hover { color: var(--phosphor); }

.lb-error {
  margin: 0 12px 6px;
  padding: 7px 9px;
  border: 1px solid var(--red-alert, var(--amber));
  color: var(--red-alert, var(--amber));
  font-size: 12.5px;
  line-height: 1.5;
  text-align: left;
}

/* The same error on the canvas, under the top SAVE, against the right edge.
   Over the canvas, so it takes the panel's ground to stay legible. */
.lb-error.lb-error-top {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 30;
  margin: 0;
  max-width: min(360px, calc(100% - 24px));
  background: var(--bg-panel);
  pointer-events: none;
}
.lb-error.lb-error-top[hidden] { display: none; }

/* A phone gets a read-only preview: the canvas above, the details below, and
   nothing that edits. Building wants a pointer and a wide screen. */
@media (max-width: 767px) {
  #layouts-page { padding: 16px 16px 48px; }
  .lo-row { flex-direction: column; gap: 10px; }
  .lo-starters { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .lo-new-choices { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tpl-body { max-height: 58vh; }
  .lo-starter-fig, .lo-starter-fig .lo-fig { width: 52px; height: 52px; }
  .lo-starter-name { font-size: 11px; letter-spacing: 1px; }
  #lb-page { grid-template-columns: 1fr; grid-template-rows: 55vh auto; overflow-y: auto; }
  #lb-panel { border-left: none; border-top: 1px solid var(--phosphor-faint); overflow: visible; }
}

/* Read only: a phone's preview, or an admin reviewing a member's suggestion
   (FEATURED_SPREADS_PLAN.md §6) at any size. Nothing that edits is shown; the
   suggestion keeps CREATE ADMIN COPY, its one way on. */
#lb-page.is-readonly .lb-bar-actions:not(.lb-view-actions),
#lb-page.is-readonly .lb-angle,
#lb-page.is-readonly .lb-remove,
#lb-page.is-readonly #lb-save,
#lb-page.is-readonly .lb-hint { display: none; }
#lb-page.is-readonly .lb-card { cursor: default; }
.lb-view-actions[hidden],
#lb-promote[hidden] { display: none; }

/* PROMOTE TO FEATURED's dialog: a .card-modal, centred like every other. */
.lb-promote-panel { width: min(460px, 100%); }
.lb-promote-panel .lb-field { margin-top: 12px; }
.lb-promote-panel .lb-lab em { font-style: normal; letter-spacing: 0.5px; opacity: 0.7; }
.lb-promote-panel select.lb-input { padding: 6px 8px; }
.lb-promote-lede { margin: 0; }
.lb-promote-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.lb-promote-actions [hidden] { display: none; }

/* ══ Freestyle (grimoire-freestyle.js) ═══════════════════════════════════
   A table and a deck: no slots, no oracle. */

/* No oracle pane, and no DRAW bar -- the deck is how a card is drawn. */
#detail-oracle.fs-hidden,
#draw-controls.fs-hidden { display: none !important; }

/* A Freestyle card has no position to name. */
.rank-badge:empty { display: none; }

#fs-deck {
  position: absolute;
  right: 18px;
  bottom: 18px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
#fs-deck[hidden] { display: none; }

.fs-deck-stack {
  position: relative;
  width: 84px;
  height: 147px;
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.fs-deck-stack:active { cursor: grabbing; }

/* Three backs, each a little off the one beneath: a deck, not a card. */
.fs-back {
  position: absolute;
  inset: 0;
  border: 1px solid var(--phosphor-dim);
  background:
    repeating-linear-gradient(45deg, var(--phosphor-faint) 0 1px, transparent 1px 7px),
    repeating-linear-gradient(-45deg, var(--phosphor-faint) 0 1px, transparent 1px 7px),
    var(--card-ground);
  box-shadow: 0 0 10px var(--phosphor-glow);
}
.fs-back:nth-child(1) { transform: translate(-4px, 4px); opacity: 0.55; }
.fs-back:nth-child(2) { transform: translate(-2px, 2px); opacity: 0.8; }
.fs-deck-stack:hover .fs-back:nth-child(3) {
  border-color: var(--phosphor);
  box-shadow: 0 0 18px var(--phosphor-glow-strong);
}

/* Runes come from a bag of stones, drawn as one: a drawstring pouch in place
   of the stacked backs (grimoire.html, .fs-bag). */
#fs-deck.is-rune .fs-deck-stack { width: 97px; height: 118px; }
#fs-deck.is-rune .fs-back { display: none; }
.fs-bag { display: none; position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
#fs-deck.is-rune .fs-bag { display: block; }
.fs-bag {
  filter: drop-shadow(0 0 6px var(--phosphor-glow));
  transition: filter 0.15s;
}
.fs-bag-body {
  fill: var(--card-ground);
  stroke: var(--phosphor-dim);
  stroke-width: 1.1;
  stroke-linejoin: round;
}
.fs-bag-fold { fill: none; stroke: var(--phosphor-faint); stroke-width: 1; stroke-linecap: round; }
.fs-bag-cord { fill: none; stroke: var(--phosphor-dim); stroke-width: 1.3; stroke-linecap: round; }
.fs-bag-rune {
  fill: none;
  stroke: var(--phosphor-dim);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.6;
}
.fs-deck-stack:hover .fs-bag { filter: drop-shadow(0 0 14px var(--phosphor-glow-strong)); }
.fs-deck-stack:hover .fs-bag-body,
.fs-deck-stack:hover .fs-bag-cord { stroke: var(--phosphor); }


.fs-deck-count,
.fs-deck-hint {
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  white-space: nowrap;
  text-align: center;
}
.fs-deck-hint { color: var(--phosphor); max-width: 150px; white-space: normal; line-height: 1.5; }
/* The count alone, no word and no frame: bare text centred on the foot of the
   deck or bag, lapping over it, large enough to read at a glance. A dark halo
   keeps it legible over the backs; above the stack so they never cover it,
   and clicks pass through to the stack beneath. */
#fs-deck-count {
  position: relative;
  z-index: 1;
  margin-top: -20px;
  font-family: 'VT323', monospace;
  font-size: 24px;
  line-height: 1.1;
  letter-spacing: 1px;
  color: var(--phosphor);
  text-shadow: 0 0 3px var(--bg), 0 0 3px var(--bg), 0 0 8px var(--phosphor-glow-strong);
  pointer-events: none;
}
/* The bag's drawing stops short of the foot of its box, so on a rune table
   the count climbs further to lap the bag itself. */
#fs-deck.is-rune #fs-deck-count { margin-top: -27px; }

/* The card in the hand while it is dragged, at the table's zoom. */
.fs-drag {
  position: fixed;
  z-index: 2000;
  pointer-events: none;
  transform: translate(-50%, -50%) rotate(-4deg);
  border: 1px solid var(--phosphor);
  background:
    repeating-linear-gradient(45deg, var(--phosphor-faint) 0 1px, transparent 1px 7px),
    var(--card-ground);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 18px var(--phosphor-glow-strong);
  opacity: 0.92;
}
.fs-drag.is-rune { border-radius: 38% / 30%; }

#fs-end {
  position: absolute;
  top: 12px;
  right: 14px;
  z-index: 60;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 2px;
  padding: 6px 12px;
  cursor: pointer;
}
#fs-end[hidden] { display: none; }
#fs-end:hover { background: var(--wash); border-color: var(--phosphor); }
#fs-end.armed { border-color: var(--amber); color: var(--amber); }

/* Over a corner of a card that can still be turned: the turn icon in place of
   the terminal's block cursor. `visibility`, because terminal.js sets the
   block's opacity inline on every move. */
body.fs-rot-hover #cursor { visibility: hidden !important; }
#fs-rot-cursor {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  transform: translate(-50%, -50%);
  color: var(--phosphor);
  filter: drop-shadow(0 0 4px var(--phosphor-glow-strong));
}
#fs-rot-cursor[hidden] { display: none; }
body.fs-rot-hover #card-canvas .tarot-card { cursor: none; }

/* The card's art is not the browser's to drag: in Freestyle a press on a card
   moves the card. (The script also cancels dragstart; this covers Safari.) */
#card-canvas .tarot-card img {
  -webkit-user-drag: none;
  user-drag: none;
}
#card-canvas .tarot-card[data-drawn="true"] { user-select: none; -webkit-user-select: none; }

/* ── Freestyle's Notes pane ──
   The oracle pane's place and glass, with the reader's own words in it. Open,
   it stands over the right of the table; shut, it is a strip, and the deck and
   END SESSION move to stay clear of whichever it is. */
#fs-notes {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  z-index: 70;
  width: 36px;
  display: flex;
  border-left: 1px solid var(--phosphor-faint);
  background-color: rgba(var(--bg-rgb), 0.6);
  backdrop-filter: blur(13px) saturate(0.35) brightness(0.5);
  -webkit-backdrop-filter: blur(13px) saturate(0.35) brightness(0.5);
  box-shadow: inset 0 0 24px var(--phosphor-glow-faint), -4px 0 20px rgba(var(--shade), 0.4);
  transition: width 0.3s ease;
}
#fs-notes[hidden] { display: none; }
#fs-notes.is-open { width: 360px; }

/* The strip is .panel-strip, as the oracle's is; shown whenever the pane is
   shut rather than by the oracle's own collapsed class. */
#fs-notes-strip { display: flex; width: 36px; }
#fs-notes.is-open #fs-notes-strip { display: none; }
.fs-notes-icon { width: 24px; height: 24px; }
.fs-notes-title { display: flex; align-items: center; gap: 6px; }
.fs-notes-head { display: flex; justify-content: space-between; align-items: center; user-select: none; }
.fs-notes-tools { display: flex; align-items: center; gap: 6px; }
#fs-notes-copy-btn.copied { color: var(--amber); opacity: 1; }
/* The fold mark is the phone's control; the ▶ is the desktop's. One arrow
   either way, as on the oracle's header. */
#fs-notes-collapse-icon { opacity: 0.6; padding-left: 2px; }
@media (min-width: 768px) { #fs-notes-collapse-icon { display: none; } }
.fs-notes-head-icon { width: 18px; height: 18px; flex-shrink: 0; }

#fs-notes-content {
  display: none;
  flex: 1;
  flex-direction: column;
  min-width: 0;
}
#fs-notes.is-open #fs-notes-content { display: flex; }

.fs-notes-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

#fs-notes-text {
  flex: 1;
  margin: 12px 14px 6px;
  resize: none;
  background: rgba(var(--bg-rgb), 0.35);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1.7;
  letter-spacing: 0.3px;
  padding: 10px 12px;
  outline: none;
}
#fs-notes-text:focus { border-color: var(--phosphor-dim); }
#fs-notes-text::placeholder { color: var(--phosphor-dim); opacity: 0.6; }

.fs-notes-foot {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 0 14px 12px;
  font-size: 10.5px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  min-height: 14px;
}

#detail-content-row.fs-notes-open #fs-deck { right: 378px; }
#detail-content-row.fs-notes-open #fs-end  { right: 374px; }
#detail-content-row.fs-notes-shut #fs-deck { right: 54px; }
#detail-content-row.fs-notes-shut #fs-end  { right: 50px; }

/* On a phone the pane is the oracle's sheet, in size and in handling: fixed
   to the foot of the screen, 45% of it to start, its header pinned and the
   handle (grimoire-freestyle.js, "Notes on a phone"). Folded, it is the header
   alone -- there is no strip on a phone. --notes-h is set on
   #detail-content-row so the deck can keep clear of it as well. */
@media (max-width: 767px) {
  #fs-notes {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto !important;
    height: var(--notes-h, 45vh);
    z-index: 40;
    flex-direction: column;
    /* Unclipped, so the grab ring can stand half above the sheet's top line,
       as the oracle's does; the notes are clipped by #fs-notes-content. */
    overflow: visible;
    border-left: none;
    border-top: 1px solid var(--phosphor-faint);
    transition: height 0.25s ease;
  }
  #fs-notes.sheet-dragging { transition: none; }
  #fs-notes #fs-notes-strip { display: none !important; }
  #fs-notes #fs-notes-content { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; }
  #fs-notes:not(.is-open) #fs-notes-text,
  #fs-notes:not(.is-open) .fs-notes-foot { display: none; }

  #fs-notes .fs-notes-head {
    flex: 0 0 auto;
    position: relative;
    background: var(--bg-panel);
    touch-action: none;
    cursor: grab;
  }
  #fs-notes.sheet-dragging .fs-notes-head { cursor: grabbing; }
  /* The grab mark is the ring the oracle's sheet wears (.sheet-grip, with the
     oracle's rules above), not the bar this header used to draw. */
  #fs-notes .fs-notes-head .panel-collapse-btn { display: none; }

  /* The sheet lies over the table's own controls, never under them: the deck,
     END SESSION and recentre belong to the table, and a raised sheet covers
     the table. */
  #fs-notes { z-index: 70; }

  /* The deck rides above the sheet at whatever height it has been given --
     drawn smaller than on a desktop, where the table is not a strip -- and
     steps aside when the sheet is raised past where it fits
     (grimoire-freestyle.js, fsDeckFit): with that much sheet the reader is
     writing, and a deck pushed up would cover the header. How far up is
     --fs-deck-lift, measured from where the sheet's top really is against the
     stage's foot (fsSheetCover); --notes-h alone assumed the two feet agree. */
  #detail-content-row.fs-notes-open #fs-deck,
  #detail-content-row.fs-notes-shut #fs-deck { right: 12px; bottom: calc(var(--fs-deck-lift, var(--notes-h, 45vh)) + 12px); }
  #detail-content-row.fs-deck-tight .fs-deck-hint,
  #detail-content-row.fs-deck-tight .fs-cast-left { display: none; }
  #detail-content-row.fs-deck-cramped #fs-deck { visibility: hidden; pointer-events: none; }
  #fs-deck { gap: 5px; }
  .fs-deck-stack { width: 60px; height: 105px; }
  #fs-deck.is-rune .fs-deck-stack { width: 69px; height: 84px; }
  .fs-deck-hint { max-width: 120px; font-size: 10px; }
  #fs-deck-count { margin-top: -16px; font-size: 20px; }
  #fs-deck.is-rune #fs-deck-count { margin-top: -21px; }
}

/* ── Freestyle on a touch screen ──
   A table, not a spread stepped through: one finger pans the bare table, two
   pinch to zoom, and the page itself must neither scroll nor zoom under them. */
#card-stage.fs-table { touch-action: none; }

/* On a phone the table is handled as every spread is: pinch to zoom, and the
   recentre button in the stage's top right (#mob-recentre, which frames the
   table here -- grRecentre). No zoom widget: it duplicated the pinch, and sat
   over the Notes sheet (owner, 2026-09-26). END SESSION takes the top left,
   the corner recentre leaves free. */
@media (max-width: 767px) {
  #card-stage.fs-table #zoom-widget { display: none; }
  #fs-end,
  #detail-content-row.fs-notes-open #fs-end,
  #detail-content-row.fs-notes-shut #fs-end { top: 8px; left: 8px; right: auto; }
}

/* The turn handle: on the card last touched, at its foot corner -- clear of
   the clarifier badge at the head. Only where there is no hover to find a
   corner with; a mouse has the corner itself and the turn cursor. */
.fs-rot-handle {
  display: none;
  position: absolute;
  right: -14px;
  bottom: -14px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: var(--bg-panel);
  border: 1px solid var(--phosphor);
  color: var(--phosphor);
  box-shadow: 0 0 10px var(--phosphor-glow-strong);
  z-index: 12;
  touch-action: none;
}
@media (hover: none) {
  #card-canvas .tarot-card.fs-selected > .fs-rot-handle { display: flex; }
  /* Picked up: lit so it cannot be missed (owner, 2026-10-04) -- a phosphor
     outline, a strong glow close in and a wide one round it. It was a single
     soft 22px glow, easy to take for the card's own. */
  #card-canvas .tarot-card.fs-selected .card-frame,
  #card-canvas .tarot-card.fs-selected .card-with-image {
    box-shadow: 0 0 0 2px var(--phosphor),
                0 0 16px 2px var(--phosphor),
                0 0 44px 12px var(--phosphor-glow-strong);
  }
}

/* ── Runes on the canvas: the stone and nothing else ──
   A drawn stave is a stone, not a tile -- in a spread as on a Freestyle
   table: no frame, no position name, no stave name or sound, no voice meter,
   no MERKSTAV badge. All of that is in the stone's meaning modal instead
   (grCardModalHtml); the voice shows on the stone as its brightness ("A
   stone's voice", below). This retires the framed stone of "Runes: frame kept,
   stone inside it" further up, at the owner's call (2026-09-25).

   Drawn stones only: an undrawn slot in a spread keeps its frame, which is
   what shows the reader where to draw. The tile keeps the frame's size
   (1 : 1.22 in a spread), so every rune layout lies exactly as it did, and the
   stone is drawn to the tile's full height -- the space the frame and its
   lettering used to take (owner, 2026-09-25: tried at half, restored). Scoped by the canvas's id, so it outranks every
   frame rule above whatever order they come in -- and by the landing's card
   mount's (#lp-card-mount), where a guest's single rune is the same stone
   (owner, 2026-10-05). :is() keeps the id's weight. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-frame {
  border: none;
  background: none;
  box-shadow: none;
  filter: none;
  overflow: visible;
  animation: none;
}
/* The frame's top-and-bottom vignette too. The rune rule further up already
   hides it; this says so again where the stone-only look is defined, because
   losing that one rule (a comment closed early swallowed it) put a gradient
   box round every stone. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-frame::before,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-frame::after { display: none; }
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] > .rank-badge,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] > .orientation-badge,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder .card-name,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder .rune-phoneme,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder .divider,
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder .card-meta { display: none; }
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder { padding: 0; justify-content: center; }
/* The stone's box is square and as tall as the tile, centred in it. A stone
   is a portrait oval in that square, so on a tile narrower than it is tall the
   box overhangs the sides but the stone itself does not. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder.is-stone .rune-stone {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 100%;
  width: auto;
  aspect-ratio: 1 / 1;
  margin: 0;
  transform: translate(-50%, -50%);
}
/* The TEXT style: the bare glyph, as large as the tile allows. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .card-placeholder.is-rune:not(.is-stone) .sigil { font-size: 96px; }
/* A rim, not a halo. The stone's general glow (.rune-stone) is 10px wide,
   and around a stone this size, with no frame drawn over it any more, it read
   as a square haze about the tile. A voice that shouts or thunders still
   swells it on purpose ("A stone's voice"). */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] .rune-stone {
  filter: drop-shadow(0 0 2px var(--phosphor-glow))
          drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
}
/* The clarifier badge rides the tile's top-right corner, which the stone never
   reaches, instead of hanging off its side into the next stone. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"] > .clarifier-badge {
  top: 0;
  left: auto;
  right: 0;
}

/* Merkstav is amber, as a reversed draw is everywhere else. The frame that used
   to say so is gone, so the stone says it: the mark (already amber), its edge,
   and its glow. The TEXT style's bare glyph likewise. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"].inverted .rune-stone {
  filter: drop-shadow(0 0 3px rgba(var(--halo-amber-rgb), 0.5))
          drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
}
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"].inverted .rune-stone-body {
  stroke: rgba(var(--amber-rgb), 0.75);
  stroke-width: 1.2;
}
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"].inverted .card-placeholder .sigil {
  color: var(--amber);
  text-shadow: 0 0 10px var(--halo-amber), 0 0 26px rgba(var(--halo-amber-rgb), 0.3);
}
/* The stone in focus -- stepped to in a spread, or picked up on a touch
   screen at a Freestyle table -- glows, in place of the frame's glow. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"].selected .rune-stone {
  filter: drop-shadow(0 0 5px var(--phosphor-glow-strong))
          drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
}
@media (hover: none) {
  /* A stone has no frame to outline: a bright glow tight round the stone and
     a wide one round that, as strong as a picked-up card's. */
  :is(#card-canvas, #lp-card-mount) .tarot-card.is-rune[data-drawn="true"].fs-selected .rune-stone {
    filter: drop-shadow(0 0 3px var(--phosphor))
            drop-shadow(0 0 12px var(--phosphor))
            drop-shadow(0 0 30px var(--phosphor-glow-strong));
  }
}

/* On a Freestyle table the tile is square and a little larger than a card is
   wide -- a stone there lies about its own middle -- and the slot a stone is
   dropped into is a bare outline too: there is no fixed place to show.
   FS_RUNE_W and FS_BODY_H.runes in grimoire-freestyle.js, and CAST_TILE in
   layout/freestyle.py, are this number. */
#card-stage.fs-table #card-canvas .tarot-card.is-rune { width: 144px; }
#card-stage.fs-table .tarot-card.is-rune .card-frame {
  aspect-ratio: 1 / 1;
  border: none;
  background: none;
  box-shadow: none;
  filter: none;
  overflow: visible;
  /* The active slot's flash is a square glow; the ghost stone's own dashed
     outline lights instead (.ghost-card.card-active .rune-stone-ghost). */
  animation: none;
}
#card-stage.fs-table .tarot-card.is-rune .card-placeholder { padding: 0; justify-content: center; }

/* A stone in the hand is a stone, not a tile. */
.fs-drag.is-rune { border-radius: 50%; }

/* The Voice meter in a stone's meaning modal: the card's own meter, given the
   width a card would give it. */

/* The stave upright, under the stone's name in its meaning modal: a
   reference for the mark, which on the table lies at whatever angle it fell.
   Always phosphor and never turned -- it is the letter, not this draw of it. */
.cm-rune-ref {
  flex: none;
  margin: 0;
  font-family: 'Noto Sans Runic', 'Segoe UI Historic', serif;
  font-size: 46px;
  line-height: 1;
  color: var(--phosphor);
  text-shadow: 0 0 10px var(--phosphor-glow), 0 0 26px var(--phosphor-glow-faint);
}

/* ── A drawn rune's Voice as a crescent (voice-arc.js) ──
   The landing's guest draw and the share images. Drawn inside the stone (a
   group in its own SVG), so it shares the stone's glow; its reading sits
   under the stone. Moved here from login.html on 2026-10-14. */
.lp-voice-arc path { fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.lp-voice-arc .lp-arc-track { stroke: var(--phosphor-faint); stroke-width: 4px; }
.lp-voice-arc .lp-arc-tick  { stroke: color-mix(in srgb, var(--phosphor-dim) 55%, transparent); stroke-width: 1px; }
.lp-voice-arc .lp-arc-fill  { stroke: var(--phosphor); stroke-width: 4px; opacity: 0.9;
                              stroke-dasharray: 1; stroke-dashoffset: 0;
                              animation: lpArcRise 520ms cubic-bezier(0.16, 0.9, 0.3, 1) both; }
.lp-voice-arc .lp-arc-fill.is-below { stroke: var(--phosphor-dim); opacity: 0.7; }
.lp-voice-arc .lp-arc-null  { stroke: var(--amber); stroke-width: 1.5px; }
@keyframes lpArcRise { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) { .lp-voice-arc .lp-arc-fill { animation: none; } }

/* ── A stone's voice ──
   On the canvas -- a spread or a Freestyle table -- a stave shines as loudly as it spoke: WHISPERED
   dim but plain to see, HUSHED brighter, SPOKEN as any stave, SHOUTED a slow
   swell of glow round the stone with the mark itself held steady, THUNDERED a
   quick flash of both. data-voice is the band (grimoire-cards.js, _voiceAttr).
   The flash is the mark turning white-hot in its own colour -- phosphor, or
   amber for a merkstav stave -- so it reads from across the table and not only
   up close; keeping it for THUNDERED alone is what tells the two apart. */
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune {
  --voice-ink:  var(--phosphor);
  --voice-hot:  color-mix(in srgb, var(--phosphor) 40%, white);
}
:is(#card-canvas, #lp-card-mount) .tarot-card.is-rune.inverted {
  --voice-ink:  var(--amber);
  --voice-hot:  color-mix(in srgb, var(--amber) 50%, white);
}

:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="whispered"] .rune-stone-mark,
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="whispered"] .sigil { opacity: 0.42; }
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="whispered"] .rune-stone-cut { opacity: 0.45; }
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="hushed"] .rune-stone-mark,
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="hushed"] .sigil { opacity: 0.7; }

:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="thundered"] .rune-stone-mark {
  animation: voiceFlashMark 0.9s ease-in-out infinite;
}
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="shouted"] .rune-stone {
  animation: voiceFlashStone 2.4s ease-in-out infinite;
}
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="thundered"] .rune-stone {
  animation: voiceFlashStone 0.9s ease-in-out infinite;
}
/* The TEXT style has no stone to glow, so a shouted glyph's glow swells while
   the glyph itself keeps its colour and brightness. */
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="shouted"] .sigil {
  animation: voiceGlowGlyph 2.4s ease-in-out infinite;
}
:is(#card-canvas, #lp-card-mount) .card-placeholder[data-voice="thundered"] .sigil {
  animation: voiceFlashGlyph 0.9s ease-in-out infinite;
}

@keyframes voiceFlashMark {
  0%, 100% { fill: var(--voice-ink); opacity: 0.55; filter: drop-shadow(0 0 2px var(--voice-ink)); }
  50%      { fill: var(--voice-hot); opacity: 1;
             filter: drop-shadow(0 0 3px var(--voice-hot)) drop-shadow(0 0 9px var(--voice-ink)); }
}
@keyframes voiceFlashStone {
  0%, 100% { filter: drop-shadow(0 0 6px transparent) drop-shadow(0 2px 3px rgba(var(--shade), 0.55)); }
  50%      { filter: drop-shadow(0 0 16px var(--voice-ink)) drop-shadow(0 2px 3px rgba(var(--shade), 0.55)); }
}
@keyframes voiceGlowGlyph {
  0%, 100% { text-shadow: 0 0 4px var(--voice-ink); }
  50%      { text-shadow: 0 0 10px var(--voice-ink), 0 0 26px var(--voice-ink), 0 0 44px var(--voice-ink); }
}
@keyframes voiceFlashGlyph {
  0%, 100% { color: var(--voice-ink); opacity: 0.55; text-shadow: 0 0 4px var(--voice-ink); }
  50%      { color: var(--voice-hot); opacity: 1;
             text-shadow: 0 0 6px var(--voice-hot), 0 0 22px var(--voice-ink), 0 0 40px var(--voice-ink); }
}

/* The meaning modal's Voice meter speaks the same way, for a card as for a
   stone: its bar and its reading (band and number) dim for WHISPERED and
   HUSHED, swell with glow for SHOUTED, and flash for THUNDERED, on the stave's
   timings. The VOICE heading and the (?) stay steady -- they label the meter,
   they are not its reading. data-voice from grimoire-cards.js, _voiceAttr. */
.cm-voice {
  --voice-ink: var(--phosphor);
  --voice-hot: color-mix(in srgb, var(--phosphor) 40%, white);
}
.cm-voice[data-voice="whispered"] .sig-meter-fill,
.cm-voice[data-voice="whispered"] .sig-meter-read { opacity: 0.42; }
.cm-voice[data-voice="hushed"] .sig-meter-fill,
.cm-voice[data-voice="hushed"] .sig-meter-read { opacity: 0.7; }

.cm-voice[data-voice="shouted"] .sig-meter-fill { animation: voiceGlowBar 2.4s ease-in-out infinite; }
.cm-voice[data-voice="shouted"] .sig-meter-read > * { animation: voiceGlowGlyph 2.4s ease-in-out infinite; }
/* THUNDERED must read as a flash, not a swell: the stave's timing, but on a
   meter this small the stave's own curve (voiceFlashGlyph) blurred into a
   glow. So the meter goes dark and bare between pulses, and burns white-hot
   briefly at each one. */
.cm-voice[data-voice="thundered"] .sig-meter-fill { animation: voiceFlashBar 0.9s linear infinite; }
.cm-voice[data-voice="thundered"] .sig-meter-read > * { animation: voiceFlashRead 0.9s linear infinite; }
/* The track clips its fill; a glowing bar needs its halo let out. */
.cm-voice[data-voice="shouted"] .sig-meter-track,
.cm-voice[data-voice="thundered"] .sig-meter-track { overflow: visible; }

@keyframes voiceGlowBar {
  0%, 100% { box-shadow: 0 0 4px var(--voice-ink); }
  50%      { box-shadow: 0 0 8px var(--voice-ink), 0 0 18px var(--voice-ink); }
}
@keyframes voiceFlashBar {
  0%, 30%, 70%, 100% { background: var(--voice-ink); opacity: 0.3; box-shadow: none; }
  45%, 55% { background: white; opacity: 1;
             box-shadow: 0 0 6px var(--voice-hot), 0 0 16px var(--voice-ink), 0 0 28px var(--voice-ink); }
}
@keyframes voiceFlashRead {
  0%, 30%, 70%, 100% { color: var(--voice-ink); opacity: 0.35; text-shadow: none; }
  45%, 55% { color: var(--voice-hot); opacity: 1;
             text-shadow: 0 0 4px white, 0 0 12px var(--voice-hot), 0 0 26px var(--voice-ink); }
}

/* Without motion, a loud stave is simply lit: the flash's peak, held. */
@media (prefers-reduced-motion: reduce) {
  #card-canvas .card-placeholder[data-voice="thundered"] .rune-stone-mark {
    animation: none;
    fill: var(--voice-hot);
    filter: drop-shadow(0 0 3px var(--voice-hot)) drop-shadow(0 0 9px var(--voice-ink));
  }
  #card-canvas .card-placeholder[data-voice="shouted"] .rune-stone,
  #card-canvas .card-placeholder[data-voice="thundered"] .rune-stone {
    animation: none;
    filter: drop-shadow(0 0 16px var(--voice-ink)) drop-shadow(0 2px 3px rgba(var(--shade), 0.55));
  }
  #card-canvas .card-placeholder[data-voice="shouted"] .sigil {
    animation: none;
    text-shadow: 0 0 10px var(--voice-ink), 0 0 26px var(--voice-ink);
  }
  #card-canvas .card-placeholder[data-voice="thundered"] .sigil {
    animation: none;
    color: var(--voice-hot);
    text-shadow: 0 0 6px var(--voice-hot), 0 0 22px var(--voice-ink);
  }
  .cm-voice[data-voice="shouted"] .sig-meter-fill {
    animation: none;
    box-shadow: 0 0 8px var(--voice-ink), 0 0 18px var(--voice-ink);
  }
  .cm-voice[data-voice="shouted"] .sig-meter-read > * {
    animation: none;
    text-shadow: 0 0 10px var(--voice-ink), 0 0 26px var(--voice-ink);
  }
  .cm-voice[data-voice="thundered"] .sig-meter-fill {
    animation: none;
    background: var(--voice-hot);
    box-shadow: 0 0 6px var(--voice-hot), 0 0 16px var(--voice-ink);
  }
  .cm-voice[data-voice="thundered"] .sig-meter-read > * {
    animation: none;
    color: var(--voice-hot);
    text-shadow: 0 0 6px var(--voice-hot), 0 0 22px var(--voice-ink);
  }
}

/* ── Casting ──
   Under the bag: how many stones, and CAST. Runes only. */
.fs-cast {
  display: flex;
  align-items: stretch;
  gap: 6px;
  margin-top: 2px;
}
.fs-cast[hidden] { display: none; }
.fs-cast-qty {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--phosphor-dim);
  background: var(--bg-panel);
}
.fs-cast-n {
  min-width: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--halo);
}
.fs-cast-step {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--phosphor-faint);
}
.fs-cast-step button {
  flex: 1;
  background: none;
  border: none;
  color: var(--phosphor-dim);
  font-family: inherit;
  font-size: 9px;
  line-height: 1;
  padding: 3px 7px;
  cursor: pointer;
}
.fs-cast-step button + button { border-top: 1px solid var(--phosphor-faint); }
.fs-cast-step button:hover:not(:disabled) { color: var(--phosphor); background: var(--wash); }
.fs-cast-step button:disabled { opacity: 0.3; cursor: default; }
.fs-cast-btn {
  background: var(--bg-panel);
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 2px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}
.fs-cast-btn:hover:not(:disabled) { background: var(--wash); border-color: var(--phosphor); }
.fs-cast-btn:disabled { opacity: 0.4; cursor: default; }
.fs-cast.is-casting .fs-cast-btn { opacity: 1; border-color: var(--amber); color: var(--amber); }
/* A finger needs more than a mouse does. */
@media (hover: none) {
  .fs-cast-step button { font-size: 11px; padding: 7px 12px; }
  .fs-cast-btn { padding: 8px 14px; }
}

/* The notice a phone gets for the Spread Studio (_nav_rail.html). Its tag
   sits under the title, both centred. The head's right padding keeps clear of
   the close button; the same on the left keeps the centre true. */
.cm-head.cm-head-stack {
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  padding-left: 24px;
}
.desktop-only-actions { display: flex; justify-content: center; margin-top: 8px; }
.desktop-only-actions .term-btn { width: auto; padding: 6px 36px; }

/* The builder's restore prompt: two choices side by side. */
.lb-draft-actions { gap: 12px; }

/* The settings gear is fixed at the window's top-right on these pages too
   (_settings_button.html). In a narrow window their headings reach that
   corner, so the right-aligned count keeps clear of it. */
#archive-page .arc-head-row,
#layouts-page .arc-head-row { padding-right: 48px; }
@media (max-width: 767px) {
  #archive-page .arc-head-row,
  #layouts-page .arc-head-row { padding-right: 0; }   /* the gear is in the phone header */
}

/* The builder's panel runs to the top of the window, where the settings gear
   is fixed (6px down, 36px tall). Its first section starts below the gear, so
   the Name field's counter is not under it. Phones keep the gear in their
   header, so they keep the panel's own spacing. */
#lb-panel { padding-top: 34px; }
@media (max-width: 767px) {
  #lb-panel { padding-top: 0; }
}

/* ── Feedback ────────────────────────────────────────────────────────────
   The FEEDBACK modal (_feedback_modal.html, feedback.js) and its way in on the
   rail. A .card-modal like every other, so it opens centred with its whole
   frame in view on phones too; these rules are only its contents. */

/* The rail's Feedback row is a button among links: the .nav-item look, minus
   what a button brings with it. An action, so it never takes nav-current.
   Community, above it, is a link that leaves the page, and shares the row's
   alignment with it. */
button.nav-item.nav-action,
a.nav-item.nav-action {
  background: none;
  border: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  /* The profile block is padded 12px, the places above 8px: pulled out by the
     difference so this icon lines up with Archive's. */
  margin: 0 -4px;
  width: calc(100% + 8px);
}

/* Collapsed, the strip's megaphone is the way in; the row would be a second
   one stacked over the avatar. Likewise Community and Settings, each with its
   own icon in the strip. Desktop only: on a phone there is no strip, and the
   drawer's row is the only way in. */
@media (min-width: 768px) {
  html:not(.rail-expanded) #left-panel #sb-nav-feedback,
  html:not(.rail-expanded) #left-panel #sb-nav-community,
  html:not(.rail-expanded) #left-panel #sb-nav-settings { display: none; }
}

/* The guests' way in: a dotted-underlined word under the landing reading
   (login.html) and in How It Works' "Community driven". */
.lp-feedback-link {
  background: none;
  border: none;
  border-bottom: 1px dotted var(--phosphor-dim);
  padding: 2px 0;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
  cursor: pointer;
  transition: color 0.15s, text-shadow 0.15s;
}
.lp-feedback-link:hover,
.lp-feedback-link:focus-visible {
  color: var(--phosphor);
  text-shadow: 0 0 8px var(--halo);
}

.fb-panel .cm-head { margin-bottom: 12px; }

.fb-form { display: flex; flex-direction: column; gap: 10px; }

.fb-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.fb-message,
.fb-email {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;          /* 16px: below it, iOS zooms the page on focus */
  line-height: 1.5;
  padding: 8px 10px;
  outline: none;
  caret-color: var(--phosphor);
}
.fb-message { resize: vertical; min-height: 140px; max-height: 50vh; }

/* Phones: the one modal that does not sit in the middle. It is a form, and the
   keyboard that opens with it takes the lower half of the screen -- centred,
   the SEND button and half the box sat under it. So it rides near the top of
   what is visible, whole frame still in view, and is never taller than the
   space the keyboard leaves. feedback.js keeps --fb-top and --fb-avail in step
   with the visual viewport as the keyboard comes and goes; the fallbacks are
   for a browser without one. The owner's call, 2026-09-25. */
@media (max-width: 767px) {
  #feedback-modal {
    align-items: flex-start;
    padding-top: var(--fb-top, max(16px, 5vh));
  }
  #feedback-modal .card-modal-panel {
    max-height: calc(var(--fb-avail, 82vh) - 32px);
  }
  .fb-message { min-height: 96px; max-height: 30vh; }
}
.fb-message:focus,
.fb-email:focus { border-color: var(--phosphor-dim); }
.fb-message::placeholder,
.fb-email::placeholder { color: var(--phosphor-dim); opacity: 0.7; }

.fb-count {
  min-height: 0;
  margin-top: -6px;
  text-align: right;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
.fb-count:empty { display: none; }

/* The checkbox: the consent gate's square, so every tick in the product is
   the same mark. */
.fb-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
}
.fb-check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 3px 0 0;
  border: 1px solid var(--phosphor-dim);
  background: transparent;
  cursor: pointer;
}
.fb-check input[type="checkbox"]:checked {
  background: var(--phosphor);
  box-shadow: 0 0 8px var(--phosphor-glow-strong), inset 0 0 0 2px var(--bg);
}
.fb-check input[type="checkbox"]:focus-visible { outline: 1px solid var(--phosphor); outline-offset: 2px; }
.fb-check span,
.fb-reply-to {
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: var(--phosphor-dim);
}
.fb-reply-to { margin: -4px 0 0 25px; }
.fb-reply-addr { color: var(--phosphor); overflow-wrap: anywhere; }
.fb-email-row { margin-left: 25px; }
.fb-email-row[hidden],
.fb-reply-to[hidden] { display: none; }

/* The honeypot: present for a script, absent for a person. */
.fb-hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.fb-status {
  min-height: 1.4em;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
.fb-status.err  { color: var(--red-alert); }
.fb-status.info { color: var(--amber); }
.fb-status.ok   { color: var(--phosphor); text-shadow: 0 0 8px var(--halo); }

.fb-actions { display: flex; justify-content: flex-end; }
/* The Discord line beside SEND: margin-right:auto keeps it at the left edge
   and the button at the right, where it has always been. */
.fb-elsewhere {
  margin-right: auto;
  align-self: center;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
.fb-elsewhere a { color: var(--phosphor-dim); border-bottom: 1px dotted var(--phosphor-faint); }
.fb-elsewhere a:hover { color: var(--phosphor); text-shadow: 0 0 8px var(--phosphor-glow); }
.fb-actions .term-btn.fb-send { width: auto; margin: 0; padding: 6px 36px; }

/* ── Install App ─────────────────────────────────────────────────────────
   The rail row and landing link (.install-entry) and _install_modal.html;
   install.js decides when either shows. */

/* The entries ship [hidden], and .nav-item's display:flex would override the
   attribute. */
.install-entry[hidden] { display: none !important; }

/* Collapsed on desktop, the strip's icon is the way in and the row goes, as
   Feedback's does. */
@media (min-width: 768px) {
  html:not(.rail-expanded) #left-panel #sb-nav-install { display: none; }
}
/* In the strip, Install App sits above Feedback and takes the margin that
   drops the pair to the foot; Feedback keeps it while Install App is hidden. */
#left-panel-strip .install-entry:not([hidden]) + .strip-feedback { margin-top: 10px !important; }

/* BETA EARLY ACCESS (_beta_modal.html): the install modal's type. */
.beta-panel { width: min(480px, 100%); }
.beta-note,
.beta-list {
  margin: 0 0 12px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1.7;
  color: var(--phosphor-dim);
}
.beta-list { padding-left: 22px; }
.beta-note b { color: var(--phosphor); font-weight: normal; }
.beta-note a { color: var(--phosphor); text-decoration: underline; text-underline-offset: 3px; }
.beta-note a:hover { text-shadow: 0 0 8px var(--phosphor-glow); }
.beta-note [hidden],
.beta-actions [hidden] { display: none; }
.beta-actions { gap: 10px; margin-top: 18px; }

.inst-panel { width: min(480px, 100%); }
.inst-panel [data-install-guide][hidden] { display: none; }

.inst-steps {
  margin: 0 0 12px;
  padding-left: 22px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  line-height: 1.7;
  color: var(--phosphor-dim);
}
.inst-steps li + li { margin-top: 8px; }
.inst-steps b,
.inst-note b { color: var(--phosphor); font-weight: normal; }

.inst-note {
  margin: 0 0 12px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: var(--phosphor-dim);
}

/* The platform's own Share and Add marks, drawn inline so the reader matches
   what they are told to tap against what is on their screen. */
.inst-glyph {
  width: 1.15em;
  height: 1.15em;
  vertical-align: -0.2em;
  fill: none;
  stroke: var(--phosphor);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.inst-link-row { display: flex; gap: 8px; margin-bottom: 4px; }
.inst-link {
  flex: 1 1 auto;
  min-width: 0;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;          /* below 16px, iOS zooms the page on focus */
  padding: 6px 10px;
  outline: none;
  -webkit-user-select: text;
  user-select: text;
}
.inst-link-row .term-btn.inst-copy { width: auto; margin: 0; padding: 6px 16px; }
.fb-actions .term-btn.fb-send:disabled { opacity: 0.4; cursor: default; }

/* ── NEW REVELATION: the tradition control ───────────────────────────────
   Every tradition in one row, above the question, because everything under it
   depends on the answer. The chosen one in reverse video, as NEW REVELATION
   itself is: that is how the terminal says "this one". */
.nr-trad {
  display: flex;
  box-sizing: border-box;
  /* The form's own column: as wide as the question and the chips under it. */
  width: 100%;
  min-width: 280px;
  max-width: 480px;
  border: 1px solid var(--phosphor-dim);
  margin-bottom: 12px;
}
/* The question still opens the form's spacing, whether or not the control is
   above it. */
#nr-panel .nr-trad + .nr-block { margin-top: 0; }
.nr-trad[hidden] { display: none; }
.nr-trad-seg {
  flex: 1 1 0;
  min-width: 0;
  background: transparent;
  border: none;
  border-right: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 2px;
  padding: 7px 6px;
  transition: color 0.15s, background 0.15s;
}
.nr-trad-seg:last-child { border-right: none; }
.nr-trad-seg:hover { color: var(--phosphor); background: var(--wash-faint); }
.nr-trad-seg[aria-checked="true"] {
  background: var(--phosphor);
  color: var(--bg);
  box-shadow: 0 0 14px var(--phosphor-glow);
}
.nr-trad-seg:focus-visible { outline: 1px solid var(--phosphor); outline-offset: 2px; }

/* ── NEW REVELATION: the spread and deck picker ──────────────────────────
   A .card-modal like every other, so it opens centred with its whole frame in
   view on phones too. These rules are its contents. */
.nr-pick-panel { width: min(620px, 100%); }
.nr-pick-panel .cm-head { margin-bottom: 10px; }

.nr-pick-search {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 10px;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;          /* 16px: below it, iOS zooms the page on focus */
  padding: 7px 10px;
  outline: none;
  caret-color: var(--phosphor);
}
.nr-pick-search:focus { border-color: var(--phosphor-dim); }
.nr-pick-search[hidden] { display: none; }

.nr-pick-list { display: flex; flex-direction: column; gap: 6px; }
.nr-pick-section { display: flex; flex-direction: column; gap: 6px; }
.nr-pick-section[hidden] { display: none; }
.nr-pick-head {
  margin: 8px 0 0;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phosphor-dim);
}

.nr-opt-row { border: 1px solid var(--phosphor-faint); }
.nr-opt-row[hidden] { display: none; }
.nr-opt-row:has(.nr-opt.is-current) {
  border-color: var(--phosphor-dim);
  background: var(--wash-faint);
}
/* Freestyle has no fixed shape, and its row says so: the ghost-card border. */
.nr-opt-row:has(.nr-opt--freestyle) { border-style: dashed; }

.nr-opt {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 10px;
  background: transparent;
  border: none;
  color: var(--phosphor);
  text-align: left;
  font-family: 'Share Tech Mono', monospace;
  cursor: pointer;
  transition: background 0.15s;
}
.nr-opt:hover { background: var(--wash-faint); }
.nr-opt:focus-visible { outline: 1px solid var(--phosphor); outline-offset: -3px; }

.nr-opt-thumb {
  flex: 0 0 76px;
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--phosphor-dim);
}
.nr-opt:hover .nr-opt-thumb,
.nr-opt.is-current .nr-opt-thumb { color: var(--phosphor); }
.nr-opt-thumb svg { max-width: 100%; max-height: 100%; overflow: visible; }

/* A deck's three cards, fanned. */
.nr-opt-thumb--deck { position: relative; }
.nr-fan-card {
  position: absolute;
  top: 3px;
  width: 36px;
  height: 62px;
  border: 1px solid var(--phosphor-dim);
  background: var(--bg);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 6px var(--phosphor-glow-faint);
}
.nr-fan-card picture,
.nr-fan-card img { display: block; width: 100%; height: 100%; object-fit: cover; }
.nr-fan-0 { left: 4px;  transform: rotate(-11deg); z-index: 1; }
.nr-fan-1 { left: 20px; transform: rotate(0deg);   z-index: 2; top: 0; }
.nr-fan-2 { left: 36px; transform: rotate(11deg);  z-index: 3; }
.nr-fan-stone { border: none; background: none; box-shadow: none; height: 50px; top: 9px; }
.nr-fan-stone .rune-stone { width: 100%; height: 100%; }
.nr-fan-glyph { font-size: 18px; color: var(--phosphor); }

.nr-opt-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.nr-opt-name {
  font-size: 15px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--phosphor);
}
/* Tags here are the meaning modal's pills (.cm-orient): a soft rounded tint,
   no outline, so a tag never reads as one of the buttons it sits on. The
   option in use, and a fact worth knowing before choosing one (Freestyle's
   NO ORACLE), are amber, as a reversal is there. */
.nr-opt-now,
.nr-opt-tag {
  display: inline-flex;
  align-items: center;
  margin-left: 6px;
  border: none;
  border-radius: 999px;
  padding: 2px 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 2px;
  white-space: nowrap;
  vertical-align: middle;
  color: var(--amber);
  background: rgba(var(--amber-rgb), 0.14);
  text-shadow: 0 0 6px var(--halo-amber);
}
.nr-opt-meta {
  font-size: 11px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
}
.nr-opt-desc {
  font-size: 13px;
  line-height: 1.5;
  color: var(--phosphor-dim);
}

.nr-opt-more {
  display: block;
  margin: -4px 10px 8px 100px;
  padding: 2px 0;
  background: none;
  border: none;
  color: var(--phosphor-dim);
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  cursor: pointer;
}
.nr-opt-more:hover,
.nr-opt-more:focus-visible { color: var(--phosphor); outline: none; }
.nr-opt-positions {
  margin: 0 10px 10px 100px;
  padding-left: 18px;
  columns: 2;
  column-gap: 20px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  line-height: 1.7;
  color: var(--phosphor-dim);
}
.nr-opt-positions[hidden] { display: none; }

/* The way to the builder: a row like the spreads, a link rather than a
   choice, its thumbnail Spread Studio's compass with a small + at the top right
   (owner, 2026-10-10). The compass is its picture-size drawing
   (_icon_layouts_large.html), as large as the spreads' shapes beside it; the
   + sits in the corner the compass's turn leaves empty. */
a.nr-opt { text-decoration: none; box-sizing: border-box; }
.nr-opt-thumb--new { position: relative; }
.nr-opt-thumb--new .nav-icon { width: 56px; height: 56px; }
.nr-opt-new-plus {
  position: absolute;
  top: 4px;
  right: 4px;
  font-size: 18px;
  line-height: 1;
}

@media (max-width: 767px) {
  .nr-opt { gap: 10px; padding: 8px; }
  .nr-opt-thumb { flex-basis: 60px; height: 60px; }
  /* The deck fan is drawn at a fixed size, and at 36px a card its three cards
     spread wider than the phone's thumb slot -- they ran out over the deck's
     name. Smaller cards, closer together, in a slot the fan actually fits,
     turns included. */
  .nr-opt-thumb--new .nav-icon { width: 48px; height: 48px; }
  .nr-opt-new-plus { top: 2px; right: 2px; }
  .nr-opt-thumb--deck { flex-basis: 66px; height: 60px; }
  .nr-opt-thumb--deck .nr-fan-card { width: 28px; height: 49px; top: 5px; }
  .nr-opt-thumb--deck .nr-fan-0 { left: 4px; }
  .nr-opt-thumb--deck .nr-fan-1 { left: 19px; top: 2px; }
  .nr-opt-thumb--deck .nr-fan-2 { left: 34px; }
  .nr-opt-thumb--deck .nr-fan-stone { height: 40px; top: 10px; }
  /* Under the description, not under the picture: the row's 8px padding, the
     60px thumbnail and the 10px gap put the text at 78px, as 100px does on a
     desktop. */
  .nr-opt-more,
  .nr-opt-positions { margin-left: 78px; }
  .nr-opt-positions { columns: 1; }
}

/* ── Prospective options and the interest modal ──────────────────────────
   PROSPECTIVE_FEATURES_PLAN.md §3-4. A feature being considered is shown on
   purpose and must be told from a lock at a glance: a lock says "this exists",
   this says "this might, and you can have a say". So: a dashed outline and a
   CONSIDERED tag. Not a lock, and not greyed out like a disabled control --
   the name is shown in full, and the tag carries the status. The tag is the
   meaning modal's pill (.cm-orient), as every tag is; the dashed outline
   stays on the option itself. */
.pf-tag {
  display: inline-flex;
  align-items: center;
  border: none;
  border-radius: 999px;
  padding: 2px 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 2px;
  color: var(--phosphor);
  background: var(--wash);
  vertical-align: middle;
  text-transform: uppercase;
  white-space: nowrap;
}
.nr-opt-name .pf-tag { margin-left: 6px; }

/* In the spread and deck picker: last, dashed like Freestyle's ghost card. */
.nr-opt-row:has(.nr-opt--prospective) { border-style: dashed; }

/* In the tradition control: out of the radio set, dashed inside its cell. */
.nr-trad-seg--prospective {
  outline: 1px dashed var(--phosphor-faint);
  outline-offset: -4px;
}
/* The ✧ alone in a cell this narrow: the mark, not a pill round it. */
.nr-trad-seg--prospective .pf-tag { padding: 0 3px; background: none; font-size: 11px; }
.nr-trad-seg--prospective:focus-visible { outline: 1px solid var(--phosphor); outline-offset: 2px; }

/* The modal. A .card-modal.stacked, so it opens over the spread picker. */
.pf-panel { width: min(480px, 100%); }
.pf-panel .cm-head { flex-wrap: wrap; margin-bottom: 12px; padding-right: 26px; }
.pf-image {
  display: block;
  max-width: 100%;
  max-height: 220px;
  margin: 0 auto 12px;
  border: 1px solid var(--phosphor-faint);
}
.pf-image[hidden] { display: none; }
.pf-pitch {
  margin: 0 0 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 15px;
  line-height: 1.6;
  color: var(--phosphor);
}
.pf-progress {
  margin: 0 0 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--amber);
}
.pf-progress[hidden] { display: none; }
/* Always present: the disclaimer is part of the modal, not small print. */
.pf-honesty {
  margin: 0 0 16px;
  padding: 8px 10px;
  border-left: 2px solid var(--phosphor-faint);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  line-height: 1.55;
  color: var(--phosphor-dim);
}

.pf-member { display: flex; flex-direction: column; gap: 8px; }
.pf-member[hidden],
.pf-guest[hidden] { display: none; }

/* A toggle: pressed, it reads in reverse video, as a chosen segment does. */
.pf-want {
  width: 100%;
  padding: 10px;
  background: transparent;
  border: 1px solid var(--phosphor);
  color: var(--phosphor);
  font-family: 'VT323', monospace;
  font-size: 26px;
  letter-spacing: 3px;
  cursor: pointer;
  text-shadow: 0 0 8px var(--halo);
  box-shadow: 0 0 10px var(--phosphor-glow), inset 0 0 10px var(--phosphor-glow);
  transition: background 0.15s, color 0.15s, box-shadow 0.15s;
  touch-action: manipulation;
}
.pf-want:hover { background: var(--wash); }
.pf-want.is-on {
  background: var(--phosphor);
  color: var(--bg);
  text-shadow: none;
  box-shadow: 0 0 16px var(--phosphor-glow-strong);
}
.pf-want:disabled { opacity: 0.5; cursor: default; }
.pf-want:focus-visible { outline: 1px solid var(--phosphor); outline-offset: 3px; }

.pf-note-label {
  margin-top: 6px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1.5px;
  color: var(--phosphor-dim);
}
.pf-optional { opacity: 0.7; }
.pf-note {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  min-height: 70px;
  max-height: 30vh;
  background: var(--bg);
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor);
  font-family: 'Share Tech Mono', monospace;
  font-size: 16px;          /* 16px: below it, iOS zooms the page on focus */
  line-height: 1.5;
  padding: 8px 10px;
  outline: none;
  caret-color: var(--phosphor);
}
.pf-note:focus { border-color: var(--phosphor-dim); }
.pf-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 0; }
.pf-count {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
.pf-note-foot .term-btn.pf-save { width: auto; margin: 0 0 0 auto; padding: 4px 18px; font-size: 20px; }
.pf-note-foot .term-btn.pf-save[hidden] { display: none; }

.pf-guest p {
  margin: 0 0 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 14px;
  color: var(--phosphor);
}
.pf-guest .term-btn.pf-join { display: inline-block; width: auto; margin: 0; padding: 4px 22px; font-size: 22px; text-decoration: none; }

.pf-status {
  min-height: 1.2em;
  margin-top: 10px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
.pf-status.err  { color: var(--red-alert); }
.pf-status.info { color: var(--amber); }
.pf-status.ok   { color: var(--phosphor); text-shadow: 0 0 8px var(--halo); }

/* The weekly Oracle ceiling's answer, under CHANNEL ORACLE (grimoire.js,
   grOracleLimitNote). Amber, as the other caveats about the service are. */
.oracle-limit-note {
  margin-top: 12px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: var(--amber);
}
.oracle-limit-note[hidden] { display: none; }
/* CHANNEL ORACLE's price and what is left (grOracleQuote): the price lit,
   the balance dim, either side under the button. */
.oracle-cost {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 6px 12px 0;
  font-family: 'Share Tech Mono', monospace;
  font-size: 12px;
  letter-spacing: 1.5px;
}
.oracle-cost[hidden] { display: none; }
.oracle-cost .oc-price { color: var(--phosphor); text-shadow: 0 0 6px var(--phosphor-glow-strong); }
.oracle-cost .oc-balance { color: var(--phosphor-dim); }
/* Short of credits, CHANNEL ORACLE waits and looks it (grOracleCreditsNote). */
#oracle-btn:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; text-shadow: none; }
#oracle-btn:disabled:hover { background: transparent; }

/* A way from a refusal or a notice to the FAQ answer that explains it
   (/faq#<anchor>). It takes the colour of the line it ends, so an amber note
   gets an amber link and a dim status a dim one, with the site's dotted rule
   under it; a new tab, so a reading in progress is not left behind. */
.faq-why {
  color: inherit;
  white-space: nowrap;
  letter-spacing: 1.5px;
  border-bottom: 1px dotted currentColor;
  margin-left: 8px;
  opacity: 0.85;
}
.faq-why:hover, .faq-why:focus-visible { opacity: 1; text-shadow: 0 0 8px currentColor; }


/* A newer build is on the server and this page is in the middle of something
   (app-update.js). Top and centred, above every modal but under the cursor;
   amber, as the other notes about the service are. */
.app-update {
  position: fixed;
  top: calc(env(safe-area-inset-top) + 12px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 4200;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100vw - 32px);
  padding: 8px 10px 8px 14px;
  background: var(--bg-panel);
  border: 1px solid var(--amber);
  font-family: 'Share Tech Mono', monospace;
  font-size: 13px;
  letter-spacing: 1px;
  color: var(--amber);
}
.app-update-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.app-update button {
  background: none;
  border: 1px solid var(--amber);
  color: var(--amber);
  font: inherit;
  letter-spacing: inherit;
  padding: 2px 10px;
  cursor: pointer;
}
.app-update .app-update-x { border-color: transparent; padding: 2px 6px; font-size: 16px; line-height: 1; }
.app-update button:hover,
.app-update button:focus-visible { background: var(--amber); color: var(--bg); }

/* ── Bottom bar (phone) ──────────────────────────────────────────────────
   Mock-up (owner, 2026-10-04). Three keys astride the top edge of a panel
   across the foot of a phone (_nav_rail.html, #tabbar), the three
   fundamental solids: Archive a square, New Revelation a triangle, Settings
   a circle. The triangle holds a plus (the Oracle's eye, first); pressed, it
   starts a revelation. (It opened a dial of New Revelation and
   the Spread Studio at first; the Studio is the rail's alone now.) All
   three are solid, filled with the panel's ground; the grab handles (the
   grip, the oracle sheet's) are thin rings with carets, which is what keeps
   the circle from reading as one. (Tried first: three circles; then squares
   with a diamond +.) During a revelation the row tucks away to the lip -- the
   grip alone, at the foot -- and the grip raises it again.

   One set of properties says how much of the foot it takes, and everything
   that stands on the foot reads them: the row itself, #main's reserve, and
   both bottom sheets (the oracle, Freestyle's notes). So nothing
   measures the bar while it moves, and nothing can disagree with it.
     --tb-row      the row's full height: the panel and, above it, the
                   --tb-rise the triangle's apex stands over its top edge.
                   Whole: the sheets stand on the apex, never under it, so no
                   key ever covers the foot of a sheet.
     --tb-lip-now  the grip's strip: 0 off a revelation, --tb-lip during one
     --tb-row-now  the row: open off a revelation; shut during one unless
                   raised
   The safe-area inset is under all of it and #screen already pads for it.
   --tb-row is a literal, not calc(gap + rise + panel), 10 + 22 + 54:
   tabbarTargetH (nav-rail.js) reads it, and an unregistered property hands
   calc() back unresolved.

   #main reserves what the bar takes at rest -- the row off a revelation, the
   lip during one -- and that is fixed for as long as the mode lasts. Raising
   the row mid-revelation lays it OVER the foot of the stage instead of
   shrinking the stage: the stage, the canvas and its scale never change size
   under the spread. The cards are refitted to the band above it instead
   (visibleCardBand, terminal.js), exactly as they are for the oracle sheet. */
#tabbar { display: none; }

:root {
  --tb-lip: 60px;
  --tb-rise: 22px;
  /* Clear room above the triangle's apex, so whatever ends at the row's top
     -- the page's last row, a sheet's foot -- stops short of the apex rather
     than on it (owner, 2026-10-04). Inside the row: the keys keep their place
     on screen and the row's top rises by this much. */
  --tb-gap: 10px;
  --tb-row: 86px;
  --tb-lip-now: 0px;
  --tb-row-now: var(--tb-row);
}
html.tb-reading           { --tb-lip-now: var(--tb-lip); --tb-row-now: 0px; }
html.tb-reading.tb-raised { --tb-row-now: var(--tb-row); }

@media (max-width: 767px) {
  #main:has(#tabbar)                   { margin-bottom: var(--tb-row); }
  html.tb-reading #main:has(#tabbar)   { margin-bottom: var(--tb-lip); }

  #tabbar {
    display: block;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Over the sheets (40) and Freestyle's notes (70), under the drawer's
       scrim (199) and every modal. */
    z-index: 80;
    padding-bottom: env(safe-area-inset-bottom);
    /* No ground of its own (owner, 2026-10-04): the screen's static shows
       round the keys, as it does behind the header, and only the keys --
       each filled -- stand on it. A painted panel read as a separate bar. */
    background: none;
    /* So the bar's box is not the bar's to take: a touch between the keys is
       on whatever lies under it. The keys and the grip take theirs back. */
    pointer-events: none;
  }

  /* The row. Its height is what slides; its contents are pinned to its top
     and clipped at its foot, so they rise out of the lip as it opens and sink
     back into it as it shuts. */
  .tb-row {
    position: relative;
    height: var(--tb-row-now);
    /* Clipped at the foot only, for the slide. Above, the triangle's apex
       sits on the row's top edge, and overflow:hidden cut its glow off flat;
       the 20px over the top is the glow's room. */
    clip-path: inset(-20px 0 0 0);
    transition: height 0.45s ease;
  }
  /* Shut, its buttons are out of the tab order too -- after the slide. */
  html.tb-reading:not(.tb-raised) .tb-row {
    visibility: hidden;
    transition: height 0.45s ease, visibility 0s linear 0.45s;
  }
  .tb-row-inner {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: var(--tb-row);
    box-sizing: border-box;
    padding-top: var(--tb-gap);
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: start;
    justify-items: center;
    background: none;
  }
  /* No line along the panel's top edge (owner, 2026-10-04): the keys
     astride it say where it is. Nor does the tucked lip, any more (.tb-grip). */

  /* Archive and Settings are their icons alone (owner, 2026-10-04): the
     archive box is already a square and the gear already a circle, so the
     square and circle once drawn round them only said it twice. 36px icons,
     larger than they sat in their frames and still well under the 68x59
     triangle, their lines held at 1.8px rather than swelling with the size;
     lit when they are where you are, dim with a faint halo otherwise. Each
     sits in a 44px button -- the touch target -- placed so the icon's foot
     rests level with the triangle's base (its stroke runs 57.5-58.5px down
     the row): the button starts 18.5px down, the icon 4px inside it. The
     flight (nav-rail.js, _tbFly) lands its square and circle on the icons. */
  .tb-key {
    position: relative;
    z-index: 1;
    pointer-events: auto;
    box-sizing: border-box;
    width: 44px;
    height: 44px;
    margin-top: 18.5px;
    border: none;
    background: none;
    color: var(--phosphor-dim);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    cursor: pointer;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.15s;
  }
  .tb-key:not(.tb-plus) svg {
    width: 36px;
    height: 36px;
    display: block;
    position: relative;
    stroke-width: 1.8;
    filter: drop-shadow(0 0 3px var(--phosphor-glow));
  }
  .tb-key:not(.tb-plus) svg * { vector-effect: non-scaling-stroke; }
  .tb-key:active,
  .tb-key.nav-current { color: var(--phosphor); }
  .tb-key.nav-current:not(.tb-plus) svg { filter: drop-shadow(0 0 6px var(--halo)); }

  /* The + is a triangle, apex up: 68px across and 59 tall (equilateral), its
     apex at the row's top, --tb-rise over the edge, where the sheets stand.
     The frame is the SVG behind the icon (.tb-tri); the button stays an
     upright box, so its whole rectangle is the hit area. The plus inside sits
     low, on the triangle's centroid two thirds of the way down, where it is
     widest; centred in the box it would crowd the apex. */
  .tb-plus {
    width: 68px;
    height: 59px;
    margin-top: 0;
    border: none;
    background: none;
    box-shadow: none;
    color: var(--phosphor);
  }
  .tb-plus .tb-tri {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    filter: drop-shadow(0 0 5px var(--halo)) drop-shadow(0 0 2px var(--halo));
  }
  .tb-tri polygon {
    fill: var(--bg-panel);
    stroke: var(--phosphor);
    stroke-width: 1;
    stroke-linejoin: miter;
    vector-effect: non-scaling-stroke;
  }
  .tb-plus:active .tb-tri polygon { fill: var(--wash); }
  /* Not where you are -- the Archive, the Studio -- it stands as the other
     keys do there: dim ink, and only their faint halo rather than its glow
     (owner, 2026-10-04). Lit is for the current place alone. */
  /* Its lit state is the triangle's own glow, not the square keys' lit box:
     .tb-key.nav-current outranks .tb-plus, and drew a glowing rectangle round
     the triangle's button. */
  .tb-plus.nav-current { box-shadow: none; }
  .tb-plus:not(.nav-current) { color: var(--phosphor-dim); }
  .tb-plus:not(.nav-current) .tb-tri polygon { stroke: var(--phosphor-dim); }
  .tb-plus:not(.nav-current) .tb-tri { filter: drop-shadow(0 0 5px var(--phosphor-glow)); }
  /* The plus (_icon_plus.html), low in the triangle on its centroid, two
     thirds of the way down, where it is widest -- centred in the box it would
     crowd the apex. Lit with the triangle. It took the place of the Oracle's
     eye (owner, 2026-10-04). */
  .tb-plus .nav-plus {
    position: absolute;
    left: 50%;
    top: calc(59px * 2 / 3);
    width: 24px;
    height: 24px;
    transform: translate(-50%, -50%);
  }
  .tb-plus.nav-current .nav-plus { filter: drop-shadow(0 0 4px var(--halo)); }

  /* The grip. Tucked, it is squaring the circle (.tb-sigil) -- circle,
     triangle, square, circle, outside in -- the three keys' solids in one
     sign; raised, the oracle sheet's ring with one
     caret, pointing down, the way the row will go (.tb-ring). Under the
     row, so it holds still while the row rises above it. The whole lip is
     the hit area. No line along its top, tucked or not (owner, 2026-10-04):
     the sign stands on the static like the rest of the bar, part of the
     canvas rather than a strip under it. */
  .tb-grip { display: none; }
  html.tb-reading .tb-grip {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: var(--tb-lip);
    box-sizing: border-box;
    /* Lifted 8px off the foot (owner, 2026-10-04), further from the phone's
       own navigation under the screen: the sign centres in the lip's upper
       52px. */
    padding: 0 0 8px;
    background: none;
    border: none;
    color: var(--phosphor-dim);
    cursor: pointer;
    pointer-events: auto;
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
  }
  .tb-grip svg { display: block; }
  .tb-ring { width: 20px; height: 20px; }
  .tb-ring circle { fill: var(--bg-panel); }
  /* 46px, in a 60px lip (52px, and 8px under it): its outer circle the size of the crystal ball in
     the Oracle's icon (owner, 2026-10-04) -- that ring is 136-138 of the
     200px source across its outer edge, 43.5px at the icon's 64px, and the
     outer circle here spans 22.9 of the sign's 24 units. The lip grew from
     26px to hold it.

     Its lines hold 1.2px at any size rather than growing with it: drawn in
     the sign's own units they came out 1.7px at this size, and the flight
     (nav-rail.js, _tbFly) starts from copies drawn at 1.2px -- the sign
     would visibly thin the moment it came apart. */
  .tb-sigil {
    width: 46px;
    height: 46px;
    stroke: currentColor;
    stroke-width: 1.2;
    stroke-linejoin: miter;
    filter: drop-shadow(0 0 3px var(--halo));
  }
  .tb-sigil > * { vector-effect: non-scaling-stroke; }
  html.tb-reading:not(.tb-raised) .tb-grip { color: var(--phosphor); }
  html.tb-raised .tb-sigil,
  html.tb-reading:not(.tb-raised) .tb-ring { display: none; }
  html.tb-reading .tb-grip:active { color: var(--phosphor); }

  /* The flight (nav-rail.js, _tbFly): copies of the grip's shapes flying to
     the keys as the row rises, and home as it shuts. Left, top, width and
     height ease at the row's pace, and the outline holds 1px through it.
     Over the bar, and the real keys and sign stand hidden while they fly. */
  .tb-fly {
    position: fixed;
    z-index: 82;
    overflow: visible;
    pointer-events: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.2;
    filter: drop-shadow(0 0 3px var(--halo));
  }
  .tb-fly > * { vector-effect: non-scaling-stroke; }
  .tb-fly.is-flying {
    transition: left 0.45s ease, top 0.45s ease, width 0.45s ease, height 0.45s ease,
                color 0.45s ease, opacity 0.45s ease;
  }
  html.tb-flying .tb-key,
  html.tb-flying .tb-sigil { visibility: hidden; }


  /* The header's own plus and gear are the bar's now. */
  body:has(#tabbar) .mob-dup { display: none !important; }

  /* The Journal's list runs under the bar (owner, 2026-10-12). Stopped at the
     row's top, as everywhere else, its rows ended in a hard edge against the
     keys. Here #main gives up its reserve, the list scrolls on to the foot,
     and the bar takes a ground that fades from the screen's black at the
     foot to nothing at its top, so the rows sink into it rather than being
     cut off. The list's own foot room grows by the row, so its last entry
     still scrolls clear of the keys. The bar keeps pointer-events: none, so a
     tap on a row seen through the fade still reaches it. */
  #main:has(#archive-page):has(#tabbar) { margin-bottom: 0; }
  #main:has(#tabbar) #archive-page { padding-bottom: calc(48px + var(--tb-row)); }
  body:has(#archive-page) #tabbar {
    background: linear-gradient(to top,
      rgba(var(--bg-rgb), 1)    calc(env(safe-area-inset-bottom) + 30px),
      rgba(var(--bg-rgb), 0.85) 50%,
      rgba(var(--bg-rgb), 0.4)  78%,
      rgba(var(--bg-rgb), 0)    100%);
  }

  /* Nothing on the stage shows through the raised bar (owner, 2026-10-04).
     The bar has no ground, so the static shows round its keys; a card under
     it -- panned there, or anywhere on a Freestyle table -- showed between
     them too. The stage is clipped at the row's top instead: the cards stop
     at the bar's edge in every spread, and the static stays. The stage ends
     where #main does, on the lip, so the row's height is the clip; it moves
     at the row's pace. */
  html.tb-reading #main:has(#tabbar) #card-stage {
    clip-path: inset(0 0 var(--tb-row-now) 0);
    transition: clip-path 0.45s ease;
  }

  /* Both bottom sheets stand on the bar, and move with it at its pace. */
  #main:has(#tabbar) #detail-oracle,
  #main:has(#tabbar) #fs-notes {
    bottom: calc(var(--tb-lip-now) + var(--tb-row-now) + env(safe-area-inset-bottom));
    transition: height 0.25s ease, bottom 0.45s ease;
  }
  #main:has(#tabbar) #detail-oracle.sheet-dragging,
  #main:has(#tabbar) #fs-notes.sheet-dragging { transition: bottom 0.45s ease; }
  /* While the bar moves, a height re-clamped to the new room eases at the
     bar's pace too, so a sheet at the top stop keeps its top edge still. */
  html.tb-moving #main:has(#tabbar) #detail-oracle,
  html.tb-moving #main:has(#tabbar) #fs-notes {
    transition: height 0.45s ease, bottom 0.45s ease;
  }
  /* Except under a finger: a drag that tucks the bar on its way down
     (grSheetInit) follows the finger at once, and the row slides away
     beneath it. */
  html.tb-moving #main:has(#tabbar) #detail-oracle.sheet-dragging { transition: none; }
}

/* The new-revelation form on a phone, drawn tighter (owner, 2026-10-04): the
   draw options closer under the question -- 22px from the box rather than 34
   (this margin and .nr-chips' 14px padding) -- and RECENT up under BEGIN, with
   none of the 20px every .nr-block takes: the status line's reserved room
   under the button is the gap. Ids here because #nr-panel
   .nr-block sets both margins and would outrank a class alone. */
@media (max-width: 767px) {
  #nr-panel .nr-config { margin-top: 8px; }
  #nr-panel .nr-recent { margin-top: 0; }
}


/* ── Sharing a Revelation ─────────────────────────────────────────────────
   SHARING_PLAN.md §6. The modal (share.js, _share_modal.html) wears the
   meaning modal's frame; the buttons that open it sit beside the Grimoire's
   title, in the phone's top bar, and on every Archive entry. The state is
   always said in words -- SHARE, SHARED -- and the dot that says "changed
   since shared" comes with a title saying so, never color alone. */

.share-modal .card-modal-panel { width: min(520px, 100%); }
/* One rhythm: every block of the body sits 16px from the next, none carries
   its own margin. */
.share-modal #share-modal-body { display: flex; flex-direction: column; gap: 16px; }
.share-modal #share-modal-body > * { margin: 0; }
.share-modal .sh-title {
  font-size: 15px;
  letter-spacing: 3px;
  color: var(--phosphor);
  padding-right: 32px;
}
.share-modal .sh-line {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--phosphor);
}
.share-modal .sh-line + .sh-line { margin-top: 6px; }
.share-modal .sh-dim { color: var(--phosphor-dim); }
.share-modal .sh-switch {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  cursor: pointer;
}
.share-modal .sh-switch .ec-toggle { margin-top: 2px; flex: 0 0 auto; }
.share-modal .sh-switch-text { display: flex; flex-direction: column; gap: 3px; }
.share-modal .sh-switch-label { font-size: 14px; color: var(--phosphor); letter-spacing: 1px; }
.share-modal .sh-switch-sub { font-size: 12.5px; color: var(--phosphor-dim); }
.share-modal .sh-switch-caveat { font-size: 12.5px; color: var(--amber); }
.share-modal .sh-switch-caveat[hidden] { display: none; }
.share-modal .sh-btn {
  background: transparent;
  border: 1px solid var(--phosphor-dim);
  color: var(--phosphor);
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 2px;
  padding: 9px 14px;
  cursor: pointer;
  white-space: nowrap;
}
.share-modal .sh-btn:hover:not(:disabled),
.share-modal .sh-btn:focus-visible { border-color: var(--phosphor); background: var(--wash); }
.share-modal .sh-btn:disabled { opacity: 0.6; cursor: default; }
.share-modal .sh-btn-primary {
  background: var(--phosphor);
  border-color: var(--phosphor);
  color: var(--bg);
  min-width: 140px;
}
.share-modal .sh-btn-primary:hover:not(:disabled),
.share-modal .sh-btn-primary:focus-visible { background: var(--phosphor); filter: brightness(1.1); }
.share-modal .sh-btn-danger { border-color: var(--red-alert); color: var(--red-alert); }
.share-modal .sh-btn-danger:hover:not(:disabled),
.share-modal .sh-btn-danger:focus-visible { border-color: var(--red-alert); background: transparent; filter: brightness(1.2); }
.share-modal .sh-btn.copied { color: var(--amber); border-color: var(--amber); }
.share-modal .sh-btn.copy-failed { color: var(--red-alert); border-color: var(--red-alert); }
/* The quiet action: the buttons' lettering, no box -- for what sits beside
   something else rather than asking to be pressed. */
.share-modal .sh-quiet {
  background: none;
  border: none;
  padding: 4px 0;
  color: var(--phosphor-dim);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 2px;
  cursor: pointer;
}
.share-modal .sh-quiet:hover,
.share-modal .sh-quiet:focus-visible { color: var(--phosphor); }
.share-modal .sh-quiet-danger:hover,
.share-modal .sh-quiet-danger:focus-visible { color: var(--red-alert); }
.share-modal .sh-row { display: flex; flex-wrap: wrap; gap: 10px; }
/* The share's own theme (owner, 2026-10-05): the Include switches' row, with
   the Yin/Yang switch's track and knob (.yy-track, .yy-knob) at the Include
   switch's width, so the text lines up. Its knob follows the share's theme
   (.is-yang), never the screen's, whatever :root[data-theme] says. */
.share-modal .sh-yy {
  flex: 0 0 auto;
  background: none;
  border: none;
  padding: 0;
  margin-top: 0;
  line-height: 0;
  cursor: pointer;
}
.share-modal .sh-yy:disabled { opacity: 0.6; cursor: default; }
.share-modal .sh-yy .yy-track { width: 34px; height: 20px; }
.share-modal .sh-yy .yy-knob { transform: translateX(0) rotate(0deg); }
.share-modal .sh-yy.is-yang .yy-knob { transform: translateX(14px) rotate(180deg); }
.share-modal .sh-yy:hover:not(:disabled) .yy-track { border-color: var(--phosphor); box-shadow: inset 0 0 10px var(--phosphor-glow); }
.share-modal .sh-yy:focus-visible { outline: none; }
.share-modal .sh-yy:focus-visible .yy-track { outline: 1px solid var(--phosphor); outline-offset: 2px; }
/* The link, with COPY (and SHARE… where the device has a share sheet) as one
   segmented control. */
.share-modal .sh-link {
  display: flex;
  border: 1px solid var(--phosphor-dim);
}
.share-modal .sh-link input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  color: var(--phosphor);
  font-family: inherit;
  font-size: 13px;
  padding: 9px 10px;
  user-select: all;
  -webkit-user-select: all;
  outline: none;
}
.share-modal .sh-link:focus-within { border-color: var(--phosphor); }
.share-modal .sh-link .sh-btn { border: none; border-left: 1px solid var(--phosphor-dim); padding: 9px 12px; }
.share-modal .sh-thumb img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--phosphor-faint);
}
.share-modal .sh-thumb figcaption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 4px;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--phosphor-dim);
}
/* A picked platform's image: tall shapes are held to a height, whole. */
.share-modal .sh-thumb-port img,
.share-modal .sh-thumb-story img {
  width: auto;
  max-width: 100%;
  max-height: 340px;
  margin: 0 auto;
}
.share-modal .sh-thumb-acts { display: flex; gap: 14px; flex-wrap: wrap; justify-content: flex-end; }
/* GET AN IMAGE FOR (SHARE_IMAGE_PLAN.md §3.2): the platforms as quiet chips;
   the picked one lit, the last one picked on this device faintly marked. */
.share-modal .sh-platforms { display: flex; flex-direction: column; gap: 6px; margin-top: -4px; }
.share-modal .sh-plat-head { font-size: 11px; letter-spacing: 2px; color: var(--phosphor-dim); }
.share-modal .sh-plat-row { display: flex; flex-wrap: wrap; gap: 6px; }
.share-modal .sh-plat {
  background: transparent;
  border: 1px solid var(--phosphor-faint);
  color: var(--phosphor-dim);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 1px;
  padding: 5px 9px;
  cursor: pointer;
  white-space: nowrap;
}
.share-modal .sh-plat:hover:not(:disabled),
.share-modal .sh-plat:focus-visible { color: var(--phosphor); border-color: var(--phosphor-dim); }
.share-modal .sh-plat.is-last { border-color: var(--phosphor-dim); }
.share-modal .sh-plat.is-on { color: var(--amber); border-color: var(--amber); }
.share-modal .sh-plat:disabled { opacity: 0.6; cursor: default; }
.share-modal .sh-status {
  font-size: 12.5px;
  color: var(--phosphor-dim);
}
/* A line's space above what the switch just did, apart from the switch's own
   text (owner, 2026-10-05); nothing to say, no space taken. */
.share-modal .sh-status { padding-top: 6px; }
.share-modal .sh-status:empty { display: none; }
.share-modal .sh-changed {
  border-left: 2px solid var(--amber);
  padding: 2px 0 2px 14px;
}
.share-modal .sh-changed .sh-line:first-child { color: var(--amber); }
.share-modal .sh-changed .sh-btn { margin-top: 12px; }
/* Nothing changed: the same block, unlit -- a dim rule, the button shown but
   unavailable, and a line under it saying when it will be. */
.share-modal .sh-changed.sh-current { border-left-color: var(--phosphor-faint); }
.share-modal .sh-changed.sh-current .sh-btn { margin-top: 0; }
.share-modal .sh-update-why {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--phosphor-dim);
}
/* The foot: CREATE A LINK before there is a link; afterwards the date it was shared
   and STOP SHARING, under a rule. */
.share-modal .sh-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  border-top: 1px solid var(--phosphor-faint);
  padding-top: 16px;
}
.share-modal .sh-foot-split { justify-content: space-between; font-size: 12px; letter-spacing: 1px; padding-top: 12px; }
.share-modal .sh-stop-confirm {
  border: 1px solid var(--red-alert);
  padding: 14px;
}
.share-modal .sh-stop-confirm .sh-row { margin-top: 14px; }
/* Side by side where they fit; full width, one above the other, where not. */
.share-modal .sh-stop-confirm .sh-btn { flex: 1 1 auto; }

/* The Grimoire's header button, beside the title; and its stand-in in the
   phone's top bar. Dim until shared, lit once shared, with an amber dot when
   the reading has changed since. */
.share-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  background: transparent;
  border: 1px solid transparent;
  color: var(--phosphor-dim);
  font-family: inherit;
  font-size: 13px;
  font-weight: bold;
  letter-spacing: 2px;
  padding: 3px 8px;
  cursor: pointer;
  position: relative;
}
.share-btn[hidden] { display: none; }
.share-btn .share-icon { width: 16px; height: 16px; display: block; }
.share-btn:hover, .share-btn:focus-visible { color: var(--phosphor); border-color: var(--phosphor-faint); }
.share-btn.is-shared {
  color: var(--phosphor);
  text-shadow: 0 0 6px var(--phosphor-glow);
}
.share-btn.is-changed::after {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 6px var(--amber);
}
/* On a desktop the reading's button stands on the stage, under the Mind
   Machine switch and in the switch's own lettering, or in the switch's place
   once a finished reading has put the switch away (owner, 2026-10-05). Boxed
   and a size up from the header's, so it is found. Until the reading is
   shared it swings as every waiting control does (beginReady); shared, it
   holds steady and lit. */
.stage-share {
  position: absolute;
  top: 44px;
  left: 10px;
  z-index: 60;
  gap: 9px;
  padding: 7px 12px;
  border: 1px solid var(--phosphor-dim);
  background: var(--bg-panel);
  font-size: 15px;
  letter-spacing: 3px;
}
#card-stage.mm-spent .stage-share { top: 8px; }
.stage-share .share-icon { width: 22px; height: 22px; }
.stage-share:not(.is-shared):not([hidden]) { animation: beginReady 0.7s ease-in-out infinite; }
.stage-share:not(.is-shared):hover,
.stage-share:not(.is-shared):focus-visible { animation: none; color: var(--phosphor); border-color: var(--phosphor); }
.stage-share.is-shared {
  border-color: var(--phosphor);
  box-shadow: 0 0 10px var(--phosphor-glow), inset 0 0 8px var(--phosphor-glow-faint);
}
/* On a phone, the same button at the stage's top left, across from the ⊙
   (owner, 2026-10-16; it was a small one in the top bar, and went unfound).
   A size down to suit the screen, and under the sheets as the ⊙ is (z 30):
   it belongs to the stage, and a sheet raised over the stage covers it. */
@media (max-width: 767px) {
  .stage-share,
  #card-stage.mm-spent .stage-share {
    top: 8px;
    left: 8px;
    z-index: 30;
    gap: 7px;
    padding: 6px 10px;
    font-size: 13px;
    letter-spacing: 2px;
  }
  .stage-share .share-icon { width: 20px; height: 20px; }
}

/* An Archive entry's share button, under its options button and outside the
   entry's link (a button cannot sit inside an anchor). Always showing --
   dim, or lit once shared -- since whether a reading is public is the one
   thing on the row a reader must never have to hover to learn. */
.ae-share {
  position: absolute;
  right: 6px;
  top: 48px;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 3px;
  color: var(--phosphor-dim);
  opacity: 0.55;
  cursor: pointer;
  padding: 0;
}
.ae-share .share-icon { width: 18px; height: 18px; }
.ae-share:hover, .ae-share:focus-visible { opacity: 1; background: var(--wash); color: var(--phosphor); }
.ae-share.is-shared { opacity: 1; color: var(--phosphor); filter: drop-shadow(0 0 4px var(--phosphor-glow)); }
.ae-shared {
  color: var(--phosphor);
  border: 1px solid var(--phosphor-dim);
  border-radius: 2px;
  padding: 1px 6px;
  font-size: 11.5px;
}
/* SHARE in the options menu is hidden for a reading that cannot be shared
   yet; the menu's buttons are display: block, which would outrank [hidden]. */
.archive-overflow-btn[hidden] { display: none; }

/* ── Paid features (owner, 2026-10-10) ─────────────────────────────────────
   Every paid feature is shown to every member while billing is on; what the
   member has not got carries a crown at its far right (_icon_crown.html), is
   dimmed like an unavailable choice, and opens the pitch modal when pressed
   (billing.js). Amber, the color the app already uses for what asks for
   attention without being an error. */
.paid-crown {
  display: inline-flex;
  align-items: center;
  flex: none;
  margin-left: auto;
  padding-left: 10px;
  color: var(--amber);
}
.paid-crown-icon { width: 16px; height: 16px; display: block; }

.nr-opt--locked .nr-opt-thumb,
.nr-opt--locked .nr-opt-body { opacity: 0.45; }
.nr-opt--locked:hover .nr-opt-thumb,
.nr-opt--locked:hover .nr-opt-body { opacity: 0.6; }

/* The rail's Spread Studio without Studio: a button, not a link. */
.strip-icon-btn--locked {
  position: relative;
  background: transparent;
  border: none;
  cursor: pointer;
}
.strip-icon-btn--locked .nav-icon { opacity: 0.55; }
.paid-crown--corner {
  position: absolute;
  top: 2px;
  right: 2px;
  margin: 0;
  padding: 0;
}
.paid-crown--corner .paid-crown-icon { width: 11px; height: 11px; }
.nav-item--locked {
  width: 100%;
  background: transparent;
  border: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.nav-item--locked .nav-icon,
.nav-item--locked .nav-item-label { opacity: 0.55; }

.clm-source.locked .paid-crown { padding-left: 6px; }

/* The Mind Machine without a plan that includes it (owner, 2026-10-10): the
   rail's player dimmed with its crown, and a crown beside the switches in
   Settings and on the Grimoire's stage. */
#mm-sidebar-player.mm-locked .mm-player-icon,
#mm-sidebar-player.mm-locked .mm-player-label { opacity: 0.55; }
#mm-sidebar-player.mm-locked { gap: 8px; }
#mm-sidebar-player.mm-locked .mm-player-label { letter-spacing: 1.5px; }
#mm-sidebar-player.mm-locked .paid-crown { padding-left: 0; margin-left: 0; }
.strip-icon-btn--locked .strip-icon { opacity: 0.55; }
.paid-crown--inline { margin-left: 0; padding-left: 6px; vertical-align: -2px; display: inline-flex; }
.paid-crown--inline .paid-crown-icon { width: 13px; height: 13px; }

/* The Journal menu's EXPORT without Oracle: dimmed, crown at the far right. */
.archive-overflow-btn--locked { display: flex; align-items: center; opacity: 0.7; }
.archive-overflow-btn--locked:hover { opacity: 1; }

/* "EXPORTING…" under a Journal entry while its file is made. */
.ae-export-note {
  font-family: 'Share Tech Mono', monospace;
  font-size: 11.5px;
  letter-spacing: 1.5px;
  color: var(--amber);
  padding: 4px 0 2px;
}

/* The slim pitch modal: the feature, then each plan that includes it with
   what the plan includes (the feature's line lit), then SEE PLANS. The lists
   come from wyrdos/billing/plans.py, as /pricing's cards do. */
.pitch-modal .card-modal-panel { max-width: 480px; }
.pitch-body { display: flex; flex-direction: column; gap: 12px; }
.pitch-text { margin: 0; line-height: 1.6; color: var(--phosphor); }
/* A feature's tag under its text (plans.FEATURES "tag"): the app's pill. */
.pitch-tag { margin: -2px 0 0; }
.pitch-tag .cm-orient { display: inline-block; padding: 2px 8px; }
.pitch-incl { margin: 4px 0 0; }
.pitch-offers { display: flex; flex-direction: column; gap: 10px; }
.pitch-offer {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px;
  border: 1px solid var(--phosphor-faint);
}
.pitch-offer--best { border-color: rgba(var(--amber-rgb), 0.6); }
.pitch-offer-head { display: flex; justify-content: space-between; gap: 12px; letter-spacing: 2px; }
.pitch-offer-price { color: var(--amber); }
.pitch-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.pitch-list li {
  position: relative;
  padding-left: 18px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--phosphor-dim);
}
.pitch-list li::before { content: "\2713"; position: absolute; left: 0; color: var(--phosphor); }
.pitch-list li.lit { color: var(--amber); }
.pitch-list li.lit::before { content: "\25C6"; color: var(--amber); }
.pitch-list li.accent, .pitch-list li.accent::before { color: var(--amber); }
.pitch-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 4px; }
.pitch-see {
  color: var(--amber);
  border-color: var(--amber);
  text-decoration: none;
}
