/* Hallmark · pre-emit critique: P5 H4 E5 S4 R5 V5
 *
 * Hallmark · genre: playful · macrostructure: Narrative Workflow
 * hero: H9 Custom Illustration Centerpiece (Tier B hand-built SVG kite)
 * nav: N12 banner + retract (banner content = status) · footer: Ft5 Statement
 * features: F3 tabular spec sheet · cta: booking form
 * theme: custom (tuned) · vibe: "crayon warmth, navy ink, kite rainbow"
 * paper: oklch(96.5% 0.012 85) · accent: oklch(64% 0.170 35)
 * display: Bricolage Grotesque · body: IBM Plex Sans
 * axes: light / geometric-sans / warm
 * studied: no · context: user-provided (audience/use/tone answered) · enrichment: Tier B SVG
 * motion: kite-draw · stage-reveal · hover-lift
 * nav: N12 · footer: Ft5 · slop: pass (42–45)
 * honest: pass (46) · chrome: pass (47) · tokens: pass (48) · responsive: pass (49) · icons: pass (30)
 *
 * ── audit 2026-08-02 ──────────────────────────────────────────────────────
 * contrast: pass (40–41), re-verified by computation. Three pairs were failing:
 *   the coral focus ring on the navy banner (1.95:1) now flips to paper (9.03:1);
 *   form-control borders moved off --color-rule (1.21:1 on paper-3) onto
 *   --color-rule-strong (3.17:1); --color-error deepened 52% → 49% (4.48 → 5.12:1).
 * mobile: pass (34, 49, 50–57), audited statically — NOT re-checked in a browser.
 *   The banner line was clipping its tel link on every phone width: a flex parent
 *   cannot ellipsize, so overflow:hidden hard-cut the highest-intent affordance on
 *   the page. The hours are now the shrinkable half and drop out below 34rem.
 *
 * ── perf pass 2026-08-03 ──────────────────────────────────────────────────
 * The page now loads with zero third-party requests. Four changes:
 *   fonts self-hosted (public/fonts/, @font-face below) — drops the
 *     googleapis + gstatic connections off the critical path, and drops the
 *     cyrillic/greek/latin-ext subsets Google shipped but this page never uses;
 *   tokens.css moved from @import here to its own <link> — the two stylesheets
 *     now download in parallel instead of nose-to-tail;
 *   public/_headers replaces the `max-age=0, must-revalidate` that Workers
 *     static assets send by default, so repeat visits actually hit cache;
 *   the PNG icons are palette-quantised (brand-mark 23.8 KB → 4.9 KB).
 * Nothing visual changed — same two faces, same weights, same tokens.
 *
 * ── perf pass 2 · fonts 2026-08-03 ────────────────────────────────────────
 * PageSpeed sat at 98. The whole 2.2-point loss was LCP (1.64), FCP (0.41) and
 * SI (0.18) — TBT and CLS already score a clean 100, so there was nothing to
 * win in JS or layout stability. LCP is .hero__title, text, `font-display:
 * swap`, which means LCP does not settle until the real font paints: the
 * 800ms gap between FCP and LCP was the font transfer.
 * The four woff2 went 145.3K → 51.6K (tools/subset-fonts.py, rationale in the
 * @font-face comment below) — roughly 480ms off Lighthouse's throttled link.
 * UNLIKE perf pass 1, this one DOES change rendering: pinning Bricolage's opsz
 * axis makes 16–18px display text ~3% narrower (40px+ unchanged, and narrower
 * never overflows). Filenames gained a -v2 suffix because /fonts/* is cached a
 * year immutable; ?v= cannot bust that, only a rename can.
 * Still outstanding: tokens.css + page.css are two render-blocking requests.
 * Inlining them (comment-stripped) puts the whole page in one 8.7K brotli
 * round trip and is what takes the score to 100 — deferred because it needs a
 * build step, which this project deliberately does not have.
 *
 * ── design review 2026-08-03 (live site) ──────────────────────────────────
 * Four defects found by reviewing the deployed page in a browser. The 2026-08-02
 * audit above was static, which is why three of these survived it.
 *   chrome: the banner's ✕ was a text glyph in a button with no `font: inherit`,
 *     so it rendered in the UA default (Arial) — and U+2715 is outside both
 *     shipped subsets anyway. It is an SVG path now, drawn in currentColor.
 *   controls: the <select> still had `appearance: auto` — native chevron, native
 *     interior — so it was the one control the browser drew rather than we did.
 *     Now `appearance: none` + a masked .select::after in --color-ink-2. Every
 *     `background:` shorthand on a field became `background-color:` to stop it
 *     resetting the mark.
 *   anchors: scroll-margin counted banner + bar, but the browser resolves it at
 *     jump time and the banner has always retracted by then — every in-page link
 *     landed a banner-height low. Now --bar-h only; see the rule below and the
 *     matching retract logic in page.js, which are a pair.
 *   dates: the DOB field was type="date", which renders in the *browser's*
 *     locale, not lang="vi" — a Vietnamese page was showing mm/dd/yyyy. It is a
 *     numeric text field parsed as ngày/tháng/năm in both page.js and the Worker.
 * The page's slot placeholders are untouched — still 14, still unfilled.
 */

/* tokens.css KHÔNG còn @import ở đây — nó được nạp bằng <link> trong <head>
   trước file này. `@import` bắt trình duyệt tải xong VÀ phân tích page.css rồi
   mới biết tới tokens.css, tức là hai request nối đuôi nhau trên critical path;
   hai thẻ <link> thì tải song song. */

/* ══════════════════════════════════════════════════════════
   Fonts · self-hosted
   ══════════════════════════════════════════════════════════

   Hai họ chữ này trước đây nạp từ fonts.googleapis.com. Tự host để bỏ hai kết
   nối cross-origin (DNS + TLS tới googleapis rồi tới gstatic) khỏi critical
   path — chữ hiện ra sớm hơn khoảng hai vòng round-trip.

   Chỉ giữ HAI subset: `latin` và `vietnamese`. Bản Google phục vụ IBM Plex Sans
   kèm cả cyrillic/cyrillic-ext/greek/latin-ext — trang này không dùng chữ nào
   trong đó. Đã quét toàn bộ index.html (kể cả phần ẩn): không có một ký tự nào
   rơi riêng vào latin-ext, nên bỏ hẳn hai file đó, đỡ 62 KB.

   Vì sao phải bỏ chứ không để đấy cho chắc: tiếng Việt dùng các dấu tổ hợp
   U+0304/U+0308/U+0329, mà Google xếp chúng vào CẢ ba dải latin/latin-ext/
   vietnamese. Hễ còn khai báo latin-ext là Chrome thấy trùng và tải luôn file
   đó dù không cần chữ nào trong ấy — đã đo thấy trên máy.

   Hệ quả cần biết khi thêm nội dung: ký tự ngoài hai dải này sẽ rơi về font hệ
   thống. Tiếng Việt, dấu câu, — … “ ” và ₫ đều nằm gọn trong latin+vietnamese
   nên không sao; hiện chỉ có ✕ (U+2715, nút đóng banner) là dùng font hệ thống,
   và nó vốn đã vậy từ trước. Nếu sau này thêm chữ Đông Âu hay ký hiệu lạ thì
   phải tải lại subset tương ứng.

   `unicode-range` giữ nguyên y hệt Google nên trình duyệt vẫn chỉ tải đúng
   subset nào chứa ký tự đang cần.

   Cả hai đều là variable font (đã kiểm bằng fonttools):
     Bricolage Grotesque — opsz 12..96, wght 200..800
     IBM Plex Sans       — wght 100..700
   Vì vậy MỘT file lo mọi độ đậm. Google khai báo IBM Plex thành ba @font-face
   400/500/600 trỏ chung một file; gộp lại thành một dải 400..600 cho gọn.

   Dải wght khai báo bám đúng mức đang dùng trong trang, không nới rộng hơn:
   mọi chỗ 700/800 đều là --font-display (Bricolage), --font-body chỉ chạy
   400/500/600.

   ── Bốn file đã được subset lại (hậu tố -v2) — 145.3K → 51.6K ──────────
   Font là thứ nặng nhất trên đường dựng trang: đo bằng đường mạng giả lập
   của Lighthouse (1.6 Mbps) thì 145K ngốn ~750ms, và vì `font-display: swap`
   nên LCP (tiêu đề hero) chỉ chốt khi font thật vẽ xong. Cắt 94K ≈ 480ms.

   Ba việc, làm bằng tools/subset-fonts.py (KHÔNG phải bước build — chỉ là
   công thức chạy tay, giống lệnh `magick` trong <head> của index.html):
     1. Ghim trục `opsz` của Bricolage ở 40. Trang không hề dùng
        font-optical-sizing hay font-variation-settings, mà riêng trục đó
        chiếm MỘT NỬA file (49.1K → 23.3K). Ghim ở 40 vì tiêu đề hero vẽ ở
        đó (--text-display sàn 2.5rem). Đã đo phần còn lại: chữ 16–18px
        (.nav__brand-name, .spec__name) hẹp đi ~3%, từ 40px trở lên không
        đổi. Hẹp đi thì chỉ dễ xuống dòng hơn chứ không tràn — nhưng đây
        VẪN là thay đổi hiển thị, cần nhớ nếu sau này design.md có luật về
        optical sizing. (Giữ trục mà bó hẹp dải: đã thử, chỉ còn 44.9K —
        không đáng, vì nặng là ở delta chứ không ở dải.)
     2. Bó `wght` về đúng dải đang dùng, khớp với khai báo dưới đây.
     3. Bỏ 80 ký tự khỏi hai subset `latin` mà trang không thể vẽ tới.

   *** BẪY: một phần tiếng Việt nằm trong Latin-1. ***
   à á â ã è é ê ì í ò ó ô õ ù ú ý là U+00C0–00FF. Subset `vietnamese` của
   Google CHỈ chứa phần thêm (ă đ ơ ư, U+1EA0–1EF9). Cắt U+0000-00FF để
   "dọn dấu tiếng Tây" là hỏng dấu toàn trang. Danh sách giữ lại nằm ở
   KEEP_LATIN1 trong script; chỉ những chữ thật sự không dùng mới bị bỏ
   (Ä Å Æ Ç Ñ Ö Ø Ü ß ä å æ ç ë ñ ö ø ü …).

   Sau mỗi lần chạy lại: `python3 tools/subset-fonts.py --check` phải in OK —
   nó soát mọi ký tự trong index.html cùng các chuỗi page.js/src/index.js
   chèn lúc chạy, đối chiếu với cmap thật của bốn file.

   Đổi TÊN FILE (-v2 → -v3 …) mỗi lần sinh lại, đừng ghi đè: public/_headers
   cho font cache 1 năm `immutable`, ghi đè byte tại chỗ thì khách cũ ôm bản
   cũ cả năm. Đây là chỗ ?v= KHÔNG cứu được.

   Tải lại khi Google lên phiên bản mới: lấy CSS ở fonts.googleapis.com với một
   User-Agent Chrome hiện đại (không có UA đó Google trả về woff cũ), hút các
   URL .woff2 của hai subset trên về một thư mục tạm, rồi chạy script trỏ vào
   thư mục đó.
   BẮT BUỘC giữ đủ subset `vietnamese` — thiếu nó là hỏng dấu ế/ữ toàn trang. */

@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-vietnamese-v2.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-latin-v2.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-vietnamese-v2.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-latin-v2.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ══════════════════════════════════════════════════════════
   Base
   ══════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }

html {
  overflow-x: clip;
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  overflow-x: clip;
  margin: 0;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: 1.62;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  padding-bottom: env(safe-area-inset-bottom);
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin: 0;
  overflow-wrap: anywhere;
  min-width: 0;
  text-wrap: balance;
}

p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a {
  color: inherit;
  text-decoration-color: var(--color-rule);
  text-underline-offset: 0.22em;
}
a:hover { text-decoration-color: var(--color-accent); }

img, svg { max-width: 100%; height: auto; display: block; }

:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
  /* never transitioned — the ring is instant */
}

::selection { background: var(--color-accent-soft); color: var(--color-ink); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* the skip link is the one visually-hidden element that must surface on focus */
.skip:focus-visible {
  position: fixed;
  inset-block-start: var(--space-sm);
  inset-inline-start: var(--space-sm);
  z-index: var(--z-toast);
  inline-size: auto; block-size: auto;
  margin: 0; padding: var(--space-sm) var(--space-md);
  overflow: visible; clip-path: none;
  background: var(--color-paper);
  border: var(--rule-bold) solid var(--color-navy);
  border-radius: var(--radius-sm);
  font-weight: 600;
  text-decoration: none;
}

/* Every in-page target clears the fixed bar it would otherwise hide under.
   --bar-h only, not banner + bar: the browser computes this offset at jump
   time, and the banner has always retracted by the time the jump lands, so
   counting it left every anchor sitting a banner-height too low. page.js
   guarantees the pairing — the banner is only ever on screen at scroll top,
   where no anchor jump starts. Change one and you must change the other. */
:target,
main[id],
section[id],
footer[id] {
  scroll-margin-block-start: calc(var(--bar-h) + var(--space-md));
}

.wrap {
  width: 100%;
  max-width: 76rem;
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}

.measure { max-width: var(--measure); }

.muted  { color: var(--color-muted); }
.ink-2  { color: var(--color-ink-2); }

/* Fill-in slot — every value the clinic still has to supply.
   Deliberately visible so nothing fake ships as if it were real. */
.slot {
  font-style: normal;
  color: var(--color-accent-deep);
  background: var(--color-accent-soft);
  border-bottom: var(--rule-bold) dashed var(--color-accent);
  padding-inline: 0.3em;
  border-radius: 4px 4px 0 0;
  overflow-wrap: break-word;
}

/* ══════════════════════════════════════════════════════════
   Buttons
   ══════════════════════════════════════════════════════════ */

.btn {
  --btn-y: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 48px;
  padding: var(--space-sm) var(--space-lg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  border: var(--rule-bold) solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  text-decoration: none;
  transform: translateY(var(--btn-y));
  transition:
    background-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    transform var(--dur-micro) var(--ease-out);
}

.btn--primary {
  background: var(--color-accent-deep);
  color: var(--color-accent-ink);
  border-color: var(--color-accent-deep);
}
.btn--ghost {
  background: transparent;
  color: var(--color-ink);
  border-color: var(--color-navy);
}

@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover { background: var(--color-accent); border-color: var(--color-accent); --btn-y: -1px; }
  .btn--ghost:hover   { background: var(--color-paper-2); }
}
.btn:active { --btn-y: 1px; }
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--color-paper-3);
  color: var(--color-muted);
  border-color: var(--color-rule);
  cursor: not-allowed;
  --btn-y: 0;
}
.btn[data-state="loading"] { color: transparent; position: relative; pointer-events: none; }
.btn[data-state="loading"]::after {
  content: "";
  position: absolute;
  inline-size: 18px; block-size: 18px;
  border: 2px solid var(--color-accent-ink);
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 720ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ══════════════════════════════════════════════════════════
   N12 · Banner + retracting nav
   ══════════════════════════════════════════════════════════ */

:root { --banner-h: 48px; --bar-h: 72px; }

.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: var(--z-sticky);
  transform: translateY(0);
  transition: transform var(--dur-long) var(--ease-out);
}
.nav.is-compact   { transform: translateY(calc(var(--banner-h) * -1)); }
.nav.is-dismissed { transform: none; }
.nav.is-dismissed .nav__banner { display: none; }

.nav__banner {
  block-size: var(--banner-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  padding-inline: var(--page-gutter);
  background: var(--color-navy);
  color: var(--color-paper);
  font-size: var(--text-sm);
}
.nav__banner-text {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  white-space: nowrap;
}
/* Only the hours shrink. text-overflow does nothing on a flex container, so the
   ellipsis lives on this child — which flex blockifies, so it actually elides. */
.nav__banner-hours {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav__banner-sep { flex: none; }
.nav__banner .slot {
  background: transparent;
  color: var(--color-paper);
  border-bottom-color: var(--color-ribbon-amber);
  white-space: nowrap;
}
/* The tel link is the highest-intent affordance on the page. It never shrinks,
   never wraps, and never clips — at any width. */
.nav__banner-tel { flex: none; font-weight: 600; text-decoration: none; }
/* --color-focus is only 1.95:1 on --color-navy; paper is 9.03:1. */
.nav__banner :focus-visible { outline-color: var(--color-paper); }
.nav__banner-x {
  display: grid;
  place-items: center;
  inline-size: 32px; block-size: 32px;
  flex: none;
  background: none;
  border: 0;
  border-radius: 50%;
  color: inherit;
  /* buttons don't inherit type — without this the UA default (Arial) wins */
  font: inherit;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out);
}
.nav__banner-x svg {
  inline-size: 14px;
  block-size: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
}
.nav__banner-x:hover  { background: var(--color-overlay-hi); }
.nav__banner-x:active { transform: scale(0.94); }

.nav__bar {
  block-size: var(--bar-h);
  background: var(--color-paper);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.nav__bar-inner {
  block-size: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}
.nav__brand {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  text-decoration: none;
  margin-inline-end: auto;
  min-width: 0;
}
.nav__brand img { inline-size: 48px; block-size: 48px; border-radius: 50%; flex: none; }
.nav__brand-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-base);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--color-navy);
}
.nav__brand-name span { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--color-muted); letter-spacing: 0.02em; }

.nav__links { display: none; gap: var(--space-lg); }
.nav__link {
  white-space: nowrap;
  font-size: var(--text-base);
  font-weight: 500;
  text-decoration: none;
  padding-block: var(--space-xs);
  border-bottom: var(--rule-bold) solid transparent;
  transition: border-color var(--dur-micro) var(--ease-out);
}
.nav__link:hover  { border-bottom-color: var(--color-accent); }
.nav__link:active { border-bottom-color: var(--color-accent-deep); }

/* Below 34rem the mark carries the brand on its own — the wordmark would
   push Menu + Đặt lịch off a 320 px screen. The banner sheds its hours for the
   same reason: the full line needs ~355 px and a 320 px screen offers ~236 px. */
@media (max-width: 33.99rem) {
  .nav__brand-name { display: none; }
  .nav__bar-inner { gap: var(--space-sm); }
  .nav__brand img { inline-size: 44px; block-size: 44px; }
  .nav__banner-hours,
  .nav__banner-sep { display: none; }
}

.nav__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-block-size: 44px;
  padding-inline: var(--space-sm);
  background: none;
  border: var(--rule-bold) solid var(--color-rule-strong);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out);
}
.nav__toggle:hover  { border-color: var(--color-navy); }
.nav__toggle:active { border-color: var(--color-accent-deep); }
.nav__sheet {
  display: grid;
  gap: var(--space-xs);
  padding: var(--space-md) var(--page-gutter) var(--space-lg);
  background: var(--color-paper);
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.nav__sheet[hidden] { display: none; }
.nav__sheet .nav__link { border-bottom: var(--rule-hair) solid var(--color-rule-2); padding-block: var(--space-sm); }

@media (min-width: 60rem) {
  .nav__links  { display: flex; }
  .nav__toggle { display: none; }
  .nav__sheet  { display: none !important; }
}

/* ══════════════════════════════════════════════════════════
   Hero · H9 custom illustration centerpiece
   ══════════════════════════════════════════════════════════ */

.hero {
  padding-block: calc(var(--banner-h) + var(--bar-h) + var(--space-xl)) var(--space-3xl);
  overflow: visible;
}
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: center;
}
.hero__kicker {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-muted);
  margin-block-end: var(--space-md);
}
.hero__title {
  font-size: var(--text-display);
  font-weight: 800;
  max-width: 15ch;
}
.hero__title u {
  text-decoration: none;
  background-image: linear-gradient(
    180deg,
    transparent 38%,
    var(--color-accent-soft) 38%,
    var(--color-accent-soft) 92%,
    transparent 92%);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.hero__lede {
  margin-block-start: var(--space-lg);
  max-width: 46ch;
  color: var(--color-ink-2);
  font-size: var(--text-md);
}
.hero__actions {
  margin-block-start: var(--space-xl);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}
.hero__note {
  margin-block-start: var(--space-md);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.hero__art { justify-self: center; inline-size: min(100%, 30rem); }

@media (min-width: 60rem) {
  .hero { padding-block: calc(var(--banner-h) + var(--bar-h) + var(--space-2xl)) var(--space-4xl); }
  .hero__grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: var(--space-2xl); }
  .hero__art { justify-self: end; }
}

/* ── The kite, drawn once on load ──────────────────────── */
.kite__stroke {
  fill: none;
  stroke: var(--color-navy);
  stroke-width: 5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.kite__face { fill: none; stroke: var(--color-navy); stroke-width: 4.5; stroke-linecap: round; }
.kite__panel { stroke: none; }
.kite__panel--a { fill: var(--color-ribbon-green); }
.kite__panel--b { fill: var(--color-ribbon-sky); }
.kite__panel--c { fill: var(--color-ribbon-amber); }
.kite__panel--d { fill: var(--color-ribbon-red); }
.kite__ribbon { fill: none; stroke-width: 11; stroke-linecap: round; }
.kite__ribbon--1 { stroke: var(--color-ribbon-red); }
.kite__ribbon--2 { stroke: var(--color-ribbon-amber); }
.kite__ribbon--3 { stroke: var(--color-ribbon-green); }
.kite__ribbon--4 { stroke: var(--color-ribbon-sky); }
.kite__ribbon--5 { stroke: var(--color-ribbon-plum); }
.kite__ground { fill: none; stroke: var(--color-ribbon-green); stroke-width: 4; stroke-linecap: round; }

.kite [data-draw] {
  stroke-dasharray: var(--len, 1200);
  stroke-dashoffset: var(--len, 1200);
  animation: draw 1100ms var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}
.kite [data-fade] {
  opacity: 0;
  animation: fade var(--dur-long) var(--ease-out) forwards;
  animation-delay: calc(420ms + var(--i, 0) * 70ms);
}
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes fade { to { opacity: 1; } }

/* ══════════════════════════════════════════════════════════
   Stages · Narrative Workflow (F4 step sequence)
   ══════════════════════════════════════════════════════════ */

.stages {
  padding-block: var(--space-3xl) var(--space-2xl);
  background: var(--color-paper-2);
  border-block: var(--rule-hair) solid var(--color-rule);
}
.stages__intro { margin-block-end: var(--space-2xl); }
.stages__intro h2 { font-size: var(--text-2xl); max-width: 22ch; }
.stages__intro p { margin-block-start: var(--space-md); color: var(--color-ink-2); max-width: 50ch; }

.stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
  padding-block: var(--space-xl);
  border-top: var(--rule-bold) solid var(--color-rule);
}
/* gate 54: numeral + heading share one column, stacked. Never side by side. */
.stage__head { display: block; }
.stage:first-of-type { border-top-width: 3px; border-top-color: var(--color-navy); }

.stage__num {
  font-family: var(--font-display);
  font-size: var(--text-numeral);
  font-weight: 800;
  line-height: 0.82;
  letter-spacing: -0.05em;
  color: var(--color-accent-deep);
  font-variant-numeric: tabular-nums;
}
.stage__title { font-size: var(--text-xl); margin-block-start: var(--space-xs); max-width: 22ch; }
.stage__body { margin-block-start: var(--space-md); color: var(--color-ink-2); max-width: 56ch; }
.stage__list { display: grid; gap: var(--space-xs); align-content: start; }
/* The marker is absolutely positioned, not a grid track: an inline <em> inside
   the <li> would become its own grid item and get flung into a 12 px column. */
.stage__list li {
  position: relative;
  padding-inline-start: var(--space-lg);
  font-size: var(--text-base);
  color: var(--color-ink-2);
}
.stage__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.62em;
  inline-size: var(--space-xs);
  block-size: var(--space-xs);
  border-radius: 2px;
  transform: rotate(45deg);
  background: var(--color-ribbon-sky);
}
.stage:nth-of-type(2) .stage__list li::before { background: var(--color-ribbon-amber); }
.stage:nth-of-type(3) .stage__list li::before { background: var(--color-ribbon-green); }
.stage:nth-of-type(4) .stage__list li::before { background: var(--color-ribbon-plum); }

@media (min-width: 60rem) {
  /* deliberately uneven halves — prose leads, the checklist sits narrower beside it */
  .stage { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: var(--space-2xl); padding-block: var(--space-2xl); }
  .stage__list { padding-block-start: var(--space-xl); }
}

/* one orchestrated scroll reveal — stages only, once.
   Scoped to .js so the stages are visible if the script never runs. */
.js .reveal { opacity: 0; transform: translateY(10px); }
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-long) var(--ease-out), transform var(--dur-long) var(--ease-out);
}

/* ══════════════════════════════════════════════════════════
   Services · F3 tabular spec sheet
   ══════════════════════════════════════════════════════════ */

.services { padding-block: var(--space-3xl) var(--space-2xl); }
.services__head { margin-block-end: var(--space-xl); }
.services__head h2 { font-size: var(--text-2xl); max-width: 20ch; }
.services__head p { margin-block-start: var(--space-md); color: var(--color-ink-2); max-width: 56ch; }

.spec { border-top: var(--rule-bold) solid var(--color-navy); }
.spec__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2xs) var(--space-xl);
  padding-block: var(--space-lg);
  border-bottom: var(--rule-hair) solid var(--color-rule);
  transition: background-color var(--dur-micro) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .spec__row:hover { background: var(--color-paper-2); }
}
.spec__name { font-family: var(--font-display); font-weight: 700; font-size: var(--text-md); letter-spacing: -0.015em; }
.spec__desc { color: var(--color-ink-2); font-size: var(--text-base); max-width: 58ch; }

@media (min-width: 48rem) {
  .spec__row { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); padding-block: var(--space-lg); }
}

/* ══════════════════════════════════════════════════════════
   Booking form
   ══════════════════════════════════════════════════════════ */

.book {
  padding-block: var(--space-3xl);
  background: var(--color-paper-3);
  border-block: var(--rule-hair) solid var(--color-rule);
}
.book__grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2xl); }
@media (min-width: 60rem) {
  .book__grid { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: var(--space-3xl); }
}
.book__aside h2 { font-size: var(--text-2xl); max-width: 16ch; }
.book__aside p { margin-block-start: var(--space-md); color: var(--color-ink-2); max-width: 48ch; }
.book__alt {
  margin-block-start: var(--space-xl);
  display: grid;
  gap: var(--space-sm);
  font-size: var(--text-base);
}
.book__alt dt { font-size: var(--text-sm); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-muted); }
.book__alt dd { margin: 0 0 var(--space-md); }

.form { display: grid; gap: var(--space-lg); }
/* Without JS the form cannot reach an endpoint, so the submit button ships
   disabled and this takes over. Never let a booking silently post to nowhere. */
.form__noscript {
  padding: var(--space-md);
  background: var(--color-paper);
  border: var(--rule-bold) solid var(--color-rule-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  color: var(--color-ink);
}
.form__pair { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-lg); }
@media (min-width: 40rem) { .form__pair { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }

.field { display: grid; gap: var(--space-2xs); }
.field label { font-size: var(--text-sm); font-weight: 600; letter-spacing: 0.02em; }
.field input,
.field select,
.field textarea {
  font: inherit;
  font-size: var(--text-base);
  color: var(--color-ink);
  /* background-color, never the shorthand: the select's mark is drawn by
     .select::after, and a `background:` here would reset it. Same below. */
  background-color: var(--color-paper);
  min-block-size: 48px;
  padding: var(--space-sm) var(--space-md);
  border: var(--rule-bold) solid var(--color-rule-strong);
  border-radius: var(--radius-sm);
  inline-size: 100%;
  transition: border-color var(--dur-micro) var(--ease-out), background-color var(--dur-micro) var(--ease-out);
}
.field textarea { min-block-size: 112px; resize: vertical; line-height: 1.5; }
/* The select was the one control the browser drew: `appearance: auto` kept the
   native chevron, so it read as a different family from every other field.
   The mark is a masked pseudo-element rather than a background-image data URI
   so its colour stays a token instead of a hex baked into the SVG. */
.field select {
  appearance: none;
  padding-inline-end: var(--space-xl);
}
/* The wrapper exists only to hang the mark off. It was a ::after on .field
   itself at first, which quietly claimed its own grid row and shunted the
   select one row down — the wrapper keeps the mark out of .field's flow. */
.select { position: relative; display: grid; }
.select::after {
  content: "";
  position: absolute;
  inset-inline-end: var(--space-md);
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 12px;
  block-size: 8px;
  background-color: var(--color-ink-2);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  pointer-events: none;
}
.field input::placeholder, .field textarea::placeholder { color: var(--color-muted); }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--color-navy); }
.field input[disabled], .field select[disabled], .field textarea[disabled] {
  background-color: var(--color-paper-3); color: var(--color-muted); cursor: not-allowed;
}
.field__hint { font-size: var(--text-sm); color: var(--color-muted); }
/* the slot is always reserved — an appearing error must not shift the form */
.field__err {
  min-block-size: 1lh;
  font-size: var(--text-sm);
  color: var(--color-error);
  font-weight: 500;
}
.field__err[hidden] { display: block; visibility: hidden; }

.field[data-state="error"] input,
.field[data-state="error"] select,
.field[data-state="error"] textarea { border-color: var(--color-error); background-color: var(--color-error-wash); }
.field[data-state="success"] input,
.field[data-state="success"] select { border-color: var(--color-success); }

.form__foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.form__status {
  font-size: var(--text-base);
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}
.form__status[hidden] { display: none; }
.form__status[data-tone="success"] { color: var(--color-success); }
.form__status[data-tone="error"]   { color: var(--color-error); }

/* ══════════════════════════════════════════════════════════
   Ft5 · Statement footer
   ══════════════════════════════════════════════════════════ */

.foot { padding-block: var(--space-2xl) var(--space-xl); }
.foot__line {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.75rem, 4.5vw, 3rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
  max-width: 22ch;
  overflow-wrap: anywhere;
}
.foot__cols {
  margin-block-start: var(--space-2xl);
  padding-block-start: var(--space-lg);
  border-top: var(--rule-hair) solid var(--color-rule);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
}
@media (min-width: 48rem) {
  .foot__cols { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-xl); }
}
.foot__col dt {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-muted);
  margin-block-end: var(--space-2xs);
}
.foot__col dd { margin: 0; font-size: var(--text-base); color: var(--color-ink-2); overflow-wrap: break-word; }
.foot__link { white-space: nowrap; font-weight: 500; }
.foot__link:active { text-decoration-color: var(--color-accent-deep); }

.foot__meta {
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-md);
  border-top: var(--rule-hair) solid var(--color-rule-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  align-items: baseline;
  justify-content: space-between;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* ══════════════════════════════════════════════════════════
   Reduced motion
   ══════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 150ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 150ms !important;
  }
  .kite [data-draw] { stroke-dasharray: none; stroke-dashoffset: 0; animation: fade 150ms linear forwards; }
  .kite [data-fade] { animation: fade 150ms linear forwards; animation-delay: 0ms; }
  .js .reveal { transform: none; }
  .js .reveal.is-in { transition: opacity 150ms linear; }
  .btn[data-state="loading"]::after { animation-iteration-count: infinite !important; }
}
