/* BourneWise glass refinement layer.
   Existing geometry and spacing stay untouched; this layer only defines the
   shared translucent material, edge light and depth hierarchy. */

:root {
  --bw-palette-cloud: var(--paper);
  --bw-palette-1: var(--paper-raised);
  --bw-palette-2: var(--oat);
  --bw-palette-6: var(--terracotta);
  --bw-palette-9: var(--ink);
  --bw-palette-on-9: var(--paper);
  --bw-palette-gem: var(--terracotta);
  /* GLASS — one material, defined once.
     The tint is neutral white on purpose. It used to be a colour-mix of
     --bw-palette-1, i.e. the palette's own cream, which is why panels read
     yellow: measured across all 180 groups the composited surface sat at
     |R−B| 68 mean / 89 max. Glass should take its warmth from the backdrop
     it is over, never add its own — a panel that carries cream in the tint
     is warm even where the backdrop behind it is cold, and no opacity
     setting can fix that because the cream is in the panel.
     The backdrop still comes through: saturate() lifts what is behind rather
     than staining the panel, which is the part that is supposed to shift as
     the palette rotates. */
  --ui-glass-tint: 255 255 255;
  /* Thinner than before. At .50/.66 the panels were frosted plastic: you could
     tell something was behind them but not what, and a material you cannot see
     through is not read as glass. The lost opacity is bought back by
     brightness() in the filter, which lifts the backdrop instead of covering
     it — so the panel gets lighter under dark text while staying transparent. */
  /* FROSTED, not clear. Clear glass refracts — you read shapes through it.
     Frosted glass scatters — you read light and colour through it but not
     shape. That difference is carried by three things together, and any one
     alone just looks like blurrier clear glass: a much wider blur so no edge
     behind survives, saturation pulled back toward neutral because scattering
     desaturates, and a fine grain in the surface itself. */
  --ui-glass: rgb(var(--ui-glass-tint) / .34);
  /* ⭐ Frosted, not painted (owner's call, 2026-08-26). At .76 the carriers were
     opaque enough that the range simply stopped behind them and they read as
     three white boards. Frost is the opposite instruction: keep the colour,
     lose the detail. So the white comes down and the work moves into the
     backdrop filter — a heavier blur plus a brightness lift, which raises the
     luminance of what is behind without covering it.
     ⭐ 2026-08-26, second pass — owner asked for it clear, not merely frosted:
     "晶莹透亮". So the white drops much further and the character comes from a
     shorter, sharper blur with real saturation and brightness behind it, which
     is what reads as glass rather than as tracing paper. The range now shows
     through at ~58% where it was 24% two passes ago.
     ⚠️ This is a chosen trade and it is a real one: the earlier note here
     recorded that .50 measures 3.39:1 and .42 measures 3.28:1 on the darkest
     group. Those numbers still hold. Small type on these carriers no longer
     clears AA on the darker half of the palette; the decision was made looking
     at the rendering, with the measurement in hand. */
  --ui-glass-strong: rgb(var(--ui-glass-tint) / .42);
  --ui-glass-soft: rgb(var(--ui-glass-tint) / .22);
  --ui-glass-warm: rgb(var(--ui-glass-tint) / .44);
  /* Specular top edge + hairline bottom: the two cues that separate "glass"
     from "a translucent rectangle". Applied as an inset shadow so it rides
     the radius without a pseudo-element. */
  /* A rim on all four sides, not just the top, plus a soft glow falling in
     from the top edge. Real glass is brightest where light bends at the
     boundary, and that rim is most of what identifies the material at a
     glance — a single top highlight reads as a bevel instead. */
  /* Frosted glass has a softer, wider rim than clear glass — the scattering
     spreads the edge light instead of concentrating it in a specular line. */
  /* A soft edge, not a drawn one. Frosted glass has no specular line at its
     rim — the scattering that hides shapes behind it also spreads the edge
     light, so a crisp 1px highlight on all four sides reads as a cut acrylic
     sheet rather than frost. The hard rim is replaced by a diffuse inward
     glow: bright at the top where light falls, fading over ~14px, with only
     a trace of a rim left to keep the panel from dissolving entirely. */
  /* ⚠️⚠️ NO WHITE. This was four stacked white insets, the strongest a 50%
     white glow 10px down from the top edge — the pale band along the composer
     and every panel. White is asked about here for the third time: the white
     underlays were retired in August, and this is the last of it.

     What replaces it is not nothing, because a panel with no edge dissolves
     into the range behind it. It is the SAME edge drawn in ink instead of in
     light: a hairline rim and a whisper of inner shade, which defines the panel
     without putting a colour on the page that no palette group contains. */
  --ui-glass-edge:
    inset 0 0 0 1px rgb(28 26 23 / .10),
    inset 0 1px 0 0 rgb(28 26 23 / .06),
    inset 0 -10px 14px -14px rgb(28 26 23 / .10);
  /* Softened with the rim. At 12% this drew a visible dark outline around
     every frosted panel — the one hard edge on a material whose whole point
     is that it has none. */
  /* A 1px border at 5% ink is a line you cannot see — the panel edge was being
     carried entirely by the glass highlight, and on the lighter palette groups
     there was nothing there at all. Thin is the rule; invisible is a bug. */
  --ui-line: color-mix(in srgb, var(--ink) 11%, transparent);
  --ui-line-strong: color-mix(in srgb, var(--ink) 28%, transparent);
  --ui-ink-soft: color-mix(in srgb, var(--ink) 72%, transparent);
  /* ELEVATION — three steps, each a contact shadow plus an ambient one.
     A single wide blur is what reads as coarse: with nothing tight underneath
     it, the card has no seat and the blur's edge becomes the only thing the
     eye can find, so it looks like a smudge rather than a lift. The short
     first layer does the seating; the long layers stay under .05 alpha and
     never gain a spread, because spread is what turns a shadow into a band. */
  --ui-shadow:
    0 1px 2px rgba(42,32,22,.05),
    0 6px 14px rgba(42,32,22,.045),
    0 14px 32px rgba(42,32,22,.035);
  --ui-shadow-raised:
    0 1px 2px rgba(42,32,22,.055),
    0 8px 20px rgba(42,32,22,.05),
    0 22px 48px rgba(42,32,22,.045);
  --ui-shadow-overlay:
    0 1px 2px rgba(42,32,22,.06),
    0 12px 28px rgba(42,32,22,.06),
    0 36px 76px rgba(42,32,22,.055);
  /* Blur in the 22–26px band the owner specified. Saturation stays at 128%
     rather than Apple's 180%: at 180% the warm palettes bloom and the cast
     comes straight back through the backdrop instead of through the tint. */
  --ui-glass-filter: blur(30px) saturate(136%) brightness(1.26);
  --ui-glass-filter-strong: blur(38px) saturate(140%) brightness(1.32);
  /* The grain is the part that actually reads as "frosted". feTurbulence at a
     high base frequency is per-pixel noise, so it stays fine at any DPI and
     costs one inlined SVG rather than a texture file. */
  --ui-frost-grain: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>\
<filter id='f'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='140' height='140' filter='url(%23f)' opacity='0.42'/></svg>");

  /* SOLID — the other material, and deliberately not a weaker glass.
     Actions and badges are opaque, flat, and seated by a tight shadow. They
     were getting no shadow at all, which is why their edges looked frayed: a
     solid block laid straight onto a drifting blurred landscape has nothing
     between its edge and the contour lines running underneath, so the boundary
     shimmers as the background moves. The shadow is short and slightly darker
     than the glass ladder's — it is a contact shadow, not elevation. */
  --ui-solid-shadow:
    0 1px 1px rgba(42,32,22,.16),
    0 2px 6px rgba(42,32,22,.11);
  /* One radius, two sizes. These were eight-value wobble radii — a
     hand-drawn gesture that reads as a wavering edge under a blur and is the
     source of most of the 26 distinct radii on the site. Glass has a
     machined edge; that is most of what makes it read as glass. */
  --ui-radius: 14px;
  --ui-radius-small: 10px;
  --ui-radius-chip: 6px;   /* badges, tags, inline code — a third size is a
                              system; twenty-six loose numbers is not. */
  --ui-radius-pill: 999px;
  --ui-hairline: 1px;
}

/* Registered colours make custom properties genuinely interpolable. The
   runtime supplies the 400ms top-to-bottom stagger between landscape planes. */
@property --bw-palette-cloud { syntax: "<color>"; inherits: true; initial-value: #DED8CD; }
@property --bw-palette-water { syntax: "<color>"; inherits: true; initial-value: #B4AA9A; }
@property --bw-palette-1 { syntax: "<color>"; inherits: true; initial-value: #D7D0C4; }
@property --bw-palette-2 { syntax: "<color>"; inherits: true; initial-value: #CEC4B5; }
@property --bw-palette-3 { syntax: "<color>"; inherits: true; initial-value: #C8BDAD; }
@property --bw-palette-4 { syntax: "<color>"; inherits: true; initial-value: #C2B49F; }
@property --bw-palette-5 { syntax: "<color>"; inherits: true; initial-value: #BBAA92; }
@property --bw-palette-6 { syntax: "<color>"; inherits: true; initial-value: #B39F82; }
@property --bw-palette-7 { syntax: "<color>"; inherits: true; initial-value: #A99275; }
@property --bw-palette-8 { syntax: "<color>"; inherits: true; initial-value: #A0866A; }
@property --bw-palette-9 { syntax: "<color>"; inherits: true; initial-value: #967B60; }
@property --bw-palette-10 { syntax: "<color>"; inherits: true; initial-value: #88705A; }
@property --bw-palette-gem { syntax: "<color>"; inherits: true; initial-value: #B5502C; }

html[data-skin="paper"] {
  transition:
    --bw-palette-cloud 1.5s var(--ease-in-out), --bw-palette-water 1.5s var(--ease-in-out),
    --bw-palette-1 1.5s var(--ease-in-out), --bw-palette-2 1.5s var(--ease-in-out),
    --bw-palette-3 1.5s var(--ease-in-out), --bw-palette-4 1.5s var(--ease-in-out),
    --bw-palette-5 1.5s var(--ease-in-out), --bw-palette-6 1.5s var(--ease-in-out),
    --bw-palette-7 1.5s var(--ease-in-out), --bw-palette-8 1.5s var(--ease-in-out),
    --bw-palette-9 1.5s var(--ease-in-out), --bw-palette-10 1.5s var(--ease-in-out),
    --bw-palette-gem 1.5s var(--ease-in-out);
}

html { color-scheme: light; }
html[data-skin="paper"],
html[data-skin="paper"] body { background-color: var(--bw-palette-cloud); }
body { text-rendering: optimizeLegibility; }
a, button, input { -webkit-tap-highlight-color: transparent; }
button, a { transition-timing-function: var(--ease-out); }
::selection { background: color-mix(in srgb, var(--bw-palette-gem) 28%, transparent); }

/* Typography contract: BourneWise never relies on italics or underlines for
   emphasis. Weight, colour, spacing and glass hierarchy carry that work. */
html[data-skin="paper"] body :is(em, i, cite, dfn) { font-style: normal !important; }
html[data-skin="paper"] body a {
  text-decoration: none !important;
  border-bottom: 0 !important;
}
html[data-skin="paper"] body :is(.gild, .reading-body .gild) {
  border-bottom: 0 !important;
  box-shadow: none !important;
}
html[data-skin="paper"] body .inkline svg { display: none !important; }

/* One wordmark specification everywhere: Pacifico 400, 18px, with the same
   optical baseline and stroke. Page-specific wordmark rules may size the
   symbol, but never change the lettering. */
html[data-skin="paper"] body :is(.brand-word, .brand b, .wordmark span) {
  font-family: var(--font-brand) !important;
  font-style: normal !important;
  font-weight: 400 !important;
  font-size: 18px !important;
  line-height: 1 !important;
  letter-spacing: 0 !important;
  -webkit-text-stroke: .45px currentColor;
  transform: translateY(-1px);
}

/* Clarity baseline --------------------------------------------------------
   Blur belongs only to named UI carriers below. Reading/casting surfaces stay
   unfiltered, so pointer movement cannot recreate the old full-width tan box. */
body[data-page="app"] .reading-body .rw-s0,
body[data-page="app"] .reading-body .rw-s4 { filter: none !important; }
body[data-page="about"] .page-fx { display: none; }

html[data-skin="paper"] body .mtn-sky {
  animation: none !important;
  background: var(--bw-palette-cloud) !important;
  transition: background-color 1.5s var(--ease-in-out);
}
/* Fill OPACITY is not set here any more. It was — at .18 !important — which
   outspecifies the engine's own injected sheet, so the ridges painted at 18%
   of the curated colour no matter what mountain-range.js said. The palettes
   looked washed out because 85% of every card was being blended away here,
   not because the cards lacked colour. Same trap as the contour widths above,
   same fix: one definition, living with the geometry. */
/* Written for the cloud group, which is the other clipped node in the range.
   Excluding the moire group matters: its bands are also clipped <use>, and at
   .13 !important the texture is decided here by a rule that was never about it. */
html[data-skin="paper"] body .mtn-bg [clip-path]:not(.moire) > use { opacity: .13 !important; }
/* ⚠️⚠️⚠️ REVERTED, and the reasoning that removed it was wrong in a way worth
   keeping on the page.

   This near-black !important was read as a fourth instance of the pattern this
   file has three real instances of — refinement.css overriding the range with a
   hard-coded value where the engine already had a palette-aware one. The engine
   does inject ten tinted rules
   (`.contour.l1 use { stroke: color-mix(…var(--bw-palette-1) 42%…) }`), so it
   looked like the same mistake, and the note above it even stated the ownership
   backwards. All true, and the conclusion was still wrong.

   ⭐ A contour tinted with its OWN ridge's colour at 42% alpha, drawn on top of
   that same ridge, is invisible. Removing this ink took the entire line drawing
   off the landscape and left flat colour bands — the home page stopped being a
   drawing and became a gradient. The tinted rules had never been visible, so
   they had never been looked at; "the engine was already deciding it correctly"
   was an assumption, not an observation.

   The ink is the drawing. It is meant to be near-black and it is meant to win. */
html[data-skin="paper"] body .mtn-bg .contour use,
html[data-skin="paper"] body .mtn-bg .cloud-contour use {
  stroke: color-mix(in srgb, #141413 30%, transparent) !important;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
/* Contour WIDTH is not set here any more. It was — at 1.6/2.45/1.9 with
   !important — and because that outspecifies the engine's own injected
   sheet, two separate rounds of thickening mountain-range.js changed
   nothing at all on screen. One definition, and it lives with the
   geometry that sizes itself against it. Only opacity stays here. */
html[data-skin="paper"] body .mtn-bg .contour use:nth-child(3n+1) { opacity: .72; }
html[data-skin="paper"] body .mtn-bg .contour use:nth-child(3n+2) { opacity: .5; }
html[data-skin="paper"] body .mtn-bg .contour use:nth-child(3n) { opacity: .62; }
html[data-skin="paper"] body .mtn-bg .fill,
html[data-skin="paper"] body .mtn-bg [clip-path] > use {
  transition: fill 1.5s var(--ease-in-out);
}

@media (prefers-reduced-motion: reduce) {
  html[data-skin="paper"] { transition-duration: 190ms; }
  html[data-skin="paper"] body .mtn-sky,
  html[data-skin="paper"] body .mtn-bg .fill,
  html[data-skin="paper"] body .mtn-bg [clip-path] > use { transition-duration: 190ms; }
}

/* Shared glass carriers --------------------------------------------------- */
html[data-skin="paper"] body :is(
  .m-card, .band, .tile, .card, .plan, .topup, .pack, .cost, .ribbon,
  .panel, .toc, .pull, .headline
) {
  background: var(--ui-glass) !important;
  background-image: none !important;
  border: var(--ui-hairline) solid var(--ui-line) !important;
  box-shadow: var(--ui-glass-edge), var(--ui-shadow) !important;
  backdrop-filter: var(--ui-glass-filter);
  -webkit-backdrop-filter: var(--ui-glass-filter);
}
/* .card-hero is out of this list. The others stand on the page on their own,
   where a warmer glass is a legitimate step in the ladder; .card-hero sits
   INSIDE .card, so a second translucency composites over the first and
   draws a seam across the middle of the card. Three separate files were
   painting this one element — this was the third. */
html[data-skin="paper"] body :is(.band, .ribbon, .plan.feat) {
  background: var(--ui-glass-warm) !important;
}
html[data-skin="paper"] body .headline,
html[data-skin="paper"] body .headline :is(.lbl, p, b) {
  background-image: none !important;
  color: var(--ink) !important;
}
html[data-skin="paper"] body :is(
  .ledger, .side-foot, .composer, .method-menu, .acct-menu,
  .signin-coach, .guide-coach, .top.stuck
) {
  background: var(--ui-glass-strong) !important;
  border-color: var(--ui-line) !important;
  box-shadow: var(--ui-glass-edge), var(--ui-shadow) !important;
  backdrop-filter: var(--ui-glass-filter-strong);
  -webkit-backdrop-filter: var(--ui-glass-filter-strong);
}
html[data-skin="paper"] body :is(
  .ledger, .side-foot, .ledgerstrip, .plan, .topup, .panel, .toc
)::before {
  content: none !important;
}
html[data-skin="paper"] body .ledgerstrip {
  background: var(--ui-glass);
  border: var(--ui-hairline) solid var(--ui-line);
  border-radius: var(--ui-radius);
  box-shadow: var(--ui-glass-edge), var(--ui-shadow);
  backdrop-filter: var(--ui-glass-filter);
  -webkit-backdrop-filter: var(--ui-glass-filter);
}
html[data-skin="paper"] body .m-card:nth-child(2n),
html[data-skin="paper"] body .pack:nth-child(2n),
html[data-skin="paper"] body .cost:nth-child(2n) {
  border-radius: var(--ui-radius);
}
/* Deleted 2026-08-26. Of its three selectors, two matched nothing on the whole
   site — there is no .band anywhere, and pricing's featured plan is
   .plan.featured, not .plan.feat — so the only element it reached was
   .btn.terracotta, where it was wrong: the rule further down fills that button
   with clay stepped toward ink, and this put --ink back on top of it. Dark on
   dark, measured 2.28:1 for settings' "Get units". It also outspecified the
   correct colour despite sitting 1,000 lines earlier, because :is() takes the
   specificity of its heaviest argument and .plan.feat .choose is (0,3,0). */

/* Shared page chrome ------------------------------------------------------ */
body:not([data-page="app"]) .pagehead,
body[data-page="pricing"] .pagehead {
  width: min(1180px, calc(100% - 48px));
  margin: 14px auto 0;
  padding: 8px 12px 8px 16px;
  border: var(--ui-hairline) solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-glass);
  box-shadow: var(--ui-glass-edge), var(--ui-shadow);
  backdrop-filter: var(--ui-glass-filter);
  -webkit-backdrop-filter: var(--ui-glass-filter);
}
body:not([data-page="app"]) .brand b,
body[data-page="pricing"] .brand b {
  font-family: var(--font-brand);
  font-weight: 400;
  -webkit-text-stroke: .45px currentColor;
  letter-spacing: 0;
}
body:not([data-page="app"]) .pagehead .ghost,
body[data-page="pricing"] .pagehead .ghost {
  min-height: var(--ctl);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
  line-height: 1;
  border-radius: var(--ui-radius-pill);
}
body:not([data-page="app"]) .pagehead .cta,
body[data-page="pricing"] .pagehead .cta {
  min-height: var(--ctl);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-block: 0;
  line-height: 1;
  white-space: nowrap;
}
body[data-page="about"] .slab-nav .cta {
  min-height: var(--ctl-compact);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-block: 0;
  line-height: 1;
  white-space: nowrap;
}
body:not([data-page="app"]) .pagehead .ghost:hover,
body[data-page="pricing"] .pagehead .ghost:hover { background: var(--ui-glass-warm); }

/* App shell --------------------------------------------------------------- */
body[data-page="app"] .sidebar {
  padding: 16px 12px 12px;
  background: color-mix(in srgb, var(--paper-raised) 62%, transparent);
  border-right-color: var(--ui-line);
  box-shadow: inset -1px 0 0 rgba(226,218,205,.28);
}
body[data-page="app"] .side-top { padding-inline: 4px; }
body[data-page="app"] .topbar { min-height: 66px; padding: 16px 32px 12px; align-items: center; }
body[data-page="app"] .topbar .ghost {
  min-height: var(--ctl-compact); display: inline-flex; align-items: center; padding: 0 12px;
  border-radius: var(--ui-radius-pill); font-size: 13px; font-weight: 500 !important;
}
body[data-page="app"] .topbar .ghost:hover { background: var(--ui-glass-warm); }
body[data-page="app"] .claude-note { opacity: .72; max-width: 400px; }
body[data-page="app"] .claude-mark { animation-duration: 4.8s; }
body[data-page="app"] .brand-btn,
body[data-page="app"] .side-toggle,
body[data-page="app"] .new-cast,
body[data-page="app"] .castings .casting,
body[data-page="app"] .casting-row { border-radius: var(--ui-radius-small); }
body[data-page="app"] .new-cast { min-height: var(--ctl); padding: 8px; color: var(--ink); }
body[data-page="app"] .new-cast .ic { width: 26px; height: 26px; }
body[data-page="app"] .ledger {
  margin: 12px 2px 0; padding: 16px; border: var(--ui-hairline) solid var(--ui-line) !important;
  border-radius: var(--ui-radius); background: var(--ui-glass-warm);
}
body[data-page="app"] .ledger::before { display: none; }
body[data-page="app"] .ledger:hover { transform: none; border-color: var(--ui-line-strong) !important; }
body[data-page="app"] .ledger-meter { height: 4px; }
body[data-page="app"] .exchange {
  min-height: var(--ctl); border-radius: var(--ui-radius-small); box-shadow: 0 6px 18px rgba(31,29,26,.13);
  background: var(--ink); color: var(--paper-raised);
}
body[data-page="app"] .exchange:hover { background: var(--terracotta); box-shadow: 0 8px 22px rgba(181,80,44,.18); transform: none; }
body[data-page="app"] .side-foot { min-height: var(--ctl); border-radius: var(--ui-radius-small); }
body[data-page="app"] .side-foot::before { inset: 0; border-radius: inherit; mask-image: none; -webkit-mask-image: none; }

body[data-page="app"].is-empty .thread { padding-bottom: 8px; }
body[data-page="app"] #heroEmpty { padding-bottom: clamp(30px, 5vh, 54px); }
body[data-page="app"] .greeting { max-width: 790px; min-height: 4.25em; gap: 16px; }
body[data-page="app"] .greeting h1 {
  max-width: 700px; font-size: clamp(38px, 5.2vw, 68px); font-weight: 500;
  min-height: 4.08em; line-height: 1.02; letter-spacing: -.045em; text-wrap: balance;
  color: var(--ink);
}
body[data-page="app"] .greeting h1 .bw-reveal-word {
  display: inline-block;
  opacity: 0;
  filter: blur(9px);
  transform: translate3d(0,.16em,0) scale(.985);
  animation: bwBlurReveal 1350ms cubic-bezier(.2,.72,.2,1) forwards;
  animation-delay: calc(var(--word-index) * 105ms);
  will-change: opacity, filter, transform;
}
body[data-page="app"] .greeting h1[data-reveal-phase="hold"] .bw-reveal-word {
  opacity: 1;
  filter: blur(0);
  transform: none;
  animation: none;
  will-change: auto;
}
body[data-page="app"] .greeting h1[data-reveal-phase="exit"] .bw-reveal-word {
  opacity: 1;
  filter: blur(0);
  transform: none;
  animation: bwBlurLeave 1050ms cubic-bezier(.55,.05,.75,.35) forwards;
  animation-delay: calc(var(--word-reverse) * 38ms);
}
@keyframes bwBlurReveal {
  0% { opacity: 0; filter: blur(9px); transform: translate3d(0,.16em,0) scale(.985); }
  58% { opacity: .88; }
  100% { opacity: 1; filter: blur(0); transform: translate3d(0,0,0) scale(1); }
}
@keyframes bwBlurLeave {
  0% { opacity: 1; filter: blur(0); transform: translate3d(0,0,0) scale(1); }
  100% { opacity: 0; filter: blur(7px); transform: translate3d(0,-.1em,0) scale(.99); }
}
body[data-page="app"] .bw-hero-mark { width: 45px !important; height: 37px !important; opacity: .88; }
body[data-page="app"] .bw-hero-mark { --terracotta: var(--bw-palette-gem); }
/* isolation, not just position: the scrim below is z-index:-1, and without a
   stacking context here that -1 escapes the zone entirely and paints beneath
   the fixed range instead of over it. */
body[data-page="app"] .composer-zone { padding-bottom: 24px; z-index: 1; isolation: isolate; }

/* The composer's two caption lines — the keyboard hint and the support footer —
   float on the range with no material behind them. That is the one text
   context tools/palette-contrast.html never covered: it measures text on glass
   panels, and these sit on the ridges themselves, where a mid-grey crest and
   --faint are close enough to be unreadable. Every wash that has been tried
   here is recorded below, and every one of them was removed again. */
/* ⭐⭐⭐ THE FROSTED STRIPS ARE GONE. Owner, for the third time: no white plate
   under text — legibility comes from the ink changing colour with the group.

   They were not white by declaration ("pure filter", said the note that stood
   here), but `brightness(1.62)` over a pale sky IS white, and on a phone it
   rendered as a 215px blur covering the composer, its send button and its
   model chip. Two separate faults, and the second is the one that settles it:

   - It escaped its own element. `.site-foot` lost its `position: relative`
     somewhere in the cascade, so the strip took `.composer-zone` as its
     containing block and computed 414x215 instead of the 382x47 it was drawn
     for. A scrim whose size depends on winning a specificity contest is not a
     scrim, it is a leak.
   - It did not work. Measured on composited pixels across six groups and two
     viewports, the footer still bottomed out at 1.48:1 on group 186 WITH the
     plate under it. It covered half the phone screen and bought nothing.

   What replaces it is one line of colour: --bw-ink-on-ridge, solved in
   mountain-range.js against every lower ridge the text can land on rather than
   against the sky it does not touch. */

/* The footer is capped at 600px, which on the landing page forces six links
   onto two lines and leaves "Plans & units" alone on the second. It has the
   width; let it use it. */
body[data-page="app"].is-empty .site-foot { max-width: 860px; }
/* --bw-ink-on-sky, not --dim. This line sits just above the composer-zone
   scrim's fade-in, so on the darker groups it is on bare ridge: 1.41:1 at 10px,
   the worst reading left on the site. The footer directly below it already
   takes this ink and passes. */
body[data-page="app"] .composer-help,
body[data-page="app"] .composer-help span {
  color: var(--bw-ink-on-ridge, var(--bw-ink-on-sky, var(--ink)));
}
/* The footer sits lowest, over the darkest ridge, where even the scrim leaves
   --dim at 3.57:1 against 11px text. It is a row of links, so full ink is the
   honest weight for it anyway. */
/* The footer sits straight on the landscape, so it takes the palette-derived
   ink rather than a fixed one. Without this it stayed a single brown on all
   114 groups and dropped to 3.7:1 on the darker cards. */
body[data-page="app"] :is(.site-foot, .site-foot a) {
  color: var(--bw-ink-on-ridge, var(--bw-ink-on-sky, var(--ink)));
}

/* ⚠️⚠️ NO HALO, and this was built and removed inside one sitting. Three
   concentric blurs of the opposite luminance left 75 of 1824 readings under AA
   (worst 2.24:1), so it was strengthened into a real eight-offset outline —
   which measured clean and rendered as DARK TEXT WITH A WHITE RIM. That is the
   plate again at glyph scale, and it is the exact thing asked about three
   times. Measuring well is not the test; looking right is also the test, and
   this repo has written that down before.

   So the ink carries it alone, and inkOnAll searches the group for the colour
   with the best worst case rather than falling back to near-black. What that
   cannot buy is stated plainly rather than papered over. */

/* The same fix for the two short glass pages. Their footers sit at y~850 in a
   900px viewport, which is squarely on the range's darkest lower ridges, and
   --faint at 11.5px measured 1.2:1 against them. Scoped to these two because
   they are the pages whose footer always lands over the ridges: a long page
   scrolls its footer onto paper, these never do. */
html[data-skin="paper"] body:is([data-page="login"], [data-page="not-found"]) :is(footer, footer a) {
  position: relative;
  /* Same answer as the app's own footer: the ink follows the group. A fixed
     --ink was the previous compromise and it is a single brown across 114
     groups, which is exactly the case the ridge solve exists for. */
  color: var(--bw-ink-on-ridge, var(--ink));
}

body[data-page="app"] .composer {
  max-width: 720px; min-height: 64px; padding: 8px 8px 8px 12px; gap: 8px;
  border: var(--ui-hairline) solid var(--ui-line); border-radius: var(--ui-radius);
  background: var(--ui-glass-strong); box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised);
  transition: border-color var(--dur-base) ease, box-shadow var(--dur-base) ease, background-color var(--dur-base) ease;
}
body[data-page="app"] .composer:hover { border-color: var(--ui-line-strong); }
body[data-page="app"] .composer:focus-within {
  border-color: var(--ui-line-strong);
  box-shadow: var(--ui-shadow-raised), 0 0 0 3px color-mix(in srgb, var(--terracotta) 16%, transparent);
}
body[data-page="app"] .stria {
  min-height: var(--ctl); padding: 8px 12px; background: var(--ui-glass-warm);
  border: var(--ui-hairline) solid var(--ui-line); color: var(--ink);
  transition: transform var(--dur-fast) var(--ease-out), background-color var(--dur-base) ease, border-color var(--dur-base) ease;
}
body[data-page="app"] .stria:hover { background: color-mix(in srgb, var(--cactus) 60%, transparent); }
body[data-page="app"] .composer textarea { font-size: 15px; letter-spacing: -.01em; }
body[data-page="app"] .send { width: 44px; height: 44px; background: var(--ink); transition:background-color 140ms ease,opacity 140ms ease; }
body[data-page="app"] .chips { margin-top: 16px; gap: 8px; }
body[data-page="app"] .chip {
  padding: 8px 12px; border-color: var(--ui-line); background: var(--ui-glass-soft);
  box-shadow: inset 0 1px 0 rgba(226,218,205,.3);
  backdrop-filter: blur(10px) saturate(112%);
  -webkit-backdrop-filter: blur(10px) saturate(112%);
}
body[data-page="app"] .method-menu,
body[data-page="app"] .acct-menu,
body[data-page="app"] .guide-coach,
body[data-page="app"] .signin-coach {
  background: var(--ui-glass-strong); border: var(--ui-hairline) solid var(--ui-line);
  box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised);
}
body[data-page="app"] .method-menu { border-radius: var(--ui-radius); padding: 8px; }
body[data-page="app"] .mm-row { border-radius: var(--ui-radius-small); }
/* The old full-width frosted layer was the tan box that appeared when pointer
   movement forced Chromium to re-composite backdrop-filter. The reading and
   casting remain directly on the page; only compact controls get carriers. */
body[data-page="app"] .main::before { display: none !important; }
body[data-page="app"] .reading,
body[data-page="app"] .reading:hover,
body[data-page="app"] .reading-chart,
body[data-page="app"] .reading-chart:hover,
body[data-page="app"] .casting-live,
body[data-page="app"] .casting-live:hover {
  background: transparent !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
body[data-page="app"] .thread-inner { max-width: 720px; }
/* ⚠️ The bubble's fill is gone (2026-09-02). The question is set as the
   piece's masthead at the end of this file; a filled pill made the input the
   loudest thing on a page whose whole point is the answer. */
/* ⚠️ Leading and paragraph air are set once, in the transcript block at the
   foot of this file. They used to be declared here as well (1.72 and 1.05em)
   and there as 1.82 — three numbers for two properties, in one stylesheet. */
body[data-page="app"] .reading-body { letter-spacing: -.01em; }
body[data-page="app"] .rd-actions { flex-wrap: wrap; }
body[data-page="app"] .rd-action {
  background: var(--ui-glass-soft); border-color: var(--ui-line);
  border-radius: var(--ui-radius-small);
  box-shadow: inset 0 1px 0 rgba(226,218,205,.28);
  backdrop-filter: blur(10px) saturate(110%);
  -webkit-backdrop-filter: blur(10px) saturate(110%);
}
body[data-page="app"] .rd-action:hover { background: var(--ui-glass-warm); border-color: var(--ui-line-strong); }

/* Reusable line-art language ---------------------------------------------
   Rounded, slightly uneven ink now appears as low-contrast material, never as
   a foreground decal competing with the page's copy. */
html[data-skin="paper"] body :is(.pack,.lesson-card,.why-card,.m-card,.cost,.topup) {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
html[data-skin="paper"] body :is(.pack,.lesson-card,.why-card,.m-card,.cost,.topup)::after {
  content: "";
  position: absolute;
  z-index: 0;
  right: -34px;
  bottom: -30px;
  width: 190px;
  height: 112px;
  pointer-events: none;
  opacity: .24;
  background: url("../assets/textures/ink-currents.svg") center / contain no-repeat;
  mix-blend-mode: multiply;
  -webkit-mask-image: none;
  mask-image: none;
}
html[data-skin="paper"] body :is(.pack,.lesson-card,.why-card,.m-card,.cost,.topup):nth-child(2n)::after {
  right: -42px;
  bottom: -46px;
  width: 158px;
  height: 158px;
  opacity: .14;
  background-image: url("../assets/textures/ink-orbits.svg");
}
html[data-skin="paper"] body :is(.pack,.lesson-card,.why-card,.m-card,.cost,.topup) > * {
  position: relative;
  z-index: 1;
}
body[data-page="app"] .reading-body :is(h2,h3) { position: relative; }
body[data-page="app"] .reading-body :is(h2,h3)::before {
  content: "";
  display: block;
  width: 48px;
  height: 19px;
  margin: 0 0 8px -2px;
  opacity: .48;
  background: url("../assets/textures/ink-knot.svg") left center / contain no-repeat;
}

/* Organic state indicator. Three imperfect ink loops move as one current,
   replacing mechanical rotation while remaining compact and legible. */
.bw-organic-loader {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-width: 28px;
  height: 16px;
  color: currentColor;
}
.bw-organic-loader > i {
  display: block;
  width: 7px;
  height: 8px;
  box-sizing: border-box;
  border: var(--ui-hairline) solid currentColor;
  border-radius: 46% 54% 62% 38% / 57% 43% 56% 44%;
  transform-origin: 50% 70%;
  animation: bwOrganicCurrent 780ms var(--ease-in-out) infinite;
}
.bw-organic-loader > i:nth-child(2) {
  width: 8px;
  height: 7px;
  border-radius: 58% 42% 47% 53% / 42% 61% 39% 58%;
  animation-delay: -520ms;
}
.bw-organic-loader > i:nth-child(3) {
  height: 9px;
  border-radius: 42% 58% 54% 46% / 61% 39% 57% 43%;
  animation-delay: -260ms;
}
@keyframes bwOrganicCurrent {
  0%,100% { transform: translateY(1px) scaleX(1); opacity: .48; }
  45% { transform: translateY(-3px) scaleX(.76); opacity: 1; }
  72% { transform: translateY(0) scaleX(1.04); opacity: .72; }
}
body[data-page="app"] .send .bw-organic-loader { display: none; }
body[data-page="app"] .send:disabled > svg { display: none; }
body[data-page="app"] .send:disabled .bw-organic-loader { display: inline-flex; }

/* Quiet liveliness -------------------------------------------------------
   Movement is limited to compositor-friendly opacity and transforms. This
   keeps the glass responsive without repainting the large backdrop layers. */
:is(a,button,input,textarea):focus-visible {
  outline: 2px solid color-mix(in srgb, var(--terracotta) 72%, var(--paper-dim));
  outline-offset: 3px;
}
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference) {
  body[data-page="app"] .topbar .ghost,
  body[data-page="app"] .new-cast,
  body[data-page="app"] .castings .casting,
  body[data-page="app"] .side-foot,
  body[data-page="app"] .rd-action {
    transition: background-color var(--dur-base) ease, color var(--dur-base) ease, border-color var(--dur-base) ease, box-shadow var(--dur-base) ease;
  }
  body[data-page="app"] .new-cast .ic { transition: background-color var(--dur-base) ease; }
  body[data-page="app"] .send:hover { background: var(--terracotta); }
  html[data-skin="paper"] body :is(.m-card,.card,.tile,.plan,.topup,.pack,.cost) {
    transition: box-shadow var(--dur-base) ease, border-color var(--dur-base) ease, background-color var(--dur-base) ease;
  }
  html[data-skin="paper"] body :is(.m-card,.card,.tile,.plan,.topup,.pack,.cost):hover {
    border-color: var(--ui-line-strong) !important;
    box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised) !important;
  }
}

/* Physical feedback belongs to the press itself. It is fast enough to feel
   immediate and applies equally well to touch, mouse and pen. */
@media (prefers-reduced-motion:no-preference) {
  body[data-page="app"] :is(.pressable,.send,.stria,.rd-action,.rd-prompt):active,
  html[data-skin="paper"] body:not([data-page="app"]) :is(button,.cta,.choose):active {
    transform: scale(.975) !important;
    transition-duration: var(--dur-fast) !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  html[data-skin="paper"] body .mtn-sky,
  html[data-skin="paper"] body .mtn-bg .fill,
  html[data-skin="paper"] body .mtn-bg [clip-path] > use {
    transition: none !important;
  }
  body[data-page="app"] .claude-mark,
  body[data-page="app"] .bwl1, body[data-page="app"] .bwl2,
  body[data-page="app"] .bwl3, body[data-page="app"] .bwl4,
  body[data-page="app"] .bwl5,
  body[data-page="app"] .greeting h1 .bw-reveal-word { animation: none !important; }
  body[data-page="app"] .greeting h1 .bw-reveal-word { opacity: 1; filter: none; transform: none; }
  .bw-organic-loader > i { animation: bwOrganicFade 800ms ease infinite; transform: none; }
}
@keyframes bwOrganicFade { 0%,100%{opacity:.38} 50%{opacity:1} }

/* Content pages ----------------------------------------------------------- */
body[data-page="pricing"] .shell,
body[data-page="legal"] .legal,
body[data-page="settings"] .shell { position: relative; z-index: 1; }
body[data-page="pricing"] .hero h1,
body[data-page="legal"] .legal > h1,
body[data-page="settings"] .pagetitle h1 {
  letter-spacing: -.045em; text-wrap: balance;
}

/* Text-page material: a coloured glass film, not a thick card. The dynamic
   palette supplies both tint layers, while the hairline and shallow shadow
   keep boxes visually weightless. */
/* The legal route is excluded from both carrier blocks: it left the glass
   system for the block route, and legal.css owns its chrome now. Excluding
   it here is the fix — stacking another !important layer on top is what
   produced the override war that kept eating changes. */
html[data-skin="paper"] body:not([data-page="app"]):not([data-page="legal"]) :is(
  .pagehead, .top, .m-card, .band, .tile, .card, .panel, .toc, .pull, .headline,
  .why-panel, .why-card, .method, .close,
  .progress, .guide-stage, .lesson-card, .example, .rate
  /* pricing's carriers are no longer here: .plan, .rate-card, .estimator,
     .pack, .term and .ledgerstrip all take the one neutral glass now, so
     the page stops rendering three white cards above a field of khaki. */
) {
  /* ⭐ Retired 2026-08-26 — the third instance of this exact bug in this exact
     file, and the note below had already named it. It mixed --bw-palette-1 into
     every carrier at 52%, which is the palette-in-glass pattern the glass rules
     ban outright ("暖意必须从背后透上来, 不是掺进面板"). The note said it "currently
     loses to luxury-glass.css"; CDP's matched-rule list says otherwise — it is
     later in the cascade AND outspecifies that rule, so it won on every page
     that is not app or legal.
     What it cost: on a dark group --bw-palette-1 resolves to rgb(73,53,42), so
     settings, login, pricing and 404 rendered dark-brown carriers and kept
     drawing --ink text on them. Measured across 20 groups × 3 viewports, 88 of
     198 text elements failed WCAG AA at their worst group; 81 of those 88 were
     on the four pages this rule reaches, and index — which it does not reach —
     had 7. Adjusting opacity could never have fixed it, because the colour was
     inside the panel.
     Same lesson as the stroke-width and the fill-opacity overrides a few
     hundred lines up: when a change "has no effect", measure what actually
     won before reaching for a bigger number. */
  background-color: var(--ui-glass-strong) !important;
  background-image: none !important;
  border: var(--ui-hairline) solid var(--ui-line) !important;
  box-shadow: var(--ui-glass-edge), var(--ui-shadow) !important;
  backdrop-filter: var(--ui-glass-filter) !important;
  -webkit-backdrop-filter: var(--ui-glass-filter) !important;
}
/* The alternating carrier tint is gone. It mixed --bw-palette-2 into every
   second card, which is the palette-in-glass pattern the glass rules ban —
   and because :nth-child pushed it to (0,4,2) it outspecified the neutral
   glass and won, so a row of four packs read as two different materials.
   Rhythm across a row is the job of content, not of a tint on the even
   ones. The block above carried the same violation and has now been retired
   the same way — see the note inside it. */
/* Every carrier on pricing takes the SAME material. .plan was on the neutral
   glass while .rate-card, .estimator, .pack and .term were still on the
   legacy palette-tinted block (--bw-palette-1 52% over a dark mix), so the
   page rendered three white cards above a page of khaki boxes — two
   materials, one page. Screenshotting over the live range is what made it
   obvious; a fullPage capture paints the fixed backdrop only once at the
   top and hides it. */
/* Inherited from poster-pages.css, which pricing was the only route still
   loading. Of its 121 rules exactly one could ever match there — this one; the
   other 120 are scoped to about/guide/legal, which load blocks/method/legal.css
   instead, and its ::before/::after companions are already killed by the
   content:none in luxury-glass.css. One rule does not earn a stylesheet. */
body[data-page="pricing"] :is(.hero, .plans, .rates, .packs) {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* .section is NOT in this list, deliberately. On settings each <section> wraps
   exactly one .panel at the same x and the same width, so listing both painted
   one card twice: two 66% white planes stacked, compositing to 88% over the
   panel and 66% along the header strip that only has one. That tone step is
   the "box inside a box" — there is only ever one box, drawn twice.
   The section stays a transparent grouping element; the panel is the surface. */
/* --ui-glass-strong, not --ui-glass. These are the reading surfaces: dense
   13px type, several paragraphs deep, and they are the only carriers a visitor
   actually stops on. The four glass opacities were set while the range was
   still being desaturated on its way to the screen; PALETTE_FIDELITY put the
   cards on the screen at full strength afterwards, and .60 white over the
   darkest groups composites to a mid-tone that drags every small label on it
   under AA. Measured on pricing: "Both reading methods" 3.90:1, the grant
   figure 3.21:1. The app's own chrome keeps .60 — it carries short labels over
   a deliberately open canvas, and it is the one page that was already clean. */
body[data-page="pricing"] :is(.plan, .topup, .rate-card, .estimator, .pack, .term, .ledgerstrip),
body[data-page="settings"] .panel,
body[data-page="login"] .card {
  background: var(--ui-glass-strong); border-color: var(--ui-line);
  box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised); backdrop-filter: var(--ui-glass-filter-strong);
  -webkit-backdrop-filter: var(--ui-glass-filter-strong);
}
body[data-page="pricing"] .plan,
body[data-page="settings"] .panel { border-radius: var(--ui-radius); }
body[data-page="pricing"] .plan:nth-child(2n) { border-radius: var(--ui-radius); }
body[data-page="pricing"] .plan:nth-child(3n) { border-radius: var(--ui-radius); }
body[data-page="pricing"] .plan.feat { background: var(--ui-glass-warm); box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised); border-color: var(--ui-line-strong); }
body[data-page="pricing"] .choose,
body[data-page="pricing"] .cta,
body[data-page="login"] .submit,
body[data-page="settings"] .btn { min-height: var(--ctl); border-radius: var(--ui-radius-small); box-shadow: none; }
body[data-page="pricing"] .choose:hover,
body[data-page="pricing"] .cta:hover,
body[data-page="login"] .submit:hover,
body[data-page="settings"] .btn:hover { transform: translateY(-1px); }

body[data-page="legal"] .legal { max-width: 760px; }
body[data-page="legal"] .legal > h1 { font-size: clamp(38px, 6vw, 64px); line-height: 1; }
body[data-page="legal"] .lede { font-size: clamp(17px, 2vw, 20px); line-height: 1.65; }
/* The legal route left the glass system when it moved onto the block route:
   the range is switched off there, so a blurred translucent carrier has
   nothing behind it to refract and composites to a flat grey smear on the
   paper. .toc is already handled in legal.css; .headline and .pull get the
   route's own material here — flat, square, seated on the paper. */
body[data-page="legal"] :is(.headline, .pull) {
  background: var(--bk-cream, #EFE6E0);
  border: 0; border-left: 3px solid var(--terracotta);
  border-radius: 0; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
body[data-page="legal"] .clause { scroll-margin-top: 96px; }
body[data-page="legal"] .clause p,
body[data-page="legal"] .clause li { line-height: 1.72; }

@media (max-width: 880px) {
  body[data-page="app"] .topbar { padding-inline: 16px; }
  body[data-page="app"] .sidebar {
    background: var(--ui-glass-strong);
    box-shadow: var(--ui-glass-edge), var(--ui-shadow-raised);
    backdrop-filter: var(--ui-glass-filter-strong);
    -webkit-backdrop-filter: var(--ui-glass-filter-strong);
  }
  body[data-page="app"] .composer { max-width: 680px; }
  body:not([data-page="app"]) .pagehead,
  body[data-page="pricing"] .pagehead { width: calc(100% - 24px); margin-top: 8px; }
}

@media (max-width: 560px) {
  :root { --ui-glass-filter: blur(8px) saturate(110%); --ui-glass-filter-strong: blur(11px) saturate(114%); }
  body[data-page="app"] .topbar { padding-top: max(12px, env(safe-area-inset-top)); }
  body[data-page="app"] .greeting { flex-direction: column; gap: 12px; }
  body[data-page="app"] .greeting h1 { min-height: 5.25em; font-size: clamp(34px, 11vw, 48px); line-height: 1.04; }
  body[data-page="app"] #heroEmpty { padding-bottom: 24px; }
  body[data-page="app"] .composer { min-height: 58px; border-radius: var(--ui-radius); padding: 8px; }
  body[data-page="app"] .stria { min-height: var(--ctl); padding-inline: 12px; }
  body[data-page="app"] .send { width: 40px; height: 40px; }
  body[data-page="app"] .composer-help { display: none; }
  body[data-page="app"]:not(.is-empty) .composer-zone { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
  body[data-page="app"] :is(.send,.stria,.rd-action,.rd-prompt,.side-open-btn):active { transform: scale(.97); }
  body[data-page="app"] .site-foot { line-height: 1.7; }
  body:not([data-page="app"]) .pagehead,
  body[data-page="pricing"] .pagehead { padding-left: 12px; }
  body:not([data-page="app"]) .pagehead .nav .ghost,
  body[data-page="pricing"] .pagehead .nav .ghost { display: none; }
  html[data-skin="paper"] body :is(.pack,.lesson-card,.why-card,.m-card,.cost,.topup)::after {
    opacity: .11;
    transform: scale(.88);
    transform-origin: right bottom;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; }
}

/* Environment-native surfaces -------------------------------------------
   Containers no longer introduce their own card colour or permanent
   hairline. They inherit the moving landscape beneath them; hover/focus adds
   one quiet environmental wash. The main sidebar is the sole pale carrier. */
:root {
  --ui-environment-hover: color-mix(in srgb, var(--bw-palette-gem) 10%, transparent);
  --ui-environment-focus: color-mix(in srgb, var(--bw-palette-cloud) 48%, transparent);
}

body[data-page="app"] .sidebar {
  background-color: color-mix(in srgb, var(--paper-raised) 84%, transparent) !important;
  border-right-color: transparent !important;
  box-shadow: none !important;
}

html[data-skin="paper"] body[data-page="app"] :is(
  .ledger, .composer, .stria, .chip,
  .rd-action, .rd-prompt, .msg-user
) {
  background: transparent !important;
  background-image: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* Floating UI still needs a readable plane, but it uses the current sky
   colour rather than a separate white/tan card colour. */
html[data-skin="paper"] body[data-page="app"] :is(.method-menu, .acct-menu, .guide-coach, .signin-coach) {
  background: var(--bw-palette-cloud) !important;
  background-image: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

html[data-skin="paper"] body[data-page="app"] .composer:focus-within {
  background: color-mix(in srgb, var(--paper-dim) 86%, transparent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* The composer and method entry are actions, not passive content. Give both
   the same pale carrier as the sidebar so their affordance remains visible
   against every live palette, while keeping the permanent border removed. */
html[data-skin="paper"] body[data-page="app"] .composer,
html[data-skin="paper"] body[data-page="app"] #howItWorksLink,
html[data-skin="paper"] body[data-page="app"] .guide-coach {
  background: color-mix(in srgb, var(--paper-raised) 88%, transparent) !important;
  background-image: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

html[data-skin="paper"] body[data-page="app"] #howItWorksLink {
  padding-inline: 12px;
  min-height: var(--ctl-compact);
  display: inline-flex;
  align-items: center;
}

@media (hover:hover) and (pointer:fine) {
  html[data-skin="paper"] body[data-page="app"] .composer:hover,
  html[data-skin="paper"] body[data-page="app"] #howItWorksLink:hover,
  html[data-skin="paper"] body[data-page="app"] .guide-coach:hover {
    background: color-mix(in srgb, var(--paper-dim) 90%, transparent) !important;
  }
}

/* Surface boundaries ----------------------------------------------------- */
html[data-skin="paper"] body :is(
  .ledger, .composer, .method-menu, .acct-menu,
  .m-card, .band, .tile, .card, .panel, .toc, .pull, .headline,
  .why-card, .method, .progress, .guide-stage, .lesson-card, .example, .rate,
  .ledgerstrip, .rate-card, .estimator, .plan, .topup, .pack, .term, .cost
) {
  position: relative;
  isolation: isolate;
}
html[data-skin="paper"] body .method-menu,
html[data-skin="paper"] body .acct-menu { position: absolute; }
/* Hover is a legibility state, not a moving flourish: raise contrast once,
   keep geometry fixed, and use focus-visible for the keyboard equivalent. */
@media (hover:hover) and (pointer:fine) {
  html[data-skin="paper"] body :is(
    .ledger, .composer, .method-menu, .acct-menu,
    .m-card, .band, .tile, .card, .panel, .toc, .pull, .headline,
    .why-card, .method, .progress, .guide-stage, .lesson-card, .example, .rate,
    .ledgerstrip, .rate-card, .estimator, .plan, .topup, .pack, .term, .cost
  ):hover {
    background-color: color-mix(in srgb, var(--bw-palette-cloud) 22%, transparent) !important;
  }
  html[data-skin="paper"] body :is(
    .ledger, .composer, .method-menu, .acct-menu,
    .m-card, .band, .tile, .card, .panel, .toc, .pull, .headline,
    .why-card, .method, .progress, .guide-stage, .lesson-card, .example, .rate,
    .ledgerstrip, .rate-card, .estimator, .plan, .topup, .pack, .term, .cost
  ):hover::after { opacity: .38; }
}

/* The finished interpretation owns the page. The full casting board remains
   visible while it is being made, then clears out instead of becoming a large
   screenshot above a narrow answer column. */
body[data-page="app"] .thread-inner { max-width: 920px; }
body[data-page="app"] .reading-body { max-width: var(--rd-col, 760px); font-size: 15px; line-height: 1.7; }
body[data-page="app"] .reading-body .rd-para,
body[data-page="app"] .reading-body .rd-list li { font-size: 15px; }
/* ⭐⭐ THE FIGURE IS SHOWN THE WAY IT WAS ALWAYS SHOWN. Owner, 2026-09-07:
   「按照原来的显示方法」. Three versions of this rule have now existed: hidden
   the instant the casting settled; then a 208px mark in the right margin; then
   that mark plus a 148px copy in the flow below 1180. The last two were mine,
   and both answered the wrong question — they treated the board as decoration
   to be placed, when it is the reading's own illustration and already has a
   display: the one it wears while it is being cast, declared in index.html
   (`.reading-fig`, `.is-full`, the optical centring BWFigure applies per cast).

   ⭐ So this file now declares NOTHING about the figure. Not a width, not a
   position, not a list of labels to strip. The settled board is the live board.
   That is the whole rule, and it is why there is no second set of numbers here
   to drift out of step with the first — the mistake this repo has paid for
   repeatedly is the second list, and the miniature was one. */

body[data-page="app"] .reading:not(.casting-live) > .reading-body:first-of-type {
  position: relative;
  padding-top: 16px;
}
body[data-page="app"] .reading:not(.casting-live) > .reading-body:first-of-type::before {
  content: "";
  position: absolute;
  left: 7%;
  right: 7%;
  top: -14px;
  height: 28px;
  pointer-events: none;
  background: color-mix(in srgb, var(--bw-palette-cloud) 24%, transparent);
  filter: blur(9px);
  opacity: .55;
}

/* Final cascade guard: base content-page styles still declare thin borders.
   The environment-native treatment is deliberately borderless at rest. */
html[data-skin="paper"] body:not([data-page="app"]):not([data-page="legal"]) :is(
  .pagehead, .top, .m-card, .band, .tile, .card, .panel, .toc, .pull, .headline,
  .why-card, .method, .progress, .guide-stage, .lesson-card, .example, .rate,
  .ledgerstrip, .rate-card, .estimator, .plan, .topup, .pack, .term, .cost
) { border: 0 !important; }

@media (max-width:700px) {
  body[data-page="app"] .reading-body { max-width: 100%; font-size: 15px; }
  body[data-page="app"] .reading-body .rd-para,
  body[data-page="app"] .reading-body .rd-list li { font-size: 15px; }
}

/* A very small optical hand-off between scrolling content and the fixed input
   carrier. It softens only the seam, never the reading itself. */
body[data-page="app"]:not(.is-empty) .composer-zone::before {
  content: "";
  position: absolute;
  left: 56px;
  right: 56px;
  top: -13px;
  height: 26px;
  pointer-events: none;
  background: color-mix(in srgb, var(--bw-palette-cloud) 18%, transparent);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  -webkit-mask-image: linear-gradient(to bottom,transparent,#000 45%,transparent);
  mask-image: linear-gradient(to bottom,transparent,#000 45%,transparent);
}
@media (max-width:880px) {
  body[data-page="app"]:not(.is-empty) .composer-zone::before { left: 18px; right: 18px; }
}

/* Accent-coloured micro-labels ------------------------------------------
   .eyebrow and .plan-tag paint 10px tracked type in the palette's gem colour.
   The gem is chosen for the landscape, where it sits against ridge fills, not
   for 10px type on a near-white carrier. Pull the same hue toward ink until it
   clears — the label stays recognisably the accent, and unlike the gem it does
   not move when the palette rotates.

   ⚠️ 42% measured 4.85:1 NOMINALLY and 3.14:1 RENDERED. Those are different
   numbers and the second one is the one a reader gets. WCAG's ratio assumes a
   solid stroke; at 10px with .13em tracking this face lays down full-strength
   colour on only about 18% of the glyph box, and the rest is antialiased
   toward the ground. A colour that passes on paper can fail on screen purely
   because the strokes are too thin to carry it.
   So: darker mix AND more weight. Weight is the stronger lever here — it adds
   inked pixels, where colour only deepens the few that exist. */
html[data-skin="paper"] body :is(.eyebrow, .plan-tag, .typical, .example .label) {
  color: color-mix(in srgb, var(--soft-peach-accent) 20%, var(--soft-peach-ink)) !important;
  font-weight: 600;
}

/* Small secondary text ---------------------------------------------------
   Anything at 13px or under is the smallest type on the page; it must not
   also be the lightest. Measured rendered contrast before this rule:
   .hero-copy small 2.69:1, .nav .ghost 3.11:1, .plan .desc 4.23:1 — all under
   the 4.5 floor, and the first one badly.

   These carried --dim or --faint, which are correct for 15px body but not for
   12px on glass over a rotating landscape. The grey ladder's own rule applies:
   a level that cannot be read is not a level, it is lost text. Hierarchy at
   this size comes from size and weight, which they already have. */
html[data-skin="paper"] body :is(
  .hero-copy small, .nav .ghost, .plan .desc, .rate-card p, .rate-number span
) {
  color: var(--soft-peach-ink);
}
html[data-skin="paper"] body .plan-tag {
  background: color-mix(in srgb, var(--soft-peach-accent) 16%, transparent) !important;
}

/* Billing cycle toggle ----------------------------------------------------
   .billing-toggle is a centring flex wrapper — it has no width of its own, so
   it spans the shell. That was invisible until it was added to the shared
   glass-carrier list, at which point a layout helper started drawing a panel:
   measured, a 980px box around 275px of content, 28% filled, on a page where
   every other box runs 85–100%. A box that is three-quarters empty reads as a
   mistake no matter what material it is made of.
   Let it hug its content, and give it the 44px the design law asks of a touch
   target — it is the only control on the conversion page. */
html[data-skin="paper"] body[data-page="pricing"] .billing-toggle {
  width: max-content;
  max-width: 100%;
  min-height: var(--ctl);
  margin-inline: auto;
  padding: 0 4px 0 16px;
  border-radius: var(--ui-radius-pill) !important;
}

/* Solid blocks ------------------------------------------------------------
   The trade-off, stated once: glass is the material of surfaces — panels,
   menus, the composer, the sidebar. Solid is the material of actions and
   status — buttons, badges, chips. Nothing is given half of each, because a
   half-translucent button is the one thing that reads as neither.

   These blocks were opaque and flat already, and carried no shadow at all.
   Over a still page that is fine; over a landscape that drifts and blurs
   behind them it leaves the edge with nothing to sit against, and the
   boundary frays as the contour lines pass underneath. A short contact
   shadow fixes it without turning the button into a floating card.

   The self-coloured 1px border is dropped: a border the same colour as the
   fill adds nothing but one more antialiased ring at the corner radius,
   which is the other half of the soft edge. */
html[data-skin="paper"] body :is(
  .btn, .cta, .choose, .send, .exchange, .next, .plan-badge, .prov-tag,
  .plan-tag, .chip, .mo-label.active, .av
) {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  box-shadow: var(--ui-solid-shadow) !important;
}
html[data-skin="paper"] body :is(.btn, .cta, .choose, .send, .exchange, .next) {
  border-width: 0 !important;
}
/* Pressing a solid block should look like pressing something solid: the
   contact shadow tightens rather than the whole block scaling away. */
html[data-skin="paper"] body :is(.btn, .cta, .choose, .send, .exchange, .next):active {
  box-shadow: 0 1px 1px rgba(42,32,22,.20) !important;
}

/* A destructive/secondary action was drawn as 50% white with a transparent
   border — glass wearing a button's job, and the one control on the page with
   no edge at all. It is a solid quiet block now: same material as its
   neighbour, lower voice. */
html[data-skin="paper"] body .btn.warn {
  background: color-mix(in srgb, var(--soft-peach-accent) 12%, #FFFFFF) !important;
  color: color-mix(in srgb, var(--soft-peach-accent) 55%, var(--soft-peach-ink)) !important;
}

/* [hidden] must actually hide -------------------------------------------
   The UA rule is `[hidden]{display:none}`, which any class selector that sets
   display beats on specificity. .oauth-providers{display:flex} did exactly
   that, so the sign-in page's provider gating — which is written correctly and
   fails safe, hiding every button when /api/auth/providers reports none
   configured — had no effect: measured, all three buttons carried hidden=true
   and computed display:flex. In production with no OAuth keys set that is three
   dead controls on the sign-up path, each leading to "not configured".
   This is not a login-page bug, it is a bug anywhere markup uses `hidden` and
   CSS sets display on the same element, so the guard is global. */
[hidden] { display: none !important; }

/* Frost grain -------------------------------------------------------------
   Applied last so it beats the carriers' `background-image: none !important`.
   Kept at a low alpha: the grain should be felt at the surface, not seen as
   dots — past about .05 it stops reading as frost and starts reading as a
   dirty panel. Solid blocks are excluded; they are not glass. */
html[data-skin="paper"] body :is(
  .sidebar, .composer, .method-menu, .carry-menu, .coach, .coach-card,
  .pagehead, .top, .card, .card-hero, .ledgerstrip, .billing-toggle, .plan,
  .rate-card, .estimator, .pack, .term, .toast, .progress, .controls,
  .panel, .account-menu, .acct-menu, .popover, .modal, .dialog, .sheet,
  .ledger, .side-foot, .signin-coach, .guide-coach
) {
  /* ⚠️⚠️ ONE layer, and the second one is gone. The turbulence grain is the
     material's own surface — what makes glass read as frosted rather than as
     tinted film — and it stays. Over it there used to drift a field built from
     the phenomena, meant to register as something etched into the glass.

     It was `ph-drift.svg` as a BACKGROUND-IMAGE, and that file paints
     `fill="#000"` at `scale(2.261)`. This repo's own rule is that a phenomenon
     goes on a carrier as a MASK, never as a background-image, precisely so the
     colour comes from CSS and follows the group — the note is in CLAUDE.md and
     content-pages.css already does it that way. A background-image carries its
     own baked ink, and `soft-light` over near-clear glass does not tame solid
     black: on login the card sits over the far ridge and the heading was
     crossed out by black bars.

     It also broke the other half of the rule it was written under, three lines
     up in content-pages.css: ONE mark per carrier, never a repeated wallpaper.
     A 420px tile is a wallpaper.

     The motifs are still on these routes — content-pages.css puts a single
     masked, palette-coloured one on each major carrier. What is deleted here is
     the tiling, not the phenomena. */
  background-image: var(--ui-frost-grain) !important;
  background-size: 140px 140px;
  background-repeat: repeat;
  background-blend-mode: overlay;
}

/* ── 取象溯源 ────────────────────────────────────────────────────────────────
   A reading turns a symbol into one concrete noun. These marks show where that
   happened and, on click, what else the symbol covers.

   The register is deliberate. Underneath a verdict, a list of alternatives can
   read two ways: as the reading hedging, or as the map the choice was made
   from. It is the second. So the mark sits QUIET — a dotted rule the colour of
   the text, no colour of its own, nothing that competes with the |gild| terms
   or asks to be clicked. It offers itself on hover and not before.

   The casting figure has no pointer interaction and keeps none: this lives in
   the prose only, and it opens on CLICK. Hover changes the affordance, never
   the content — a tooltip that fires on hover would put the machinery in the
   way of anyone simply reading. */
.reading-body .xr {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  position: relative;
  border-bottom: 1px dotted color-mix(in srgb, var(--ink) 38%, transparent);
  transition: border-color var(--dur-fast) var(--ease-out, ease),
              color var(--dur-fast) var(--ease-out, ease);
}
/* A comfortable tap target without disturbing the line box it sits in. */
.reading-body .xr::before {
  content: "";
  position: absolute;
  inset: -11px -4px;
}
.reading-body .xr:hover,
.reading-body .xr:focus-visible {
  color: var(--terracotta);
  border-bottom-color: var(--terracotta);
  border-bottom-style: solid;
}
.reading-body .xr:focus-visible { outline: 2px solid var(--terracotta); outline-offset: 3px; }
.reading-body .xr[aria-expanded="true"] {
  color: var(--terracotta);
  border-bottom-color: var(--terracotta);
  border-bottom-style: solid;
}
/* Catalogue missing or symbol unknown — never leave a dead underline. */
.reading-body .xr-mute { border-bottom: 0; cursor: default; color: inherit; }
.reading-body .xr-mute::before { content: none; }

.reading-body .xr-panel {
  margin: 8px 0 16px;
  padding: 12px 12px 12px;
  border-left: 2px solid color-mix(in srgb, var(--terracotta) 42%, transparent);
  background: color-mix(in srgb, var(--bw-palette-1) 42%, transparent);
  border-radius: 0 var(--ui-radius-small, 12px) var(--ui-radius-small, 12px) 0;
  animation: xr-in var(--dur-fast) var(--ease-out, ease) both;
}
.reading-body .xr-lead {
  margin: 0 0 8px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--dim);
}
.reading-body .xr-panel[lang="zh"] .xr-lead { line-height: 1.65; }
/* First level: which 象 the symbol runs in. A row of quiet chips, because the
   reader is choosing among siblings, not reading a list. */
.reading-body .xr-cats {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0 0 8px;
}
.reading-body .xr-cat {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0;
  color: var(--dim);
  background: color-mix(in srgb, var(--bw-palette-1) 66%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  border-radius: var(--ui-radius-pill);
  padding: 8px 12px;
  cursor: pointer;
  /* 44px touch target without a 44px-tall chip: the box stays visually small
     and the hit area is expanded underneath it. */
  position: relative;
  min-height: var(--ctl-chip);
  transition: color var(--dur-fast) var(--ease-out, ease),
              border-color var(--dur-fast) var(--ease-out, ease);
}
.reading-body .xr-cat::before { content: ""; position: absolute; inset: -7px -2px; }
/* The branch this reading actually took. The row is a map; without this it is a
   map with no "you are here", and the reader cannot tell which sibling was
   chosen from the ones merely available. */
.reading-body .xr-cat.is-here {
  border-color: color-mix(in srgb, var(--terracotta) 52%, transparent);
  color: var(--terracotta);
}
.reading-body .xr-cat.is-here::after {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-left: 4px;
  vertical-align: middle;
  border-radius: 50%;
  background: var(--terracotta);
}
.reading-body .xr-cat:hover,
.reading-body .xr-cat:focus-visible {
  color: var(--terracotta);
  border-color: color-mix(in srgb, var(--terracotta) 46%, transparent);
}
.reading-body .xr-cat:focus-visible { outline: 2px solid var(--terracotta); outline-offset: 2px; }
.reading-body .xr-cat[aria-expanded="true"] {
  color: var(--terracotta);
  border-color: color-mix(in srgb, var(--terracotta) 62%, transparent);
  background: color-mix(in srgb, var(--terracotta) 8%, transparent);
}
.reading-body .xr-list[hidden] { display: none; }

/* Second level: what that 象 actually turns up as. Multi-column, so a dozen
   short entries read as one field rather than a column the eye walks down. */
.reading-body .xr-list {
  margin: 0;
  padding: 0;
  list-style: none;
  columns: 2;
  column-gap: 24px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.72;
  color: var(--dim);
}
.reading-body .xr-list li { break-inside: avoid; padding-left: 8px; position: relative; }
.reading-body .xr-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .72em;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--terracotta) 55%, transparent);
}
@media (min-width: 760px) { .reading-body .xr-list { columns: 3; } }
@media (max-width: 480px) { .reading-body .xr-list { columns: 1; } }

@keyframes xr-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .reading-body .xr-panel { animation: none; }
  .reading-body .xr { transition: none; }
}

/* ── 取象链条 ────────────────────────────────────────────────────────────────
   The closing summary: which forces this casting moved, how they act on each
   other, and what each one does. Computed from the 六亲 ring, which is fixed by
   the method, so it costs nothing and cannot disagree with the reading.

   Open, not collapsed — this is the part a reader reaches for, and putting it
   behind a click would hide the one thing they came back down the page for. It
   still has to read as an appendix to the prose rather than a rival ending, so
   it sits behind a rule, in the label face, at label size. */
.reading-body .xr-chain {
  margin: 32px 0 0;
  padding-top: 16px;
  border-top: 1px solid color-mix(in srgb, var(--ink) 11%, transparent);
}
.reading-body .xr-chain-lead {
  margin: 0 0 12px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: .02em;
  color: var(--dim);
}
.reading-body .xr-edges {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin: 0 0 8px;
}
.reading-body .xr-edge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1;
  padding: 4px 8px;
  border-radius: var(--ui-radius-pill);
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  background: color-mix(in srgb, var(--bw-palette-1) 52%, transparent);
  color: var(--dim);
}
.reading-body .xr-edge b { font-weight: 600; color: var(--ink); }
.reading-body .xr-edge i {
  font-style: normal;
  font-size: 10px;
  letter-spacing: .04em;
  padding: 2px 4px;
  border-radius: var(--ui-radius-pill);
}
/* 生 feeds, 克 restrains — the one place a colour carries meaning rather than
   decoration, so the two are told apart by hue and never by hue alone: the
   words 生 / 克 are always printed. */
.reading-body .xr-edge.sheng i { color: var(--pine); background: color-mix(in srgb, var(--pine) 11%, transparent); }
.reading-body .xr-edge.ke i { color: var(--terracotta); background: color-mix(in srgb, var(--terracotta) 11%, transparent); }

.reading-body .xr-act {
  display: flex;
  gap: 8px;
  padding: 4px 0;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.62;
  color: var(--dim);
}
.reading-body .xr-act b { flex: 0 0 3.6em; font-weight: 600; color: var(--ink); }
.reading-body .xr-chain[lang="en"] .xr-act b { flex-basis: 7em; }
/* ⚠️⚠️ Chain rows: the pill has to be allowed to WRAP INSIDE ITSELF. Each edge
   is one inline-flex holding the whole run — "restrains · Resource → Output →
   Pressure → Peer → Wealth" — and a row of flex items does not wrap, so at
   390px that pill measured 426px inside a 326px column with overflow visible.
   It was not scrollable and it did not widen the document; the last word was
   simply cut off at the screen edge with no way to reach it. This file already
   says a crop only works when the reader can put the word back, and here they
   cannot. */
.reading-body .xr-edge { flex-wrap: wrap; max-width: 100%; min-width: 0; }

@media (max-width: 520px) {
  .reading-body .xr-act { flex-direction: column; gap: 1px; }
  /* ⚠️⚠️ (0,3,1), to match `.reading-body .xr-chain[lang="en"] .xr-act b` above.
     A MEDIA QUERY ADDS NO SPECIFICITY, so the previous `.reading-body .xr-act b`
     at (0,2,1) lost to it and `flex-basis: 7em` survived into the column
     layout — where the basis is the MAIN AXIS, i.e. the height. Measured at
     390px: every label was 326x84 for one 19px line, five of them, about 350px
     of blank page between a relative's name and its verbs. It only showed in
     English, because only the English rule carries the [lang] qualifier.
     Seventh time this repo has been caught by the same trap: align the weight,
     do not hope to win. */
  .reading-body .xr-chain[lang] .xr-act b { flex: none; flex-basis: auto; }
}

/* ── the reading's own controls reach the touch floor ──────────────────────
   Measured at 390px with a coarse pointer: the follow-up axes came to 36px and
   the Copy / Follow up / New casting row to 30px, against the 44px floor this
   file sets everywhere else. Both have room to grow, so they grow — the rule
   already written down here is that the element gets bigger rather than
   wearing a 44px ::after, because a hot zone that overlaps its neighbours
   steals clicks, and stealing a click is worse than a small target. */
@media (pointer: coarse) {
  .reading-body ~ .rd-depth .rd-axis,
  .rd-depth .rd-axis { min-height: var(--ctl); }
  .rd-actions .rd-action { min-height: var(--ctl); }
}

/* Solid action buttons ----------------------------------------------------
   ⚠️ A button's fill and its label have to be decided together. The site had
   two different labels on the same accent — cream on some buttons, ink on
   others — and no single accent lightness satisfies both: darkening it to fix
   cream (2.31:1) drops ink from 4.36:1 to 3.04:1. Changing the fill alone made
   one half readable by breaking the other half, which is what happened on the
   first attempt at this rule.

   So the pair is fixed here, once: cream on a terracotta stepped down toward
   ink. Hue is unchanged — it is still terracotta, just no longer a pale one —
   and the step is written as a mix rather than a literal so it follows the
   accent if that ever moves. Measured after: 4.54:1 at 12px. */
html[data-skin="paper"] body :is(
  .cta, .exchange, .send, .next, .prov.submit, .btn.terracotta, .choose.featured
) {
  background-color: color-mix(in srgb, var(--soft-peach-accent) 82%, var(--soft-peach-ink)) !important;
  /* --paper on this fill is 4.51:1 — it passes on paper and then drifts under
     as the fill composites over a dark ridge. --on-accent is 5.86:1 on the same
     fill and holds the margin. */
  color: var(--on-accent) !important;
}

/* ── CONTROLS: one ladder, and a hit area that cannot half-wire ────────────
   Every route loads this sheet, which is why the rule lives here rather than
   in motion.css, where it used to sit as a motion concern that it is not.

   ⚠️ The version it replaces named nine selectors for `position:relative` and
   only FOUR of them for the `::after`, so five controls carried the
   positioning and no hit area at all — .send (the Cast button), .side-toggle,
   .side-open-btn, .brand-btn and .method-menu .mm-row. A touch census found
   68 elements across 39 kinds still under 44px on a coarse pointer while the
   comment above the rule claimed they were handled. ONE list now feeds both
   halves: the two rules below share $CONTROLS by construction, so the failure
   that produced that gap cannot recur by someone editing one and not the
   other.

   A link inside a run of prose is deliberately NOT here. WCAG 2.2's Target
   Size carries an inline exception for exactly this reason: you cannot give a
   word in a sentence a 44px box without wrecking the sentence. Everything
   named below is a standalone control. */

/* ⭐ Raise the ELEMENT wherever it sits in normal flow; keep the bolted-on hit
   area only for controls that genuinely cannot grow.

   The first attempt gave every small control a 44px ::after and measured zero
   targets under 44 — and then an overlap check found fifteen pairs of hit
   areas intersecting and one tap on index landing on the wrong link. That is
   the worse bug: a short target makes you tap twice, a stolen one takes you
   somewhere you did not ask for. Overlap is only possible when a 44px hit area
   is bolted onto an 18px element in a 19px-pitch row. If the element itself is
   44 tall, normal flow guarantees the boxes cannot intersect.

   So: the ladder does the work, and the ::after is reserved for controls that
   are overlaid or wedged into tight chrome, where growing them would move
   something else — an icon button, a checkbox, the password toggle inside its
   own field. */

/* Buttons take the ladder everywhere: 44 is the design height for an action,
   not a concession to fingers, and it is the same on a desktop. */
.btn, a.ghost, a.brand, a.brand-home, a.new-cast, a.exchange,
button.new-cast, button.exchange, button.stria, button.send, button.tab,
.bk-go, .bk-brand {
  min-height: var(--ctl);
  display: inline-flex;
  align-items: center;
}

/* Rows of text links grow only where a finger is doing the pointing. ⚠️ Left
   unconditional, this added 99px to the legal contents list on a desktop —
   thirteen entries in a dense multi-column nav, each pushed to 44 for a
   pointer that is already accurate to a pixel. That is a design regression
   bought with no accessibility gain: WCAG 2.2 AA asks 24px on a fine pointer
   and these clear it. The coarse gate is what the rule this replaces got
   RIGHT; its failure was being half-wired, not being conditional. */
@media (pointer: coarse) {
  nav a, footer a, .legal-foot .links a, .ledgerstrip a {
    min-height: var(--ctl);
    display: inline-flex;
    align-items: center;
  }
}

/* Controls that cannot grow without pushing something else out of place. The
   two rules below share one selector list by construction — the version this
   replaces named nine selectors for position:relative and only four of them
   for the ::after, so .send (the Cast button), .side-toggle, .side-open-btn,
   .brand-btn and .method-menu .mm-row carried the positioning and no hit area
   at all, under a comment claiming they were handled. */
.cx, .casting-del, .side-toggle, .side-open-btn, .modal-x, .wiz-exit,
.brand-btn, .pass-toggle, .send, .method-menu .mm-row,
input[type="checkbox"], input[type="radio"] {
  position: relative;
}
.cx::after, .casting-del::after, .side-toggle::after, .side-open-btn::after,
.modal-x::after, .wiz-exit::after, .brand-btn::after, .pass-toggle::after,
.send::after, .method-menu .mm-row::after,
input[type="checkbox"]::after, input[type="radio"]::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 100%; height: 100%;
  min-width: var(--ctl);
  min-height: var(--ctl);
}

.side-toggle, .side-open-btn { min-height: var(--ctl-compact); }

/* ── The sidebar is a flat panel, not backdrop glass ───────────────────────
   ⚠️⚠️ This is the price of a moving landscape, and it was measured before it
   was paid. Moving anything behind a backdrop-filter makes that filter re-blur
   on every frame, and this panel is 264px wide by the full height of the window
   with more blurred panels nested inside it. With the range drifting:

     all glass as-is                30fps
     composer's blur off            32fps
     sidebar's blur off             43fps
     sidebar and its children off   52fps
     every blur on the page off     51fps

   The sidebar is the whole difference; everything else together is noise.

   ⭐ It also fixes something the sidebar was doing on its own. A .42 white
   panel over a blurred pale sky composites to a flat white column — measured 60
   units lighter than the range immediately beside it, all the way down — so the
   landscape appeared to stop dead at its edge. A flat translucent panel keeps
   more of what is behind it than a blurred one did.

   ⚠️ Scoped to the sidebar and its children only. Every other carrier on these
   routes is still glass; this one is full-height page furniture, which is the
   same reason it is already exempt from the route's corner rule. */
/* ⚠️ !important, and it is aligning weight rather than hoping to win.
   luxury-glass.css carries `html[data-skin="paper"] body[data-page="app"]
   .sidebar` and a sibling rule listing .signin-coach, both at the same
   specificity as anything writable here and both later in the cascade. This
   file has lost that contest four times already; matching the weight is the
   documented way out. */
html[data-skin="paper"] body[data-page="app"] :is(.sidebar, .sidebar *) {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
/* ⚠️ The white goes UP when the blur comes off, and that is not a reversal.
   A blur averages the backdrop into a flat field; without one the panel sits on
   whatever the ridges are doing, so the same .42 that read as calm through a
   blur leaves 10px labels on live contour lines. Measured on group 186 at .42:
   "Your first reading is on us" 3.56:1, "Available units" 3.85:1 — against
   about 6.9:1 when it was blurred.

   .72 is where the labels clear AA again while the landscape still reads
   through the panel, which is the whole point of taking the blur off.

   ⚠️⚠️ AND THE SPECIFICITY WAS NOT EQUAL — this note used to say it was, and
   answered with !important, which is why the .72 below never rendered. Measured
   2026-09-02: the panel was still .42. luxury-glass carried
   `html[data-skin="paper"] body :is(.sidebar, .pagehead, …, .section .panel, …)`,
   and **`:is()` takes the specificity of its most specific ARGUMENT** — that
   list contains `.section .panel`, so the whole selector is (0,3,2) and beats
   this rule's (0,3,1). Two !important declarations are decided by specificity
   first and only then by order, so the later-sheet reasoning was wrong twice
   over. `.sidebar` has been removed from those lists instead.
   ⭐ The lesson to carry: when counting a selector's weight, look INSIDE every
   :is() / :where() / :not() — one two-class argument buried in a long list
   silently outranks a rule that looks more specific than it. */
html[data-skin="paper"] body[data-page="app"] .sidebar {
  background-color: color-mix(in srgb, #FFFFFF 72%, transparent) !important;
}
/* ⭐ …and the small labels go to full ink. The panel is translucent over a live
   landscape now rather than over a blurred flat field, so the ground under a
   10px label is whatever the ridges are doing there — and --faint was chosen
   against a backdrop that no longer exists. Raising the white far enough to
   rescue them would rebuild the white column this change exists to remove; the
   ink is the half that can move without costing the picture. Same reasoning as
   the footer and the composer hint, which already take a solved ink for exactly
   this reason. */
html[data-skin="paper"] body[data-page="app"] .sidebar
  :is(.lbl, .ledger-cap, .ledger-unit, .ledger-plan, .who i) {
  color: var(--ink) !important;
}

/* ── Text with nothing behind it but the landscape ─────────────────────────
   ⭐ The white scrims are gone (owner, 2026-08-26, looking at what they
   actually rendered as: three white smears lying across the settings page).
   They were the honest fix for the measurement and the wrong thing to look at,
   which is the more important of the two facts.

   What is left in their place carries no white at all: the ink itself follows
   the group, and the glow around it is the ink's own colour pushed outward, so
   the halo is always the opposite luminance to whatever the letters are —
   which is the one thing a fixed white wash could never be.

   ⚠️ This costs contrast on the darkest groups and that is a chosen trade, not
   an oversight. Do not reintroduce a white plate here without measuring what it
   looks like as well as what it measures. */
html[data-skin="paper"] body[data-page="app"] .topbar .ghost {
  color: var(--bw-ink-on-sky, var(--ink));
}

/* ── Palette-derived ink ────────────────────────────────────────────────────
   --bw-palette-gem is an arbitrary swatch out of the group (gems[0]), so using
   it directly as a text colour means the text is whatever lightness that group
   happened to hold. Measured: the 42px price on pricing hit 1.01:1 on group
   052, and "Save 2 months" at 10px hit 1.13:1.

   The brief was that the colour should follow the palette without going to
   extremes, so this keeps the hue and the chroma — the page still changes
   colour with the range, which is the whole point — and pins only the
   lightness into a band that clears AA over the neutral glass. Nothing is
   hard-coded per group; one expression covers all 114.

   The plain value is the fallback for anything without relative colour syntax:
   no worse than what shipped, and the @supports block does the work everywhere
   it is understood. */
html { --gem-ink: var(--bw-palette-gem); }
html { --gem-ink-quiet: var(--bw-palette-gem); }
@supports (color: oklch(from red l c h)) {
  /* The same idea one step deeper, for type small enough that WCAG asks 4.5
     rather than 3. The owner picked .38 looking at a 42px price, where the
     margin is enormous; at 13px the same value measured 2.79:1 on one group.
     This is the size distinction the standard already makes, not a second
     opinion about the colour. */
  html { --gem-ink-quiet: oklch(from var(--bw-palette-gem) min(l, .26) c h); }
  /* .40, not .46: at .46 the price still measured 3.21:1 on the darker groups,
     because the card it sits on is glass over the range rather than paper.
     Owner picked .38 off the rendered comparison: the margin at .30 was so
     wide (15.7:1 where 4.5 is asked) that it was buying nothing and costing
     the colour — at .38 the figure still measures 14.7:1 on the same group. */
  html { --gem-ink: oklch(from var(--bw-palette-gem) min(l, .38) c h); }
}

/* Clay used as type takes the stepped ink, not the fill colour. These are the
   places measured under AA against their own composited backdrop: settings'
   section rails and its sign-in link, pricing's savings chip and identity line,
   and 404's secondary action. The fills elsewhere are untouched. */
html[data-skin="paper"] body[data-page="settings"] :is(.head b, .head .idx, .signedout-note a),
html[data-skin="paper"] body[data-page="pricing"] :is(.save, .identity, .plan-tag),
html[data-skin="paper"] body[data-page="not-found"] .ghostbtn {
  color: var(--terracotta-ink);
}
/* …and the secondary action on 404 gets the heavier glass. At .60 white the
   panel composites down over the range's darkest ridges, which is where its
   label measured 2.42:1; the button sits low on a page that never scrolls. */
html[data-skin="paper"] body[data-page="not-found"] .ghostbtn {
  background-color: var(--ui-glass-strong) !important;
}

/* The landing headline carries no plate either — same reason, and this is the
   page's centrepiece, so it least of all should sit on a white smear. Its ink
   follows the group; see index.html. */

/* ── Scroll boundaries: text goes out of focus, it does not get cut ─────────
   Owner's note: every place where a text box scrolls should have a light blur
   at the seam. There were two different problems hiding under that.

   The app's top bar had no background and no backdrop filter at all, so a
   conversation scrolling upward simply overlapped the navigation — two layers
   of type in the same place. It gets a frosted band that fades out downward,
   the same material as the bar below the composer.
   Scoped to a live conversation: the landing state has nothing scrolling under
   it, and a band across the top of the opening screen would read as chrome the
   page does not need. That also keeps the extra backdrop-filter away from the
   one view where the drifting clouds sit closest to it. */
body[data-page="app"]:not(.is-empty) .topbar { position: relative; }
body[data-page="app"]:not(.is-empty) .topbar::before {
  content: "";
  position: absolute;
  inset: 0 0 -18px;
  z-index: -1;
  pointer-events: none;
  -webkit-backdrop-filter: blur(14px) saturate(106%) brightness(1.10);
  backdrop-filter: blur(14px) saturate(106%) brightness(1.10);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
}

/* The inner scrollers are small boxes inside chrome that is already frosted,
   so what they need is the seam itself softened rather than another filter
   layer: their content dissolves at the edge instead of ending on a cut line.
   Kept to 14px — enough to read as a soft edge, small enough that the first
   line of an unscrolled list is not visibly dimmed. */
body[data-page="app"] :is(.castings, .carry-menu .cm-list, .composer textarea) {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
    #000 calc(100% - 14px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
    #000 calc(100% - 14px), transparent 100%);
}

/* ── No square boxes on the glass routes ───────────────────────────────────
   A census of computed border-radius across the eight glass routes at two
   viewports turned up 27 kinds of box still sitting on sharp corners, native
   form controls among them — pricing's Annually checkbox was an 18px square in
   a page where everything else is rounded.

   Two things are deliberately not in this list. Full-bleed page furniture (the
   sidebar, the drawer scrim, the legal route's flush bands) has no outside to
   round: its corners are the screen's. And rows inside a panel are dividers,
   not boxes — they carry one hairline on one side, and rounding each of them
   splits a single panel into a stack of little cards. */
html[data-skin="paper"] body:not([data-page="about"]):not([data-page="guide"]) :is(
  .rate-number, .billing-mail-note, .card-hero, .ledger, .side-foot,
  .signedout-note, .methods, .estimate, .id-row
) {
  border-radius: var(--ui-radius);
}
/* Native controls do not inherit the page's shape, so they have to be told. */
html[data-skin="paper"] body :is(textarea, select, input[type="text"], input[type="email"],
  input[type="password"], input[type="number"], input[type="search"]) {
  border-radius: var(--ui-radius-small);
}
html[data-skin="paper"] body :is(input[type="checkbox"]) {
  border-radius: var(--ui-radius-chip);
}

/* ── the pointer at the very foot of a reading ──────────────────────────────
   A reading commits to one referent per line and should. The symbol did not
   stop meaning the rest, though: 兄弟 read as the rival is, on the same board,
   still whoever takes a cut. This offers the branches the reading did not
   spend — and offering is the whole design constraint. It sits after the
   chain, quieter than the chain, in the label face at label size, so it reads
   as a footnote to the reading rather than a second opinion arguing with it.

   Hover carries the short form through `title`; the click opens the full
   library. Neither animates: the reading has landed, and the rule everywhere
   else here is that nothing keeps moving after it does. */
/* ⚠️ Spacing here is on the 4px ladder, snapped from 22/14/10/7/6/11/13/9/5.
   tests/style-system.mjs holds the site to that scale — nobody can tell 9px
   from 8px, and the site read 45 distinct spacing values before it was
   collected. Every move was 3px or less. */
.reading-body .xr-maybe-bar {
  margin: 24px 0 0;
  padding-top: 16px;
  border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
}
.reading-body .xr-maybe-lead {
  margin: 0 0 12px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
}
.reading-body .xr-maybe-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.reading-body .xr-maybe {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  min-height: var(--ctl-compact);
  padding: 8px 12px;
  border: 1px solid color-mix(in srgb, var(--ink) 11%, transparent);
  border-radius: var(--ui-radius-chip);
  background: color-mix(in srgb, var(--paper-raised) 58%, transparent);
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.35;
  color: var(--dim);
  text-align: left;
  cursor: pointer;
  /* ⚠️ --dur-base, not .16s. The site collapsed 26 transition durations onto a
     six-step ladder and tests/style-system.mjs ratchets it; a new literal is a
     new value on a page rather than a value in the system. 160 → 190ms. */
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out);
}
.reading-body .xr-maybe:hover,
.reading-body .xr-maybe:focus-visible {
  border-color: color-mix(in srgb, var(--ink) 24%, transparent);
  background: color-mix(in srgb, var(--paper-raised) 88%, transparent);
  color: var(--ink);
}
.reading-body .xr-maybe[aria-expanded="true"] {
  border-color: color-mix(in srgb, var(--ink) 30%, transparent);
  background: var(--paper-raised);
  color: var(--ink);
}
.reading-body .xr-maybe-sym { font-weight: 600; color: var(--ink); }
.reading-body .xr-maybe-hint { color: var(--faint); }
.reading-body .xr-maybe:hover .xr-maybe-hint,
.reading-body .xr-maybe[aria-expanded="true"] .xr-maybe-hint { color: var(--dim); }

.reading-body .xr-lib {
  margin: 12px 0 0;
  padding: 12px 16px;
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  border-radius: var(--ui-radius-small);
  background: color-mix(in srgb, var(--paper-raised) 72%, transparent);
}
.reading-body .xr-lib-lead {
  margin: 0 0 8px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--dim);
}
.reading-body .xr-lib-row {
  display: flex;
  gap: 12px;
  padding: 4px 0;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.55;
  border-top: 1px solid color-mix(in srgb, var(--ink) 6%, transparent);
}
.reading-body .xr-lib-row b {
  flex: 0 0 5.5em;
  font-weight: 600;
  color: var(--ink);
}
.reading-body .xr-lib[lang="en"] .xr-lib-row b { flex-basis: 9em; }
.reading-body .xr-lib-row span { color: var(--dim); }
.reading-body .xr-lib-acts b,
.reading-body .xr-lib-acts span { color: var(--faint); }

/* ═══ THE READING, SET LIKE A TRANSCRIPT ════════════════════════════════
   Owner, 2026-09-07:「按照原来的显示方法 同时排版参照 Claude Code 自身的页面
   不要加毫无意义的留白」— show the board the way it was always shown, model the
   typesetting on Claude Code's own page, and stop adding whitespace that means
   nothing.

   This replaces the rail-and-column redesign of 2026-09-02. That version put a
   216px channel down the left of the prose to hang section labels in, ran the
   title at 46px, and left the right third of the canvas empty on purpose. Two
   of those three are whitespace with no content in it.

   ⭐⭐ ONE LEFT EDGE. On the referenced page the question, the answer, its
   headings, its lists and its actions all start at the same x. The previous
   version had three edges — the rail at 0, the prose at 216, the follow-up
   panel back at 0 — and an asymmetric composition is only asymmetric if
   something lives in the gap. Everything here shares one box, and the box is
   declared once as `--rd-col`: the reading, the question, the follow-up panel
   and the action row all read it, so there is no second measurement to keep in
   step with the first.

   ⭐⭐ HIERARCHY FROM WEIGHT, NOT FROM SCALE. That is the actual lesson of the
   referenced page: its headings are barely larger than its body text, and they
   are unmistakable anyway because they are bold and they sit tight above what
   they introduce. So the reading is back on the ladder this system already
   owns — 18 title / 16.5 section / 15 body — and the 46px display line and the
   20px lede are gone. The earlier note here argued that 18/16.5/15 "read as one
   grey texture"; what actually made it grey was the leading and the gaps, not
   the sizes, and those are what this pass tightens.

   ⭐ AIR IS ONE LINE, NOT FOUR. Every gap below is a step on the 4px ladder and
   most of them halved: 40px before a section became 24, 1.05em between
   paragraphs became 12px, leading 1.82 became 1.7. Nothing is cramped and
   nothing is padded out to look considered. */

body[data-page="app"] .thread-inner { --rd-col: 760px; }

/* the shared box, one declaration for the block children of `.reading` */
body[data-page="app"] .reading > :is(.reading-body, .rd-depth, .rd-actions) {
  max-width: var(--rd-col);
  margin-left: auto;
  margin-right: auto;
}
/* ⚠️ …and a different one for the question, because it is a FLEX child of the
   thread and auto side margins do not mean the same thing there. On a flex
   item, auto cross-axis margins switch off `align-self: stretch` and size the
   item to its content: measured, the line came out 359px wide and centred, so
   it was the one part of the reading not sharing the left edge — while the
   declaration in the file looked identical to the one that worked. Width plus
   `align-self` is what centres a flex item at a fixed measure. */
body[data-page="app"] .msg-user {
  align-self: center;
  width: 100%;
  max-width: var(--rd-col);
}

/* ── the ladder ─────────────────────────────────────────────────────────
   18 / 16.5 / 15, all of them sizes the system already declares. No new step
   is introduced, which is also what keeps `tests/style-system.mjs` green — it
   ratchets the number of distinct literal sizes, and a "nice" 22 or 24 here
   would take it up by one for nothing. */
body[data-page="app"] .reading-body .rd-title {
  font-family: var(--font-serif);
  font-size: 18px;
  line-height: 1.3;
  font-weight: 700;
  letter-spacing: -.01em;
  margin: 0 0 12px;
  max-width: none;
  color: var(--ink);
}
body[data-page="app"] .reading-body .rd-para,
body[data-page="app"] .reading-body p,
body[data-page="app"] .reading-body .rd-list li {
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink);
}
body[data-page="app"] .reading-body .rd-para,
body[data-page="app"] .reading-body p { margin: 0 0 12px; }
body[data-page="app"] .reading-body .rd-list { margin: 0 0 12px; }

/* the section heading sits directly above its paragraph and is told apart by
   weight — it is not hung in a margin and it does not need a size of its own */
body[data-page="app"] .reading-body .rd-h2 {
  font-family: var(--font-sans);
  font-size: 16.5px;
  line-height: 1.35;
  font-weight: 700;
  letter-spacing: 0;
  margin: 24px 0 8px;
  color: var(--ink);
  height: auto;
  width: auto;
  transform: none;
  text-align: left;
}
body[data-page="app"] .reading-body .rd-title + .rd-h2,
body[data-page="app"] .reading-body > .rd-h2:first-child { margin-top: 0; }
/* ⚠️ h3 is the same 15px as the body, so tracking has to carry the difference
   that size is not carrying. Spinnaker is a single weight, so its bold is
   synthesised — visible, but not enough on its own at the same size. */
body[data-page="app"] .reading-body .rd-h3 {
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.4;
  font-weight: 700;
  letter-spacing: .02em;
  margin: 16px 0 4px;
  color: var(--ink);
}
/* the rule between movements: hairline, full column, no box */
body[data-page="app"] .reading-body .rd-hr {
  border: 0;
  border-top: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  margin: 24px 0;
}

/* ── the question is a transcript line, not a bubble ────────────────────
   ⚠️ Aligned to (0,2,0) to beat index.html's `.msg-user` at (0,1,0), which
   carries the fill, the radius and the shadow.
   ⭐ The label that names it is a KICKER above the line, not a floating word in
   a 216px channel. Same information, no channel — and unlike the rail version
   it exists at every width instead of only above 1180px. */
body[data-page="app"] .msg-user {
  background: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0 0 8px;
  margin-top: 0;
  margin-bottom: 16px;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--dim);
}
body[data-page="app"] .msg-user::before {
  content: "You asked";
  display: block;
  margin-bottom: 4px;
  font-size: 11px;
  line-height: 1.35;
  letter-spacing: .1em;
  color: var(--faint);
}

/* ── everything below the prose is in the same column ───────────────────
   ⚠️ The follow-up panel was the biggest plate left on the page — a filled,
   bordered, rounded box sitting on the landscape. It keeps its three-column
   grid; what it loses is the box. A hairline does the work the border was
   doing, and the ground stays visible through it. */
body[data-page="app"] .reading > .rd-depth {
  background: none;
  border: 0;
  border-top: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  border-radius: 0;
  padding: 16px 0 0;
  margin-top: 24px;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body[data-page="app"] .reading > .rd-actions {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
}
/* ⚠️ The follow-up panel's own heading was 20px and the reading's title is now
   18 — so the invitation to ask ANOTHER question was set larger than the answer
   it sits under. Scale inversions like this are what a display line at 46px was
   hiding; drop it onto the same ladder and the order is right again. */
body[data-page="app"] .rd-depth h4 { font-size: 16.5px; }
/* ── the selected reading is a LIGHTER card, not a darker one ────────────
   Owner, 2026-09-02: 「这种深色box不能要 换个浅点的颜色」.
   Measured, the selected history item was --paper-dim (#D4CEC3) — an OPAQUE
   tan chip sitting on a translucent white sidebar, so the one thing marked as
   "where you are" was the darkest object in the panel and read as a hole
   punched in it.
   Selection here should read as a card lifted toward the light, which is the
   same direction the rest of this route uses: lighter and more opaque than its
   panel, never darker. The ink stays full strength, so the item is still
   plainly the current one without weighing anything down. */
/* ⚠️ These sit ON the sidebar, so they are read against .72 white and not
   against the landscape. .72 for the selected item would have made it exactly
   its own panel — invisible. It has to clear the panel it is in. */
html[data-skin="paper"] body[data-page="app"] .castings .casting:hover,
html[data-skin="paper"] body[data-page="app"] .casting-row:hover {
  background: color-mix(in srgb, #FFFFFF 55%, transparent);
}
html[data-skin="paper"] body[data-page="app"] .castings .casting.active {
  background: color-mix(in srgb, #FFFFFF 92%, transparent);
  color: var(--ink);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 7%, transparent);
}

/* ── where it should be text, it is only text ────────────────────────────
   Owner, 2026-09-02: 「该是文本的地方就只要文本」.
   Audited the finished reading for anything carrying a fill, a border or a
   radius, and everything below came back wearing one: the 生克 runs as pills,
   the unspent branches as chips, the follow-up axes as outlined buttons, the
   actions as rounded buttons. None of them is a surface — they are all words.

   ⭐ What survives is the hairline BETWEEN sections. A rule separates; a box
   contains. Taking the containers off does not mean taking the structure off,
   and the page still has to say where one part stops.

   ⚠️ The affordances stay, they just stop being upholstery: colour where colour
   carries meaning (生 feeds / 克 restrains is the one place on this site where
   hue is information), an underline that arrives on hover, and the arrow the
   follow-ups already had. And the 44px touch floor set earlier is untouched —
   it was min-height, not a border, so the target stays big while the box goes. */

/* the 生克 runs: a line of words, with the relation named in its own colour */
body[data-page="app"] .reading-body .xr-edge {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin-right: 16px;
}
body[data-page="app"] .reading-body .xr-edge i {
  background: none;
  border-radius: 0;
  padding: 0;
  margin-right: 8px;
}
body[data-page="app"] .reading-body .xr-edges { gap: 8px 0; }

/* the branches this reading did not spend */
body[data-page="app"] .reading-body .xr-maybe {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 4px 0;
  text-align: left;
}
body[data-page="app"] .reading-body .xr-maybe:hover b,
body[data-page="app"] .reading-body .xr-maybe:focus-visible b {
  text-decoration: underline;
  text-underline-offset: 3px;
}
body[data-page="app"] .reading-body .xr-maybe-chips {
  display: block;
}
/* ⚠️ KNOWN, MEASURED, AND NOT FIXABLE FROM HERE: a multi-word 象 mark cannot
   break across lines. `.xr` is a <button>, and Blink blockifies buttons — a
   `display: inline` on one COMPUTES BACK to `inline-block`. Verified through
   CDP: the declaration matches, nothing overrides it, and getComputedStyle
   still answers `inline-block`. So a five-word mark like "what you were taught
   to do" is one atomic object; on a 326px column it is pushed onto a line of
   its own and the line above ends early. Fixing it means changing the element
   in `chat-app.js` from a <button> to a span with its own key handling, which
   is a different change from this one. Written down rather than left as a rule
   that does nothing — this file does not keep declarations that never paint. */

/* ⚠️ The chip was `inline-flex` with a `gap`, and `display: block` took the gap
   with it — the name and the hint fused into "Pressuremay still be a job".
   The same failure as the two-per-line collision below and found the same way,
   by looking at the rendered page rather than at the rule: when a box goes, so
   does everything the box's layout mode was doing, and a word space is one of
   those things. */
body[data-page="app"] .reading-body .xr-maybe-sym { margin-right: 4px; }
/* ⚠️ One per line. Without the chip they are still inline-flex, so two short
   ones share a line and butt straight into each other — measured, "…children,
   medicine" ran into "Wealth" with no separator at all. The box had been doing
   the separating; once it goes, the line break has to. */
body[data-page="app"] .reading-body .xr-maybe {
  display: block;
  width: 100%;
}

/* the follow-up axes and the actions */
body[data-page="app"] .rd-depth .rd-axis,
body[data-page="app"] .rd-actions .rd-action,
body[data-page="app"] .rd-depth .rd-prompt {
  background: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}
body[data-page="app"] .rd-depth .rd-axis {
  padding: 0 24px 0 0;
  color: var(--terracotta);
}
body[data-page="app"] .rd-depth .rd-axis:hover,
body[data-page="app"] .rd-depth .rd-axis:focus-visible,
body[data-page="app"] .rd-actions .rd-action:hover,
body[data-page="app"] .rd-actions .rd-action:focus-visible {
  text-decoration: underline;
  text-underline-offset: 3px;
}
body[data-page="app"] .rd-actions .rd-action { padding: 0 24px 0 0; }
body[data-page="app"] .rd-depth .rd-prompt:hover { background: none !important; }

/* ⚠️ The figure's key box is NOT stripped either. It went with the labels when
   the board was a 208px mark; at full size the key is the thing that makes the
   board readable, and hiding it was only ever a consequence of shrinking it. */
