/* Contrast hardening. Loaded last so the token overrides below win.
 *
 * Why this file exists. The atelier palette was set for a static cream page, but several roles are
 * painted on surfaces that are lighter than the page (#ffffff, --surface #faf8f2, --surface-soft
 * #f1f1e8), where they fall under WCAG 2.2 AA 4.5:1 for body text. Measured with the real browser
 * (taste-skill inspect.mjs, 2026-10-16):
 *
 *   .section-heading h2 small  #929887   2.73:1
 *   .section-heading > a       #68765f   4.44:1  (0.06 short)
 *   .editor-card p             #838b79   3.25:1
 *   .companion-epilogue        #929b86   2.89:1  (inherited by the .epilogue-dialogue bubbles)
 *
 * The fix is to darken the ink, never to lift surfaces: the warm paper ground is the brand. Every
 * value below was checked against all four grounds the site actually paints on (#f8f5ed, #f6f4ed,
 * #ffffff, #faf8f2, #f1f1e8) plus the epilogue gradient #f0f2e9.
 */

/* --- tokens ----------------------------------------------------------------------------------- */

/* Secondary ink used on light surfaces. Worst case 5.02:1 on the lightest ground. */
/* The quiet secondary ink and the micro-label ink. Both values, like --muted below, were set by
   sampling the REAL rendered backdrop rather than by trusting CSS colours — see the note on --muted. */
:root {
  --ink-quiet: #4a5448;
  /* Darker step for the small tracked label, which sits at 11px and needs the extra margin. */
  --ink-label: #4f5949;
}

/* --muted is the most-used text colour on the site (45% of text nodes). ui-refinement.css set it to
   #687167, which reaches only 4.46:1 on --surface-soft #f1f1e8 -- just under AA.
 *
 * Darkened a second time, to #4a5448, after an independent re-check sampled the REAL rendered
 * backdrop behind these elements rather than trusting the CSS. On paper the previous value looked
 * fine; measured against what is actually behind the glyphs it was not, because much of this text
 * sits on translucent glass over the scene illustration:
 *
 *   .header nav a          13px  author colour vs sampled backdrop 3.79:1
 *   #archive-result-count  12px  3.02:1   (ground sampled (225,208,216))
 *   .footer-base > span    13px  4.23:1 on #home, 4.24:1 on #archive -- the SAME string passes on
 *                                #studio/#data (4.55) and fails on the illustration-heavy routes
 *   .prism-rail-label      15px  4.35:1
 *
 * #4a5448 clears 4.5:1 against all four sampled grounds, worst case 5.29. It stays one step lighter
 * than --ink-label #4f5949 in the other direction, so the tonal hierarchy is preserved rather than
 * collapsed. A colour-contrast audit that only reads CSS values cannot find this class of failure;
 * that is why this number came from pixel sampling. */
:root {
  --muted: #4a5448;
}

/* ui-refinement.css re-declares --muted for night, where it already clears AA (7.4:1 on #21322f),
   but scene.css and quiet-magic.css both set a night --muted as well. Keep them consistent. */
html[data-scene-theme=night] {
  --muted: #b3c6bb;
}

/* --- the four measured offenders -------------------------------------------------------------- */

.section-heading h2 small {
  color: var(--ink-label);
}

.section-heading > a,
.section-heading > button {
  color: var(--ink-quiet);
}

.editor-card p {
  color: var(--ink-quiet);
}

/* The epilogue bubbles inherited a light colour from .companion-epilogue and then pulsed their own
   `opacity` between .35 and 1, so the copy was already at 2.89:1 at full opacity and faded to
   1.4:1 at the bottom of the pulse. Darken the ink and pulse the colour instead of the opacity:
   the pulse is still a pulse, but every frame of it clears AA. */
.companion-epilogue {
  color: var(--ink-quiet);
}

.epilogue-dialogue span {
  opacity: 1;
  color: var(--ink-quiet);
  animation: dialogue-turn 6s ease-in-out infinite;
}

@keyframes dialogue-turn {
  0%, 45%, 100% { color: var(--ink-quiet); translate: 0 2px; }
  10%, 35% { color: #4a5344; translate: 0 -4px; }
}

/* Night keeps its own bubble colour (room-life.css / scene.css); lighten rather than darken. */
html[data-scene-theme=night] .companion-epilogue {
  color: #dbe6dc;
}

html[data-scene-theme=night] .epilogue-dialogue span {
  color: #dbe6dc;
  animation-name: dialogue-turn-night;
}

@keyframes dialogue-turn-night {
  0%, 45%, 100% { color: #dbe6dc; translate: 0 2px; }
  10%, 35% { color: #f2f6ef; translate: 0 -4px; }
}

/* The site already gives reduced-motion users a static, fully legible bubble. Keep that, and set
   it explicitly so the colour animation cannot re-introduce a low-contrast frame. */
@media (prefers-reduced-motion: reduce) {
  .epilogue-dialogue span,
  html[data-scene-theme=night] .epilogue-dialogue span {
    animation: none;
    opacity: 1;
  }
}

/* --- the archive's standing note --------------------------------------------------------------
 * #archive-result-count carries class="library-note", whose upgrade.css colour #74796f reads
 * 3.78:1 against the archive's warm-pink ground (sampled #fae8eb) — under AA at 12px. Night is
 * already fine (scene.css re-colours it to #b5cec4, 7.94:1) and wins on specificity, so this only
 * has to carry the day theme. #5d635a measures 5.2:1 there and keeps the note quieter than the
 * body ink. */
.library-note {
  color: #5d635a;
}

/* --- headline measure -------------------------------------------------------------------------
 * At 768px the hero still ran its desktop two-column grid, so .hero-copy had ~38% of 1240px for a
 * 36px headline and the motto wrapped to three lines, breaking the two-line rule. The existing
 * single-column rule only fires at 640px (style.css), which leaves 641-820px broken. Collapse the
 * hero to one column for the whole tablet range instead of shrinking the type further. */
@media (max-width: 820px) {
  .hero.appreciation-hero {
    grid-template-columns: 1fr;
  }

  .appreciation-hero .hero-art {
    min-height: 260px;
  }
}

/* The home workflow is a real <ol> now (see home-workflow.js): the three steps are a genuine
   sequence, so the markup says so. Two defaults have to be undone for that to be a semantics-only
   change:
     - the list's own reset and its markers, because each card already shows "01 / 新的阅读" and a
       second set of numbers would duplicate the order;
     - the <li> wrappers, which would otherwise become the grid items and break the three-column
       track that is declared on .home-workflow-grid. `display: contents` promotes the <a> cards back
       to grid items without touching the layout or the divider borders between them. */
.home-workflow-grid {
  margin: 0;
  padding: 0;
  list-style: none;
}

.home-workflow-grid > li {
  display: contents;
}

/* --- text over the scene image -----------------------------------------------------------------
 * The header, main and footer sit at z-index 1 over the fixed .site-scene illustration, which gets
 * only a rgba(248,245,237,.16) scrim. Measuring the real backdrop pixels behind the footer text
 * (tools/probe-footer-pixels.mjs, home page, 1440px) gives:
 *
 *   .footer-base .brand   backdrop avg #cabd b1  (light)   #2d463b ink -> 5.57:1   ok
 *   .footer-base > span   backdrop avg #665860  (dark!)  #5f6a5c ink -> 1.18:1   INVISIBLE
 *   .footer-base small    backdrop avg #726467  (dark!)  #5f6a5c ink -> 1.01:1   INVISIBLE
 *
 * The lower half of the atelier illustration is a dark bookcase, so one ink cannot serve all three
 * islands. A colour-contrast audit that only reads CSS colours cannot see this: the failure exists
 * only against the picture. Two fixes, both required:
 *   1. a local paper scrim so the footer is no longer fought over by the bookshelf;
 *   2. a text-shadow halo, so the copy survives the scrim being turned down by the
 *      "背景强度" slider or replaced by a darker night scene.
 * Only the footer and the epilogue are touched; the main column still floats on the illustration. */
footer {
  position: relative;
}

footer::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(246, 244, 237, 0) 0,
    rgba(246, 244, 237, .72) 38%,
    rgba(246, 244, 237, .93) 100%
  );
}

/* The epilogue line ("读到这里了吗？") is white-ish on that same dark bookcase; give it a dark
   scrim instead, which is the direction its colour already wants. */
.companion-epilogue {
  position: relative;
}

.companion-epilogue::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(28, 38, 33, .18), rgba(28, 38, 33, .42));
}

.footer-base .brand,
.footer-base > span,
.footer-base small,
.companion-epilogue {
  text-shadow: 0 1px 2px rgba(255, 255, 255, .85), 0 0 14px rgba(246, 244, 237, .6);
}

/* Night: the scrim and the halo both have to flip. */
html[data-scene-theme=night] footer::before {
  background: linear-gradient(
    180deg,
    rgba(13, 26, 23, 0) 0,
    rgba(13, 26, 23, .74) 38%,
    rgba(13, 26, 23, .94) 100%
  );
}

html[data-scene-theme=night] .companion-epilogue::before {
  background: linear-gradient(180deg, rgba(8, 16, 14, .2), rgba(8, 16, 14, .5));
}

html[data-scene-theme=night] .footer-base .brand,
html[data-scene-theme=night] .footer-base > span,
html[data-scene-theme=night] .footer-base small,
html[data-scene-theme=night] .companion-epilogue {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6), 0 0 14px rgba(9, 18, 16, .55);
}

/* Readers who ask for less transparency get the flat surfaces instead of the scrim. */
@media (prefers-reduced-transparency: reduce) {
  footer::before,
  .companion-epilogue::before {
    background: var(--paper);
  }
}

/* room-life.js appends this link AFTER <footer>, so it sits on the raw illustration just above the
   footer scrim and measured 2.53:1. Give it a ground of its own rather than darkening the ink: an
   inset shadow paints the paper behind the glyphs and fades out at the edges, so it reads as a
   pool of light rather than a new pill component. */
.companion-stage-link {
  color: var(--ink-quiet);
  padding: 6px 16px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 40px rgba(248, 245, 237, .88), 0 0 22px 10px rgba(248, 245, 237, .6);
}

html[data-scene-theme=night] .companion-stage-link {
  color: #dbe6dc;
  box-shadow: inset 0 0 0 40px rgba(13, 26, 23, .88), 0 0 22px 10px rgba(13, 26, 23, .6);
}

/* --- minimum type size --------------------------------------------------------------------------
 * The page shipped 61 text blocks under 13px, down to 7px. Three of them were body copy that a
 * visitor is meant to read. The two rules below draw the line explicitly:
 *
 *   >= 12px  anything a visitor reads (footer, captions, rail cards, hints, statuses, controls)
 *   >= 11px  tracked micro-labels, which are decorative register marks, not copy
 *
 * This is deliberately not a blanket `font-size: 13px` on every small element. The glasshouse rails
 * are a fixed 145px / 1fr / 145px grid (glasshouse.css) that was sized around 9-11px text; forcing
 * 13px there would reflow and overflow those cards. Raising the rails to 12px keeps them inside
 * their boxes while clearing the readability floor, and every value here is a floor, so no
 * intrinsically larger element is touched. */
:is(
  .home-workflow-card span, .home-workflow-card small,
  .feature-bottom, .hero-foot,
  .editor-note .eyebrow, .eyebrow,
  .art-number, .section-heading h2 small,
  .prism-message strong, .prism-message p,
  .prism-rail-label, .prism-foot, .prism-foot a, .prism-note-link,
  .nature-label, .peek-message, .garden-preferences label, .garden-preferences output,
  .scene-settings p, .scene-settings label, .scene-settings legend,
  .scene-background-status, .scene-data-link, .scene-shuffle, .scene-toggle-text,
  .reading-shelf-item span, .reading-preferences span, .projection-photo-credit,
  .cast-bookmarks button small, .stage-note, .touch-help,
  .footer-base small, .companion-stage-link,
  .room-directory h2, .epilogue-dialogue
) {
  font-size: max(12px, 1em);
}

/* Decorative register marks: a strict 11px floor, still tracked, never smaller.
   glasshouse.css sets this exact span with a THREE-part selector
   (.prism-heading>div>span -> 0,1,1) at 9px, and again at 7px inside (max-width:650px). An
   :is() wrapper only carries its most specific argument (0,1,0), so it loses on specificity, not on
   source order. The `html` prefix takes this to (0,1,2), which wins in every media query, and the
   rule below deliberately sits outside any media query so it cannot be re-lowered at 650px.
   Literal 11px rather than `max(11px, 1em)`: at these sizes `1em` resolves against a 14px parent and
   would drag the mark up to body size, losing the register mark entirely and re-wrapping the hero
   label band. .eyebrow is in this list only to hold its 11px; ui-refinement.css already set it
   there, so including it changes nothing. */
html :is(.eyebrow, .prism-heading > div > span, .art-number, .section-heading h2 small) {
  font-size: 11px;
}

/* Copy that a visitor actually reads: the spoken epilogue bubbles and the quotation credit. Both
   measured 12px, one notch under the floor. Two earlier attempts at this failed for two different
   reasons, both worth recording so they are not repeated:
     - the span itself is already targeted by a two-part selector elsewhere, so a plain
       `.epilogue-dialogue span` (0,1,0) loses on specificity rather than on source order;
     - room-life.css re-sizes the same span to 11px inside (max-width:700px), so a `max(12px, 1em)`
       floor resolves against an 11px parent and lands back on 12px. A floor written in `1em` cannot
       defend itself against a media query that lowers the parent.
   So this sets a literal size, from a (0,1,2) selector, unconditionally.
   Caution for future edits: never write the comment terminator sequence as prose inside a CSS
   comment. Doing that here twice ended the comment early and silently swallowed the rule below,
   which cost two verification rounds. */
html .epilogue-dialogue span,
html .literary-quotes cite {
  font-size: 13px;
}

/* glasshouse.css drops the rail copy a step at 900px (the rails narrow to 145px there), so the
   floor has to be re-asserted from 900px down, not 700px. */
@media (max-width: 900px) {
  :is(.prism-message strong, .prism-message p, .prism-foot, .prism-foot a, .prism-note-link,
      .nature-label, .peek-message, .editor-card .primary, .feature p) {
    font-size: max(12px, 1em);
  }
}

/* The h2 "从文字出发" carries an English gloss in a <small>. At 390px the pair no longer fits two
   lines, and style.css already hides the same pattern for .section-heading at 640px
   (.section-heading h2 small{display:none}); the copy is a repeat of the Chinese anyway, so drop it
   on the narrowest widths rather than shrinking the Chinese headline. */
@media (max-width: 640px) {
  .section-heading h2 small {
    display: none;
  }
}

/* ============================================================================================
   Accent system: typography, the featured control, and swappable colour presets
   ============================================================================================
   Three custom properties drive every accent colour on the site, so a preset is one declaration
   rather than a rewrite of every rule that happens to be green:

       --accent        the solid ground of the featured control
       --accent-2      the second stop of its gradient
       --accent-glow   the warm highlight, also the hairline under the eyebrow rule

   Night already overrides --ink/--paper, so it overrides these too. Scene presets are selected by
   html[data-scene=...], which ac-boot.js mirrors up from the canvas (the canvas carries data-scene,
   but a parent cannot be selected from a child attribute in CSS, so the value is copied to <html>).
   A visitor override is the same three properties written onto :root as an inline style, which is
   why nothing below uses !important. */

:root {
  --accent: #263e34;
  --accent-2: #3a6349;
  --accent-glow: #ad9570;
}

/* Night: the page ground is dark, so the featured control becomes a pale fill (the existing night
   treatment) and the gradient runs between two papers rather than two inks. */
html[data-scene-theme='night'] {
  --accent: #d7eadf;
  --accent-2: #b9d6c6;
  --accent-glow: #e8d9b4;
}

/* Scene presets. Each stays inside the ink-and-paper range rather than becoming neon: the point is a
   recognisable shift per scene, not a different brand. green is the default and needs no rule. */
html[data-scene='frost'] {
  --accent: #24414a;
  --accent-2: #3d6b77;
  --accent-glow: #a9c9d6;
}

html[data-scene='waters'] {
  --accent: #1f3f3b;
  --accent-2: #356b5f;
  --accent-glow: #8fc7b6;
}

html[data-scene='bronze'] {
  --accent: #3b2f22;
  --accent-2: #6a5537;
  --accent-glow: #dcbe86;
}

html[data-scene='lotus'] {
  --accent: #4a3441;
  --accent-2: #7d566e;
  --accent-glow: #e2b6ca;
}

html[data-scene-theme='night'][data-scene='frost'] {
  --accent: #cfe2ea;
  --accent-2: #a8c6d3;
  --accent-glow: #a9c9d6;
}

/* Scene changes are a colour change, not a state change, so cross-fade the accent rather than
   swapping it. Registered as a real colour property so the transition interpolates instead of
   snapping. Falls back to a hard swap where the browser lacks @property — no visual break. */
@property --accent {
  syntax: '<color>';
  inherits: true;
  initial-value: #263e34;
}

@property --accent-2 {
  syntax: '<color>';
  inherits: true;
  initial-value: #3a6349;
}

html {
  transition: --accent 600ms ease, --accent-2 600ms ease;
}

/* --- the featured control -------------------------------------------------------------------
   specificity note: `html .primary` is (0,1,1) and so outranks the `html[...] .primary:hover` rule
   in ui-refinement.css at (0,2,0) by class count. That means the night solid-fill rule would win
   over this hover rule, which is why the hover restatement below is written as `html .primary:hover`
   at (0,2,1). Rule order alone would not have been enough. */
html .primary {
  border: 0;
  border-radius: 999px;
  color: var(--paper);
  background-image: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
  background-size: 170% 170%;
  background-position: 0% 50%;
  box-shadow: 0 4px 14px rgb(38 62 52 / 18%);
  transition: background-position 420ms cubic-bezier(0.22, 0.68, 0.24, 1), transform 240ms cubic-bezier(0.22, 0.68, 0.24, 1),
    box-shadow 240ms ease;
}

html .primary:hover {
  background-image: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
  background-position: 100% 50%;
  transform: translateY(-2px);
  box-shadow: 0 10px 24px rgb(38 62 52 / 26%);
}

html .primary:active {
  transform: translateY(0);
  box-shadow: 0 3px 10px rgb(38 62 52 / 22%);
}

/* The arrow drifts as the button lifts. Kept separate from the button transform so the glyph can
   move further than the plate without distorting it. */
html .primary > span {
  display: inline-block;
  transition: transform 240ms cubic-bezier(0.22, 0.68, 0.24, 1);
}

html .primary:hover > span {
  transform: translate(3px, -3px);
}

/* Night keeps the pale fill from ui-refinement.css but gains the same radius and motion, so the
   control reads as one component in both themes. */
html[data-scene-theme='night'] .primary,
html[data-scene-theme='night'] .primary:hover {
  border-radius: 999px;
  background-image: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
  color: #19352f;
}

/* Reduced motion: no travel and no sweep, but keep a visible hover state so the control still
   answers the pointer. */
@media (prefers-reduced-motion: reduce) {
  html .primary,
  html .primary:hover,
  html .primary > span,
  html .primary:hover > span {
    transform: none;
    transition: none;
  }

  html .primary:hover {
    filter: brightness(1.08);
  }
}

/* --- emphasised typography ------------------------------------------------------------------
   The headline takes more weight from the same variable serif (measured ink goes from 6,576 to
   10,061 at 42px, a 53% gain). The eyebrow line is set in a brush face, which is the reference
   direction in the study images: it is an accent, not a replacement for the serif. */
html .hero.appreciation-hero h1,
html .new-motto,
html .page-top h1 {
  font-weight: 900;
  letter-spacing: 0.01em;
}

/* The new sentence is longer than the one it replaces, and the copy column is fixed, so the size
   has to come down or the line breaks three ways. Measured: .hero-copy is 585px with 51.84px of
   padding per side, leaving 481px. The size itself lives in ui-refinement.css on the .new-motto
   rule that already carries !important for this element; redeclaring it here would only add another
   layer to the cascade. */
html .new-motto > span {
  display: block;
}

/* The markup keeps <br> between the spans so the sentence reads correctly even with CSS off, but
   once the spans are block-level those breaks would each add an empty line. */
html .new-motto > br {
  display: none;
}

/* Arrangement is chosen by measured fit, not by device name.
   The joined first line ("消弭于劳牍中的文学微光，") is 12 glyphs and needs roughly 461px at a 46px
   base. Measured column widths: 482px at 1440px, 705px at 820px, but only 387px at 1024px, because
   the hero's copy column is a fraction of the viewport and the fraction shrinks faster than the
   viewport does. So a two-line arrangement only fits on genuinely wide screens; everywhere else the
   three-line ladder is the one that fits, including 1024px tablets.
   Both arrangements come from one string in the DOM, so search, translation and screen readers see
   a single sentence. */
/* Three lines, one arrangement.
   A two-line variant was tried and removed: its first line ("消弭于劳牍中的文学微光，") needs 558px
   at the hero's 46px base, and the copy column measures 482px at 1440px, 477px at 1600px and 387px
   at 1024px — the column is a fraction of the viewport and shrinks faster than the viewport grows, so
   there is no width where the joined line fits. An earlier estimate of 461px for that line was wrong
   because it measured the two segments separately and added them; the letter-spacing of the 900
   weight applies per character and the joined line carries more of it.
   The ladder is in em, not px or vw: it is a proportion, so it survives every viewport and the media
   queries that already resize .new-motto without being restated per breakpoint. */
html .new-motto > span {
  display: block;
}

/* The markup keeps <br> between the spans so the sentence reads correctly with CSS off, but once
   the spans are block-level those breaks would each add an empty line. */
html .new-motto > br {
  display: none;
}

html .new-motto .motto-l1 {
  font-size: 0.8em;
  line-height: 1.44;
}

html .new-motto .motto-l2 {
  font-size: 0.98em;
  line-height: 1.38;
}

html .new-motto .motto-l3 {
  font-size: 1.26em;
  line-height: 1.32;
}
html .eyebrow,
html .hero-foot {
  font-family: 'STKaiti', 'KaiTi', KaiTi, 'Noto Serif SC', serif;
}

html .eyebrow {
  font-weight: 400;
  letter-spacing: 0.18em;
}

/* The foot line was set in a pale gold that measured under AA on the fixed scrim. */
html .appreciation-hero .hero-foot {
  color: var(--ink-label);
  font-weight: 600;
  letter-spacing: 0.08em;
}

/* --- home workflow panel: frosting, corner light, climbing plants ---------------------------
   The panel is wrapped by workflow-vines.js, which adds .wf-frame. The plants are a sibling of the
   grid rather than children of it, because the grid carries overflow:hidden AND backdrop-filter:
   both clip to the border box, so a plant drawn inside would be cut off at the very edge it is
   supposed to be climbing over. */

html .wf-frame {
  position: relative;
  isolation: isolate;
}

html .wf-frame > .home-workflow-grid {
  position: relative;
  z-index: 1;
  overflow: hidden;
  border-radius: 18px;
  border-color: var(--glass-line);
  background: var(--glass-bg);
  box-shadow: inset 0 1px 0 #ffffff7a, 0 12px 34px rgb(58 47 61 / 10%);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
}

/* The flowing colour band. A conic gradient rotating behind the frosted surface, plus a highlight
   that sweeps across it. Both sit at z-index -1 inside the grid, which has its own stacking context
   from isolation further up, so they stay behind the step text without needing to cover it.
   Periods are long (17s and 9s) on purpose: this is a surface the visitor reads on, and a fast cycle
   behind text is a distraction rather than an effect. */
html .wf-frame > .home-workflow-grid::before {
  content: '';
  position: absolute;
  inset: -35%;
  z-index: -1;
  pointer-events: none;
  background: conic-gradient(
    from 0deg at 50% 50%,
    var(--accent),
    var(--accent-glow),
    var(--accent-2),
    var(--accent),
    color-mix(in oklab, var(--accent-glow) 70%, var(--accent)),
    var(--accent)
  );
  opacity: 0.2;
  filter: blur(30px);
  animation: wf-flow-spin 17s linear infinite;
}

html .wf-frame > .home-workflow-grid::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(104deg, transparent 36%, #ffffff9e 50%, transparent 66%);
  transform: translate3d(-62%, 0, 0);
  animation: wf-flow-sweep 9s ease-in-out infinite;
}

@keyframes wf-flow-spin {
  to {
    rotate: 360deg;
  }
}

@keyframes wf-flow-sweep {
  0%,
  100% {
    transform: translate3d(-62%, 0, 0);
  }
  50% {
    transform: translate3d(62%, 0, 0);
  }
}

/* The steps sit above the corner light and keep their own transparent grounds. */
html .wf-frame > .home-workflow-grid > li {
  position: relative;
  z-index: 1;
}

html .wf-vines {
  position: absolute;
  inset: -8px;
  z-index: 2;
  pointer-events: none;
}

/* Sprigs are held well back. The illustration the panel sits on is full of muted foliage; the only
   leaf art available in the project is a saturated vector green (#9db884 -> #3f6b56), noticeably
   brighter than anything in the scene, so at full strength it reads as a sticker applied on top
   rather than growth arriving from the edge. Size, opacity and a slight desaturation pull it back
   until it behaves like an edge detail. The honest fix is tonally matched art, which is what the
   imagegen pass is for; this is the best treatment with the assets on hand.
   Angles are near-horizontal here on purpose: a sprig is drawn growing upward from its base, so to
   climb along the top edge it has to be laid almost on its side, and each corner lies along its own
   edge rather than all pointing the same way. */
html .wf-vine {
  position: absolute;
  height: 30%;
  aspect-ratio: 512 / 402;
  background: var(--wf-sprig) center / contain no-repeat;
  filter: saturate(0.62) brightness(0.96);
  opacity: 0;
}

html .wf-frame[data-vines='ready'] .wf-vine {
  opacity: 0.6;
  animation: wf-grow 760ms cubic-bezier(0.22, 0.68, 0.24, 1) var(--wf-delay, 0ms) both;
}

html .wf-vine-tl {
  top: 0;
  left: 0;
  transform: rotate(104deg) scaleX(-1);
  transform-origin: 4% 96%;
  --wf-delay: 60ms;
}

html .wf-vine-tr {
  top: 0;
  right: 0;
  transform: rotate(-104deg) rotateY(180deg);
  transform-origin: 96% 96%;
  --wf-delay: 190ms;
}

html .wf-vine-bl {
  bottom: 0;
  left: 0;
  transform: rotate(14deg) scaleX(-1);
  transform-origin: 4% 4%;
  --wf-delay: 320ms;
}

html .wf-vine-br {
  bottom: 0;
  right: 0;
  transform: rotate(-14deg);
  transform-origin: 96% 4%;
  --wf-delay: 450ms;
}

/* Night: the panel's own tokens go dark, and the saturated leaf green has to come down further or it
   glows. Dimming and desaturating together, rather than dropping opacity, keeps the shapes legible
   against the dark ground instead of fading them into it. */
html[data-scene-theme='night'] .wf-frame .wf-vine {
  filter: saturate(0.34) brightness(0.56);
}

html[data-scene-theme='night'] .wf-frame[data-vines='ready'] .wf-vine {
  opacity: 0.5;
}

/* Sprigs unfurl from the corner they belong to. Only opacity and transform, so this stays off the
   layout and off the main thread.
   The closing keyframe deliberately names no opacity. With `animation-fill-mode: both` the final
   keyframe keeps applying after the animation ends, so an opacity written here would override the
   resting value set on the rule — the sprigs measured 0.82 for exactly that reason while the rule
   said 0.6, and night never dimmed them because the animation was still the winner. Ending on
   `opacity: 0` is not what happens: with no opacity in the final keyframe, the animated value is
   taken from the element's own resting opacity, which is what the rules above and the night and
   quiet overrides then control. */
@keyframes wf-grow {
  from {
    opacity: 0;
    translate: 0 10px;
  }
  to {
    translate: 0 0;
  }
}

/* Quiet mode and reduced motion keep the plants — they are part of the composition, not motion —
   but stop them arriving on a timer. */
html[data-motion='quiet'] .wf-frame .wf-vine,
html[data-garden-quiet='true'] .wf-frame .wf-vine {
  animation: none;
  opacity: 0.72;
}

@media (prefers-reduced-motion: reduce) {
  html .wf-frame[data-vines='ready'] .wf-vine {
    animation: none;
    opacity: 0.72;
  }

  /* The flowing surface holds still but keeps its colour, so the panel does not go flat. */
  html .wf-frame > .home-workflow-grid::before,
  html .wf-frame > .home-workflow-grid::after {
    animation: none;
  }

  html .wf-frame > .home-workflow-grid::after {
    transform: none;
    opacity: 0.55;
  }
}

/* Quiet mode, which the site already exposes, stops the flow the same way. */
html[data-motion='quiet'] .wf-frame > .home-workflow-grid::before,
html[data-motion='quiet'] .wf-frame > .home-workflow-grid::after,
html[data-garden-quiet='true'] .wf-frame > .home-workflow-grid::before,
html[data-garden-quiet='true'] .wf-frame > .home-workflow-grid::after {
  animation: none;
}

/* On narrow layouts the panel becomes a single column and the plants would sit over the step copy,
   so they step back rather than shrink. */
@media (max-width: 700px) {
  html .wf-vines {
    inset: -6px;
  }

  html .wf-vine {
    height: 24%;
  }

  html .wf-frame[data-vines='ready'] .wf-vine {
    opacity: 0.48;
  }
}

/* --- first screen: drifting light ------------------------------------------------------------
   A single soft band of the scene's own accent colour, drifting diagonally behind the hero on a nine
   second cycle. It replaces the idea of moving the glasshouse's conic-gradient border up here: that
   border is a bright rainbow ring, and behind a portrait illustration it competes with the artwork
   rather than suggesting light. A low-opacity band on a long period reads as light crossing a room,
   which is the effect being asked for.
   Behaviour: opacity and transform only — no layout, no paint of the following content, no main
   thread work. The band is wider than the hero so it enters and leaves rather than pulsing. */
html .hero.appreciation-hero {
  position: relative;
  isolation: isolate;
}

html .hero.appreciation-hero::after {
  content: '';
  position: absolute;
  inset: -12% -6%;
  z-index: -1;
  pointer-events: none;
  opacity: 0.5;
  background: linear-gradient(
    104deg,
    transparent 26%,
    color-mix(in oklab, var(--accent-glow) 62%, transparent) 45%,
    color-mix(in oklab, var(--accent-2) 44%, transparent) 57%,
    transparent 76%
  );
  transform: translate3d(-38%, 0, 0);
  animation: hero-drift 9s ease-in-out infinite;
}

@keyframes hero-drift {
  0%,
  100% {
    transform: translate3d(-38%, 0, 0);
    opacity: 0.6;
  }
  50% {
    transform: translate3d(38%, 0, 0);
    opacity: 0.85;
  }
}

/* Night already darkens the page; the band is reduced rather than removed so the light still
   crosses the first screen after dark. */
html[data-scene-theme='night'] .hero.appreciation-hero::after {
  opacity: 0.34;
}

/* Quiet mode and reduced motion hold the band still. It stays visible as a static wash, so the
   first screen keeps its depth without animating. */
html[data-motion='quiet'] .hero.appreciation-hero::after,
html[data-garden-quiet='true'] .hero.appreciation-hero::after {
  animation: none;
  transform: none;
  opacity: 0.4;
}

@media (prefers-reduced-motion: reduce) {
  html .hero.appreciation-hero::after {
    animation: none;
    transform: none;
    opacity: 0.4;
  }
}

/* ============================================================================================
   Palette: dusk mauve, and the removal of green from the neutrals
   ============================================================================================
   The brief was to reduce the dark-green elements, including the text colours. The important part is
   that the neutrals themselves were green, not just the accent: --ink-quiet #4a5448 and
   --ink-label #4f5949 are green-grey, --muted was #4a5448, and --paper #f6f4ed is a warm green
   white. Changing only --accent would have left the page reading green, so the whole neutral family
   moves at once, from a selector with more weight than the plain :root blocks in the earlier files.

   Contrast was computed before this was written, not after:
     ink   #3a2f3d on paper 11.73     quiet #5f5663 on paper 6.47
     text  #423a45 on paper 10.09     label #584f5c on paper 7.22
   The accent is deliberately darker than the decorative mauve family, because that family cannot
   carry text: the vivid #8a5cc4 holds white at only 4.40:1 and #d0628f at 3.33:1, both under AA for
   a 13px label. #5d3a8a holds it at 8.61:1. --accent-glow is a light tone and is only ever used as
   a translucent wash, never behind text. */
html:root {
  --paper: #f8f5f7;
  --surface: #fdfbfc;
  --surface-soft: #f3eff3;
  --ink: #3a2f3d;
  --text: #423a45;
  --muted: #5f5663;
  --ink-quiet: #5f5663;
  --ink-label: #584f5c;
  --line: #e4dfe6;
  --pale: #f0ebf1;
  --gold: #a9857f;
  --accent: #5d3a8a;
  --accent-2: #a83f78;
  --accent-glow: #e8b0c8;
  --glass-bg: rgba(253, 251, 252, 0.72);
  --glass-line: rgba(255, 255, 255, 0.7);
  --glass-shadow: 0 14px 44px rgba(58, 47, 61, 0.1);
}

/* Night keeps its own dark ground, but the neutrals are re-tinted here so the theme does not stay
   green after dark. The dark --paper and --surface values live in scene.css and quiet-magic.css, so
   they are restated rather than left half-converted. */
html[data-scene-theme='night']:root {
  --paper: #221d26;
  --surface: #2b2531;
  --surface-soft: #322b39;
  --ink: #ece6ee;
  --text: #e6dfe9;
  --muted: #c0b6c6;
  --ink-quiet: #bdb3c3;
  --ink-label: #b6abc0;
  --line: rgba(226, 216, 230, 0.22);
  --pale: #3a3241;
  --gold: #cbb0a8;
  --accent: #e6dcf2;
  --accent-2: #d8b9d2;
  --accent-glow: #f0cfe0;
  --glass-bg: rgba(34, 29, 38, 0.8);
  --glass-line: rgba(240, 232, 244, 0.22);
}

/* The five scene presets were built around a green accent. They are re-tinted into the same mauve
   family so the scenes still shift the page, just no longer toward green. */
html[data-scene='frost']:root {
  --accent: #3f4a7a;
  --accent-2: #6f6aa8;
  --accent-glow: #b8c4e8;
}

html[data-scene='waters']:root {
  --accent: #3a4a68;
  --accent-2: #5f7a9c;
  --accent-glow: #a8c4dc;
}

html[data-scene='bronze']:root {
  --accent: #6a4630;
  --accent-2: #a87648;
  --accent-glow: #e0c098;
}

html[data-scene='lotus']:root {
  --accent: #6b3a6a;
  --accent-2: #a8508f;
  --accent-glow: #e8b0d8;
}

/* --- the three step links as floating orbs ---------------------------------------------------
   The steps were tall cards separated by 1px rules. They become three round frosted buttons, each
   with the flowing colour band behind its own glass and each bobbing on its own phase.
   Two constraints shaped this:
     * the cards are grid cells, so the cell keeps its column and the anchor inside is centred and
       fixed to a square. Without the fixed square the anchor would stretch to the cell and the
       circle would become an oval.
     * the 01/02/03 label is the <ol>'s own numbering made visible, so it stays inside the orb
       rather than being dropped.
   The arrow glyph is hidden: at this size it collides with the label, and the whole orb is already
   the affordance. */
html .home-workflow-grid {
  align-items: center;
  padding: 36px 0;
}

html .home-workflow-grid > li {
  display: flex;
  justify-content: center;
  align-items: center;
}

html .home-workflow-card {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  width: 196px;
  height: 64px;
  min-height: 0;
  padding: 0 22px;
  border: 1px solid var(--glass-line);
  border-right: 1px solid var(--glass-line);
  /* Pill, not a circle. The circles clipped their captions and the reference button is a pill. */
  border-radius: 999px;
  overflow: hidden;
  isolation: isolate;
  position: relative;
  text-align: center;
  color: var(--paper);
  /* The wave. Layered gradients rather than a border-image or a mask, so it stays resolution
     independent and needs no extra asset. The palette is the site's mauve family only — the request
     was to copy the SHAPE and the HOVER BEHAVIOUR of the reference buttons, not their blue/orange
     colouring. */
  background:
    radial-gradient(120% 190% at 12% -40%, color-mix(in oklab, var(--accent-glow) 62%, transparent), transparent 62%),
    conic-gradient(from 168deg at 42% 128%, var(--accent), var(--accent-2) 38%, color-mix(in oklab, var(--accent-glow) 50%, var(--accent)) 60%, var(--accent) 100%);
  box-shadow: inset 0 1px 0 #ffffff4d, 0 8px 22px rgb(58 47 61 / 18%);
  transition: filter 280ms ease, box-shadow 280ms ease, transform 280ms cubic-bezier(0.22, 0.68, 0.24, 1);
}

/* The rotating band stays, dimmed, so the pill still has living colour without competing with the
   wave face above it. */
html .home-workflow-card::before {
  content: '';
  position: absolute;
  inset: -60%;
  z-index: -1;
  pointer-events: none;
  background: conic-gradient(
    from 0deg,
    var(--accent),
    var(--accent-glow),
    var(--accent-2),
    color-mix(in oklab, var(--accent-glow) 60%, var(--accent)),
    var(--accent)
  );
  opacity: 0.55;
  filter: blur(16px);
  animation: wf-flow-spin 12s linear infinite;
}

/* Gloss: a soft highlight across the top and a narrower one along the bottom edge, which is what
   gives the reference button its glassy, slightly domed face. */
html .home-workflow-card::after {
  content: '';
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background:
    linear-gradient(180deg, #ffffff7a 0%, #ffffff1f 22%, transparent 52%),
    radial-gradient(120% 70% at 50% 116%, #ffffff4d, transparent 70%);
}

/* Hover: brighter and more saturated, not a different hue. `saturate` deepens the existing mauve
   rather than shifting it, and the lift replaces the old bobbing motion. */
html .home-workflow-card:hover,
html .home-workflow-card:focus-visible {
  filter: brightness(1.14) saturate(1.22);
  transform: translateY(-3px);
  box-shadow: inset 0 1px 0 #ffffff66, 0 14px 30px rgb(58 47 61 / 28%);
}

@media (prefers-reduced-motion: reduce) {
  html .home-workflow-card,
  html .home-workflow-card:hover {
    transition: none;
    transform: none;
  }
}

@keyframes wf-orb-bob {
  0%,
  100% {
    translate: 0 -7px;
  }
  50% {
    translate: 0 7px;
  }
}

/* The earlier liquid-glass rules for these controls have been removed.

   The buttons went through three mechanisms in one session — an SVG turbulence filter, a
   single-context WebGL refraction pass, then the vendored CSS displacement library — and the
   stylesheet had retained a block from each. Two of the survivors actively fought the design that
   replaced them: an `opacity: 0` on the button's ::before would have blanked the wave gradient that
   is now the entire visible surface, and a `background: transparent; filter: none` would have erased
   the fill and the hover response.

   What is kept is the one rule that matters for correctness: the library's layer is pinned into the
   layout contract, at zero opacity, so it can be switched back on by removing a single declaration
   rather than by re-deriving the geometry. The module (liquid-glass-css.js, nikdelvin/liquid-glass,
   MIT) remains in the tree. */
html .home-workflow-card > .lg-filter-layer {
  position: absolute;
  inset: 0;
  z-index: -1;
  display: block;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
}

/* The caption rides above any decorative layer. Kept from when the glass layer sat here: it costs
   nothing and it is what keeps the label on top if that layer returns. */
html .home-workflow-card > span,
html .home-workflow-card > strong {
  position: relative;
  z-index: 2;
}

/* --- in-page scroll-driven transitions -------------------------------------------------------
   The routing decision was
   to keep the discrete hash routes and put the scroll work inside a page, so none of this touches
   history, deep links or focus order.

   The motion is CSS scroll-driven animation (`animation-timeline: view()`), which this browser
   supports natively. That matters beyond elegance: the timeline is advanced by the compositor, so
   there is no scroll listener to throttle and no main-thread work while the page moves.

   Guarding shape: the properties that would hide content are written INSIDE the @supports block, so a
   browser without scroll timelines renders the sections plainly. If `opacity` were set outside, a
   browser that ignores `animation-timeline` would leave every section invisible — the classic
   scroll-animation failure. */
@supports (animation-timeline: view()) {
  html .scroll-scene {
    /* The resting opacity lives on the RULE, not in the keyframes. Anything written in a keyframe
       only applies while the timeline advances, and a section can sit before its range start — which
       is exactly what happens to every section below the fold. Putting the readable value here means
       the worst case is a section with no motion, never a section with no content. */
    opacity: 1;
    animation-name: scene-enter;
    animation-duration: 1ms; /* required, but the timeline overrides it */
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    /* start as the section's top reaches the bottom of the viewport, finish once it is a third of
       the way in — so the section has arrived before it reaches reading position */
    animation-range: entry 0% entry 66%;
  }

  /* Only transform animates. An opacity below the readable floor here would strand every
     not-yet-scrolled-to section at that value, which the project's route smoke test flags as
     invisible content. */
  @keyframes scene-enter {
    from {
      transform: translate3d(0, 18px, 0) scale(0.994);
    }
    to {
      transform: none;
    }
  }
}

/* The full-screen expansion. The hero already has the drifting light band; this adds the section
   growing to full width as it is scrolled into reading position, driven by the same kind of timeline
   but keyed to the section's own view range. */
@supports (animation-timeline: view()) {
  html .scroll-scene.hero.appreciation-hero {
    animation-name: scene-expand;
    animation-range: cover 0% cover 40%;
  }

  @keyframes scene-expand {
    from {
      opacity: 0.5;
      transform: translate3d(0, 22px, 0) scale(0.978);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }
}

/* Scroll stacking. Each section is pinned at the top and the next one slides over it, which needs
   the section to be no taller than the viewport or its end would be covered before it is read.
   scroll-scenes.js only marks sections that satisfy that, so an over-tall block is never pinned. */
html[data-scroll-stack] .scroll-scene.scroll-stackable {
  position: sticky;
  top: 0;
  /* a section sliding over the previous one needs to sit above it; the stacking order follows the
     document order, so a later section wins without any per-section z-index */
  z-index: 1;
}

/* Scroll snap. OFF by default, and deliberately so.

   The first version turned it on for every page with slide-sized sections and paired it with
   `scroll-behavior: smooth`. That combination is what makes a page feel like it is fighting the
   wheel: every flick starts an animated scroll that then forces the viewport onto a snap point, so
   the page keeps moving after the user has stopped. The user reported exactly that.

   Two changes follow. `scroll-behavior: smooth` is gone — snap alignment should not animate the
   page. And snap is now opt-in per page via `data-scroll-snap="force"`, because a site whose sections
   are a few hundred pixels tall (this one: 187–635px against a 900px viewport) has no clean
   full-screen rhythm for snapping to serve. The scroll-driven section transitions do not depend on
   snap at all, so they keep working with it off. */
html[data-scroll-snap='force'] {
  scroll-snap-type: y proximity;
}

html[data-scroll-snap='force'] .scroll-scene.scroll-snap {
  scroll-snap-align: start;
  scroll-snap-stop: normal;
}

/* Reduced motion and the site's quiet mode: the sections are shown in their end state, with the
   snap disabled. Stated explicitly rather than by omitting the rules, because a timeline that never
   advances would otherwise freeze a section at `opacity: 0.001`. */
html[data-scroll-scenes='quiet'] .scroll-scene {
  animation: none;
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html .scroll-scene,
  html .scroll-scene.hero.appreciation-hero {
    animation: none;
    opacity: 1;
    transform: none;
  }

  html[data-scroll-snap='force'] {
    scroll-snap-type: none;
  }
}

html .home-workflow-card span {
  font-size: 10px;
  line-height: 1;
  letter-spacing: 0.1em;
  white-space: nowrap;
  color: color-mix(in oklab, var(--paper) 80%, transparent);
}

html .home-workflow-card strong {
  font: 900 15px/1.3 var(--serif);
  letter-spacing: 0;
  color: var(--paper);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The explanatory line does not fit a 122px circle without breaking to three lines, and the orb
   already carries the action and the number. */
html .home-workflow-card small {
  display: none;
}

html .home-workflow-card b {
  display: none;
}

/* The middle step used to be emphasised with a surface fill and a gold top border; at orb size the
   fill is invisible and the border would break the circle, so emphasis moves to a brighter glass. */
html .home-workflow-card:nth-child(2) {
  background: color-mix(in oklab, var(--accent-2) 74%, transparent);
  border-top: 1px solid var(--glass-line);
  padding-top: 10px;
}

html .home-workflow-card:hover {
  transform: scale(1.05);
  box-shadow: inset 0 1px 0 #ffffff73, 0 14px 32px rgb(58 47 61 / 26%);
}

@media (prefers-reduced-motion: reduce) {
  html .home-workflow-card,
  html .home-workflow-card::before {
    animation: none;
  }
}

/* Narrow layout: the panel stacks, so the pills go full width and keep a comfortable tap target. */
@media (max-width: 700px) {
  html .home-workflow-grid {
    padding: 22px 0;
  }

  html .home-workflow-grid > li {
    width: 100%;
  }

  html .home-workflow-card {
    width: min(100%, 320px);
    height: 60px;
  }

  html .home-workflow-card strong {
    font-size: 15px;
  }
}

