/* =============================================================================
   ตามสั่ง (TarmSung) — Base, layout, components, sections
   Steps 3-6 & 11-13 of website_building.md
   ========================================================================== */

/* ---- Reset / base ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html {
  -webkit-text-size-adjust: 100%;
  /* No scroll-behavior here: the script eases these itself, on the page's own
     curve. Native smooth is fast and close to linear — it stops rather than
     settles. Left as auto so the script is the only thing moving the page. */
  scroll-padding-top: calc(var(--header-h) + var(--space-5));
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; height: auto; }
/* No negative tracking on headings: Anuphan/Plex Sans Thai vowels and tone
   marks collide under it (a11y bug carried over from the old system). */
h1, h2, h3, h4 { font-family: var(--font-display); line-height: var(--leading-tight); font-weight: var(--weight-bold); }
/* Thai runs without spaces between words, so the browser is free to break inside
   one — and did: "เว็บไซต์" came apart as "เว็บไซ" / "ต์", leaving a lone
   tone-marked consonant on its own line under the brand underline, and "ตามสั่ง"
   split as "ตาม" / "สั่ง". word-break: keep-all does not help here; the spec
   defines it for CJK and Chrome still applies its Thai dictionary. The reliable
   fix is to mark the words: a break can then only land between them.
   Wrap any Thai word that must stay whole in .nb. */
.nb { white-space: nowrap; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
:target { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }

/* Visible focus everywhere — never removed without a replacement */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Skip link */
.skip-link {
  position: absolute; left: var(--space-4); top: -100px;
  background: var(--color-brand); color: var(--color-text-on-brand);
  padding: var(--space-2) var(--space-4); border-radius: var(--radius-md);
  z-index: 200; transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus { top: var(--space-3); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- Paper --------------------------------------------------------------
   One sheet, fixed to the viewport, behind everything. Consistency was the
   reason to do this, but doing it per-section would have meant six or eight
   identical layers all painting the same grain — and paper is the whole sheet,
   not a property of each block. The tinted, ink and ember bands simply cover it,
   which is what a printed panel does to the stock underneath.
   Two-level opacity like the Maya Green build: some on the rect inside the SVG,
   the rest on the element, which controls it more finely than one value and
   stops the noise banding. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.22'/%3E%3C/svg%3E");
  background-size: 160px 160px;
  opacity: 0.5;
}
:root[data-theme="dark"] body::before { opacity: 0.34; }

/* ---- Ruling -------------------------------------------------------------
   Ruling the whole sheet was tried and looked cheap: six screens of unbroken
   lined paper turns every band into the same notebook and leaves the eye nowhere
   to rest. It earns its place only where the page is literally an order pad —
   the hero ticket, the price tickets, the order form — so it is opt-in per
   section (.section--ruled) rather than a property of the page. */
:root { --rule-line: color-mix(in oklch, var(--color-border) 60%, transparent); }
.section--ruled {
  background-image: linear-gradient(var(--rule-line) 1px, transparent 1px);
  background-size: 100% 2rem;
}
.section--tint.section--ruled {
  background-image: linear-gradient(var(--rule-line) 1px, transparent 1px), var(--grain-surface);
  background-size: 100% 2rem, 160px 160px;
}

/* The paragraph above claimed the tinted bands covering the grain was correct,
   the way a printed panel covers the stock. That holds on the work pages. It does
   not hold here: on the home page the tinted bands and the footer are 3,300px of
   roughly 6,000, so "the paper shows between the panels" left almost no paper.
   Those surfaces carry the grain in their own background instead — a layer above
   their fill and below their text, so nothing enters the stacking order and no
   text sits under a speckle. Baked at the strength body::before ends up with
   after its element opacity (0.22 x 0.5, and 0.22 x 0.34 in the dark) so a band
   and the sheet around it are the same stock. */
:root {
  --grain-surface: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.11'/%3E%3C/svg%3E");
}
:root[data-theme="dark"] {
  --grain-surface: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.075'/%3E%3C/svg%3E");
}

/* ---- Layout helpers ----------------------------------------------------- */
.container {
  width: 100%; max-width: var(--container);
  /* Fluid like everything else in the frame — this was the last fixed length
     left in the page shell. */
  margin-inline: auto; padding-inline: clamp(1.25rem, 4vw, 2rem);
}
/* Section rhythm, measured against the screen rather than fixed. A page whose
   bands are always 96px apart reads the same on every device and fits none of
   them; tying the step to vh is what makes a long page feel composed. */
.section { padding-block: clamp(3rem, 8vh, 6rem); }
.section--tight { padding-block: clamp(2.25rem, 5.5vh, 4rem); }

/* ---- Page rhythm -------------------------------------------------------
   Backgrounds were written inline on the sections, and two tinted ones ran back
   to back — "จุดเด่น" and "ทีมในครัว" merged into a single grey slab, and the last
   two sections merged into a single pale one. They alternate from here now.

   .section--ink is the spine. Every other band on this page is warm paper, so
   one full-bleed charcoal section splits the scroll into a before and an after
   and stops it reading as an unbroken column of cards. It sits on ขั้นตอน, the
   middle of the story and the most typographic section — numbers and a rail,
   which is exactly what carries on a dark ground. */
.section--tint {
  background-color: var(--color-bg-subtle);
  background-image: var(--grain-surface);
  background-size: 160px 160px;
}
.section--ink {
  position: relative;
  background: linear-gradient(168deg, var(--charcoal-850) 0%, var(--charcoal-900) 55%, var(--charcoal-950) 100%);
  color: var(--paper-25);
}
.section--ink .section-head h2,
.section--ink h3 { color: var(--paper-25); }
.section--ink p,
.section--ink .section-head p { color: var(--charcoal-300); }
.section--ink .eyebrow { background: color-mix(in oklch, var(--color-brand) 22%, transparent); color: var(--ember-300, var(--color-brand)); }
/* --color-brand-text is ember-700 in the light theme, which is 2.6:1 on this
   charcoal. On a dark ground the tag has to take a light ember. */
.section--ink .step-kitchen-tag, .section--ink .num { color: var(--ember-300); }
.section--ink .step-card { background: transparent; border-color: color-mix(in oklch, var(--paper-25) 14%, transparent); }
.section--ink .steps::before { border-top-color: color-mix(in oklch, var(--paper-25) 26%, transparent); }
.section--ink .step-num { background: var(--color-brand); color: var(--color-text-on-brand); }
.section-head { max-width: 52rem; margin-inline: auto; text-align: center; margin-bottom: var(--space-7); }
.section-head.left { margin-inline: 0; text-align: start; }

.eyebrow {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-display); font-weight: var(--weight-semibold);
  font-size: var(--text-sm); letter-spacing: 0.02em;
  color: var(--color-brand-text);
  background: var(--color-brand-soft);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  margin-bottom: var(--space-4);
}
.section-head h2 { font-size: var(--text-3xl); }
.section-head p { color: var(--color-text-muted); font-size: var(--text-lg); margin-top: var(--space-3); max-width: 65ch; margin-inline: auto; }

.lead { font-size: var(--text-lg); color: var(--color-text-muted); max-width: 40ch; }
.text-muted { color: var(--color-text-muted); }

/* ---- Buttons (4a) — all states ----------------------------------------- */
.btn {
  --_bg: var(--color-surface-2);
  --_fg: var(--color-text);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  font-family: var(--font-display); font-weight: var(--weight-semibold);
  font-size: var(--text-base); line-height: 1;
  padding: 0.8rem 1.3rem; min-height: 44px;
  border: 1px solid transparent; border-radius: var(--button-radius);
  background: var(--_bg); color: var(--_fg);
  transition: transform var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out), filter var(--dur-fast);
  white-space: nowrap;
}
.btn:hover { transform: translateY(-1px); }
/* "Stamp the ticket" press feedback (1.11) instead of a plain reset-to-0. */
.btn:active { transform: translateY(1px) scale(0.98); }
.btn .icon { width: 1.15em; height: 1.15em; }

.btn--primary { --_bg: var(--color-brand); --_fg: var(--color-text-on-brand); box-shadow: var(--shadow-md); }
.btn--primary:hover { --_bg: var(--color-brand-hover); box-shadow: var(--shadow-lg); }

.btn--secondary { --_bg: var(--color-surface); --_fg: var(--color-text); border-color: var(--color-border-strong); }
.btn--secondary:hover { --_bg: var(--color-surface-2); }

.btn--ghost { --_bg: transparent; --_fg: var(--color-text); }
.btn--ghost:hover { --_bg: var(--color-surface-2); }

.btn--lg { font-size: var(--text-lg); padding: 1rem 1.6rem; min-height: 52px; }
.btn--block { width: 100%; }

/* The ink of the stamp. Painted in a pseudo-element so it cannot disturb the
   button's own background, and clipped by the button's radius. */
.btn--primary { position: relative; overflow: hidden; isolation: isolate; }
.btn--primary::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(circle at var(--ink-x, 50%) var(--ink-y, 50%),
              color-mix(in oklch, var(--color-text-on-brand) 55%, transparent) 0 12%,
              transparent 42%);
  opacity: 0; transform: scale(0.2);
}
.btn--primary.stamped::after { animation: ts-stamp 320ms var(--ease-out); }
@keyframes ts-stamp {
  from { opacity: .55; transform: scale(0.2); }
  to   { opacity: 0;   transform: scale(2.4); }
}

.btn[disabled], .btn[aria-disabled="true"] { filter: saturate(0.4) opacity(0.6); cursor: not-allowed; transform: none; }

.btn .spinner { display: none; width: 1.1em; height: 1.1em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; }
.btn.is-loading .spinner { display: inline-block; animation: spin 0.7s linear infinite; }
.btn.is-loading .btn-label { opacity: 0.85; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- Header / nav (4e) -------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: 500;
  height: var(--header-h);
  background: color-mix(in oklch, var(--color-bg) 78%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-mid), background-color var(--dur-mid);
}
.site-header.scrolled { border-bottom-color: var(--color-border); box-shadow: var(--shadow-sm); }
.nav { display: flex; align-items: center; gap: var(--space-5); height: 100%; }

.brand { display: inline-flex; align-items: center; gap: var(--space-3); font-family: var(--font-display); font-weight: var(--weight-bold); font-size: var(--text-lg); }
/* The mark is a fried egg — ไข่ดาว, the thing you actually order ตามสั่ง. The
   white keeps its cream fill in both themes (a colour-shifting egg stops being
   an egg); only the yolk follows --color-brand, so it brightens in the dark
   theme alongside every other ember surface. */
.brand-mark { width: 2.25rem; height: 2.25rem; display: grid; place-items: center; }
.brand-mark svg { width: 100%; height: 100%; display: block; overflow: visible; }
.brand-mark .egg-white {
  fill: var(--paper-25); stroke: var(--charcoal-900); stroke-width: 1.3;
  stroke-linejoin: round;
}
.brand-mark .yolk {
  fill: var(--color-brand);
  transform-origin: center;
  transition: transform var(--dur-mid) var(--ease-out);
}
/* A yolk is liquid. Pointing at the mark gives it a small wobble and settle —
   the one place on the site where the logo behaves like the thing it is. */
.brand:hover .yolk { animation: ts-yolk 620ms var(--ease-out); }
@keyframes ts-yolk {
  0%   { transform: scale(1)      translateY(0); }
  30%  { transform: scale(1.14, .88) translateY(1px); }
  55%  { transform: scale(.93, 1.09) translateY(-1px); }
  78%  { transform: scale(1.04, .97) translateY(0); }
  100% { transform: scale(1)      translateY(0); }
}
.brand small { display: block; font-family: var(--font-body); font-weight: var(--weight-normal); font-size: 0.7rem; color: var(--color-text-subtle); letter-spacing: 0.06em; }

.nav-links { display: flex; align-items: center; gap: var(--space-2); margin-inline-start: auto; }
.nav-links a {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
  color: var(--color-text-muted); font-weight: var(--weight-medium); font-size: var(--text-sm);
  transition: color var(--dur-fast);
}
.nav-links a:hover { color: var(--color-text); }
.nav-links a:hover::after { transform: scaleX(1); opacity: .45; }
/* The dashed rule draws itself in from the left rather than blinking on, so
   moving down the page reads as a pen running along the order. */
.nav-links a { position: relative; }
.nav-links a::after {
  content: ""; position: absolute; left: var(--space-3); right: var(--space-3); bottom: 6px;
  border-bottom: 2px dashed var(--color-brand);
  transform: scaleX(0); transform-origin: left center; opacity: 1;
  transition: transform var(--dur-mid) var(--ease-out), opacity var(--dur-fast);
}
.nav-links a[aria-current="page"] { color: var(--color-brand-text); }
.nav-links a[aria-current="page"]::after { transform: scaleX(1); opacity: 1; }

.nav-actions { display: flex; align-items: center; gap: var(--space-2); }

.icon-btn {
  width: 44px; height: 44px; display: grid; place-items: center;
  border-radius: var(--radius-md); border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text-muted);
  transition: color var(--dur-fast), background-color var(--dur-fast), border-color var(--dur-fast);
}
.icon-btn:hover { color: var(--color-text); border-color: var(--color-border-strong); }
.icon-btn .icon { width: 1.25rem; height: 1.25rem; }
/* ---- Theme switch, ported from ScoreUp -----------------------------------
   A pill with a thumb that slides, and a sun and a moon that rotate past each
   other rather than being swapped with display:none, which is what this was. */
.theme-toggle {
  position: relative; width: 54px; height: 30px; flex: none; padding: 0;
  border-radius: 999px; border: 1px solid var(--color-border);
  background: var(--color-surface-2); cursor: pointer;
  transition: background var(--dur-mid), border-color var(--dur-mid);
  -webkit-tap-highlight-color: transparent;
}
.theme-toggle:hover { border-color: var(--color-brand); }
.theme-toggle .tt-thumb {
  position: absolute; top: 2px; left: 2px; width: 24px; height: 24px;
  border-radius: 50%; background: var(--color-surface);
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
  display: flex; align-items: center; justify-content: center;
  transition: transform .32s cubic-bezier(.4,.05,.2,1), background var(--dur-mid);
}
:root[data-theme="dark"] .theme-toggle .tt-thumb { transform: translateX(24px); }
.theme-toggle svg {
  position: absolute; width: 14px; height: 14px;
  transition: opacity .25s, transform .35s cubic-bezier(.4,.05,.2,1);
}
.theme-toggle .tt-sun  { color: var(--color-brand); opacity: 1; transform: rotate(0); }
.theme-toggle .tt-moon { color: var(--color-flame); opacity: 0; transform: rotate(-90deg); }
:root[data-theme="dark"] .theme-toggle .tt-sun  { opacity: 0; transform: rotate(90deg); }
:root[data-theme="dark"] .theme-toggle .tt-moon { opacity: 1; transform: rotate(0); }
.theme-toggle:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 2px; }

.nav-toggle { display: none; }

/* Mobile drawer */
.mobile-menu {
  position: fixed; inset: var(--header-h) 0 auto 0; z-index: 99;
  background: var(--color-surface); border-bottom: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
  padding: var(--space-4) var(--space-5) var(--space-6);
  display: none; flex-direction: column; gap: var(--space-1);
}
.mobile-menu.open { display: flex; animation: dropIn var(--dur-mid) var(--ease-out); }
.mobile-menu a { padding: var(--space-3); border-radius: var(--radius-md); font-weight: var(--weight-medium); color: var(--color-text); }
.mobile-menu a:hover { background: var(--color-surface-2); }
.mobile-menu .btn { margin-top: var(--space-3); }
@keyframes dropIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }

/* ---- Hero (5) ----------------------------------------------------------- */
.hero {
  position: relative;
  /* vh terms, not fixed space tokens: the hero has to fit the screen it is on. */
  /* Room at the top and the foot. The floors are what keeps a short laptop
     fitting; the vh terms let a tall screen breathe instead of leaving the
     hero pinned to the same height it needed at 800px. */
  padding-block: clamp(1.75rem, 7vh, 7rem) clamp(1.5rem, 6vh, 6rem);
  overflow: hidden;
}
/* Paper texture, not a gradient blob: inline SVG noise (feTurbulence) at very
   low opacity, plus a faint order-pad ruled line every 2rem. Kept subtle on
   purpose so it never competes with reading contrast. */
/* The grain is on the page, not on the hero — see body::before below. What stays
   here is the order-pad ruling. It bleeds past the section edges, which a
   background on the box itself cannot do, so it keeps its own layer. */
.hero::before {
  content: ""; position: absolute; inset: -20% -10% auto -10%; height: 130%;
  z-index: -1; pointer-events: none;
  background-image: linear-gradient(var(--color-border) 1px, transparent 1px);
  background-size: 100% 2rem;
  opacity: 0.55;
}
/* The grain. Two-level opacity like the Maya Green build — the rect inside the
   SVG carries some, the element carries the rest — which gives finer control than
   one value and keeps the noise from banding. numOctaves 3 rather than 2 for a
   tighter tooth; at 2 it reads as speckle rather than paper. */
:root[data-theme="dark"] .hero::before { opacity: 0.4; }
.hero-grid { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: var(--space-8); align-items: center; }
/* Grid items default to min-width:auto, so an unbreakable Thai string in the
   h1 (Thai has no spaces) forced this column wider than a phone screen and
   pushed the whole page out — every line on the page got clipped, not just
   the heading. */
.hero-grid > * { min-width: 0; }
/* The headline is never covered by the deck. .hero-copy had no stacking order at
   all, while the loop writes a hovered card z-index 200 — so the only thing
   keeping a lifted card off the headline was the two columns not happening to
   overlap at the width being tested. That is luck, not a rule. Above the cards
   (0-200) and above the furniture pinned to the reel (300).
   Full band, so it stays legible:
     0-99   cards on the arc          200  a card lifted by the pointer
     250    the reel's fade curtains  300  queue stub and floating chips
     400    the hero copy             500  the sticky header, above everything */
.hero-copy { max-width: 40rem; position: relative; z-index: 400; }
/* Capped by height too. Width alone said 76px on a 1280x800 laptop, where the
   headline then pushed the buttons under the fold. */
/* max() floor, or a landscape phone (390px tall) drives this to 32px — smaller
   than every h2 on the page. */
.hero h1 { font-size: max(2.4rem, min(var(--text-5xl), 8.4vh)); }
/* Was gradient-clipped text; now a flat brand ink + a thick marker underline
   instead of a fill trick. */
.hero h1 .grad {
  color: var(--color-brand-text);
  text-decoration: underline; text-decoration-color: var(--color-brand);
  text-decoration-thickness: 0.08em; text-underline-offset: 0.12em;
}
.hero p.lead { margin-top: var(--space-5); font-size: var(--text-xl); }
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }
.hero-note { margin-top: var(--space-4); font-size: var(--text-sm); color: var(--color-text-subtle); display: inline-flex; align-items: center; gap: var(--space-2); }

.pill-badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-full);
  border: 1px solid var(--color-border); background: var(--color-surface);
  font-size: var(--text-sm); color: var(--color-text-muted); box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-5);
}
.pill-badge .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-success); }

/* Hero visual — a reel of real work tumbling past. Replaces the old browser
   mockup: a fake window said "we build websites", this shows the ones we
   built. The track holds the nine cards twice and scrolls exactly one set, so
   the loop closes on itself with no seam. */
.hero-visual { position: relative; }
.reel {
  position: relative;
  /* Fluid: at a fixed 50rem this column was taller than the screen, and
     taller than the copy beside it — the hero grid centred the shorter
     column against it and left a 220px hole above the badge. */
  /* Taller: the reason for the extra room in the hero is to show more of the
     run at once, not to pad around the same three cards. */
  height: clamp(20rem, 74vh, 46rem);
  overflow: hidden;
}
/* No fade at the ends. The curtains were only invisible before because they sat
   under the deck; once they were painted on top, a card climbing out of the frame
   dissolved into a pale smear across its own face rather than leaving cleanly.
   A hard edge on a moving card reads as the card passing behind the frame, which
   is what it is doing. */

/* ---- The cards on the arc -------------------------------------------------
   Every card is placed by js/main.js each frame: it rides an ARC, not a ring.
   The wheel's centre sits off to the left of the frame, so only the right-hand
   portion of the circle shows and the column climbs while bowing out at
   mid-frame. Because no card comes round the far side, nothing ever arrives
   upside down and every face stays readable.

   --u is the unit for everything drawn ON a card. The card is sized from the
   frame, so chrome in rem would keep its pixel size while the card shrank —
   the label plate would eat half a card on a phone. --u ties them together. */
.reel {
  /* --reel-w is written in px by the script from the frame's own width. It has
     to be an absolute length, not a percentage: --slide-w feeds --slide-h too,
     and a percentage there would resolve against the frame's HEIGHT, quietly
     giving the card a different aspect ratio than the one it is drawn for. */
  /* No artificial ceiling: --reel-w is 88% of the frame, and the frame is
     already half the page. Capping at 30rem left the card at a quarter of a
     wide screen inside a column twice that, which is what made the hero read
     empty on the right. */
  --slide-w: var(--reel-w, 26rem);
  --slide-h: calc(var(--slide-w) / 1.6);
}
.reel .slide {
  --u: calc(var(--slide-h) * .06);

  position: absolute; top: 50%; left: 50%;
  width: var(--slide-w); height: var(--slide-h);
  margin-top: calc(var(--slide-h) / -2);
  margin-left: calc(var(--slide-w) / -2);
  /* Flat, not calc(var(--u) * …): a radius reads as a property of the card
     stock rather than something that should grow with the card. */
  border-radius: var(--radius-md);
  overflow: hidden;
  /* Hidden until the loop places it, so the cards never flash as an
     un-positioned pile on the way in. The loop writes an opacity on the very
     first frame — it is called synchronously, not from rAF. */
  opacity: 0;
  transform-origin: 50% 50%;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  box-shadow: var(--shadow-lg);
  transition: box-shadow var(--dur-mid) var(--ease-out);
}
/* Only the shadow is a CSS transition. The hover lift is folded into the same
   transform the loop writes every frame — a transition on transform would
   fight the travel for control of the property. */
.reel .slide[data-lift="1"] { box-shadow: var(--shadow-xl, var(--shadow-lg)); }
.reel .slide:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 3px; }
.reel .shot {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: top center; display: block;
}
/* The plate sits at the HEAD of the card, not its foot: each card's foot is
   covered by the one in front of it, so a label down there is never seen. */
.reel .plate {
  position: absolute; left: 0; right: 0; top: 0;
  padding: var(--u) calc(var(--u) * 1.5) calc(var(--u) * 2.4);
  background: linear-gradient(180deg, rgba(10,6,4,.88) 46%, rgba(10,6,4,0));
  color: #fff; display: grid; gap: calc(var(--u) * .3);
}
.reel .tag {
  /* Floors, because --u scales with the card and the card gets small: at the phone
     breakpoint this chip was rendering at 5.9px and the label was decorative rather
     than readable. Below ~9px it is not worth the ink. */
  justify-self: start; font-size: max(9px, calc(var(--u) * .5));
  letter-spacing: .1em; text-transform: uppercase;
  background: var(--color-brand); color: var(--paper-25, #fff);
  padding: calc(var(--u) * .16) calc(var(--u) * .5);
  border-radius: calc(var(--u) * .15);
}
/* No negative tracking: these are Thai subtitles, and the vowels and tone marks
   collide under it (same rule as the headings at the top of this file). */
.reel .plate h3 { font-size: max(13px, var(--u)); font-weight: 600; line-height: 1.25; }
.reel .plate h3 small {
  display: block; font-size: max(11px, calc(var(--u) * .62)); font-weight: 400;
  opacity: .75; margin-top: calc(var(--u) * .15);
}
@media (max-width: 720px) {
  /* One column now, so the reel sits under the copy. The arc is measured from
     the frame at runtime, so only the frame needs retuning here. */
  /* --reel-w is px from the script; a percentage here would feed --slide-h and
     --u and collapse the card chrome (see the note above). */
  .reel { height: 27rem; --slide-w: var(--reel-w, 20rem); }
}
/* Stopping the run is handled in the script, not here: the travel is written
   into the same transform as the hover lift, so a CSS animation-play-state has
   nothing to pause. Hovering a card lifts it out of the run instead, and
   prefers-reduced-motion holds the arc still while the lift still eases. */

.hero-float {
  /* Above the reel: the loop writes cards an inline z-index up to 200, so without
     a stacking order of its own a card simply runs over the top of this chip. */
  z-index: 300;
  /* A crisp outline, matching the queue stub. These two are the same kind of
     object — paper furniture pinned onto the reel — and they were coming from two
     different visual languages: the stub had a 1.5px strong-colour edge, this had
     --color-border at 14% opacity, which is barely visible, and leaned on its
     shadow instead. DESIGN-KITCHEN §1.10 and the motif line both say the identity
     is a sharp 1–2px border INSTEAD OF a soft shadow, so this was the exception. */
  position: absolute; background: var(--color-surface);
  border: 1.5px solid var(--color-border-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-md); padding: var(--space-3) var(--space-4);
  display: flex; align-items: center; gap: var(--space-3); font-size: var(--text-sm); font-weight: var(--weight-semibold);
}
.hero-float .chip { width: 34px; height: 34px; border-radius: var(--radius-md); display: grid; place-items: center; background: var(--color-brand-soft); color: var(--color-brand-text); }
/* Anchored to the reel's leading edge, not the column's: the reel column is
   narrower than the grid cell it sits in, so pinning these to the cell left
   them stranded in empty space. */
.hero-float.one { top: 6%; right: 16%; }
.hero-float.two { bottom: 10%; left: -4%; }
.hero-float small { display: block; font-weight: var(--weight-normal); color: var(--color-text-subtle); }

/* Ticket-style variant: a receipt-stub notch cut into the bottom edge, a
   dashed tear line under the icon slot, and a square (not soft) icon frame. */
/* A tear-off edge along the foot. The previous attempt read as a broken grey
   strip hanging under the card, and for three reasons at once: the dashed border
   ran the full width including under the rounded bottom corners, so the line
   carried on past where the card actually ends; the two 6px corner bites were
   painted at bottom:-1px, below the border rather than on it; and the card's hard
   4px offset shadow then showed through underneath all of it as a second band.
   Squaring the bottom corners lets the line meet both edges cleanly, which is all
   a tear-off needs. */
.hero-float--ticket {
  border-bottom-style: dashed; border-bottom-width: 2px;
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}
.hero-float--ticket .chip { border-radius: var(--radius-sm); }

/* Signature #1: queue-number stub clipped onto the hero visual's top edge. */
.queue-stub {
  /* 300, not 3. The old reel's cards had no z-index of their own and this only had
     to clear the fade overlays at 2; the arc reel writes each card an inline
     z-index up to 200, so a card sliced straight through the stub. Same band as
     .hero-float — everything pinned ON the reel lives above every card.
     It also fades out rather than being run over: it belongs to the hero, so once
     the hero is behind you it has nothing left to say. */
  position: absolute; z-index: 300; top: -18px; left: 24px;
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
  background: var(--color-surface); border: 1.5px solid var(--color-border-strong);
  padding: var(--space-2) var(--space-4); box-shadow: var(--shadow-md);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  /* The tear notch is gone, and with it the reason the outline did not close.
     It was two mask layers composited with `intersect`, plus the old WebKit
     `-webkit-mask-composite: source-in` alongside it — and those two properties do
     not mean the same thing. In WebKit the legacy keyword wins and composites
     differently, so the mask ate part of the border instead of just punching a
     6px dimple in the bottom edge. The outline now runs all the way round.

     Dropping it also gives the box its shadow back: a mask is applied after the
     element is painted, box-shadow included, so --shadow-md had been declared here
     the whole time and never once rendered. Border weight, colour and the square
     corners are untouched. */
}
.queue-stub.faded { opacity: 0; transform: translateY(-8px); pointer-events: none; }
.queue-label { font-family: var(--font-body); font-size: var(--text-xs); color: var(--color-text-muted); }
.queue-num { font-family: var(--font-mono); font-weight: 700; font-size: var(--text-2xl); color: var(--color-brand-text); }

/* ---- Stats / trust bar -------------------------------------------------- */
.stat .num { font-family: var(--font-mono); font-weight: var(--weight-bold); font-size: var(--text-3xl); color: var(--color-text); line-height: 1; }
.stat .num .plus { color: var(--color-brand-text); }
.stat .label { font-size: var(--text-sm); color: var(--color-text-muted); margin-top: var(--space-2); }

.logos { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--space-4) var(--space-7); opacity: 0.75; margin-top: var(--space-7); }
.logos span { font-family: var(--font-display); font-weight: var(--weight-semibold); color: var(--color-text-subtle); font-size: var(--text-lg); }

/* ---- Cards / grids (4d) ------------------------------------------------- */
.grid-auto { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }

.card {
  background: var(--card-bg); border: 1px solid var(--card-border);
  /* The sheet behind the page never showed through a card, and cards are most of
     what the eye rests on — so the stock is printed on them too, at the same
     strength, rather than the page grain being turned up until text looks dirty. */
  background-image: var(--grain-surface);
  background-size: 160px 160px;
  border-radius: var(--card-radius); padding: var(--space-6);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-mid) var(--ease-out), box-shadow var(--dur-mid) var(--ease-out), border-color var(--dur-mid);
}
.card--hover:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); border-color: var(--color-border-strong); }

.feature-icon {
  width: 3rem; height: 3rem; border-radius: var(--radius-md);
  display: grid; place-items: center; margin-bottom: var(--space-4);
  background: var(--color-brand-soft); color: var(--color-brand-text);
}
.feature-icon .icon { width: 1.5rem; height: 1.5rem; }

.card h3 { font-size: var(--text-xl); margin-bottom: var(--space-2); }
.card p { color: var(--color-text-muted); font-size: var(--text-base); }
.card ul { list-style: none; padding: 0; margin-top: var(--space-4); display: grid; gap: var(--space-2); }
.card ul li { display: flex; gap: var(--space-2); align-items: flex-start; font-size: var(--text-sm); color: var(--color-text-muted); }
.card ul li .icon { width: 1.1rem; height: 1.1rem; color: var(--color-success); flex: none; margin-top: 0.15em; }

/* ---- Why-us / feature list --------------------------------------------- */
.why-grid { display: grid; grid-template-columns: 0.9fr 1.1fr; gap: var(--space-8); align-items: center; }
.checks { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5); }
.check { display: flex; gap: var(--space-3); }
/* QC-stamp tick (3.3): square-cornered, thick border, no soft fill —
   reads like a rubber stamp of approval rather than a status pill. */
.stamp-tick, .check .tick {
  width: 2rem; height: 2rem; flex: none; border-radius: var(--radius-sm);
  border: 2px solid var(--leaf-600); background: transparent; color: var(--leaf-600);
  display: grid; place-items: center;
}
.stamp-tick svg, .check .tick svg { stroke-width: 3; }
.check h3 { font-family: var(--font-display); font-size: var(--text-base); margin-bottom: var(--space-1); }
.check p { font-size: var(--text-sm); color: var(--color-text-muted); }

/* ---- Process ------------------------------------------------------------ */
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-5); counter-reset: step; }
.step-card { position: relative; padding-top: var(--space-6); }
.step-num {
  font-family: var(--font-mono); font-weight: var(--weight-bold); font-size: var(--text-2xl);
  width: 3rem; height: 3rem; border-radius: var(--radius-md); display: grid; place-items: center;
  background: var(--color-brand); color: var(--color-text-on-brand); margin-bottom: var(--space-4); box-shadow: var(--shadow-md);
}
/* Kitchen-order tag (3.5): sits above the existing h3, doesn't replace it. */
.step-kitchen-tag {
  display: block; font-family: var(--font-mono); text-transform: uppercase;
  font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--color-brand-text);
  margin-bottom: var(--space-1);
}
.step-card h3 { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.step-card p { font-size: var(--text-sm); color: var(--color-text-muted); }

/* ---- Portfolio ---------------------------------------------------------- */
.work-grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
.work { overflow: hidden; padding: 0; display: block; color: inherit; text-decoration: none; }
/* The pop. Everything else on hover happens inside the card — the screenshot
   uncovers, the tag hops, the number turns orange — and the card itself used to
   sit still through all of it, so the piece came alive while its frame stayed
   flat.

   It grows in place: no diagonal travel, no z-index. The lift used to be
   translate(-3px, -5px) under a 6px print shadow, which sent the card up and to
   the left into the gutter and laid its shadow across the neighbour — it read as
   the hovered card jumping in front of the one beside it rather than as itself
   growing. 1.02 on a 20rem column is ~3px a side against a 24px gutter, so it
   never reaches anything. Written against .work rather than .card--hover on
   purpose — main.js strips that class off while the demos are unpublished, and
   this motion is warmth, not a promise of a click. The screenshot creeps in a
   fraction at the same time so the growth has depth instead of being flat. */
/* Written .card.work, not .work: the reveal system ends with `.reveal.in {
   transform: none }`, which is one class heavier than `.work` and sits further
   down the sheet — so once a card had faded in, its own hover lift lost the
   cascade and did nothing at all. */
.card.work:hover, .card.work:focus-visible { transform: scale(1.02); box-shadow: var(--shadow-lg); border-color: var(--color-border-strong); }
.work:hover .thumb .preview, .work:focus-visible .thumb .preview { transform: scale(1.035); }
/* Flat charcoal fallbacks (loading-state, behind the screenshot) — no more
   violet/cyan/magenta gradient blobs per work item. */
.work .thumb { aspect-ratio: 16 / 10; background: var(--color-brand); position: relative; overflow: hidden; }
.work:nth-child(2) .thumb { background: var(--charcoal-700); }
.work:nth-child(3) .thumb { background: var(--charcoal-850); }
/* Static 1280x800 screenshot of each demo (assets/work/*.webp). This used to be
   a live <iframe> of the real page: nine of them cost ~13MB on mobile and let a
   nested page steal focus and yank the parent's scroll. The card's <a> still
   opens the real demo. */
.work .preview {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; background: #fff;
  object-fit: cover; object-position: top center;
  pointer-events: none; /* clicks fall through to the card link */
  opacity: 1;
  /* Signature #3 (4.3): "lift the lid" — softened at rest, full colour on hover.
     The spec's saturate(.35)+blur(1px) went too far: this section's whole job is
     to show the work, and it read as a dead grey plate on first paint. Softened
     to a hint of a cover; the reveal still lands. */
  filter: saturate(0.72) brightness(0.96);
  transition: filter var(--dur-slow) var(--ease-out), opacity var(--dur-mid) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.work:hover .thumb .preview, .work:focus-visible .thumb .preview { filter: saturate(1) brightness(1); }
/* Touch devices never hover — they'd be stuck on the covered state forever. */
@media (hover: none) { .work .preview { filter: none; } }
.work .thumb::after { content:""; position:absolute; inset:0; z-index:1; pointer-events:none; box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.08); background: linear-gradient(180deg, transparent 60%, oklch(0.15 0.02 285 / 0.28)); opacity:.55; transition: opacity var(--dur-mid) var(--ease-out); }
.work:hover .thumb::after { opacity: .15; }
.work .thumb .tag {
  position: absolute; top: var(--space-3); left: var(--space-3); z-index: 2;
  background: oklch(1 0 0 / 0.9); color: var(--charcoal-900);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-full);
  transition: transform var(--dur-mid) var(--ease-out); transform-origin: top center;
}
.work:hover .thumb .tag, .work:focus-visible .thumb .tag { transform: translateY(-6px) rotate(-2deg); }
.work .live-badge { position: absolute; top: var(--space-3); right: var(--space-3); z-index: 2; display: inline-flex; align-items: center; gap: 6px; background: oklch(0.18 0.016 50 / 0.82); color: var(--paper-25); font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: var(--space-1) var(--space-3); border-radius: var(--radius-full); }
/* The dot no longer pulses on its own. Eight of these beating out of sync on one
   screen read as advertising, not as a studio — and it was eight permanently
   animating layers on a phone. It beats when you point at the card instead, which
   is the moment the "you can actually click this" claim matters. */
.work .live-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: oklch(0.78 0.18 145); box-shadow: 0 0 0 0 oklch(0.78 0.18 145 / 0.7); }
.work:hover .live-badge .dot, .work:focus-visible .live-badge .dot { animation: livePulse 1.8s ease-out infinite; }
@keyframes livePulse { 0% { box-shadow: 0 0 0 0 oklch(0.78 0.18 145 / 0.7); } 70% { box-shadow: 0 0 0 7px oklch(0.78 0.18 145 / 0); } 100% { box-shadow: 0 0 0 0 oklch(0.78 0.18 145 / 0); } }
.work .work-body { padding: var(--space-5); }
/* Numbering the work, from the reference. It earns its place here in a way it
   did not in the services list: those six were parallel choices, so numbering
   them implied an order that does not exist and put a meaningless column of
   figures down the page. These are seven finished jobs, and a kitchen numbers
   what it has sent out. Small, muted, and set in the mono face the ticket
   numbers elsewhere on the page already use — a docket number, not a headline.
   aria-hidden, because a screen reader announcing "zero one" before every
   project title is noise. */
.work-no {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--color-text-subtle);
  margin-bottom: var(--space-2);
}
.work:hover .work-no,
.work:focus-visible .work-no { color: var(--color-brand); }
@media (prefers-reduced-motion: no-preference) {
  .work-no { transition: color var(--dur-mid) var(--ease-out); }
}

/* The reference puts "explore the projects" beside its heading. Every demo we
   have is already on this page, so that link would have had nowhere to go —
   pointing it back at the section it sits in is the kind of thing that looks
   like a feature and works like a dead end. It goes to the order form instead,
   which is what someone who just liked the work actually wants next. */
.head-link {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--color-brand);
  text-decoration: none;
  min-height: 44px;
}
.head-link .icon { transition: transform var(--dur-mid) var(--ease-out); }
.head-link:hover .icon, .head-link:focus-visible .icon { transform: translateX(4px); }
.work h3 { font-size: var(--text-lg); margin-bottom: var(--space-1); display: flex; align-items: center; gap: 6px; }
.work h3 .ext { color: var(--color-text-muted); flex: none; transition: transform var(--dur-mid) var(--ease-out), color var(--dur-mid) var(--ease-out); }
.work:hover h3 .ext { color: var(--color-brand-text); transform: translate(2px, -2px); }
.work p { font-size: var(--text-sm); color: var(--color-text-muted); }
.work-note { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: var(--space-5); font-size: var(--text-sm); color: var(--color-text-muted); }
.work-note .icon { color: var(--color-brand-text); }
@media (prefers-reduced-motion: reduce) {
  .work .live-badge .dot { animation: none; }
  .work .thumb .preview { filter: none; transition: none; }
  .work .thumb .tag { transition: none; }
}

/* ---- Pricing (10 trust) ------------------------------------------------- */
.price-grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(3, 1fr); align-items: stretch; }
.price { display: flex; flex-direction: column; position: relative; }
.price.featured { border-color: var(--color-brand); box-shadow: var(--shadow-lg); position: relative; }
/* These had no hover state at all, which made the three cards that ask for money
   the only inert things on a page where everything else answers a cursor. The
   border is left alone on purpose — it is what marks the recommended plan. */
@media (hover: hover) {
  .card.price:hover { transform: translateY(-4px) scale(1.02); box-shadow: var(--shadow-lg); }
  .card.price.featured:hover { box-shadow: var(--shadow-xl); }
}
.price .ribbon { position: absolute; top: calc(-1 * var(--space-3)); left: 50%; transform: translateX(-50%); background: var(--color-brand); color: var(--color-text-on-brand); font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: var(--space-1) var(--space-4); border-radius: var(--radius-full); box-shadow: var(--shadow-sm); }
/* Ticket stamp for the featured plan: tilted, punched border. */
.card.price.featured .ribbon { transform: translateX(-50%) rotate(-4deg); border: 1.5px solid var(--color-text-on-brand); }
/* ...and it is stamped ON, once, when the price ticket scrolls up. It comes down
   from above the card, overshoots past its resting angle the way a hand-held
   stamp bounces off paper, and settles. Held back .3s behind the card's own
   reveal so it reads as a second beat — a stamp landing on a ticket that is
   already there — instead of arriving with it and reading as one static block.
   No idle loop: a badge that pulses forever next to a price is a nag, and this
   one has to still be readable on the tenth second of someone comparing plans.

   Movement only — no fade. Fading it in meant the badge spent the first half of
   the stamp too faint to see, so the part that carries the gesture (the drop,
   the overshoot, the settle) was over by the time it was solid enough to read.
   It is fully opaque the whole way down; the card's own reveal already brings it
   onto the page. Nothing here touches opacity, so the .js gate is gone with it:
   the badge is never hidden by CSS, script or no script. */
.js .card.price.featured.reveal.in .ribbon {
  animation: ribbonStamp .46s cubic-bezier(.2,.75,.3,1) .18s both;
}
@keyframes ribbonStamp {
  0%   { transform: translateX(-50%) rotate(7deg)    scale(1.75); }
  60%  { transform: translateX(-50%) rotate(-7deg)   scale(.93); }
  80%  { transform: translateX(-50%) rotate(-2.2deg) scale(1.045); }
  100% { transform: translateX(-50%) rotate(-4deg)   scale(1); }
}
.price h3 { font-size: var(--text-lg); color: var(--color-text-muted); font-family: var(--font-display); }
.price .amount { font-family: var(--font-mono); font-weight: var(--weight-bold); font-size: var(--text-4xl); margin-block: var(--space-2) var(--space-1); }
/* IBM Plex Mono has no Thai glyphs — a Thai price ("ประเมินตามงาน") would fall
   back to the OS monospace and stop matching the two numeric cards beside it. */
.price .amount--text { font-family: var(--font-display); font-size: var(--text-2xl); }
.price .amount .cur { font-size: var(--text-xl); color: var(--color-text-muted); vertical-align: super; }
.price .per { font-size: var(--text-sm); color: var(--color-text-subtle); }
.price ul { flex: 1; }
.price .btn { margin-top: var(--space-5); }

/* Ticket head (4.2): "TS-01" order number printed above each price card,
   with a perforation + notch below it — the card reads as a tear-off ticket. */
.ticket-head { display: flex; justify-content: space-between; align-items: center; padding-bottom: var(--space-3); margin-bottom: var(--space-4); }
.ticket-no { font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0.06em; color: var(--color-text-subtle); }
/* The tear line and the two punched notches must sit on ONE row, so both hang
   off .ticket-head itself rather than being positioned by hand against the card
   (a fixed `top` drifts the moment the head's height changes). The head bleeds
   out to the card edges with a negative inline margin = the card's padding. */
.price .ticket-head {
  position: relative;
  margin: 0 calc(-1 * var(--space-6)) var(--space-5);
  padding: 0 var(--space-6) var(--space-4);
}
.price .ticket-head::after {           /* perforation */
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background-image: repeating-linear-gradient(90deg, var(--color-border-strong) 0 6px, transparent 6px 12px);
}
.price .ticket-head::before {          /* the bite taken out of both edges */
  content: ""; position: absolute; left: -9px; right: -9px; bottom: -8px; height: 16px;
  background:
    radial-gradient(circle 8px at 0 50%, var(--color-bg) 98%, transparent 100%) left / 16px 16px no-repeat,
    radial-gradient(circle 8px at 100% 50%, var(--color-bg) 98%, transparent 100%) right / 16px 16px no-repeat;
  pointer-events: none;
}

/* ---- Testimonials ------------------------------------------------------- */
.quote p { font-size: var(--text-base); color: var(--color-text); }
.quote .stars { color: var(--warning-500); margin-bottom: var(--space-3); letter-spacing: 2px; }
.quote .who { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-4); }
.quote .avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; background: var(--color-brand); color: var(--color-text-on-brand); display: grid; place-items: center; font-family: var(--font-display); font-weight: var(--weight-bold); }
.quote .who b { font-family: var(--font-display); font-size: var(--text-sm); }
.quote .who span { display: block; font-size: var(--text-xs); color: var(--color-text-subtle); }

/* ---- Team / Member ------------------------------------------------------ */
.team-grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); max-width: 46rem; margin-inline: auto; }
.member { text-align: center; }
.member-avatar {
  width: 4rem; height: 4rem; border-radius: 50%; margin: 0 auto var(--space-4);
  background: var(--color-brand); color: var(--color-text-on-brand); display: grid; place-items: center;
  font-family: var(--font-display); font-weight: var(--weight-bold); font-size: var(--text-sm); letter-spacing: 1px;
}
/* The hat hangs off the top of the circle, so this box must not clip and must
   be the positioning parent for it. */
.member-avatar--emoji { background: var(--color-brand-soft); position: relative; overflow: visible; }
.member-emoji { width: 2.75rem; height: 2.75rem; display: block; }
.member-hat {
  position: absolute; width: 1.55rem; height: 1.55rem;
  top: 0; left: 50%;
  /* Centred and upright — tilted read as crooked, not as jaunty. */
  transform: translateX(-50%);
  transform-origin: bottom center;
  filter: drop-shadow(0 1px 1px rgb(0 0 0 / .18));
  pointer-events: none;
}
/* The mortarboard is a flat board, not a dome: at the shared height it buries the
   chick's head, so it sits a notch higher. */
.member-hat--high { top: -0.3rem; }
.member h3 { margin-bottom: var(--space-1); }
.member-role { display: block; font-size: var(--text-sm); color: var(--color-brand-text); font-weight: var(--weight-medium); margin-bottom: var(--space-3); }
.member p { font-size: var(--text-sm); }
.member ul { width: max-content; max-width: 100%; margin-inline: 0; text-align: start; }
/* The shadow on this sheet is a hard print offset, not a blur — so a member card
   lifts off the page diagonally and the offset grows to match, the way a sticker
   peels. The avatar rides along a touch further so the face leads the move.
   Deliberately bigger than the shared .card--hover lift: these are the two faces
   of the studio, and they are not links, so the motion is warmth, not a click cue. */
.member.card--hover:hover { transform: translate(-3px, -5px); box-shadow: var(--shadow-xl); border-color: var(--color-border-strong); }
.member-avatar { transition: transform var(--dur-mid) var(--ease-out); }
.member.card--hover:hover .member-avatar { transform: translateY(-3px) scale(1.06); }

/* ---- FAQ (native details = accessible) ---------------------------------- */
.faq { max-width: var(--container-narrow); margin-inline: auto; display: grid; gap: var(--space-3); counter-reset: faq; }
.faq details { background: var(--color-surface); border: 1px solid var(--color-border); border-top: 2px dashed var(--color-border-strong); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); }
.faq details[open] { border-color: var(--color-border-strong); }
/* Closed only. An open answer is being read, and a block of text that lifts when
   the cursor crosses it is a distraction rather than an invitation. No scale
   either — these run the full width of the column, and a wide bar that grows
   reads as a glitch where a card that grows reads as a card. */
@media (hover: hover) {
  .faq details:not([open]):hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--color-border-strong); }
}
.faq details::before {
  counter-increment: faq;
  content: "Q" counter(faq, decimal-leading-zero);
  display: block; font-family: var(--font-mono); font-weight: 600;
  font-size: var(--text-xs); letter-spacing: 0.04em; color: var(--color-brand-text);
  margin-bottom: var(--space-2);
}
.faq summary { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); min-height: 44px; font-family: var(--font-display); font-weight: var(--weight-semibold); font-size: var(--text-lg); }
.faq summary::-webkit-details-marker { display: none; }
.faq summary .chev { flex: none; transition: transform var(--dur-mid) var(--ease-out); color: var(--color-text-muted); }
.faq details[open] summary .chev { transform: rotate(180deg); }
/* 65ch keeps answers at a readable measure; they ran 102 chars at 1440px. */
.faq details p { color: var(--color-text-muted); margin-top: var(--space-3); max-width: 65ch; }

/* ---- CTA band ----------------------------------------------------------- */
/* Dark kitchen-board slab instead of the gradient blob, with a diagonal
   hairline over it so it reads as printed stock rather than a glow. */
.cta-band { position: relative; overflow: hidden; border-radius: var(--radius-xl); background: var(--charcoal-900); color: var(--paper-50); padding: var(--space-8); text-align: center; box-shadow: var(--shadow-xl); }
.cta-band::after {
  content:""; position:absolute; inset:0;
  background-image: repeating-linear-gradient(45deg, oklch(1 0 0 / 0.04) 0 2px, transparent 2px 16px);
}
.cta-band > * { position: relative; }
.cta-band h2 { font-size: var(--text-3xl); }
.cta-band p { margin-top: var(--space-3); font-size: var(--text-lg); opacity: 0.92; max-width: 40rem; margin-inline: auto; }
.cta-band .hero-cta { justify-content: center; margin-top: var(--space-6); }
.cta-band .btn--primary { --_bg: var(--ember-500); --_fg: var(--charcoal-950); }
.cta-band .btn--primary:hover { --_bg: var(--ember-400); }
.cta-band .btn--secondary { --_bg: transparent; --_fg: var(--paper-50); border-color: oklch(1 0 0 / 0.5); }
.cta-band .btn--secondary:hover { --_bg: oklch(1 0 0 / 0.12); }

/* ---- Contact form (4b/4c) ---------------------------------------------- */
.contact-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: var(--space-8); align-items: start; }
.contact-info .contact-item { display: flex; gap: var(--space-3); margin-bottom: var(--space-5); }
.contact-info .contact-item .ci-icon { width: 2.75rem; height: 2.75rem; flex: none; border-radius: var(--radius-md); background: var(--color-brand-soft); color: var(--color-brand-text); display: grid; place-items: center; }
.contact-info .contact-item b { font-family: var(--font-display); display: block; }
.contact-info .contact-item a, .contact-info .contact-item span { color: var(--color-text-muted); }
.contact-info .contact-item a { display: inline-flex; align-items: center; min-height: 44px; }

.form-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: var(--space-6); box-shadow: var(--shadow-md); }
/* Order-ticket header for the contact form (3.9): reuses .ticket-head/.ticket-no
   from pricing (4.2); .perforation is the same tear-line, as a standalone
   utility since the form isn't a .card.price. */
.ticket-head--form { font-family: var(--font-display); font-weight: var(--weight-semibold); font-size: var(--text-base); color: var(--color-text); }
.perforation {
  height: 1px; margin: 0 -1px var(--space-4);
  background-image: repeating-linear-gradient(90deg, var(--color-border-strong) 0 6px, transparent 6px 12px);
}
.field { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.field label { font-family: var(--font-display); font-weight: var(--weight-medium); font-size: var(--text-sm); }
.field label .req { color: var(--color-error); }
.field .hint { font-size: var(--text-xs); color: var(--color-text-subtle); }
.field input, .field select, .field textarea {
  /* min-height because WebKit's native <select> ignores most of this padding
     and rendered 29px tall next to 52px inputs — uneven and hard to tap. */
  font: inherit; width: 100%; padding: 0.75rem 0.9rem; min-height: 3.25rem;
  background: var(--color-bg); color: var(--color-text);
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-md);
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
.field textarea { min-height: 120px; resize: vertical; }
/* WebKit's native select ignores padding AND min-height, so it rendered 29px
   next to 52px inputs. Drop the native chrome and draw our own chevron. */
.field select {
  appearance: none; -webkit-appearance: none;
  padding-inline-end: 2.5rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23787984' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.9rem center;
  background-size: 1.15rem 1.15rem;
}
:root[data-theme="dark"] .field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a1a2ad' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--color-focus); box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-focus) 25%, transparent); }
.field input[aria-invalid="true"], .field textarea[aria-invalid="true"] { border-color: var(--color-error); }
.field .error-msg { display: none; color: var(--color-error); font-size: var(--text-xs); align-items: center; gap: var(--space-1); }
.field.invalid .error-msg { display: flex; }

.form-status { display: none; align-items: flex-start; flex-wrap: wrap; gap: var(--space-3); padding: var(--space-4); border-radius: var(--radius-md); margin-bottom: var(--space-4); font-size: var(--text-sm); }
.form-status.success { display: flex; background: var(--color-success-bg); color: var(--color-success); border: 1px solid color-mix(in oklch, var(--color-success) 40%, transparent); }
.form-status.error { display: flex; background: var(--color-error-bg); color: var(--color-error); border: 1px solid color-mix(in oklch, var(--color-error) 40%, transparent); }
.form-status .icon { flex: none; }
/* The status box now carries content under its line of text, so the text and the
   rescue buttons need to stack rather than sit beside the icon. */
.form-status > span { flex: 1 1 auto; }
.fs-rescue {
  flex-basis: 100%; display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-top: var(--space-3);
}
.fs-rescue .btn { font-size: var(--text-sm); padding: 0.6rem 1rem; min-height: 40px; }
.form-status .ic-ok, .form-status .ic-err { display: none; }
.form-status.success .ic-ok { display: block; }
.form-status.error .ic-err { display: block; }

/* Honeypot — kept in the DOM (bots fill it) but off-screen for humans */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- Footer ------------------------------------------------------------- */
/* The sign-off. Every other word on this page is Thai; this one is not, because
   Anuphan's lightest cut is 300 and Thai does not thin out the way Latin does at
   that size — the vowels and tone marks sit above and below the line and keep
   their mass, so a Thai word this large reads heavy no matter the weight. SERVED
   is the payoff the whole page has been building to: the kitchen opens, the
   ticket goes up, the order is cooked, and this is the plate landing. Six
   characters, sized in vw so it spans the screen rather than the container, and
   set whole rather than cropped. The negative top margin is Anuphan's metrics,
   not taste. Measured at weight 300: ascent 1.0239em, descent 0.2763em, cap
   height 0.7100em. With line-height 1 that leaves 0.1638em of air above the
   capitals, and a line box tuned by eye would have left that gap in and looked
   like a mistake — trimming exactly that amount puts the top of the box on the
   letters themselves. Below them the font's own 0.1142em of air is kept and a
   margin added on top of it: the word had been sitting flush on the page edge,
   which read as cropped even though every letter was whole. It now lands with
   floor under it, the way a plate has table around it. */
.wordmark {
  overflow: hidden;
  margin-top: var(--space-8);
  padding-bottom: 0;
  text-align: center;
  user-select: none;
}
.wordmark span {
  display: block;
  line-height: 1;
  margin-top: -0.164em;        /* air above the capitals */
  margin-bottom: -0.06em;      /* trims part of the descender gap under the baseline */
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 27.4vw;           /* measured: puts the word at ~93% of the viewport at every width */
  letter-spacing: -0.035em;
  white-space: nowrap;
  color: var(--color-text);
}
@media (prefers-reduced-motion: no-preference) {
  .wordmark span { transition: color var(--dur-mid) var(--ease-out); }
}

/* ---- The wordmark's entrance is a wipe, not a fade -----------------------
   A fade here stuttered, and the reason is size, not timing. This one element is
   the full width of the viewport and ~370px of glyph on a laptop; animating its
   opacity means every frame is a fresh composite of that whole slab against the
   footer's grain tile underneath. The word is also the last thing on the page,
   so the animation runs while the scroll is still moving — the two most expensive
   things the page does, at the same moment.

   A transform on the inner span costs the compositor nothing: the text is
   rasterised once and then only moved. The container already has overflow:hidden,
   so pushing the span down by its own height hides it completely and it rises
   back into a fixed frame. Which is also the right gesture — the page has been a
   kitchen the whole way down, and the plate comes UP to the pass. It does not
   materialise out of nothing.

   Same 1100ms and same expo-out as every other reveal; only the move is bespoke,
   and only because no other element on the page has a mask to rise out of. */
.js .wordmark.reveal { opacity: 1; transform: none; transition: none; }
.js .wordmark.reveal span { transform: translateY(100%); }
.js .wordmark.reveal:not(.in) span { will-change: transform; }
@media (prefers-reduced-motion: no-preference) {
  .wordmark.reveal span {
    transition:
      transform 1100ms cubic-bezier(0.33, 0, 0.3, 1),
      color var(--dur-mid) var(--ease-out);
  }
}
.js .wordmark.reveal.in span { transform: none; }

.site-footer {
  background-color: var(--color-bg-subtle);
  background-image: var(--grain-surface);
  background-size: 160px 160px;
  border-top: 1px solid var(--color-border);
  padding-block: var(--space-8) 0;   /* 0 at the bottom so the wordmark reaches the page edge */
  margin-top: var(--space-9);
}
.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: var(--space-7); }
.footer-brand p { color: var(--color-text-muted); font-size: var(--text-sm); margin-top: var(--space-4); max-width: 22rem; }
.footer-col h3 { font-family: var(--font-display); font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-subtle); margin-bottom: var(--space-4); }
.footer-col ul { list-style: none; padding: 0; display: grid; }
/* Tap area, not just text height: 22px links were the smallest targets on the page. */
.footer-col a { color: var(--color-text-muted); font-size: var(--text-sm); display: flex; align-items: center; min-height: 44px; }
.footer-col a:hover { color: var(--color-text); }
/* 44px per row is the tap target, and on a phone it has to stay. With a mouse
   there is nothing to miss, and at that height four 14px links spread over 176px
   read as four separate things rather than one list. 32px closes the column up
   without taking the row below the line height of the text inside it. */
@media (pointer: fine) {
  .footer-col a { min-height: 32px; }
}
.socials { display: flex; gap: var(--space-2); margin-top: var(--space-4); }
.footer-bottom { display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: space-between; align-items: center; margin-top: var(--space-7); padding-top: var(--space-5); border-top: 1px solid var(--color-border); font-size: var(--text-sm); color: var(--color-text-subtle); }

/* ---- Phone order bar ----------------------------------------------------
   Desktop keeps the header CTA; below 720px that button is hidden, so this is the
   only standing way to act on the page from the middle of it. */
.order-bar { display: none; }
@media (max-width: 720px) {
  .order-bar {
    display: flex; gap: var(--space-2); align-items: center;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 150;
    padding: var(--space-3) var(--space-4);
    padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
    background: color-mix(in oklch, var(--color-bg) 88%, transparent);
    backdrop-filter: blur(8px);
    border-top: 1px solid var(--color-border);
    /* Parked off-screen rather than hidden, so it slides rather than pops. */
    transform: translateY(110%);
    /* visibility, not just transform: a translated-away bar is still focusable, so
       the links stayed in the tab order while marked aria-hidden. Delayed on exit so
       the slide-out is still seen. */
    visibility: hidden;
    transition: transform var(--dur-mid) var(--ease-out),
                visibility 0s linear var(--dur-mid);
  }
  .order-bar.up {
    transform: none; visibility: visible;
    transition: transform var(--dur-mid) var(--ease-out), visibility 0s;
  }
  /* The drawer carries its own CTA. Leaving the order bar up behind an open menu
     put two competing "order with us" buttons on a 390px screen at once, with the
     bar sitting in the dead strip below the drawer where it reads as leftover
     furniture rather than an offer. It slides back the moment the menu closes. */
  :root.menu-open .order-bar.up {
    transform: translateY(110%); visibility: hidden;
    transition: transform var(--dur-mid) var(--ease-out),
                visibility 0s linear var(--dur-mid);
  }
  .order-bar .btn { min-height: 46px; }
  .order-bar .btn--secondary { flex: 0 0 auto; }
  /* Nothing should end up underneath the bar. */
  body { padding-bottom: 4.5rem; }
}

/* ---- The egg, used as a mark ---------------------------------------------
   The studio already owns a motif — a fried egg, the thing you actually order
   ตามสั่ง — and it appeared exactly twice on the whole page, in the header and the
   footer. Maya Green draws one leaf and repeats it until the shape becomes the
   brand. This does the same with the egg: it is the bullet on every section
   eyebrow, the tick in the QC list, and a large watermark behind the dark band.
   Drawn in CSS from two radial gradients rather than an SVG copy, so it can be
   dropped on anything with one class and costs no markup. */
.egg-dot {
  display: inline-block; flex: none;
  width: 1em; height: 1em; vertical-align: -0.12em;
  background:
    radial-gradient(circle at 62% 40%, var(--color-brand) 0 27%, transparent 28%),
    radial-gradient(circle at 45% 52%, var(--paper-25) 0 46%, transparent 47%);
  filter: drop-shadow(0 0 0 transparent);
}
/* On a pale ground the white of the egg needs an edge to exist at all. */
.egg-dot--ink { background:
    radial-gradient(circle at 62% 40%, var(--color-brand) 0 27%, transparent 28%),
    radial-gradient(circle at 45% 52%, var(--charcoal-200) 0 46%, transparent 47%); }

/* The eyebrow stops being a SaaS pill and becomes a mark plus a word. */
.eyebrow {
  display: inline-flex; align-items: center; gap: .5em;
  background: none; padding: 0; border-radius: 0;
  font-family: var(--font-mono);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--color-text-subtle);
}
.eyebrow::before {
  content: ""; width: 1.05em; height: 1.05em; flex: none;
  background:
    radial-gradient(circle at 62% 40%, var(--color-brand) 0 27%, transparent 28%),
    radial-gradient(circle at 45% 52%, currentColor 0 46%, transparent 47%);
  opacity: .9;
}
.section--ink .eyebrow { color: var(--ember-300); background: none; }

/* One egg, blown up and half-hidden, so the dark band has something of its own
   behind the type instead of being a flat rectangle. */
.section--ink {
  isolation: isolate;
  /* This band is dark whatever the theme, so it always wants the lit flame —
     the light theme's --color-flame is a deep blue and would vanish here. */
  --color-flame: var(--flame-400);
}
.section--ink::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  right: -6%; top: 50%; transform: translateY(-50%);
  width: min(46vw, 34rem); aspect-ratio: 1;
  background:
    radial-gradient(circle at 62% 40%, var(--color-brand) 0 27%, transparent 28%),
    radial-gradient(circle at 45% 52%, var(--paper-25) 0 46%, transparent 47%);
  opacity: .05;
}
.section--ink > * { position: relative; z-index: 1; }

/* The gas ring. Used on exactly two things — the live dot on a work card, and
   the process rail — because a flame is a detail in a kitchen, not the walls. */
.work .live-badge .dot { background: var(--color-flame); box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-flame) 70%, transparent); }
@keyframes livePulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-flame) 70%, transparent); } 70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
.section--ink .steps::before { border-top-color: color-mix(in oklch, var(--color-flame) 55%, transparent); }

/* ---- Ambient motion ------------------------------------------------------
   Everything that moved on this page until now needed to be hovered, focused or
   scrolled to. Nothing moved on its own, which is the difference between a page
   that feels alive and one that only reacts — the ticker and the drifting light
   are the two things zamil has that this did not.

   The ingredient rail. */
.rail-strip {
  margin-top: clamp(1.5rem, 4vh, 3rem);
  position: relative;
  overflow: hidden;
}
/* Overlay gradients, not mask-image. A mask on a container whose child is moving
   has to be recomputed every frame — the whole strip, forty gradient eggs and all,
   repainted continuously — and a theme change invalidates the lot at once, which
   is the hitch. Two overlays composite instead, and cost nothing per frame. */
.rail-strip::before, .rail-strip::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 10%;
  z-index: 1; pointer-events: none;
}
.rail-strip::before { left: 0;  background: linear-gradient(90deg, var(--color-bg), transparent); }
.rail-strip::after  { right: 0; background: linear-gradient(270deg, var(--color-bg), transparent); }
.rail-track {
  display: flex; width: max-content;
  /* Promoted, so the travel is a compositor transform rather than a repaint of
     the entire strip on every frame. Same fix the drifting lights needed. */
  will-change: transform;
  animation: rail 38s linear infinite;
}
.rail-set { display: flex; align-items: center; flex: none; }
.rail-set span {
  font-family: var(--font-display); font-weight: var(--weight-semibold);
  font-size: var(--text-lg); color: var(--color-text-subtle);
  padding-inline: clamp(1rem, 2.5vw, 2rem); white-space: nowrap;
}
/* The egg again, this time as the separator between ingredients. */
.rail-set i {
  width: .5rem; height: .5rem; flex: none;
  background:
    radial-gradient(circle at 62% 40%, var(--color-brand) 0 27%, transparent 28%),
    radial-gradient(circle at 45% 52%, var(--color-border-strong) 0 46%, transparent 47%);
}
/* Travels exactly one set, so the seam lands where the copy begins. */
@keyframes rail { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.rail-strip:hover .rail-track { animation-play-state: paused; }

/* Two slow pools of light on the dark band. They are the only thing on the page
   that moves without being asked, and they are what a hot kitchen actually looks
   like: the ember of the ring and the blue of the flame, neither of them still. */
.section--ink { overflow: hidden; }
.section--ink::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  width: 46vw; aspect-ratio: 1; left: -12vw; top: -18%;
  background: radial-gradient(circle, color-mix(in oklch, var(--color-brand) 42%, transparent) 0%, transparent 66%);
  /* No filter: the gradient is the softness. A blur on top of it cost a full
     re-rasterise of a 662px square every frame and bought nothing. */
  will-change: transform;
  animation: drift-a 26s ease-in-out infinite alternate;
}
.statement::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  width: 40vw; aspect-ratio: 1; right: -10vw; bottom: -24%;
  background: radial-gradient(circle, color-mix(in oklch, var(--flame-200) 30%, transparent) 0%, transparent 68%);
  will-change: transform;
  animation: drift-b 31s ease-in-out infinite alternate;
}
@keyframes drift-a { from { transform: translate3d(0,0,0) scale(1); } to { transform: translate3d(9vw, 7%, 0) scale(1.16); } }
@keyframes drift-b { from { transform: translate3d(0,0,0) scale(1.1); } to { transform: translate3d(-8vw, -9%, 0) scale(1); } }
.statement-in, .statement::after { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  /* Ambient motion is the first thing to go — it is atmosphere, and atmosphere is
     exactly what someone asking for less movement does not want. The light stays,
     it simply stops moving. */
  .rail-track, .section--ink::before, .statement::before { animation: none; }
}

/* ---- Services bento -------------------------------------------------------
   All six on screen at once, no click needed — the picker that stood here hid
   five services behind a sixth. Uneven tiles rather than a six-up grid: the two
   that pay the bills get the room, the rest are read in a glance, and the hole
   the odd sixth tile leaves is filled by the one thing the section was missing
   for months — somewhere to press.

   The reference this follows is indigo-on-white with soft blurred cards; only
   its composition is borrowed. Stock, grain, ember and the 1px offset shadow
   stay, so the section still belongs to this page. */
.bento { display: grid; gap: var(--space-4); }
.svc { position: relative; display: flex; flex-direction: column; padding: var(--space-5); }
/* Two rows: the two that carry the money across the top, three in a band beneath.
   Three rows of tiles made the section taller than a screen for five sentences. */
@media (min-width: 1000px) {
  .bento { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .svc { grid-column: span 6; }
  .svc--third { grid-column: span 4; }
}
/* Grows under the cursor rather than only lifting, and overshoots by a hair on
   the way — the same swell read as slow on the page's stock ease, which is tuned
   for panels sliding rather than something answering a cursor. Only the transform
   springs; shadow and border keep the page's ease so the card does not look like
   it is made of two materials.

   1.015, and no z-index. At 1.025 a half-width tile grew 8px a side into an 8px
   half-gutter and had to be lifted over its neighbour to avoid being clipped —
   which is the tile covering the one next to it, not the tile growing. It stays
   inside its own gap now. */
@media (hover: hover) {
  .svc.card--hover:hover { transform: translateY(-3px) scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
  .svc.card--hover:hover { transform: none; }
}

/* ---- One spring for everything that answers a cursor ----------------------
   Service tiles, work cards, the price plans and a closed question all move the
   same way now: overshoot, settle, 380ms. They were on the page's stock ease at
   250ms, which is the timing for a panel sliding into place — correct for
   layout, wrong for something reacting to you.

   The two team cards are deliberately left out. Their peel is warmth rather than
   a click cue — they are not links — and a spring made the faces read as
   playful in a spot the page keeps quiet.

   Only transform springs. Shadow and border stay on the stock ease: a hard
   print shadow that overshoots looks like a printing error, not a bounce. */
.svc.card--hover, .card.work, .card.price, .faq details {
  transition: transform var(--dur-spring) var(--ease-spring),
              box-shadow var(--dur-spring) var(--ease-out),
              border-color var(--dur-mid) var(--ease-out);
}
@media (min-width: 700px) and (max-width: 999px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* One column until the tiles are wide enough to hold a Thai line. Two columns in
   the 500s put three words on a line and turned every card into a column of
   fragments. */
/* The recommended one, and it says so. Tinted from the brand rather than
   --color-bg-subtle, which equals --color-surface in the dark theme and so did
   nothing there at all. */
.svc--feat {
  background-color: color-mix(in oklch, var(--color-brand) 7%, var(--color-surface));
  border-color: var(--color-border-strong);
}
.svc-ico {
  display: grid; place-items: center; width: 2.75rem; height: 2.75rem;
  margin-bottom: var(--space-4);
  background: color-mix(in oklch, var(--color-brand) 12%, var(--color-surface));
  border-radius: var(--radius-md); color: var(--color-brand);
}
.svc-ico svg { width: 1.5rem; height: 1.5rem; stroke-linecap: round; stroke-linejoin: round; }
/* NOT display:flex on the heading. A flex container makes every text node and
   every <span> its own item, so the gap lands inside words — Thai has no spaces,
   and .nb wraps the last word of most of these names. */
.svc h3 { margin: 0 0 var(--space-2); font-size: var(--text-xl); line-height: var(--leading-snug); }
.svc--third h3 { font-size: var(--text-lg); }
.svc-tag {
  display: inline-block; vertical-align: 0.18em; margin-inline-start: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--color-text-on-brand); background: var(--color-brand);
  padding: 0.1rem var(--space-2); border-radius: var(--radius-sm);
}
.svc > p { margin: 0 0 var(--space-4); }
.svc ul { margin-top: var(--space-3); margin-bottom: var(--space-4); }
/* Pinned to the floor of the tile, so the link reads across a row instead of
   drifting with the length of the sentence above it. */
.svc-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2) var(--space-3); flex-wrap: wrap;
  margin-top: auto; padding-top: var(--space-3);
  border-top: 1px dashed var(--color-border);
}
.svc-go {
  font-size: var(--text-sm); white-space: nowrap; color: var(--color-brand-text);
  text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px;
}
@media (hover: hover) { .svc-go:hover { color: var(--color-text); } }
/* The way out of the section for anyone who cannot tell which of the six is
   theirs — one line under the tiles rather than a tile of its own. */
.bento-ask {
  margin: var(--space-4) 0 0; text-align: center;
  font-size: var(--text-sm); color: var(--color-text-muted);
}
.bento-ask a {
  color: var(--color-brand-text); text-decoration: none;
  border-bottom: 1px solid currentColor; padding-bottom: 1px;
  white-space: nowrap;
}
@media (hover: hover) { .bento-ask a:hover { color: var(--color-text); } }

/* ---- Statement band -----------------------------------------------------
   The page's second anchor, and the only place on it that is nothing but words.
   Every other section is a grid of cards; this one is a held breath between the
   proof and the price. Ember rather than charcoal, so the two full-bleed bands
   read as different beats instead of a repeat, and far enough apart on the
   scroll that they never share a screen. */
.statement {
  /* Named, so the dark theme can move them. Measured for contrast against
     charcoal-950 body text: 5.4:1 at the head, 4.7:1 at the foot. */
  --statement-a: oklch(0.64 0.20 40);
  --statement-b: oklch(0.605 0.205 37);
  position: relative;
  /* Shallow on purpose: the darker end of a fuller ember gradient drops body
     text under 4.5:1. Measured 5.0:1 at the top, 4.6:1 at the foot. */
  background: linear-gradient(158deg, var(--statement-a) 0%, var(--statement-b) 100%);
  color: var(--color-text-on-brand, #fff);
  padding-block: clamp(3.5rem, 11vh, 7rem);
  overflow: hidden;
}
/* A faint ticket-paper tooth, so a large flat colour does not read as a banner. */
.statement::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .10;
  background-image:
    repeating-linear-gradient(90deg, rgba(20,12,8,.5) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply;
}
.statement-in { position: relative; z-index: 1; text-align: center; max-width: 54rem; }
.statement-mark {
  display: inline-block; font-family: var(--font-mono);
  font-size: var(--text-sm); letter-spacing: .28em; text-transform: uppercase;
  /* Full strength, not 78%: measured 4.28:1 when thinned, under the 4.5 this
     size needs. It inherits the band colour, so it follows the dark theme. */
  color: inherit;
  margin-bottom: clamp(1rem, 2.5vh, 1.75rem);
}
.statement-h {
  /* Bigger than any other heading on the page — this is the moment, and the
     section carries no other content to compete with it. */
  font-size: clamp(2.4rem, 6.5vw, 5rem);
  line-height: 1.08;
  margin-bottom: clamp(1.25rem, 3vh, 2rem);
}
.statement-h em {
  font-style: normal;
  /* Struck through with the ticket's own dashed rule rather than coloured: on an
     ember ground there is no second colour left that would still read. */
  /* currentColor, so the rule flips with the text when the band goes dark —
     pinned to the light theme it measured 1.64:1 and simply vanished. */
  box-shadow: inset 0 -0.14em 0 color-mix(in oklch, currentColor 55%, transparent);
}
.statement-p {
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  /* No opacity here: it was thinning text that already had the least contrast
     on the page. */
  line-height: 1.75; margin-inline: auto; max-width: 44rem;
}
/* In the dark theme every other surface drops to charcoal; an untouched ember
   band would be the one thing on the page still shouting. Deepened, and the text
   flips to paper because dark-on-dark-ember is unreadable. */
:root[data-theme="dark"] .statement {
  --statement-a: oklch(0.44 0.17 34);
  --statement-b: oklch(0.36 0.15 32);
  color: var(--paper-25);
}
:root[data-theme="dark"] .statement::after { background-image: none; }
@media (max-width: 720px) { .statement-in { text-align: start; } }

/* ---- Scroll-reveal (respects reduced-motion via JS gate) ---------------- */
/* The entrance only. .reveal and .card carry the same specificity and this rule
   comes later, so a bare `transition` here was overriding the hover transition of
   every `card card--hover reveal`: the lift stretched to the entrance duration AND
   kept its stagger delay, while the shadow snapped because it had fallen out of the
   property list. Once revealed, the card gets its own transition back. */
/* The hidden state is gated on html.js. Without scripting — or with a script that
   threw before the observer was wired — none of this applies and the page simply
   reads. Every modifier below inherits that safety by living under the same gate. */
/* ---- Leaving the page ----------------------------------------------------
   A link to another page used to be an instant white cut: the old page vanishes,
   the browser paints nothing, the new one appears. Cross-document view
   transitions hand that gap to the browser to animate, so the two pages
   cross-fade instead of snapping.

   Browsers without it simply navigate as before — this is opt-in and inert
   everywhere else, and the JS below covers the same ground for them. */
@view-transition { navigation: auto; }

::view-transition-old(root) { animation: vt-out 240ms cubic-bezier(0.33, 0, 0.3, 1) both; }
::view-transition-new(root) { animation: vt-in 380ms cubic-bezier(0.33, 0, 0.3, 1) both; }

/* ---- Theme wipe, ported from ScoreUp -------------------------------------
   The new theme sweeps over the old as a circle growing out of the toggle.

   Do NOT crossfade light and dark. Fading the text and the background at the same
   time drags both to the same mid-grey halfway through — ScoreUp measured the
   contrast going 16.8 → 1.1 → 16.8 — and what that reads as is the text
   flickering out and back. A wipe never blends the two themes: every pixel is at
   full contrast on both sides of the moving edge.

   Painted on one overlay element, NOT with view transitions. Safari draws the
   transition snapshot from the top of the document when the page is scrolled, so
   the whole page teleported to the top for the length of the wipe and snapped
   back on every toggle; an overlay has no snapshot to misplace. main.js builds
   the element, sets data-to to the theme the page is about to be, and animates
   its clip-path out of the toggle — everything it needs to be visible while that
   runs is here. Without these rules the element is an unstyled zero-height div
   and the toggle is a hard cut with no wipe at all, on every device. */
.theme-veil {
  position: fixed;
  inset: 0;
  z-index: 900;          /* over the header (500) and the mobile drawer (400) */
  pointer-events: none;  /* the page underneath stays live for the ~380ms */
  background: var(--color-bg);
  /* Belt and braces: main.js sets this as the first keyframe, but a browser that
     drops the animation would otherwise flash a full-screen block of the new
     colour before the swap. */
  clip-path: circle(0 at 50% 50%);
}
/* The veil is painted the colour the page is ABOUT to be — read off the opposite
   theme's tokens, since :root still carries the old one while the circle grows. */
.theme-veil[data-to="dark"]  { background: var(--charcoal-950); }
.theme-veil[data-to="light"] { background: var(--paper-25); }
/* The ambient animations hold still for the length of the wipe. A view transition
   replaces the live page with two static snapshots, so anything animating
   underneath is frozen on screen — but its clock keeps running, and when the
   snapshots are dropped it jumps forward by however long the wipe took. On a
   ticker crossing the screen that jump is very visible, and it is what reads as
   the thing stopping. Pausing means it resumes from exactly where it was.
   (ScoreUp never hit this: it has nothing running continuously to jump.) */
:root.theme-switching .rail-track,
:root.theme-switching .section--ink::before,
:root.theme-switching .statement::before {
  animation-play-state: paused;
}

/* Nothing transitions while the theme is being swapped. Every element carrying a
   transition on a colour fires one the instant the tokens change — measured here
   as the running animation count going from 30 to 93 on a single click — and all
   of them run underneath the wipe, where they cannot be seen anyway. The cost is
   real though: sixty-odd simultaneous colour transitions compete for the same
   frames as the wipe, the ticker and the reel, which is what made everything else
   look like it stalled. The wipe IS the animation; the colours behind its edge
   only have to be correct, not gradual. */
:root.theme-switching *,
:root.theme-switching *::before,
:root.theme-switching *::after {
  transition: none !important;
}
/* Except the switch itself. Its thumb sliding and the sun and moon turning past
   each other are the one thing that SHOULD move on a theme change — they are the
   feedback that the press registered. */
:root.theme-switching .theme-toggle,
:root.theme-switching .theme-toggle .tt-thumb {
  transition: transform .32s cubic-bezier(.4,.05,.2,1),
              background var(--dur-mid), border-color var(--dur-mid) !important;
}
:root.theme-switching .theme-toggle svg {
  transition: opacity .25s, transform .35s cubic-bezier(.4,.05,.2,1) !important;
}

@keyframes vt-out { to   { opacity: 0; transform: translateY(-8px); } }
@keyframes vt-in  { from { opacity: 0; transform: translateY(10px); } }

/* The fallback: the same fade, driven by a class, for browsers with no view
   transitions. It only ever runs on a link that is genuinely about to navigate. */
.is-leaving {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 240ms cubic-bezier(0.33, 0, 0.3, 1),
              transform 240ms cubic-bezier(0.33, 0, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  /* .theme-veil needs no rule here: main.js checks reduced motion first and
     swaps the theme outright without ever building the element. */
  .is-leaving { transition: none; opacity: 1; transform: none; }
}

/* ---- Motion ---------------------------------------------------------------
   Rebuilt on the Zamil system, which is the one that reads smooth.

   What made this feel like lag was never the frame rate. It was the timing. A
   reveal here started at rootMargin -8%, so an element had to be well inside the
   viewport before it began, and then ran for 700ms — scroll at any ordinary speed
   and you arrive at a block while it is still half transparent. Zamil does the
   opposite: a POSITIVE bottom margin, so a block starts revealing while it is
   still a quarter of a screen BELOW the fold, and the movement is finished by the
   time you get there. Its own source says so in as many words.

   Slower, and started earlier, is what reads as smooth. 1100ms on a long expo-out.

   The eight bespoke gestures are gone — a slip printing, a QC stamp, name tags, a
   rail, a cloche, a tear, a pot lid. Eight different ideas competing across one
   scroll, two of which were silently dead for days, and not one of them was why
   anything felt good or bad. Five primitives and one duration replace them.
   The hidden state stays gated on html.js, so a script that never runs leaves a
   page that simply reads. */
.js .reveal { opacity: 0; transform: translateY(20px); }
.reveal {
  transition:
    opacity 1100ms cubic-bezier(0.33, 0, 0.3, 1),
    transform 1100ms cubic-bezier(0.33, 0, 0.3, 1);
  transition-delay: var(--d, 0ms);
}
.reveal.in { opacity: 1; transform: none; }

/* Direction, chosen per element instead of one house move applied to everything. */
.js .reveal--left  { transform: translateX(-20px); }
.js .reveal--right { transform: translateX(20px); }
.js .reveal--scale { transform: scale(0.97); }
.js .reveal--fade  { transform: none; }
/* A pure fade needs its own curve: the shared expo-out spends most of its distance
   in the first fifth, which suits something travelling but makes an opacity change
   look abrupt no matter how long it runs. */
.reveal--fade { transition-timing-function: cubic-bezier(0.37, 0, 0.63, 1); }

/* The full-bleed bands: longer again, and no travel. A slab that size sliding is
   heavier than the abruptness it is meant to soften. */
.js .reveal--band { transform: none; }
.reveal--band { transition-duration: 1400ms; }

.reveal.d1 { --d: 90ms; } .reveal.d2 { --d: 180ms; } .reveal.d3 { --d: 270ms; }
.card--hover.in:hover { transition-delay: 0s; --d: 0ms; }

/* The order stamp stays: it is a receipt, not decoration. */
.stamp-ok {
  display: inline-block; margin-top: var(--space-3); align-self: flex-start;
  font-family: var(--font-display); font-weight: var(--weight-bold); font-size: var(--text-sm);
  letter-spacing: 0.04em; color: var(--color-success);
  border: 2px solid currentColor; border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
  transform: rotate(-8deg);
}
.stamp-ok.hit { animation: ts-accept var(--dur-mid) var(--ease-out); }
@keyframes ts-accept {
  from { opacity: 0; transform: scale(2) rotate(-18deg); }
  to   { opacity: 1; transform: scale(1) rotate(-8deg); }
}

@media (prefers-reduced-motion: reduce) {
  .reveal, .js .reveal { opacity: 1; transform: none; transition: none; }
  /* Every signature start state has to be cleared too. The blanket rule below only
     shortens durations — a card left at clip-path: inset(0 0 100% 0) would simply
     never be seen. */
  /* Every start state cleared: the blanket rule below only shortens durations,
     and a card left mid-transform would simply never arrive. */
  .js .reveal, .js .reveal--left, .js .reveal--right,
  .js .reveal--scale, .js .reveal--fade, .js .reveal--band { transform: none; opacity: 1; }
  .js .wordmark.reveal span { transform: none; }
  .faq details[open] > *:not(summary) { animation: none; }
  .stamp-ok.hit { animation: none; }
  .js .card.price.featured.reveal.in .ribbon { animation: none; transform: translateX(-50%) rotate(-4deg); }
  .brand:hover .yolk { animation: none; }
  .nav-links a::after { transition: none; }
  .work:hover .live-badge .dot { animation: none; }
  .btn:hover, .card--hover:hover,
  .member.card--hover:hover, .member.card--hover:hover .member-avatar,
  .work:hover, .work:focus-visible,
  .price:hover, .faq details:not([open]):hover,
  .work:hover .thumb .preview, .work:focus-visible .thumb .preview { transform: none; }
  /* iteration-count too: shortening an `infinite` animation to 0.001ms does not
     stop it, it makes it spin as fast as the CPU allows. */
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* =============================================================================
   Responsive (Step 13) — mobile-first refinements at content breakpoints
   ========================================================================== */
@media (max-width: 1024px) {
  .hero-grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .hero-visual { max-width: 30rem; }
  .why-grid, .contact-grid { grid-template-columns: 1fr; gap: var(--space-6); }
  .steps { grid-template-columns: repeat(2, 1fr); }
  .price-grid { grid-template-columns: 1fr; max-width: 30rem; margin-inline: auto; }
      .footer-grid { grid-template-columns: 1fr 1fr; gap: var(--space-6); }
  .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 720px) {
  .nav-links, .nav-actions .btn-desktop { display: none; }
  .nav-toggle { display: grid; }
  .checks { grid-template-columns: 1fr; }
  /* Restored: these shared a line with .bento and were removed with it. */
  .steps, .footer-grid { grid-template-columns: 1fr; }
  .hero-cta .btn { width: 100%; }
  .field-row { grid-template-columns: 1fr; }
  .cta-band { padding: var(--space-7) var(--space-5); }
  .hero-float { display: none; }
}

@media (max-width: 480px) {
  .container { padding-inline: var(--space-4); }
  .steps,   .hero-cta .btn { width: 100%; }
}
