/* ==========================================================================
   CHAZEN 茶禪 — JADE INK
   Black as foundation · Green as life · Simplicity as luxury

   Palette is theme 01 "JADE INK" from the brand's own five-theme sheet, used
   verbatim rather than reinterpreted. Deep jade and jade green are the only
   hues on the page; everything else is ink, charcoal and warm ivory. Nothing
   is tinted "for warmth" — the restraint is the identity.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   Themes. Three palettes over one token contract: LIGHT (瓷白 porcelain
   ground), DARK (墨 ink ground), GREEN (deep jade ground). Contrast was
   measured per theme, not assumed — #7FA894 is 7.1:1 on ink but only 2.4:1 on
   porcelain, so the green that carries small text differs by theme.
   Set via <html data-theme="...">; no theme attribute falls back to light.
   --------------------------------------------------------------------------- */

:root,
:root[data-theme="light"] {
  --ink:        #F7F5EF;   /* ground — 瓷白色 porcelain */
  --charcoal:   #EFEBE1;
  --jade-deep:  #2E3B34;
  --jade:       #56765E;
  --jade-text:  #3E5A47;                     /* 6.98:1 */
  --ivory:      #1C1C1C;                     /* 15.63:1 */
  --ivory-dim:  rgba(28, 28, 28, 0.68);      /* 4.97:1 */
  --ivory-faint:rgba(28, 28, 28, 0.62);      /* 4.52:1 */
  --hairline:   rgba(28, 28, 28, 0.13);
  --edge:       rgba(28, 28, 28, 0.32);
  --scrim:      247, 245, 239;
  --on-photo:   #F7F5EF;
  --cta-hover:  #3E5244;                     /* paper on this stays >=7:1 */
  --panel:      #FDFCF8;   /* menu drawer — a shade above the bar, fully opaque */
  --panel-hover:#EFEBE1;
}

:root[data-theme="dark"] {
  --ink:        #0D0D0D;   /* ground — 深墨色 ink */
  --charcoal:   #1A1A1A;
  --jade-deep:  #2E3B34;
  --jade:       #56765E;
  --jade-text:  #7FA894;                     /* 7.14:1 on ink */
  --ivory:      #D8D8CF;                     /* 13.55:1 */
  --ivory-dim:  rgba(216, 216, 207, 0.62);   /* 5.66:1 */
  --ivory-faint:rgba(216, 216, 207, 0.55);   /* 4.55:1 */
  --hairline:   rgba(216, 216, 207, 0.14);
  --edge:       rgba(216, 216, 207, 0.42);
  --scrim:      13, 13, 13;
  --on-photo:   #F7F5EF;
  --cta-hover:  #3E5244;                     /* paper on this stays >=7:1 */
  --panel:      #171717;
  --panel-hover:#232323;
  /* Light cuts — the porcelain set measures 2.10:1–3.22:1 on this ground. */
  --lq-green-text:  #D3CD79;   /* 11.83:1 */
  --lq-white-text:  #DCD5BE;   /* 13.24:1 */
  --lq-yellow-text: #DCC176;   /* 11.04:1 */
  --lq-oolong-text: #E09A5E;   /*  8.29:1 */
  --lq-rock-text:   #D08A66;   /*  6.95:1 */
  --lq-dark-text:   #C08C74;   /*  6.71:1 */
  --lq-matcha-text: #8FB577;   /*  8.37:1 */
}

:root[data-theme="green"] {
  --ink:        #1B241F;   /* ground — deep jade */
  --charcoal:   #232E27;
  --jade-deep:  #0F1613;
  --jade:       #7FA894;
  --jade-text:  #A9C6B2;                     /* 8.2:1 on this ground */
  --ivory:      #E8EDE6;                     /* 12.9:1 */
  --ivory-dim:  rgba(232, 237, 230, 0.66);   /* 5.9:1 */
  --ivory-faint:rgba(232, 237, 230, 0.58);   /* 4.7:1 */
  --hairline:   rgba(232, 237, 230, 0.15);
  --edge:       rgba(232, 237, 230, 0.42);
  --scrim:      27, 36, 31;
  --on-photo:   #F7F5EF;
  --cta-hover:  #1E2B23;                     /* paper on this stays >=7:1 */
  --panel:      #232E27;
  --panel-hover:#2C3A32;
  /* Same light cuts, re-measured against the jade ground (all still >=4.5:1). */
  --lq-green-text:  #D3CD79;   /* 9.69:1 */
  --lq-white-text:  #DCD5BE;   /* 10.85:1 */
  --lq-yellow-text: #DCC176;   /* 9.05:1 */
  --lq-oolong-text: #E09A5E;   /* 6.79:1 */
  --lq-rock-text:   #D08A66;   /* 5.69:1 */
  --lq-dark-text:   #C08C74;   /* 5.49:1 */
  --lq-matcha-text: #8FB577;   /* 6.86:1 */
}

:root {
  --jade-glow:  rgba(86, 118, 94, 0.30);

  /* --- Liquor palette ---------------------------------------------------
     The one place colour is allowed in, and it is not decoration: every hue
     below is the colour of the brewed liquor of a real tea family. Jade Ink
     stays the identity — ground, type and structure never change — but a
     section may carry the colour of the thing it is about.

     This began as a per-row --c on the tea-origins route diagram, which is
     the section that reads best on the whole site, and the reason is that
     the colour means something: the reader learns the family by its liquor
     before they have read a word. Promoted here so any page can use it.

     Rules:
       · A section carries ONE liquor, set as --c on the section.
       · --on-c is what sits ON that colour. Measured, not guessed.
       · Never use a liquor as body-text colour on the page ground — these
         are picked to be legible as FILLS, and only --lq-*-text is safe for
         words on paper. #C9C36A is 1.9:1 on porcelain.
     Contrast below is fill vs its --on-c partner. */
  --lq-green:   #C9C36A;  --on-lq-green: #1C1C1C;   /* 綠茶   9.33:1 */
  --lq-white:   #D9D2B8;  --on-lq-white: #1C1C1C;   /* 白茶  11.25:1 */
  --lq-yellow:  #D2B35F;  --on-lq-yellow:#1C1C1C;   /* 黃茶   8.40:1 */
  --lq-oolong:  #C57633;  --on-lq-oolong:#1C1C1C;   /* 烏龍   4.88:1 */
  --lq-rock:    #9B4F2D;  --on-lq-rock:  #F7F5EF;   /* 岩茶   5.43:1 */
  --lq-red:     #8C3B22;  --on-lq-red:   #F7F5EF;   /* 紅茶   6.97:1 */
  --lq-dark:    #6F3826;  --on-lq-dark:  #F7F5EF;   /* 黑茶   8.49:1 */
  --lq-matcha:  #4F6F3B;  --on-lq-matcha:#F7F5EF;   /* 抹茶   5.25:1 */

  /* Text-safe cuts, for a liquor carrying WORDS on the page ground rather
     than filling a shape. Porcelain values here; the dark and green theme
     blocks override with light cuts, because these same five measure
     2.10:1–3.22:1 on ink — a liquor that is legible on paper is invisible on
     ink, and one set cannot serve both. Ratios are vs that theme's ground. */
  --lq-green-text:  #6B6428;   /* 5.54:1 */
  --lq-white-text:  #6E6647;   /* 5.27:1 */
  --lq-yellow-text: #7A6224;   /* 5.35:1 */
  --lq-oolong-text: #8A4E1C;   /* 6.04:1 */
  --lq-rock-text:   #8A4426;   /* 6.56:1 */
  --lq-dark-text:   #6F3826;   /* 8.49:1 */
  --lq-matcha-text: #3D5A2C;   /* 7.14:1 */

  /* Type scale — 1.333 (perfect fourth). Wide steps so hierarchy is
     unmistakable at a glance; four sizes doing the same job reads as none. */
  --t-micro: 0.6875rem;   /* 11px  labels, numerals   */
  --t-small: 0.8125rem;   /* 13px  captions           */
  --t-body:  1.0625rem;   /* 17px  reading            */
  --t-lede:  1.375rem;    /* 22px  section leads      */
  --t-card:  1.375rem;   /* card + step titles, 1.29x over body */
  --t-h3:    1.9rem;     /* sub-section headings, 1.38x over card */
  --t-h2:    clamp(2.1rem, 4.2vw, 3.6rem);
  /* Was 10vw / 9.5rem, which at 1440px produced a 144px three-line headline
     that pushed the hero buttons out of an overflow:hidden container — they
     were simply absent on desktop while still visible on mobile. */
  --t-h1:    clamp(2.6rem, 6.4vw, 6.2rem);

  /* CzQuote first, then the CJK face. See the @font-face below — it claims only
     the quote codepoints, so every other Latin and CJK glyph still comes from
     Noto. */
  --serif: CzQuoteSerif, "Noto Serif TC", "Songti TC", "Source Han Serif TC", Georgia, serif;
  --sans:  CzQuoteSans, "Noto Sans TC", "PingFang TC", -apple-system, BlinkMacSystemFont,
           "Helvetica Neue", sans-serif;

  --gut: clamp(1.25rem, 4vw, 4.5rem);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}

/* Noto's TC faces give the quote marks full-width CJK metrics, so an English
   contraction in a display heading rendered as "Didn’ t" — a gap wide enough
   to read as a typo. These two families claim ONLY the apostrophe and quote
   codepoints via unicode-range, pulling them from a Latin face while every
   other glyph, Latin or CJK, still comes from Noto. Matters beyond the one
   English heading: the site ships ten languages, and French and Italian are
   full of apostrophes. If no local face matches, the stack simply falls
   through to Noto and nothing is worse than before. */
@font-face {
  font-family: CzQuoteSerif;
  src: local("Georgia"), local("Times New Roman"), local("Times");
  unicode-range: U+0027, U+0022, U+2018, U+2019, U+201C, U+201D;
}
@font-face {
  font-family: CzQuoteSans;
  src: local("Helvetica Neue"), local("Helvetica"), local("Arial");
  unicode-range: U+0027, U+0022, U+2018, U+2019, U+201C, U+201D;
}

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

/* `hidden` is a UA style (display:none) at the lowest specificity, so ANY class
   that sets display beats it — `.cta { display: inline-flex }` left the brew
   simulator's "next infusion" button on screen all through the steep. */
[hidden] { display: none !important; }

html {
  scroll-behavior: smooth;
  /* Measured at runtime by site.js. The nav wraps to two rows once the
     language dropdown and the sound/motion switches are in, and every interior
     page had a fixed top padding sized for one row — so the header sat on top
     of the h1. Anything that must clear the nav keys off this. */
  --nav-h: 5.5rem;
  scroll-padding-top: calc(var(--nav-h) + 1rem);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* `clip` (not `hidden`) on purpose: it suppresses horizontal scroll WITHOUT
   creating a scroll container. */
html { overflow-x: clip; }

body {
  background: var(--ink);
  color: var(--ivory);
  font: var(--t-body)/1.75 var(--sans);
  /* Deliberately no `overflow-x: hidden` here. It computes `overflow-y` to
     `auto`, which makes <body> a scroll container — and every `position:
     sticky` descendant then resolves against a scrollport that never scrolls,
     so it never sticks. That silently disabled every pinned gallery on the
     site: the stage scrolled away at the top of its spacer and left 2-3 blank
     viewports behind it. html's `overflow-x: clip` above covers the clipping. */
}

::selection { background: var(--jade-deep); color: var(--ink); }

img, video { display: block; max-width: 100%; }

/* --- Shared -------------------------------------------------------------- */

.shell { width: min(1440px, 100% - var(--gut) * 2); margin-inline: auto; }

.label {
  font-size: var(--t-micro);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--jade-text);
  font-weight: 500;
}

/* Section numbers are apparatus, not ornament: they mark position in a
   thirteen-part sequence the visitor is moving through. */
.num {
  font-family: var(--serif);
  font-size: var(--t-micro);
  letter-spacing: 0.2em;
  color: var(--ivory-faint);
  font-variant-numeric: tabular-nums;
}

.zh { font-family: var(--serif); }

h1, h2, h3 { font-family: var(--serif); font-weight: 500; line-height: 1.06; }
h2 { font-size: var(--t-h2); letter-spacing: -0.01em; }
h3 { font-size: var(--t-h3); }
/* Card-level headings. These are h3 for outline correctness, but they are card
   titles, not section headings, so they take the card step. */
.step h3, .step h4, .tier h3, .tier h4, .era h3, .era h4,
.post h3, .post h4, .plate h3 {
  font-family: var(--serif); font-weight: 500; font-size: var(--t-card);
  line-height: 1.2;
}

.lede {
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--ivory-dim);
  max-width: 46ch;
}

section { position: relative; }

.band { padding: clamp(4.5rem, 9vh, 7.5rem) 0; }
.band--tight { padding: clamp(3.5rem, 6vh, 5.5rem) 0; }

.rule { height: 1px; background: var(--hairline); border: 0; }

/* Skip link — the injected nav puts seven links before the content. */
.skip {
  /* Clip rather than push off-canvas. left:-9999px extends the scrollable
     area leftward, and under dir=rtl the scroll origin is the right edge, so
     the browser opened 9999px away from the content and every Arabic page
     rendered as one flat colour. */
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.skip:focus {
  position: fixed; inset-inline-start: var(--gut); inset-block-start: 0.6rem;
  width: auto; height: auto; margin: 0; overflow: visible;
  clip-path: none; z-index: 200;
  background: var(--jade-deep); color: var(--on-photo);
  padding: 0.9rem 1.4rem; font-size: var(--t-small); font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; text-decoration: none;
}

/* --- Scroll reveal ------------------------------------------------------- */

html.js [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 1.1s var(--ease), transform 1.1s var(--ease);
}
html.js [data-reveal].is-in { opacity: 1; transform: none; }
[data-reveal][data-delay="1"] { transition-delay: 0.10s; }
[data-reveal][data-delay="2"] { transition-delay: 0.20s; }
[data-reveal][data-delay="3"] { transition-delay: 0.30s; }
[data-reveal][data-delay="4"] { transition-delay: 0.40s; }
/* motion.js writes --d on the children of a [data-stagger] group, so a grid of
   eight cards arrives as eight things rather than as one block. The four
   authored delays above are one specificity step higher than this, so a
   hand-written data-delay still wins without needing to say so. */
[data-reveal] { transition-delay: var(--d, 0s); }

/* --- Reveal variants ------------------------------------------------------
   One fade-up for everything is why the interior pages read as static next to
   the home page: the same gesture 60 times is wallpaper. These are the same
   observer and the same duration — only the direction changes, so nothing
   here costs a frame more than the plain reveal did.
   -------------------------------------------------------------------------- */

/* A plate that grows into place. Used on figures and product images: the
   image is doing the arriving, so it should not also slide. */
html.js [data-reveal="rise"] { transform: translateY(34px) scale(0.985); }

/* Copy that arrives from the side it is aligned to. Reads as a column being
   set rather than as a block dropping in. */
html.js [data-reveal="left"]  { transform: translateX(-28px); }
html.js [data-reveal="right"] { transform: translateX(28px); }

/* A rule or divider draws itself. transform-origin follows the reading
   direction so it never draws backwards in Arabic. */
html.js [data-reveal="draw"] {
  opacity: 1; transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 1.2s var(--ease);
}
html:dir(rtl).js [data-reveal="draw"] { transform-origin: 100% 50%; }
html.js [data-reveal="draw"].is-in { transform: scaleX(1); }

/* A photograph uncovered rather than faded — the one gesture on the site that
   belongs to a handscroll being unrolled. clip-path is compositor-driven, so
   it is as cheap as the opacity it replaces. */
html.js [data-reveal="wipe"] {
  opacity: 1; transform: none;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1.25s var(--ease);
}
html:dir(rtl).js [data-reveal="wipe"] { clip-path: inset(0 0 0 100%); }
html.js [data-reveal="wipe"].is-in { clip-path: inset(0 0 0 0); }

/* A seal or glyph that settles into place. Small rotation only — anything
   more and it reads as a web banner. */
html.js [data-reveal="seal"] { transform: scale(0.86) rotate(-4deg); }

/* Every variant shares the reveal's own transition unless it declared its
   own, and every one of them is off under reduced motion via the existing
   [data-motion="paused"] and prefers-reduced-motion rules below. */
html.js [data-reveal="rise"].is-in,
html.js [data-reveal="left"].is-in,
html.js [data-reveal="right"].is-in,
html.js [data-reveal="seal"].is-in { opacity: 1; transform: none; }

/* Parallax and wipe both need the element to be its own layer, or the browser
   repaints the picture every frame instead of moving it. */
[data-parallax], html.js [data-reveal="wipe"] { will-change: transform; }

/* Parallax needs somewhere to go. An image sized exactly to its frame slides a
   blank band in at one edge the moment it moves, so a framed image is given
   overscan and centred inside its frame.

   The overscan is `top`, not a negative margin. A PERCENTAGE MARGIN RESOLVES
   AGAINST THE CONTAINING BLOCK'S WIDTH — including margin-block — so -9% on a
   619px-wide, 495px-tall frame pulled the image up by 56px when half the
   overscan was 45px. Every framed image on the site sat 9px off-centre, and
   once the drift was added on top, a band opened along the bottom edge. Under
   `position: absolute`, `top` resolves against HEIGHT, which is the axis this
   is actually about.

   The numbers are deliberately not close: motion.js drifts by at most 5% of
   the frame height and there is 12% of slack at each edge, so the picture
   cannot uncover an edge at any scroll position or aspect ratio. */
.split__media, .item__f, .dyn__plate, .rite__plate, .band-plate { position: relative; overflow: hidden; }
.split__media > [data-parallax],
.item__f > [data-parallax],
.dyn__plate > [data-parallax],
.rite__plate > [data-parallax],
.band-plate > [data-parallax] {
  position: absolute; top: -12%; left: 0;
  width: 100%; height: 124%; object-fit: cover;
}
/* With motion off the image sits back down flush against its frame, or it
   stays cropped by 12% for a visitor who asked for no movement. */
html[data-motion="paused"] .split__media > [data-parallax],
html[data-motion="paused"] .item__f > [data-parallax],
html[data-motion="paused"] .dyn__plate > [data-parallax],
html[data-motion="paused"] .rite__plate > [data-parallax],
html[data-motion="paused"] .band-plate > [data-parallax] {
  top: 0; height: 100%; transform: none !important;
}
@media (prefers-reduced-motion: reduce) {
  .split__media > [data-parallax],
  .item__f > [data-parallax],
  .dyn__plate > [data-parallax],
  .rite__plate > [data-parallax],
  .band-plate > [data-parallax] { top: 0; height: 100%; transform: none !important; }
}

/* --- Nav ----------------------------------------------------------------- */

.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 90;
  /* The bar holds four blocks — brand, six nav groups, four preference
     controls, and (on shop pages) the cart. Measured at 1401px that is 1406px
     of content in 1401px of room, so it wrapped to two rows on the exact
     widths between the breakpoint and a 1440 laptop. The gap carries the
     difference; 1.75rem still reads as separation, and it buys real headroom
     rather than sitting one translated word away from wrapping again. */
  display: flex; align-items: center; flex-wrap: wrap; gap: 1.75rem;
  padding: 1.1rem var(--gut);
  background: linear-gradient(var(--ink), rgba(var(--scrim),0));
  transition: background 0.5s var(--ease), backdrop-filter 0.5s var(--ease);
}
.nav.is-stuck {
  background: rgba(var(--scrim),0.9);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--hairline);
}
.brand {
  font-family: var(--serif); font-size: 1.15rem;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--ivory); text-decoration: none; white-space: nowrap;
}
.brand small {
  display: block; font-size: 0.62rem; letter-spacing: 0.5em;
  /* --jade is a rule-and-mark colour; at 9.92px it fell to 3.84:1 on the dark
     ground. --jade-text is the per-theme green that is safe carrying words. */
  color: var(--jade-text); margin-top: 3px; text-transform: none;
}
/* --- Grouped nav ---------------------------------------------------------
   The panel is absolute against .nav, not against the links row: the row is a
   flex container and on narrow widths used to be a horizontal scroller, which
   would have clipped any panel drawn inside it. */
.nav-group { position: relative; }
.nav-group__t {
  appearance: none; background: transparent; border: 0; cursor: pointer;
  padding: 0 0 3px; font: inherit; color: var(--ivory-dim);
  display: inline-flex; align-items: center; gap: .38rem;
  transition: color 0.35s var(--ease);
}
.nav-group__t::after {
  content: ""; width: 5px; height: 5px; margin-top: -2px;
  border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
  transform: rotate(45deg); transition: transform .3s var(--ease);
}
.nav-group__t:hover { color: var(--ivory); }
.nav-group__t[aria-expanded="true"] { color: var(--ivory); }
.nav-group__t[aria-expanded="true"]::after { transform: rotate(-135deg); margin-top: 2px; }
.nav-group__t:focus-visible { outline: 2px solid var(--jade); outline-offset: 4px; }
/* The group holding the current page keeps the same underline a current link
   gets, so "where am I" survives the panel being shut. */
.nav-group[data-holds="true"] .nav-group__t { color: var(--ivory); }
.nav-group[data-holds="true"] .nav-group__t::before {
  content: ""; position: absolute; left: 0; right: 14px; bottom: 0;
  height: 1px; background: var(--jade-text);
}
/* The panel must be *opaque*. It hangs over the hero video, and at 0.97 the
   headline behind it read straight through the menu — backdrop-filter does not
   save a translucent ground from high-contrast type. --panel is the theme's
   own paper, so the menu reads as a drawer of the bar, not a ghost of it. */
.nav-group__p {
  position: absolute; top: calc(100% + .55rem); inset-inline-start: -1.1rem;
  min-width: 13rem; z-index: 95;
  display: flex; flex-direction: column;
  padding: .55rem 0; background: var(--panel);
  border: 1px solid var(--hairline);
  box-shadow: 0 18px 44px -18px rgba(0,0,0,0.55);
}
/* The panel hangs .55rem clear of its trigger, and that gap belongs to neither
   box — so a pointer travelling from the word down to the menu left .nav-group,
   fired mouseleave, and the menu shut in front of it. This bridges the gap from
   inside the panel, so the pointer never leaves the group. */
.nav-group__p::before {
  content: ""; position: absolute; inset-inline: 0; bottom: 100%; height: .7rem;
}
.nav-group__p a {
  padding: .5rem 1.15rem; white-space: nowrap;
  transition: color .28s var(--ease), background-color .28s var(--ease);
}
/* The links row uses an animated underline; inside a panel a full-width
   ground reads better and stops the rule colliding with the panel edge. */
.nav-group__p a::after { display: none; }
.nav-group__p a:hover,
.nav-group__p a:focus-visible { color: var(--ivory); background: var(--panel-hover); }
.nav-group__p a[aria-current="page"] {
  color: var(--ivory); box-shadow: inset 2px 0 0 var(--jade-text);
}

.nav-links { margin-left: auto; display: flex; gap: 1.45rem; flex-wrap: wrap; }
.nav-links a {
  color: var(--ivory-dim); text-decoration: none;
  font-size: var(--t-small); letter-spacing: 0.06em;
  position: relative; padding-bottom: 3px; transition: color 0.35s var(--ease);
}
.nav-links a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: var(--jade-text);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 0.45s var(--ease);
}
.nav-links a:hover { color: var(--ivory); }
.nav-links a:hover::after { transform: scaleX(1); }
.nav-links a:focus-visible { outline: 2px solid var(--jade); outline-offset: 4px; }

/* --- Progress rail ------------------------------------------------------- */

.rail {
  position: fixed; top: 0; left: 0; height: 2px; width: 100%;
  transform-origin: 0 50%; transform: scaleX(0);
  background: linear-gradient(90deg, var(--jade-deep), var(--jade-text));
  z-index: 95;
}

/* --- Hero ---------------------------------------------------------------- */

.hero { position: relative; height: 100svh; min-height: 620px; overflow: hidden; }
.hero__media {
  position: absolute; inset: 0;
  /* Held slightly larger than the frame so the parallax drift never exposes
     an edge. */
  scale: 1.12;
}
.hero__media video, .hero__media img {
  width: 100%; height: 100%; object-fit: cover;
}
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(100deg, rgba(var(--scrim),0.95) 0%, rgba(var(--scrim),0.86) 28%,
                    rgba(var(--scrim),0.45) 54%, rgba(var(--scrim),0.18) 74%),
    linear-gradient(to top, var(--ink) 1%, transparent 30%);
}
.hero__inner {
  position: relative; z-index: 2; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding-bottom: clamp(3rem, 9vh, 6rem);
}
.hero h1 {
  font-size: var(--t-h1);
  letter-spacing: -0.025em;
  max-width: 15ch;
  text-wrap: balance;
}
/* --jade is the rule-and-mark green and fell to 2.37:1 here on the dark
   theme — over a video, on the largest word on the site. --jade-text is
   the per-theme green that is safe carrying words; the same mistake is
   called out on .brand small above. */
.hero h1 em { font-style: normal; color: var(--jade-text); }
.hero__zh {
  font-family: var(--serif);
  font-size: clamp(1.1rem, 2.4vw, 1.9rem);
  letter-spacing: 0.34em;
  color: var(--ivory-dim);
  margin-bottom: 1.6rem;
}
.hero__meta {
  display: flex; gap: 2.4rem; flex-wrap: wrap; align-items: center;
  margin-top: 2.4rem;
}

/* The fill is a dark jade plate in all three themes, so the label is always the
   paper colour. It used to be `var(--ink)`, which is only paper in the LIGHT
   theme — in dark and green `--ink` IS the ground, so the primary button on
   nearly every page rendered dark-on-dark: 1.66:1 in dark, 1.15:1 in green.
   Hover gets its own token because `--jade` is light enough in the green theme
   (#7FA894) that paper text on it drops to 2.39:1. */
.cta {
  display: inline-flex; align-items: center; gap: 0.7rem;
  padding: 1rem 1.9rem;
  background: var(--jade-deep); color: var(--on-photo);
  font-size: var(--t-small); font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  text-decoration: none; border: 0;
  transition: background 0.4s var(--ease), transform 0.4s var(--ease);
}
.cta:hover { background: var(--cta-hover); transform: translateY(-2px); }
.cta:focus-visible { outline: 2px solid var(--ivory); outline-offset: 4px; }
.cta--ghost {
  background: transparent; color: var(--ivory);
  border: 1px solid var(--edge);
}
.cta--ghost:hover { background: var(--jade-deep); color: var(--ink); }

.scroll-hint {
  position: absolute; bottom: 1.6rem; right: var(--gut); z-index: 3;
  display: flex; align-items: center; gap: 0.8rem;
  font-size: var(--t-micro); letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--ivory-faint);
}
.scroll-hint i {
  display: block; width: 1px; height: 46px; background: var(--ivory-faint);
  position: relative; overflow: hidden;
}
.scroll-hint i::after {
  content: ""; position: absolute; inset: 0; background: var(--jade);
  animation: drip 2.4s var(--ease-in-out) infinite;
}
@keyframes drip {
  0%   { transform: translateY(-100%); }
  60%  { transform: translateY(100%); }
  100% { transform: translateY(100%); }
}

/* --- Mood entry ---------------------------------------------------------- */

.moods { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2.2rem; }
.mood {
  appearance: none; cursor: pointer;
  padding: 0.85rem 1.5rem;
  background: transparent; color: var(--ivory-dim);
  border: 1px solid var(--edge); border-radius: 999px;
  font: 500 var(--t-small)/1 var(--sans); letter-spacing: 0.08em;
  transition: color 0.35s var(--ease), border-color 0.35s var(--ease),
              background 0.35s var(--ease), transform 0.35s var(--ease);
}
.mood .zh { margin-right: 0.5rem; color: var(--ivory); }
.mood:hover, .mood[aria-pressed="true"] {
  color: var(--ink); background: var(--jade-deep);
  border-color: var(--jade-deep); transform: translateY(-2px);
}
.mood:hover .zh, .mood[aria-pressed="true"] .zh { color: var(--ink); }
.mood:focus-visible { outline: 2px solid var(--jade); outline-offset: 3px; }

/* --- Pinned horizontal gallery ------------------------------------------- */

.pin { position: relative; }
.pin__stage {
  position: sticky; top: 0; height: 100svh;
  display: flex; align-items: center; overflow: hidden;
  /* The track's horizontal run is a scroll mechanic, not reading order. Left
     to itself under dir=rtl it lays out right-to-left, which pushed the
     document's scroll width from 1.5k to 11.4k pixels and parked the viewport
     on blank canvas — the whole page rendered as one flat colour in Arabic. */
  direction: ltr;
}
.pin__track {
  display: flex; gap: clamp(1rem, 2.4vw, 2.4rem);
  padding-inline: var(--gut);
  will-change: transform;
}
.plate {
  position: relative; flex: 0 0 auto;
  /* Sized from the stage, not from the page width: the pinned stage is a full
     viewport tall, so plates keyed to viewport height fill it instead of
     floating in the middle of it. */
  height: min(74svh, 680px);
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--charcoal);
}
.plate img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 1.2s var(--ease), filter 1.2s var(--ease);
  filter: saturate(0.92) brightness(1);
}
.plate:hover img { transform: scale(1.06); filter: saturate(1.05) brightness(1.04); }
.plate figcaption {
  direction: inherit;   /* captions still follow the reader's language */
  position: absolute; inset: auto 0 0 0; padding: 1.4rem;
  background: linear-gradient(transparent, rgba(20,20,18,0.88)); color: var(--on-photo);
}
.plate .t { font-family: var(--serif); font-size: 1.2rem; display: block; }
.plate .s { font-size: var(--t-small); color: rgba(255,255,255,0.80); }

/* --- Statement ----------------------------------------------------------- */

.statement { text-align: center; }
/* Scoped to the statement line, NOT every <p> in the section. As `.statement p`
   it also caught the section number, the eyebrow label and the closing lede and
   set all four to 54px: the section had no hierarchy at all, and the 11px
   eyebrow rendered as a two-line heading competing with the statement itself.
   Affected the home, tea-culture and tea-ritual closers alike. */
.statement p[data-lines] {
  font-family: var(--serif);
  font-size: clamp(1.6rem, 4.2vw, 3.4rem);
  line-height: 1.32; letter-spacing: -0.01em;
  max-width: 22ch; margin-inline: auto; text-wrap: balance;
}
.statement .fade { color: var(--ivory-faint); transition: color 0.6s var(--ease); }
.statement .fade.lit { color: var(--ivory); }

/* --- Steps (AI test / ritual) -------------------------------------------- */

.steps { display: grid; gap: clamp(1.6rem, 3vw, 3rem); margin-top: 3rem; }
.steps--4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.steps--6 { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.step {
  padding: 1.4rem 0 0;
  border-top: 1px solid var(--hairline);
  transition: border-color 0.5s var(--ease);
}
.step:hover { border-color: var(--jade); }
.step .n {
  font-family: var(--serif); font-size: 1.9rem; color: var(--jade);
  line-height: 1; display: block; margin-bottom: 0.7rem;
  transition: color 0.5s var(--ease);
}
.step:hover .n { color: var(--jade-deep); }
.step h4 { font-family: var(--serif); font-size: 1.2rem; font-weight: 500; }
.step p { font-size: var(--t-small); color: var(--ivory-dim); margin-top: 0.5rem; }

/* --- Timeline ------------------------------------------------------------ */

/* Full-bleed on purpose (the rule spans the page), but the cards inside still
   have to line up with the heading above them. Without the inline padding the
   first era sat hard against x=0 while every other element started at the
   gutter, so it read as clipped — and the last era had no trailing room. */
.timeline { display: flex; gap: 0; overflow-x: auto; scroll-snap-type: x mandatory;
            border-top: 1px solid var(--hairline); margin-top: 3rem;
            padding-inline: var(--gut); scroll-padding-inline-start: var(--gut); }
.era {
  flex: 0 0 clamp(220px, 26vw, 320px); scroll-snap-align: start;
  padding: 2rem 2rem 2.4rem 0;
  transition: opacity 0.5s var(--ease);
}
.era:hover { opacity: 1; }
.era + .era { padding-left: 2rem; border-left: 1px solid var(--hairline); }
.era .y { font-size: var(--t-micro); letter-spacing: 0.24em; color: var(--jade-text); }
.era h4 { font-family: var(--serif); font-size: 1.5rem; margin: 0.7rem 0 0.5rem; font-weight: 500; }
.era p { font-size: var(--t-small); color: var(--ivory-dim); }

/* --- Tea atlas ----------------------------------------------------------- */

/* Flex, not grid. Seven is an awkward count: auto-fit at 260px gives four
   columns at desktop, so the second row held three cards and left a dead
   column the width of a card. Flex lets the final row grow to fill instead.
   The basis is 300px rather than 260px because 260 packed five onto the first
   row and left the last two to grow to 661px each — a 2.5x size jump between
   siblings. At 300 the rows split 4 + 3 and the widths land 329 / 440. */
.atlas { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 3rem; }
.tea {
  position: relative; background: var(--ink); overflow: hidden;
  flex: 1 1 300px; min-height: 340px;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 1.8rem; text-decoration: none; color: inherit;
}
.tea__bg { position: absolute; inset: 0; z-index: 0; }
.tea__bg img {
  width: 100%; height: 100%; object-fit: cover;
  filter: saturate(0.95) brightness(0.62);
  transition: transform 1.3s var(--ease), filter 1.3s var(--ease);
}
.tea:hover .tea__bg img { transform: scale(1.08); filter: saturate(1.05) brightness(0.86); }
/* Categories with no honest photograph get a tonal field instead of a
   mislabelled one. The swatch is the category's liquor colour. */
/* Used where no photograph of that specific tea exists (High Mountain Oolong,
   Lapsang Souchong, Uji Matcha). Borrowing another tea's leaf shot would be a
   false product claim, so the panel carries the tea's LIQUOR COLOUR instead —
   but a flat fill read as a failed image load next to five real photographs.
   The radial lift and vignette make it legible as brewed liquor seen from
   above, i.e. an intentional colour study. */
.tea__bg--tone {
  background:
    radial-gradient(ellipse 78% 70% at 50% 42%,
      rgba(255,255,255,0.16), rgba(255,255,255,0.05) 46%, rgba(0,0,0,0) 72%),
    radial-gradient(ellipse 120% 110% at 50% 50%,
      rgba(0,0,0,0) 52%, rgba(0,0,0,0.30) 100%),
    var(--tone, var(--jade-deep));
}
/* A hairline ring reads as the rim of the bowl and keeps the panel from
   dissolving into the page on the dark theme. */
.tea__bg--tone::after {
  content: ""; position: absolute; inset: 0;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.10);
}
/* The aria-label already tells screen-reader users these panels stand in for a
   photograph the brand does not hold. This is the same disclosure for sighted
   users — without it the plate just looks like an image that failed. */
.tone-note {
  position: absolute; z-index: 2; inset-block-end: 1.4rem; inset-inline-start: 1.6rem;
  margin: 0; font-size: var(--t-micro); letter-spacing: 0.22em; text-transform: uppercase;
  color: rgba(255,255,255,0.82);
}
.tone-note .zh { letter-spacing: 0.1em; }
/* The scrim used to start fully clear at 26%, which left the middle of the card
   nearly bare — over the brightest leaf photographs the 13px category line fell
   to ~3.5:1. Carrying a light wash from the top instead holds every card past
   4.5:1 without flattening the picture. */
.tea::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(rgba(20,20,18,0.20), rgba(20,20,18,0.62) 46%, rgba(20,20,18,0.93));
}
.tea > *:not(.tea__bg) { position: relative; z-index: 2; }
/* The card sits on a photograph under a dark scrim, so its text is on-photo by
   nature — .en and .d below already say so explicitly. .cat did not, so it
   inherited --ivory, which is #1C1C1C in the light theme: the largest word on
   every atlas card (綠茶, 白茶, 黃茶 …) measured 1.00-1.21:1 against its own
   backdrop and was effectively invisible. */
.tea .cat { font-family: var(--serif); font-size: 1.9rem; display: block;
            color: var(--on-photo); }
.tea .en { font-size: var(--t-small); letter-spacing: 0.2em; text-transform: uppercase;
           color: #A9C6B2; display: block; margin-top: 0.3rem; }
.tea .d { font-size: var(--t-small); color: rgba(247,245,239,0.8); margin-top: 0.8rem; }
.tea .swatch {
  position: absolute; top: 1.6rem; right: 1.6rem; z-index: 2;
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--ivory-faint);
}
.tea:focus-visible { outline: 2px solid var(--jade); outline-offset: -4px; }

/* --- Split media --------------------------------------------------------- */

.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 6vw, 6rem);
         align-items: center; }
/* Width must stay authoritative here. With `align-self: stretch` the height came
   from the grid row, and `aspect-ratio` then derived the WIDTH from it — a tall
   text column produced an 850px panel in a 690px track, overflowing by 173px.
   `html { overflow-x: clip }` hid the scrollbar, so the art was just silently
   cropped instead. Centre the panel and pin width to the track. */
.split__media { align-self: center; width: 100%; min-width: 0;
                position: relative; isolation: isolate; aspect-ratio: 5/4; overflow: hidden;
                background: var(--charcoal); max-height: 68vh; }
.split__media video, .split__media img { width: 100%; height: 100%; object-fit: cover; }

/* --- Text sitting on full-bleed media ------------------------------------ */

/* A stage whose background is photography or video is dark in EVERY theme (the
   media itself carries a brightness filter), so the page tokens are wrong there
   by definition: in the light theme `--ivory` is #1C1C1C, which put the fields
   interlude heading at 2.07:1 against its own backdrop — 1.14:1 over the darker
   frames. Re-point the tokens the way `.tier--lead` does so descendants inherit
   correct-on-dark values, and lay a scrim under them so contrast does not
   depend on which frame of the video happens to be showing. */
.on-media {
  --ivory: #F7F5EF;
  --ivory-dim: rgba(255,255,255,0.88);
  --ivory-faint: rgba(255,255,255,0.74);
  --jade-text: #B7D0BE;
  --hairline: rgba(255,255,255,0.26);
  color: var(--ivory);
}
.on-media::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(ellipse 70% 60% at 50% 50%, rgba(10,12,10,0.62), rgba(10,12,10,0.34) 62%, rgba(10,12,10,0.30) 100%),
    linear-gradient(rgba(10,12,10,0.30), rgba(10,12,10,0.16) 40%, rgba(10,12,10,0.40));
}

/* --- Tiers --------------------------------------------------------------- */

.tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%,260px), 1fr));
         gap: clamp(1.5rem, 2.6vw, 2.6rem); margin-top: 3rem; }
/* Only the lead tier is a filled plate; the others are open columns, so the
   eye is drawn to one card instead of arbitrating between three equal boxes. */
.tier { padding: 2rem 0 0; border-top: 2px solid var(--hairline);
        display: flex; flex-direction: column; }
.tier--lead { padding: 2.4rem 2rem; border-top: 0; }
.tier--lead {
  background: var(--jade-deep);
  /* Re-point the tokens rather than override each property: anything nested
     here inherits correct-on-dark values automatically. */
  --ivory: #F7F5EF;
  --ivory-dim: rgba(255,255,255,0.86);
  --ivory-faint: rgba(255,255,255,0.72);
  --jade-text: #A9C6B2;
  --hairline: rgba(255,255,255,0.24);
  color: var(--ivory);
}
.tier--lead .cta { background: var(--on-photo); color: var(--jade-deep); }
.tier--lead .cta:hover { background: #A9C6B2; }
.tier h4 { font-family: var(--serif); font-size: 1.5rem; font-weight: 500; }
.tier .price { font-family: var(--serif); font-size: 2.6rem; margin: 1rem 0 0.2rem;
                line-height: 1.05; }
/* Where no price is confirmed, the slot carries a note, not a number — so it
   takes body size and normal wrapping instead of display size. */
.tier .price--tbc {
  font-family: var(--sans); font-size: var(--t-body); font-style: italic;
  color: var(--ivory-dim); letter-spacing: 0.01em; max-width: 22ch;
}
.tier .per { font-size: var(--t-small); color: var(--ivory-dim); }
.tier ul { list-style: none; margin: 1.6rem 0 2rem; display: grid; gap: 0.6rem; }
.tier li { font-size: var(--t-small); color: var(--ivory-dim); padding-left: 1.2rem; position: relative; }
.tier li::before { content: "—"; position: absolute; left: 0; color: var(--jade-text); }
.tier .cta { margin-top: auto; justify-content: center; }

/* --- Journal ------------------------------------------------------------- */

.posts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%,280px), 1fr));
         gap: clamp(1.5rem, 3vw, 2.5rem); margin-top: 3rem; }
.post { text-decoration: none; color: inherit; display: block; }
.post__img { aspect-ratio: 4/3; overflow: hidden; background: var(--charcoal); }
.post__img img { width: 100%; height: 100%; object-fit: cover;
                 filter: saturate(0.92) brightness(1);
                 transition: transform 1.2s var(--ease), filter 1.2s var(--ease); }
.post:hover .post__img img { transform: scale(1.05); filter: saturate(1.05) brightness(1.03); }
.post h4 { font-family: var(--serif); font-size: 1.3rem; font-weight: 500; margin-top: 1.1rem; }
.post .m { font-size: var(--t-micro); letter-spacing: 0.2em; text-transform: uppercase;
           color: var(--jade-text); margin-top: 0.6rem; display: block; }

/* --- Footer -------------------------------------------------------------- */

.foot { border-top: 1px solid var(--hairline); padding: clamp(3rem,7vh,5rem) 0 2rem; }
.foot__grid { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr);
              gap: clamp(2rem, 5vw, 4rem); }
.foot h5 { font-size: var(--t-micro); letter-spacing: 0.28em; text-transform: uppercase;
           color: var(--jade-text); margin-bottom: 1.1rem; font-weight: 600; }
.foot ul { list-style: none; display: grid; gap: 0.55rem; }
.foot a { color: var(--ivory-dim); text-decoration: none; font-size: var(--t-small);
          transition: color 0.35s var(--ease); }
.foot a:hover { color: var(--ivory); }
/* Legal sits above the copyright line, not inside the four content columns —
   it is site apparatus rather than something to browse. */
.foot__legal { display: flex; gap: 1.6rem; flex-wrap: wrap; margin-top: 3.5rem;
               padding-top: 1.5rem; border-top: 1px solid var(--hairline);
               font-size: var(--t-micro); letter-spacing: 0.1em; }
.foot__base { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
              margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--hairline);
              font-size: var(--t-micro); color: var(--ivory-faint); letter-spacing: 0.1em; }

/* --- Responsive ---------------------------------------------------------- */

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; }
  .foot__grid { grid-template-columns: 1fr 1fr; }
  .nav-links {
    /* Was display:none, which left all seven sections unreachable on a phone.
       A scroller keeps every link focusable and tabbable instead. */
    margin-left: 0; width: 100%; gap: 1.1rem;
    overflow-x: auto; flex-wrap: nowrap;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .nav-links::-webkit-scrollbar { display: none; }
  .nav { flex-wrap: wrap; gap: .7rem; padding-block: .8rem; }
  .nav-links a { white-space: nowrap; }
  .hero h1 { max-width: 100%; }
}

/* --- Motion preference ---------------------------------------------------
   The whole premise here is scroll motion, so reduced-motion does not simply
   disable it and leave a broken page: reveals resolve to their final state,
   the pinned track lays out as a normal scroller, and video stops autoplaying.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  html.js [data-reveal] { opacity: 1; transform: none; }
  .pin { height: auto !important; }
  .pin__stage { position: static; height: auto; padding: 2rem 0; }
  .pin__track { transform: none !important; overflow-x: auto; scroll-snap-type: x mandatory; }
  .plate { scroll-snap-align: center; }
  .hero__media { scale: 1; }
}


/* --- Theme and language controls ----------------------------------------- */

.prefs { display: flex; align-items: center; gap: 0.5rem; margin-left: 1.4rem; }
.prefs__set { display: flex; border: 1px solid var(--edge); border-radius: 999px;
              overflow: hidden; background: rgba(var(--scrim), 0.92);
              backdrop-filter: blur(8px); }   /* same band, same reason as .motion-toggle */
.prefs button {
  appearance: none; cursor: pointer; background: transparent; color: var(--ivory-dim);
  border: 0; padding: 0.42rem 0.72rem; font: 600 var(--t-micro)/1 var(--sans);
  letter-spacing: 0.12em; text-transform: uppercase;
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}
/* Scoped to the segmented theme control. As `.prefs button + button` it also
   matched the motion toggle, which follows the sound toggle — and since
   `.prefs button { border: 0 }` outranks `.motion-toggle`'s own pill border,
   the result was a lone left border rendered against border-radius: 999px: a
   stray crescent floating beside the words, on every page. */
.prefs__set button + button { border-left: 1px solid var(--edge); }
/* Same specificity problem, other half: give the pills their outline back. */
.prefs .motion-toggle { border: 1px solid var(--edge); }
.prefs .motion-toggle:hover { border-color: var(--jade-text); }
.prefs .motion-toggle[aria-pressed="true"] { border-color: var(--jade-text); }
.prefs button:hover { color: var(--ivory); }
.prefs button[aria-checked="true"] { background: var(--jade-deep); color: var(--on-photo); }
.prefs button:focus-visible { outline: 2px solid var(--jade-text); outline-offset: 2px; }

/* Language: bilingual by default. "EN" hides the Chinese, "中" hides the
   English where a paired English string exists. */
/* English hides the paired Chinese; any other locale keeps it visible.
 *
 * !important, deliberately, and this is the case it exists for: an invariant
 * that no page may locally override. This rule's specificity is only (0,2,1),
 * and every page's <style> block is parsed AFTER this file, so any page-local
 * rule that touches display on a .zh element — `.method thead th .zh`,
 * `.item h3 .zh`, `.tt-fields dd .zh` — either outranks it or ties and wins on
 * source order. Seventeen such rules existed across seven pages, several of
 * them predating the August 2026 work, and each one silently put Chinese back
 * on screen for a reader who had explicitly chosen English.
 *
 * Fixing them individually would have fixed today's seventeen and none of the
 * ones written next week: every new device that sets `.zh { display: block }`
 * to stack a translation under its English reintroduces it, and the failure is
 * invisible unless someone happens to switch to English on that exact page.
 *
 * Safe against the two things that must stay visible in English: the wordmark
 * 茶禪 is a <small> inside .brand, not a .zh, because a logotype does not
 * translate; and the language menu's own options carry lang="" attributes
 * rather than the class, so a reader in English can still find 繁體中文.
 */
html[data-lang="en"] .zh { display: none !important; }
@media (max-width: 860px) { .prefs { margin-left: auto; } }

/* --- Language dropdown ---------------------------------------------------- */

.lang { display: flex; align-items: center; gap: 0.5rem; }
.lang__label {
  font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ivory-dim);
}
.lang__select {
  appearance: none; cursor: pointer;
  background: transparent; color: var(--ivory);
  border: 1px solid var(--edge); border-radius: 999px;
  padding: 0.42rem 2rem 0.42rem 0.85rem;
  font: 600 var(--t-micro)/1.4 var(--sans); letter-spacing: 0.06em;
  /* Chevron drawn inline so no network request and no icon font. */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 0.95rem center, right 0.7rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.lang__select:hover { border-color: var(--jade-text); }
.lang__select:focus-visible { outline: 2px solid var(--jade-text); outline-offset: 2px; }
/* The native menu paints in the OS palette, so options need explicit colours. */
.lang__select option { background: var(--ink); color: var(--ivory); }

/* Right-to-left: Arabic. Chevron and padding must mirror or the control reads
   broken; the rest of the layout uses logical spacing already. */
[dir="rtl"] .lang__select {
  padding: 0.42rem 0.85rem 0.42rem 2rem;
  background-position: left 0.95rem center, left 0.7rem center;
}
[dir="rtl"] .nav { flex-direction: row-reverse; }
[dir="rtl"] .nav-links { margin-left: 0; margin-right: auto; }
[dir="rtl"] .prefs { margin-left: 0; margin-right: 1.4rem; }
[dir="rtl"] .tier li { padding-left: 0; padding-right: 1.2rem; }
[dir="rtl"] .tier li::before { left: auto; right: 0; }
[dir="rtl"] .step { text-align: right; }

/* Honest notice when a language has interface strings but no translated copy. */
.lang-note {
  margin: 0; padding: 0.9rem var(--gut);
  background: var(--jade-deep); color: var(--on-photo);
  font-size: var(--t-small); text-align: center;
}

/* Mood section media: the crackle-glaze cup, held to its own column so it
   cannot repeat the hero's escape. */
#mood { position: relative; overflow: hidden; }
.mood-media {
  position: absolute; inset: 0 0 0 auto; width: min(46%, 620px);
  overflow: hidden; pointer-events: none;
}
.mood-media video { width: 100%; height: 100%; object-fit: cover; }
.mood-media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--ink) 0%, rgba(var(--scrim),0.72) 34%,
                              rgba(var(--scrim),0.30) 72%, rgba(var(--scrim),0.15) 100%);
}
#mood .shell { position: relative; z-index: 1; }
@media (max-width: 860px) { .mood-media { display: none; } }

/* --- Motion switch -------------------------------------------------------- */

.motion-toggle {
  appearance: none; cursor: pointer;
  /* Not transparent. The nav's background is a gradient that has faded almost
     to nothing by this button's row, and the hero's scrim runs horizontally —
     it protects the headline on the left and stops well short of the controls
     on the right. These pills were therefore reading against raw video:
     3.77:1 in green and 3.83:1 in dark, against a 4.5:1 requirement. A chip of
     the page's own ground gives them a floor that does not depend on what the
     video is doing in any given frame.

     0.92, not 0.62. A 0.62 chip measured 5.66:1 against the frame that
     happened to be on screen and 2.46:1 against a white one — the whole point
     is that the ground is a moving picture, so the alpha has to hold against
     BOTH extremes. Worked out against pure white and pure black behind each
     of the three themes: 0.62 fails four of six, 0.80 fails three, 0.92 passes
     all six with 5.20:1 as the worst case. */
  background: rgba(var(--scrim), 0.92); backdrop-filter: blur(8px);
  color: var(--ivory-dim);
  border: 1px solid var(--edge); border-radius: 999px;
  padding: 0.42rem 0.85rem; font: 600 var(--t-micro)/1.4 var(--sans);
  letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.motion-toggle:hover { color: var(--ivory); border-color: var(--jade-text); }
.motion-toggle[aria-pressed="true"] { color: var(--jade-text); border-color: var(--jade-text); }
.motion-toggle:focus-visible { outline: 2px solid var(--jade-text); outline-offset: 2px; }

/* Honour the in-page pause exactly like the OS preference. */
html[data-motion="paused"] .scroll-hint i::after { animation: none; }
html[data-motion="paused"] [data-reveal] {
  opacity: 1 !important; transform: none !important;
  /* The wipe hides its element with clip-path, not opacity, so the two
     resets above would have left it clipped to nothing. */
  clip-path: none !important; transition-delay: 0s !important;
}

/* Focus visibility was inconsistent: .cta/.mood/.tea/.nav-links had rules,
   these did not, leaving the UA ring as the only indicator. */
.post:focus-visible, .brand:focus-visible, .foot a:focus-visible,
.timeline:focus-visible, .plate a:focus-visible {
  outline: 2px solid var(--jade-text); outline-offset: 3px;
}


/* Interior pages open directly against the fixed nav, so their first band must
   clear whatever height it currently is. The home page is exempt: its hero is
   full-bleed and deliberately runs underneath. */
body:not([data-page="home"]) main > :first-child,
body:not([data-page="home"]) main > .band:first-of-type {
  padding-top: calc(var(--nav-h) + clamp(1.5rem, 4vh, 3rem));
}
.nav { row-gap: 0.5rem; }

/* The nav carries eight links plus four preference controls. Below ~1200px it
   wrapped to three rows (215px tall), which no amount of page padding makes
   pleasant, so the links took their own row.

   That row used to be a horizontal scroller. Once the eight flat links became
   six grouped ones the scroller became actively harmful: nothing signalled the
   overflow, so at 390px the bar simply ended mid-word — "Shop · Gif…" — and
   Gifting and About were unreachable. Six short labels wrap to two rows at
   worst, and wrapping is also what lets a dropdown panel escape the row
   instead of being clipped inside it. */
/* Above the breakpoint the links sit inline beside the brand, and they have to
   move as ONE block. `.nav-links` carries flex-wrap for the narrow layout, so
   when the bar ran short the row shrank and fragmented internally instead of
   breaking cleanly — a language whose labels run longer than English got a
   three-row 180px header (Greek at 1421-1440) or a two-row one where English
   fitted (Vietnamese, Italian, 1421-1600). max-content refuses the shrink, so
   the bar breaks between blocks and is never more than two rows in any of the
   ten languages. Tested: 10 languages x 14 widths, on a page carrying the cart
   button — the widest the bar ever gets. */
@media (min-width: 1421px) {
  .nav-links { flex-wrap: nowrap; min-width: max-content; }
}
@media (max-width: 1420px) {
  .nav { flex-wrap: wrap; row-gap: 0.55rem; padding-block: 0.75rem; }
  .nav-links {
    order: 3; width: 100%; margin-left: 0;
    gap: 1.15rem; row-gap: 0.55rem;
    overflow: visible; flex-wrap: wrap;
  }
  .nav-links a, .nav-group__t { white-space: nowrap; }
  .prefs { margin-left: auto; }
  .nav-group__p { inset-inline-start: -0.9rem; }
}

/* Below this width the links and the theme / sound / motion / language controls
   together ran the fixed header to 424px — 50% of a 390px phone, permanently,
   with the hero video showing between the rows. They collapse into one drawer
   behind one Menu button instead.

   One disclosure, not two. An earlier pass gave the preferences their own
   Settings button and left the links loose in the bar; that is two places to
   look for "where do I go", and the loose links were the thing that ate the
   viewport. The drawer is absolutely positioned so opening it overlays the page
   rather than changing --nav-h and shoving the content down.

   `display: contents` above the breakpoint is what lets one wrapper exist at
   all: the links row and the preferences row stay direct flex children of .nav
   on desktop, so the desktop bar is unchanged by the wrapper. */
.nav-drawer { display: contents; }
.prefs-toggle { display: none; }
@media (max-width: 900px) {
  .prefs-toggle {
    display: block; margin-left: auto; cursor: pointer;
    background: transparent; color: var(--ivory-dim);
    border: 1px solid var(--edge); border-radius: 999px;
    padding: 0.42rem 0.95rem; font: 600 var(--t-micro)/1.4 var(--sans);
    letter-spacing: 0.14em; text-transform: uppercase;
  }
  .prefs-toggle[aria-expanded="true"] { background: var(--jade-deep); color: var(--on-photo); border-color: var(--jade-deep); }
  .prefs-toggle:focus-visible { outline: 2px solid var(--jade-text); outline-offset: 2px; }

  /* With the drawer open the bar above it must stop being a gradient over the
     hero video, or the brand and the Menu button float on footage while the
     menu right below them sits on paper — two grounds, one header. */
  .nav:has(.prefs-toggle[aria-expanded="true"]) {
    background: var(--panel); backdrop-filter: none;
  }
  .nav-drawer {
    display: block; position: absolute; inset-inline: 0; top: 100%;
    max-height: calc(100dvh - var(--nav-h)); overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0.25rem var(--gut) 1.15rem;
    background: var(--panel);
    border-bottom: 1px solid var(--hairline);
    box-shadow: 0 26px 60px -22px rgba(0,0,0,0.55);
  }

  /* One item per row, each a full-width target with a rule under it — a list to
     read down, not a paragraph of links to pick words out of. */
  .nav-links {
    order: 0; display: block; width: 100%; margin: 0; gap: 0;
    overflow: visible;
  }
  .nav-links > a, .nav-group { display: block; border-top: 1px solid var(--hairline); }
  .nav-links > a:first-child, .nav-group:first-child { border-top: 0; }
  .nav-links > a { padding: 0.9rem 0; font-size: var(--t-body); }
  .nav-group__t {
    width: 100%; justify-content: space-between;
    padding: 0.9rem 0; font-size: var(--t-body);
  }
  .nav-group[data-holds="true"] .nav-group__t::before { display: none; }
  .nav-group[data-holds="true"] .nav-group__t { color: var(--jade-text); }
  .nav-group__p {
    position: static; min-width: 0; padding: 0 0 0.55rem;
    background: transparent; border: 0; box-shadow: none;
    border-inline-start: 1px solid var(--edge); margin-inline-start: 0.15rem;
  }
  .nav-group__p a { padding: 0.5rem 0 0.5rem 1rem; font-size: var(--t-small); }
  .nav-group__p a:hover, .nav-group__p a:focus-visible { background: transparent; }
  .nav-group__p a[aria-current="page"] { box-shadow: none; color: var(--jade-text); }

  .prefs {
    margin: 1.1rem 0 0; padding: 1rem 0 0;
    flex-wrap: wrap; gap: 0.6rem; row-gap: 0.75rem;
    border-top: 1px solid var(--hairline);
  }
  /* 24px controls are fine in a desktop bar next to a mouse. In a thumb-driven
     drawer they are below the 44px target the platform guidelines ask for, and
     there is now room for the full size. */
  .prefs button, .prefs .lang__select { min-height: 44px; padding-inline: 0.95rem; }
}
@media (max-width: 720px) {
  .prefs { gap: 0.4rem; flex-wrap: wrap; justify-content: flex-start; }
}

/* The preference controls take 598px of the bar — 41% of a 1440 laptop — and
   the nav links get whatever is left. Restoring the pill outlines above cost
   4px, which was enough to shrink the links below their content and wrap
   "About" onto a second row, taking the header from 88px to 126px.
   The word LANGUAGE is the cheapest 90px in that row: the select beside it
   already reads "English" (or 繁體中文, or العربية) in the language itself, and
   it carries aria-label="Choose language" for anyone who cannot see that. */
@media (max-width: 1600px) {
  .lang__label { display: none; }
}


/* --- The dynasty scroll --------------------------------------------------
   Lived in tea-culture's page <style> until the home page needed it too.
   Two copies of a 27-rule component is how the two pages drift apart, so it
   moved here rather than being pasted a second time. Used by:
     tea-culture/  (full reading — body copy and keyword chips)
     index.html    (condensed — same panels, no chips)
   -------------------------------------------------------------------------- */
.dyn { flex: 0 0 auto; width: min(92vw, 1080px); height: min(78svh, 720px);
       display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1fr);
       gap: clamp(1.4rem, 3vw, 3rem); align-items: stretch;
       background: var(--charcoal); border: 1px solid var(--hairline);
       padding: clamp(1rem, 2vw, 1.6rem); }
.dyn__plate { position: relative; overflow: hidden; background: var(--ink); }
.dyn__plate img { width: 100%; height: 100%; object-fit: cover; }
.dyn__read { display: flex; flex-direction: column; justify-content: center;
             padding-inline-end: clamp(.4rem, 1.6vw, 1.6rem); min-width: 0; }
.dyn__seq { font-family: var(--serif); font-size: var(--t-micro); letter-spacing: .2em;
            color: var(--jade-text); font-variant-numeric: tabular-nums; }
.dyn__seq span { color: var(--ivory-faint); }
.dyn__years { font-family: var(--sans); font-size: var(--t-small); letter-spacing: .22em;
              color: var(--ivory-faint); font-variant-numeric: tabular-nums; margin-top: .9rem; }
/* The character carries the chapter, so it is set solid at display size —
   never a low-opacity wash behind the text. */
.dyn__glyph { font-family: var(--serif); font-size: clamp(3.6rem, 8vw, 6.4rem);
              line-height: .95; color: var(--ivory); margin: .2rem 0 0; font-weight: 500; }
.dyn__name { font-family: var(--serif); font-size: var(--t-h3); color: var(--jade-text);
             margin-top: .1rem; }
.dyn__spec { margin-top: 1.4rem; display: grid; gap: 0; }
.dyn__spec > div { border-top: 1px solid var(--hairline); padding: .7rem 0; }
.dyn__spec dt { font-size: var(--t-micro); letter-spacing: .24em; text-transform: uppercase;
                color: var(--jade-text); font-weight: 600; }
.dyn__spec dd { margin-top: .3rem; color: var(--ivory); font-size: var(--t-small); }
.dyn__spec dd .zh { color: var(--ivory-faint); }
.dyn__body { margin-top: 1.1rem; color: var(--ivory-dim); font-size: var(--t-small);
             line-height: 1.7; max-width: 54ch;
             /* The stage is a fixed height; long eras scroll inside their own
                column rather than pushing the panel out of the viewport. */
             overflow-y: auto; }
.dyn__body.zh { color: var(--ivory-faint); line-height: 1.9; margin-top: .5rem; }
.dyn__kw { list-style: none; display: flex; gap: .5rem; flex-wrap: wrap; margin-top: 1.1rem; }
.dyn__kw li { border: 1px solid var(--edge); padding: .3rem .7rem; font-size: var(--t-micro);
              letter-spacing: .14em; text-transform: uppercase; color: var(--ivory-faint); }
.dyn__kw .zh { color: var(--jade-text); letter-spacing: .06em; }

/* A fixed rail under the stage: the run is 618 to 1912, and the bar says how
   far through those thirteen centuries you currently are. */
.dyn__rail { position: absolute; inset-inline: var(--gut); bottom: clamp(1rem, 3vh, 2.2rem);
             display: flex; align-items: center; gap: 1rem;
             font-size: var(--t-micro); letter-spacing: .2em; color: var(--ivory-faint);
             font-variant-numeric: tabular-nums; }
.dyn__rail i { flex: 1 1 auto; height: 1px; background: var(--hairline); display: block; }
.dyn__rail b { display: block; height: 1px; background: var(--jade-text);
              width: calc(var(--pin-p, 0) * 100%); }

@media (max-width: 820px) {
  /* One column on a phone: the photograph becomes a band above the reading
     rather than a half-width panel too narrow to show anything. */
  .dyn { width: 86vw; height: min(82svh, 680px); grid-template-columns: 1fr;
         grid-template-rows: minmax(0, .48fr) minmax(0, 1fr); }
  .dyn__read { padding-inline-end: 0; justify-content: flex-start; }
  .dyn__glyph { font-size: clamp(3rem, 13vw, 4.2rem); }
  .dyn__body.zh { display: none; }
}


/* --- The six movements ----------------------------------------------------
   The ritual sequence, pinned. Narrower panels than the dynasty scroll: this
   is six steps read at a glance rather than four chapters read in full, so
   more than one is on screen at a time and the run has a rhythm to it.
   -------------------------------------------------------------------------- */
.ritescroll .pin__track { align-items: center; gap: clamp(1rem, 2vw, 1.8rem); }
.rite {
  flex: 0 0 auto; width: min(78vw, 460px); height: min(74svh, 660px);
  display: flex; flex-direction: column;
  background: var(--charcoal); border: 1px solid var(--hairline);
  padding: clamp(.7rem, 1.4vw, 1.1rem);
}
.rite__plate { position: relative; overflow: hidden; background: var(--ink); flex: 1 1 auto; min-height: 0; }
.rite__plate img { width: 100%; height: 100%; object-fit: cover; }
.rite__read { padding: 1.1rem .3rem .2rem; display: flex; align-items: baseline; gap: .9rem; flex-wrap: wrap; }
.rite__n { font-family: var(--serif); font-size: var(--t-lede); color: var(--jade-text); line-height: 1; }
/* The character is the step. Set solid at display size, never a watermark. */
.rite__glyph { font-family: var(--serif); font-size: clamp(1.9rem, 3.4vw, 2.6rem);
               line-height: 1; color: var(--ivory); font-weight: 500; margin: 0; }
.rite__name { font-size: var(--t-small); letter-spacing: .16em; text-transform: uppercase;
              color: var(--ivory-dim); }
.rite__rail { position: absolute; inset-inline: var(--gut); bottom: clamp(1rem, 3vh, 2rem);
              display: flex; align-items: center; gap: 1rem;
              font-family: var(--serif); font-size: var(--t-small); color: var(--ivory-faint); }
.rite__rail i { flex: 1 1 auto; height: 1px; background: var(--hairline); display: block; }
.rite__rail b { display: block; height: 1px; background: var(--jade-text);
                width: calc(var(--pin-p, 0) * 100%); }
@media (max-width: 620px) {
  .rite { width: 82vw; height: 68svh; }
}


/* --- Section plate --------------------------------------------------------
   A full-measure photograph closing a section that is otherwise unbroken
   prose. Used where a section ran past 550px with no image in it at all —
   the reader needs somewhere to rest, and the brand is a visual one.
   Deliberately wide and short: it punctuates rather than competes with the
   photography that opens a page.
   -------------------------------------------------------------------------- */
.band-plate {
  width: min(1440px, 100% - var(--gut) * 2); margin: clamp(2.4rem, 5vh, 4rem) auto 0;
  aspect-ratio: 16 / 7; overflow: hidden; position: relative; background: var(--charcoal);
}
.band-plate img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 720px) { .band-plate { aspect-ratio: 4 / 3; } }
