/* hyperfocusam.com v2 — profile page, build v5.

   v4 followed research/profile/PATTERN-LIBRARY.md and shipped a correct,
   readable fact sheet. It also scored 7/36 on research/engaging/WHY-THEY-CAN.md,
   because every shape on it was a rectangle or a 1 px rule, nothing moved, and
   nothing overlapped anything. v5 closes the code-side levers and replaces the
   fold's photo-faded-to-black with a real composite.

   What changed, lever by lever:
     2  cut-out + composite  — Sam is lifted off the studio wall (rembg,
        birefnet-portrait, alpha matting; _tools/build_assets_v5.py) and set
        against ONE curved brand field. His head breaks above the arch, which
        is the page's only overlap and its only curve.
     3  bespoke vector       — the 10-mark annotation kit, drawn by
        _tools/build_marks.py, 286 cubic nodes. Used three times: under the
        H1's key phrase, round one figure, one arrow at the first door.
     4  annotation layer     — the same kit, plus Caveat (SIL OFL) on the
        Latin signature only. No Chinese is ever set in the hand.
     6  scroll choreography  — assets/js/motion.js, GSAP + ScrollTrigger,
        loaded after the window load event so it costs nothing before paint.
     7  ambient light        — one warm radial plate behind the fold and one
        grain layer over it, both CSS. No borders doing the work of light.
     8  typeface             — unchanged. Chiron Sung HK + Chiron Hei HK.
     9  composed data        — the 累計至今 band is one composition: a lead
        numeral at display size on a light plate, three supporting figures on
        a rule system, and the circle mark on 13.
    10  token system         — see below. ONE radius, ONE shadow, ONE ramp.

   Colour is unchanged from v4: one ground, one text, ONE hot accent.
     paper   #F7F3EC   cream, the proven TC ground
     plate   #F1E9DB   the same cream one step down, for the numbers plate
     ink     #15140F   the fold and the footer
     hot     #B5340F   5.39:1 on paper
     hot-ink #E8935E   the same hue lifted for the ink band (7.8:1 on ink)
*/

/* ================================================================= tokens
   THE RAMP. Eight steps, named by role, nothing outside them.
     d1  the H1                       34 -> 60 px    Sung 600
     d2  the lead figure              68 -> 116 px   Sung 600
     d3  the supporting figures       34 -> 46 px    Sung 600
     d4  section heads                26 -> 34 px    Sung 600
     b1  body and the loud value      19 px          Hei 400/500
     b2  secondary body               17 px          Hei 400
     b3  intro lines, captions        15 px          Hei 400
     l1  labels, meta, form labels    13 px          Hei 400/600, tracked
     l2  the smallest label           12 px          Hei 600, tracked
   THE RADIUS. One: none. Every component on this page has square corners,
   which is the fact-sheet decision made explicit. The arch behind Sam is a
   SHAPE, not a component, and carries the page's only curve (--arch).
   THE SHADOW. One recipe, used on the numbers plate and nowhere else.
*/
:root {
  --paper: #F7F3EC;
  --plate: #F1E9DB;
  --ink: #15140F;
  --text: #1C1B19;
  --mute: #57534A;
  --hot: #B5340F;
  --hot-ink: #E8935E;
  --onink: #F7F3EC;
  --onink-mute: #C3BDAE;
  --line: rgba(28, 27, 25, .15);
  --line-ink: rgba(247, 243, 236, .20);

  --sung: "Chiron Sung HK", "Noto Serif TC", Georgia, "Times New Roman", serif;
  --hei: "Archivo", "Chiron Hei HK", "Noto Sans TC", -apple-system,
         "Segoe UI", system-ui, sans-serif;
  --hand: "Caveat", "Segoe Script", cursive;

  --t-d1: clamp(34px, calc(1.9rem + 2.1vw), 60px);
  --t-d2: clamp(64px, 7.4vw, 112px);
  --t-d3: clamp(32px, 3.1vw, 46px);
  --t-d4: clamp(26px, 2.2vw, 34px);
  --t-b1: 19px;
  --t-b2: 17px;
  --t-b3: 15px;
  --t-l1: 13px;
  --t-l2: 12px;

  --r: 0px;                                    /* the one radius */
  --arch: 50% 50% 0 0 / 46% 46% 0 0;           /* the one curve */
  --shadow: 0 1px 1px rgba(21, 20, 15, .04),
            0 18px 44px -26px rgba(21, 20, 15, .38);   /* the one shadow */

  --stroke: 2.4px;                             /* the kit's one stroke weight */
  --sheet: 1120px;
  --pad: 32px;
  --ease: cubic-bezier(.22, .61, .36, 1);
}

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

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--hei);
  font-size: var(--t-b1);
  line-height: 1.7;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

.sheet {
  max-width: var(--sheet);
  margin-inline: auto;
  padding-inline: var(--pad);
}

.sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 99;
  background: var(--hot); color: #fff; padding: 14px 20px;
  font-size: var(--t-b3); text-decoration: none; border-radius: var(--r);
}
.skip:focus { left: 0; }

:where(a, button, input, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--hot);
  outline-offset: 3px;
}
.fact :where(a, button):focus-visible { outline-color: var(--hot-ink); }

.lat { font-family: var(--hei); }

/* ============================================================ the kit
   Ten marks, one sprite, one stroke weight. vector-effect keeps the stroke
   at --stroke SCREEN pixels whatever the viewBox scale is, so the underline
   under a 60 px headline and the circle round a 112 px numeral are drawn by
   the same pen. Every mark is decoration: aria-hidden, never focusable. */
.mk-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.mk {
  position: absolute; overflow: visible; pointer-events: none; z-index: 1;
  stroke-width: var(--stroke);
  color: var(--hot);
}
.fact .mk { color: var(--hot-ink); }

/* The draw-on state is declared HERE, not in motion.js, and only under
   html.js. Setting it in JS meant every mark painted fully inked at first
   paint, was erased the moment GSAP arrived, and then drew itself — a visible
   flash, caught in _qa/motion/fold-load-t1400.png. Declared here the marks
   start empty and are only ever drawn. If the library never arrives, reveal()
   drops html.js and they render solid.
   1.04 rather than 1 so the round caps sit inside the hidden part; the paths
   carry pathLength="1", so this is a normalised length, not pixels. */
.mk path { stroke-dasharray: none; }
.js .mk path { stroke-dasharray: 1.04; stroke-dashoffset: 1.04; }
@media (prefers-reduced-motion: reduce) {
  .js .mk path { stroke-dasharray: none; stroke-dashoffset: 0; }
}

/* ===================================================== 1 · the fold
   PATTERN-LIBRARY §1 "Fact-sheet hero" — order unchanged from v4. What
   changed is the ground: v4 put a rectangular photo under a CSS fade, which
   is the "everything is a rectangle" tell. v5 puts a cut-out figure against
   a single curved field, and lets his head cross the field's edge. */
.fact {
  position: relative;
  background: var(--ink);
  color: var(--onink);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 782px;
  isolation: isolate;
}

/* Lever 7 — the light. One warm source behind the arch, one grain layer over
   it so the falloff does not band on an 8-bit display. Nothing here is a
   border pretending to be depth. */
.fact::before {
  content: ""; position: absolute; inset: -10% -6% -14% 14%; z-index: 0;
  background:
    radial-gradient(58% 52% at 74% 46%, rgba(232, 147, 94, .26), rgba(232, 147, 94, .07) 46%, transparent 72%),
    radial-gradient(42% 46% at 88% 88%, rgba(181, 52, 15, .30), transparent 68%);
  pointer-events: none;
}
.fact::after {
  content: ""; position: absolute; inset: 0; z-index: 3; opacity: .16;
  mix-blend-mode: overlay; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* The bar. Library §1 "Nav: none or <=7 links" — three. */
.bar { order: 1; position: relative; z-index: 4; border-bottom: 1px solid var(--line-ink); }
.bar__in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px; min-height: 72px;
}
.bar__mark {
  text-decoration: none; display: inline-flex; align-items: baseline; gap: 10px;
  min-height: 44px; padding-block: 4px;
  font-family: var(--sung); font-size: 21px; font-weight: 600; letter-spacing: .01em;
}
.bar__mark .lat {
  font-size: var(--t-l2); font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--onink-mute);
  transition: color .18s var(--ease);
}
.bar__mark:hover .lat { color: var(--hot-ink); }
.bar nav { display: flex; align-items: center; gap: 28px; }
.bar nav a {
  position: relative; font-size: 14px; text-decoration: none;
  color: var(--onink-mute); letter-spacing: .04em; padding: 12px 0;
  transition: color .18s var(--ease);
}
.bar nav a::after {
  content: ""; position: absolute; left: 0; right: 100%; bottom: 8px;
  height: 1px; background: var(--hot-ink);
  transition: right .22s var(--ease);
}
.bar nav a:hover { color: var(--onink); }
.bar nav a:hover::after { right: 0; }

/* ------------------------------------------------------------ the stage
   Lever 2. The arch is the brand field; the cut-out stands in it and crosses
   its top edge, which is the page's one overlap. Both are absolutely placed
   inside .fact__stage so the text column never has to flow round them. */
.fact__stage {
  order: 3; position: absolute; inset: 0 0 0 auto;
  width: 45%; z-index: 1; pointer-events: none;
}
.arch {
  position: absolute; bottom: 0; left: 50%; translate: -52% 0;
  width: 64%; height: 68%;
  background: linear-gradient(174deg, #C43C12 0%, var(--hot) 46%, #8E280A 100%);
  border-radius: var(--arch);
}
.cut {
  position: absolute; bottom: 0; left: 50%; translate: -50% 0;
  width: 90%; height: auto;
  filter: drop-shadow(0 26px 36px rgba(0, 0, 0, .34));
}

/* width:100% is load-bearing: .sheet sets margin-inline:auto, and a flex item
   with an auto cross-axis margin does NOT stretch — so these two shrank to
   their content and the fold's type sat 211 px right of the bar's wordmark.
   Measured 2026-09-19 at 1440. */
.fact__top { order: 2; position: relative; z-index: 4; width: 100%; padding-top: 74px; }
.fact__btm { order: 4; position: relative; z-index: 4; width: 100%; margin-top: auto;
             padding-block: 44px 62px; }
.fact__top > *, .fact__btm > * { max-width: 660px; }

.eyebrow {
  margin: 0 0 22px;
  font-size: var(--t-l1); font-weight: 600; letter-spacing: .14em;
  color: var(--hot-ink); line-height: 1.5;
}

h1 {
  font-family: var(--sung);
  font-weight: 600;
  font-size: var(--t-d1);
  line-height: 1.22;
  letter-spacing: .005em;
  margin: 0 0 20px;
  text-wrap: balance;
}
.h1__lat { font-family: var(--sung); }
h1 .u { display: inline-block; }
.brk { display: none; }
@media (min-width: 1024px) { .brk { display: inline; } }

/* The key phrase, marked. The span is inline-block so the mark can be
   positioned against its own box; the mark sits BELOW the baseline and is
   allowed to overshoot both ends, because a drawn line does. */
/* white-space:nowrap is a correctness rule, not a style one: .mk is one
   absolutely-positioned box, so if the marked phrase wraps the mark lands
   across the LAST line at full width and reads as a strike-through. Measured
   on the English H1 at 390 on 2026-09-19. Keep every marked phrase short
   enough to hold one line at 390. */
.mark-u { position: relative; display: inline-block; white-space: nowrap; }
.mark-u .mk {
  left: -3%; width: 106%; bottom: -.30em; height: .24em;
}

.whom {
  margin: 0 0 14px;
  font-size: var(--t-b3); letter-spacing: .1em; font-weight: 500;
  color: var(--onink);
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
}
/* The one place the hand is used: his Latin name, set as a signature. The
   face is bound to .sig and never to .lat, because on the English page .lat
   is the CHINESE name and Caveat has no Chinese at all — the webfont is cut
   to the eight characters of "Sam Wong" and nothing else on either page is
   set in it. */
.whom .sig {
  font-family: var(--hand); font-weight: 600; letter-spacing: .01em;
  font-size: 27px; line-height: 1; color: var(--hot-ink);
}

.lede {
  margin: 0 0 34px;
  font-size: clamp(var(--t-b2), calc(0.9rem + 0.5vw), 22px);
  line-height: 1.68;
  color: var(--onink-mute);
  max-width: 30em;
}

/* ================================================= 2 · the two doors */
.doors { position: relative; display: flex; flex-wrap: wrap; gap: 14px; margin: 0 0 30px; }
.door {
  position: relative;
  display: inline-flex; flex-direction: column; gap: 3px;
  padding: 16px 26px; border-radius: var(--r); text-decoration: none;
  border: 1px solid transparent; min-height: 48px; justify-content: center;
  transition: background-color .2s var(--ease), border-color .2s var(--ease),
              transform .2s var(--ease);
}
.door__name { font-size: var(--t-b2); font-weight: 600; letter-spacing: .01em; }
.door__who { font-size: var(--t-l1); line-height: 1.5; }
.door--go { background: var(--hot); color: #fff; }
.door--go .door__who { color: rgba(255, 255, 255, .88); }
.door--go:hover { background: #992B0B; transform: translateY(-2px); }
.door--alt { border-color: var(--line-ink); color: var(--onink); }
.door--alt .door__who { color: var(--onink-mute); }
.door--alt:hover { border-color: var(--onink-mute); background: rgba(247, 243, 236, .06);
                   transform: translateY(-2px); }

/* The arrow. One mark, pointing at the door that converts, sitting in the
   dead space to its left at 1440 and hidden below 1080 where there is none. */
/* The arrow is drawn head-up-RIGHT, so it is never mirrored — a mirrored
   arrow points away from the thing it is annotating, which is what the first
   cut did. It only appears where there is real dead space to its left. */
.doors .mk--arrow {
  display: none;
  left: -94px; bottom: -4px; width: 78px; height: 44px;
  rotate: 6deg;
}
@media (min-width: 1200px) { .doors .mk--arrow { display: block; } }

/* ============================================ 3 · the one proof signal */
.proof {
  margin: 0; padding-top: 22px; border-top: 1px solid var(--line-ink);
  font-size: 14px; line-height: 1.7; color: var(--onink-mute);
  display: flex; flex-wrap: wrap; gap: 6px 20px;
}
.proof b { color: var(--onink); font-weight: 600; }

/* ==================================================== 4 · numbers band
   Lever 12. v4 set this as four cells split by 1 px rules — a table with the
   gridlines removed. v5 composes it: the band is one light plate; the lead
   figure is set at d2 and carries the block on its own; the other three sit
   on a rule system at d3 to its right; one of them is circled by hand.
   The plate is the only element on the page that takes --shadow. */
.figures { padding-block: 62px 66px; }
.plate {
  position: relative; background: var(--plate); border-radius: var(--r);
  box-shadow: var(--shadow);
  padding: 44px 48px 46px;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0 56px; align-items: start;
}
.figures__h {
  grid-column: 1 / -1;
  margin: 0 0 30px; font-family: var(--sung); font-weight: 600;
  font-size: var(--t-d4); letter-spacing: .04em; color: var(--text);
}
.fig-lead { margin: 0; }
.figs__n {
  display: block;
  font-family: var(--sung); font-weight: 600;
  line-height: .96; letter-spacing: -.015em;
  color: var(--hot);
  font-variant-numeric: tabular-nums lining-nums;
}
.fig-lead .figs__n { font-size: var(--t-d2); }
.figs__sup { font-size: .46em; vertical-align: .52em; margin-left: .03em; }
.figs__l {
  display: block; margin-top: 14px;
  font-size: var(--t-l1); letter-spacing: .08em; color: var(--mute); line-height: 1.5;
}
.fig-rest {
  list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--line);
}
.fig-rest .fr {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 20px; padding: 16px 0; border-bottom: 1px solid var(--line);
}
.fig-rest .figs__n { font-size: var(--t-d3); }
.fig-rest .figs__l { margin-top: 0; text-align: right; }

/* The circled figure. The span is the positioning context; the ellipse is
   drawn 34% wider and 44% taller than the numeral so it clears the digits
   the way a pen does. */
.mark-c { position: relative; display: inline-block; }
.mark-c .mk {
  left: -17%; top: -21%; width: 134%; height: 144%;
}

/* ============================================== 5 · credential rows */
.who { padding-block: 10px 6px; }
.rows { margin: 0; }
.row {
  display: grid; grid-template-columns: 140px 1fr; gap: 0 32px;
  padding: 20px 0; border-bottom: 1px solid var(--line); align-items: baseline;
  transition: background-color .2s var(--ease);
}
.rows dt {
  margin: 0; font-size: var(--t-l2); font-weight: 600; letter-spacing: .16em;
  color: var(--mute); line-height: 1.9;
}
.rows dd {
  margin: 0; font-size: 18px; font-weight: 500; line-height: 1.62;
}
.rows dd b { font-weight: 600; }
.rows dd .sub { color: var(--mute); font-weight: 400; font-size: var(--t-b2); }
.rows a { color: var(--text); text-underline-offset: 3px;
          display: inline-flex; align-items: center; min-height: 44px; }
.rows a:hover { color: var(--hot); }

/* ==================================================== 6 · logo wall
   Lever 8 in Part 2 — "a designer normalises a logo wall by OPTICAL size; a
   grid normalises it by box". Every height below is solved, not chosen:
   _tools/build_assets_v5.py renders each mark named in content/logos.yaml at
   a common 240 px, measures its INK AREA (alpha x darkness), and solves the
   display height that puts every mark on the same optical weight — damped
   0.95, capped at 168 px wide and 50 px tall so nothing fills its cell.
   Measured ink spread on the shipped set: 2.7x.

   v8.2 (2026-09-21): the 4x4 grid with a 46 px row gap is gone. Sixteen marks
   at four across left a field of holes — every cell was 58 px tall around a
   26-56 px mark, so the wall read as a standalone grey block rather than as a
   receipt inside the reading column. Ten marks now sit in two rows of fixed
   84 px tiles divided by hairlines: a table with its content centred, which
   is what a logo wall is. The six marks that cannot be normalised into a tile
   are named as text in `wall.also`. */
.wall { padding-block: 52px 60px; border-bottom: 1px solid var(--line); }
.wall__intro { margin: 0 0 20px; font-size: var(--t-b3); color: var(--mute); letter-spacing: .02em; }
/* The hairline system: one rule on the top and the left of the band, then one
   on the right and the bottom of every tile. Borders rather than a gap+bg
   trick so the outer edge is exactly one hairline, not two stacked. */
.logos {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--line); border-left: 1px solid var(--line);
}
.lg {
  display: flex; align-items: center; justify-content: center;
  height: 84px; padding: 0 16px;
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
/* Monochrome ink: one treatment, at full opacity, so no mark is legible only
   because it happens to be dark. The hover lifts a single mark back to full
   ink rather than to colour. */
.logos img {
  width: auto; max-width: 100%;
  /* object-fit is a correctness rule here: a solved height plus width:auto
     plus max-width:100% SQUASHES a wide mark the moment max-width bites —
     FAO Schwarz rendered at 3.51 against its true 4.04. contain letterboxes
     inside the cell instead, so no mark is ever distorted. */
  object-fit: contain;
  filter: grayscale(1) contrast(1.16) brightness(.86);
  opacity: .82;
  transition: opacity .22s var(--ease), filter .22s var(--ease);
}
.lg:hover img { opacity: 1; filter: grayscale(1) contrast(1.24) brightness(.62); }
/* The one-line text run carrying the marks that cannot sit in a tile. It is
   the same claim as the grid above it, in the same sentence, so nothing is
   dropped by being unpicturable. */
.wall__also {
  margin: 18px 0 0; font-size: var(--t-l1); line-height: 1.6;
  color: var(--mute); letter-spacing: .02em;
}
.lg--bochk img { height: 50px }
.lg--clp img { height: 29px }
.lg--ctf img { height: 50px }
.lg--cts img { height: 32px }
.lg--garden img { height: 50px }
.lg--hkct img { height: 50px }
.lg--hkjc img { height: 33px }
.lg--hsbc img { height: 27px }
.lg--polyu img { height: 32px }
.lg--ypo img { height: 36px }

/* ================================================ 7 · rooms / receipts */
section.band { padding-block: 66px; border-bottom: 1px solid var(--line); }
.head {
  font-family: var(--sung); font-weight: 600;
  font-size: var(--t-d4); line-height: 1.3;
  letter-spacing: .02em; margin: 0 0 34px;
}
.rooms { list-style: none; margin: 0; padding: 0; }
.rooms li {
  position: relative; padding: 24px 0 24px 0; border-top: 1px solid var(--line);
  transition: padding-left .26s var(--ease);
}
.rooms li::before {
  content: ""; position: absolute; left: 0; top: -1px; width: 0; height: 1px;
  background: var(--hot); transition: width .34s var(--ease);
}
.rooms li:hover { padding-left: 14px; }
.rooms li:hover::before { width: 100%; }
.rooms li:last-child { border-bottom: 1px solid var(--line); }
.room__meta {
  display: block; font-size: var(--t-l2); letter-spacing: .14em;
  color: var(--mute); margin-bottom: 9px;
}
.room__what { display: block; font-size: var(--t-b1); font-weight: 500; line-height: 1.55; }
.room__out { display: block; font-size: var(--t-b2); color: var(--mute); margin-top: 5px; line-height: 1.55; }
.room__out b { color: var(--hot); font-weight: 600; }
.note { margin: 26px 0 0; font-size: var(--t-b2); color: var(--mute); }

/* ================================================== 8 · story + quote
   The left rule on the quotation was a 3 px solid border — the same
   rectangle tell as everywhere else. It is now the kit's bracket, drawn. */
.story { display: grid; grid-template-columns: minmax(0, 1fr) 430px; gap: 56px; align-items: start; }
.story__text p { margin: 0 0 26px; font-size: var(--t-b1); line-height: 1.78; max-width: 34em; }
.quote { position: relative; margin: 34px 0 0; padding-left: 30px; }
.quote .mk--bracket { left: 0; top: 2px; width: 16px; height: calc(100% - 4px); }
.quote blockquote { margin: 0; }
.q__t {
  margin: 0; font-family: var(--sung); font-weight: 600;
  font-size: clamp(21px, 1.85vw, 28px); line-height: 1.45; letter-spacing: .01em;
  max-width: 24em;
}
.quote__src { margin-top: 14px; font-size: var(--t-l1); letter-spacing: .06em; color: var(--mute); }
.shot { margin: 0; }
.shot figcaption { margin-top: 12px; font-size: var(--t-l1); letter-spacing: .06em; color: var(--mute); }
.shot img { width: 100%; height: auto; border-radius: var(--r); }

/* ==================================================== 9 · enquiry form */
.enq { display: grid; grid-template-columns: minmax(0, 620px) 320px; gap: 60px; align-items: start; }
fieldset { border: 0; margin: 0 0 26px; padding: 0; }
legend { padding: 0; font-size: var(--t-l1); letter-spacing: .1em; color: var(--mute); margin-bottom: 12px; }
.radios { display: flex; flex-wrap: wrap; gap: 10px; }
.radio {
  display: inline-flex; align-items: center; gap: 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r);
  padding: 13px 20px; font-size: var(--t-b2); min-height: 48px;
  transition: border-color .18s var(--ease), outline-color .18s var(--ease);
}
.radio:hover { border-color: var(--mute); }
/* The selected state is an OUTLINE, not a second box-shadow: the page
   declares one shadow recipe and this is the only other thing that was
   reaching for one. outline does not affect layout, so the swap is free. */
.radio:has(input:checked) { border-color: var(--hot); outline: 1px solid var(--hot); outline-offset: -2px; }
.radio input { accent-color: var(--hot); width: 17px; height: 17px; margin: 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 20px; }
.field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 18px; }
.lab { font-size: var(--t-l1); letter-spacing: .08em; color: var(--mute); }
input[type=text], input[type=email], input[type=tel], textarea {
  font: inherit; font-size: var(--t-b2); color: var(--text);
  background: #FFFDF9; border: 1px solid var(--line); border-radius: var(--r);
  padding: 13px 15px; min-height: 52px; width: 100%;
  transition: border-color .18s var(--ease);
}
textarea { min-height: 132px; resize: vertical; line-height: 1.6; }
input:hover, textarea:hover { border-color: var(--mute); }
input:focus, textarea:focus { border-color: var(--hot); outline-offset: 0; }
::placeholder { color: #8A857A; opacity: 1; }
.req { color: var(--hot); }
button[type=submit] {
  font: inherit; font-weight: 600; font-size: var(--t-b2); letter-spacing: .04em;
  background: var(--hot); color: #fff; border: 0; border-radius: var(--r);
  padding: 18px 28px; width: 100%; min-height: 56px; cursor: pointer;
  transition: background-color .2s var(--ease);
}
button[type=submit]:hover { background: #992B0B; }
button[type=submit][disabled] { background: var(--mute); cursor: default; }
.form-msg, .form-done { margin: 16px 0 0; font-size: var(--t-b2); line-height: 1.6; }
.form-msg { color: var(--hot); }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.enq__side .row { grid-template-columns: 1fr; gap: 6px; padding: 16px 0; }
.enq__side .rows dd { font-size: var(--t-b2); font-weight: 400; }

/* ========================================================= 10 · footer */
footer { background: var(--ink); color: var(--onink-mute); padding-block: 34px; }
.foot {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 10px 30px; align-items: center;
  font-size: 14px; letter-spacing: .03em;
}
.foot a { color: var(--onink); text-decoration: none; text-underline-offset: 4px;
          display: inline-flex; align-items: center; min-height: 44px;
          transition: color .18s var(--ease); }
.foot a:hover { color: var(--hot-ink); text-decoration: underline; }
.foot li:last-child { color: var(--onink-mute); }

/* ================================================== motion, entry states
   The fold is choreographed in CSS, everything below it in GSAP, and the
   split is a performance decision with a number behind it.

   The first cut ran the fold's stagger through GSAP too. GSAP is 115 KB and
   is fetched at first idle, so on a throttled mobile connection the fold's
   type sat at opacity 0 until it arrived: Lighthouse mobile returned a Speed
   Index of 7.1 s against an FCP of 1.0 s. Measured on the deployed preview,
   2026-09-19. These six keyframes cost nothing, run on the compositor, and
   start at the first paint — and if the library never loads, the fold has
   already finished assembling itself.

   Every entry state is set ONLY under html.js, written by the two-line boot
   script in <head>, so a reader with JavaScript off gets the finished page.
   Nothing here animates a box: opacity and transform only, CLS 0 by
   construction. */
@keyframes rise   { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@keyframes archUp { from { transform: scaleY(.88); }                  to { transform: scaleY(1); } }
@keyframes cutUp  { from { transform: translateY(22px); }             to { transform: none; } }

.js .rv {
  opacity: 0;
  animation: rise .62s var(--ease) both;
  animation-delay: var(--d, 0s);
}
/* The stagger, written out rather than counted with nth-child, because the
   fold's reading order is a decision and not a DOM accident. */
.js .fact .eyebrow { --d: .26s }
.js .fact h1       { --d: .32s }
.js .fact .whom    { --d: .40s }
.js .fact .lede    { --d: .46s }
.js .fact .doors   { --d: .52s }
.js .fact .proof   { --d: .58s }

/* The arch and the figure enter on TRANSFORM only and keep opacity 1
   throughout: the figure is the LCP element on mobile, and an LCP element
   that starts at opacity 0 is not eligible until it fades in. */
.js .arch { transform-origin: 50% 100%; animation: archUp .9s var(--ease) both; }
.js .cut  { animation: cutUp .82s var(--ease) both; animation-delay: .14s; }

@media (prefers-reduced-motion: reduce) {
  .js .rv, .js .arch, .js .cut { animation: none !important; opacity: 1 !important; transform: none !important; }
}

/* ====================================================== responsive */
@media (max-width: 1200px) {
  .fact__stage { width: 44%; }
  .cut { width: 92%; }
}

@media (max-width: 1080px) {
  .story { grid-template-columns: 1fr; gap: 38px; }
  .enq { grid-template-columns: 1fr; gap: 40px; }
  /* ten marks stay five across down to 1080; the tiles narrow, the
     hairline grid does not change shape. */
  .lg { height: 78px; padding: 0 12px; }
  .fact__top > *, .fact__btm > * { max-width: 58%; }
  .fact__stage { width: 42%; }
  .plate { padding: 36px 34px 38px; gap: 0 36px; }
}

@media (max-width: 860px) {
  :root { --pad: 24px; --stroke: 2.1px; }
  body { font-size: 18px; }
  /* Five 127 px tiles is narrower than the widest mark, so the wall goes to
     two across and five down rather than letterboxing every wordmark. */
  .logos { grid-template-columns: repeat(2, 1fr); }
  .lg { height: 76px; padding: 0 16px; }
  .fact { min-height: 0; }
  .fact::before { inset: -6% 0 0 0; }
  /* The stage is re-authored for 390, not reflowed: it stops being a right
     column and becomes a band between the positioning line and the doors,
     with the arch scaled to the figure rather than to the viewport. */
  /* position:relative, never static: the arch and the figure are absolutely
     placed INSIDE the stage, and a static stage would hand them back to
     .fact — which put the figure behind the doors at 390. */
  .fact__stage {
    position: relative; inset: auto; width: auto; height: 340px;
    margin-block: 16px 18px;
  }
  .arch { width: 262px; height: 266px; left: 50%; translate: -50% 0; }
  .cut { width: 292px; left: 50%; translate: -50% 0; }
  .fact__top { padding-top: 22px; }
  .fact__top > *, .fact__btm > * { max-width: none; }
  h1 { margin-bottom: 14px; }
  .whom { margin-bottom: 10px; }
  .whom .sig { font-size: 24px; }
  .door { padding: 14px 20px; }
  .proof { padding-top: 18px; }
  .fact__btm { padding-block: 0 40px; margin-top: 0; }
  .lede { margin-bottom: 0; }
  .doors { flex-direction: column; gap: 8px; margin-bottom: 20px; }
  .door { width: 100%; }
  .figures { padding-block: 44px 48px; }
  .plate { grid-template-columns: 1fr; gap: 26px; padding: 30px 26px 32px; }
  .figures__h { margin-bottom: 22px; }
  .row { grid-template-columns: 1fr; gap: 6px; padding: 18px 0; }
  .rows dt { line-height: 1.5; }
  .rows dd { font-size: var(--t-b2); }
  .grid2 { grid-template-columns: 1fr; }
  section.band { padding-block: 50px; }
  .rooms li:hover { padding-left: 0; }
}

@media (max-width: 560px) {
  /* 390 fold budget. Seven things, every one trimmed, both doors above the
     line. The English H1 is the binding constraint — it sets five lines of
     Latin in a CJK display face where the Chinese sets two — so at 390 the
     ramp's d1 step is pinned rather than clamped, and the stage CLIPS the
     figure at its foot instead of shrinking it. See CRITIQUE-v5.md. */
  h1 { font-size: 31px; line-height: 1.17; }
  .eyebrow { margin-bottom: 15px; }
  .fact__top { padding-top: 10px; }
  .fact__stage { height: 244px; margin-block: 8px 10px; overflow: hidden; }
  .arch { width: 222px; height: 214px; }
  .cut { width: 250px; bottom: -44px; }
  .doors { gap: 6px; margin-bottom: 16px; }
  .door { padding: 13px 18px; }
  .proof { padding-top: 14px; }
  .bar nav { gap: 20px; }
  .bar__mark .lat { display: none; }
  .logos { grid-template-columns: repeat(2, 1fr); }
  .lg { height: 72px; padding: 0 14px; }
  .radios { flex-direction: column; }
  .radio { width: 100%; }
  .fig-rest .fr { padding: 13px 0; }
}


/* ═══════════════════════════════════════════════════════════════════════
   v6 · THE DRAWN SET
   design/illustration/set, installed per site by design/illustration/install.py.
   Everything here is an <img> pointing at a file, not an inlined <svg>: the
   set is 275 KB raw on this site and inlining it would put all of it in the
   document on every route. The trade is that the .il-* classes in SET.md §1
   cannot reach the paths, so install.py bakes the palette in instead.
   ═══════════════════════════════════════════════════════════════════════ */

/* ------------------------------------------------------------ the room
   Ground, not art direction. Masked off the right third so it never crosses
   the cut-out, and faded at the foot so the doors keep a clean field. */
.fact__room {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: url("/assets/il/hero-room.svg") no-repeat 42% 34% / 118% auto;
  opacity: .34;
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, #000 16%, #000 56%, transparent 88%),
    linear-gradient(to right, #000 0, #000 48%, transparent 76%);
  mask-image:
    linear-gradient(to bottom, transparent 0, #000 16%, #000 56%, transparent 88%),
    linear-gradient(to right, #000 0, #000 48%, transparent 76%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* ------------------------------------------------------------- icons */
.il { flex: none; width: 44px; height: 44px; }
.head--il, .figures__h--il {
  display: flex; align-items: center; gap: 15px;
}
.head--il .il, .figures__h--il .il { width: 42px; height: 42px; }

/* The doors were an inline-flex column; the icon makes them a two-column
   grid so the drawing sits beside the label rather than above it. */
.door { display: grid; grid-template-columns: auto 1fr; gap: 2px 15px;
        align-items: center; }
.il--door { width: 40px; height: 40px; grid-row: 1 / 3; align-self: center; }

.rows dt { display: flex; align-items: center; gap: 12px; }
.il--row { width: 40px; height: 40px; }

/* ----------------------------------------------- 見證 / what they said */
.said__note { margin: -18px 0 34px; font-size: var(--t-b3); color: var(--mute); }
.cards {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 34px;
}
.card { display: flex; flex-direction: column; }
/* The frame is a tilted drawing, so the axis-aligned copy box inside it can
   only use the intersection. The attribution therefore lives OUTSIDE the
   paper, under the card, where a real pinned note carries it anyway. */
.card__paper { position: relative; display: flex; flex: 1; }
.card__frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 0; pointer-events: none;
}
/* Padding is a PERCENTAGE because the frame stretches with the card: the
   drawn outer edge sits at ~6.5% of the viewBox on every side and the inner
   rule at ~11%, so the copy has to hold that ratio rather than a pixel inset. */
.card__in {
  position: relative; z-index: 1; width: 100%;
  padding: 19.5% 16% 16%;
  display: flex; flex-direction: column; gap: 12px;
}
/* fr-card-222 tilts the other way (+3.1°), so its bottom-left corner is the
   one that eats copy. One nudge, not a new rule set. */
.cards .card:nth-child(2) .card__in { padding-bottom: 19%; }
.card blockquote { margin: 0; }
.card__q {
  margin: 0; font-family: var(--sung); font-weight: 600;
  font-size: 15.5px; line-height: 1.6; letter-spacing: .01em; color: var(--text);
}
.card[lang="en"] .card__q, .card__q:lang(en) { letter-spacing: 0; }
.card__zh { margin: 0; font-size: 13px; line-height: 1.72; color: var(--mute); }
.card__src {
  margin: 0; padding-top: 16px;
  font-size: var(--t-l2); letter-spacing: .1em; color: var(--mute); line-height: 1.7;
}
/* No opacity here: --mute at .8 composites to #77736a, which is 4.27:1 on
   --paper at 12px. Lighthouse caught it on the deployed card. */
.card__src span { display: block; color: var(--mute); }
.note--agg { margin-top: 10px; }

/* ------------------------------------------------------------- FAQ
   The copy pass shipped this block as a bare <dl>. Same two-column
   question/answer geometry as .row above, so the page keeps one grid. */
.faq { margin: 0; }
.qa {
  display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 0 48px;
  padding: 26px 0; border-top: 1px solid var(--line);
}
.faq .qa:last-child { border-bottom: 1px solid var(--line); }
.faq dt {
  margin: 0; font-family: var(--sung); font-weight: 600;
  font-size: 20px; line-height: 1.5; letter-spacing: .01em; color: var(--text);
}
.faq dd {
  margin: 0; font-size: var(--t-b2); line-height: 1.8; color: var(--text);
  max-width: 38em;
}

@media (max-width: 1080px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px; }
}
@media (max-width: 860px) {
  .fact__room { opacity: .26; background-size: 170% auto; background-position: 40% 30%; }
  .cards { grid-template-columns: 1fr; }
  .qa { grid-template-columns: 1fr; gap: 10px; }
  .head--il .il, .figures__h--il .il { width: 40px; height: 40px; }
}
@media (max-width: 560px) {
  .card__in { padding: 15% 12% 12%; }
  .card__q { font-size: 17px; }
  .faq dt { font-size: 18px; }
}

/* ------------------------------------------------- v7 · photography
   Five cleared frames from design/photo-library across five days and five
   jobs, each captioned to the event it was taken at. v6 ran three frames from
   one day and read as one room; the two AMBER-cleared frames in "three rooms"
   are what makes the section's claim visible. The band sits directly under the
   fold, which is where 11 of the 16 crawled references put their first
   photograph. */
.band-shot { margin: 0; background: var(--ink); }
.band-shot img { width: 100%; height: clamp(240px, 34vw, 480px); object-fit: cover; object-position: 50% 42%; }
.band-shot figcaption {
  padding-block: 12px 14px; font-size: var(--t-l2); letter-spacing: .12em;
  color: var(--onink-mute); background: var(--ink);
}
/* ------------------------------------------- v8.2 · 在場 / in the room
   Three events, three cities, one look. The frames are 4:5 because the HKPC
   source is a portrait and no landscape cut of it holds a standing figure;
   the other two follow the frame that constrains the set rather than the set
   cropping that frame into agreement. The shared grade lives in assets.yaml
   and is applied by _grade() in build.py — CSS does not touch the pictures,
   so what ships is what was measured.

   This replaces .rooms-shots, which hung the same three frames under 三個房間
   as if they captioned it. */
.inroom .head { margin-bottom: 26px; }
.inroom-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.inroom-strip--2 { grid-template-columns: repeat(2, 1fr); max-width: 720px; }
.inroom-strip--1 { grid-template-columns: 1fr; max-width: 360px; }
.inroom-shot { margin: 0; }
.inroom-shot img { width: 100%; height: auto; display: block; border-radius: var(--r); }
.inroom-shot figcaption {
  margin-top: 12px; font-size: var(--t-l2); letter-spacing: .10em;
  line-height: 1.5; color: var(--mute);
}
@media (max-width: 860px) {
  .band-shot img { height: 220px; }
  /* Three 4:5 frames stacked is three screens of scroll, so the phone gets
     them side by side at a third each and reads the captions under them. */
  .inroom-strip { gap: 10px; }
  .inroom-shot figcaption { margin-top: 8px; font-size: 11px; letter-spacing: .04em; }
}

/* ------------------------------------------------- v8 · pictures in a door
   The two doors carried an icon through v7 and may now carry a photograph
   instead, from the assets.yaml slots door_primary / door_secondary. These
   rules match NOTHING until such a slot is enabled, which is why turning the
   pictures on is an assets.yaml edit and not a stylesheet edit.

   .door__cap is the composite label. EDITS.md Round 2: where the room behind
   Sam is a generated canvas, the page says so beside the picture — it is
   never captioned as one of his rooms. */
/* v8.2: the thumb spans EVERY row, and the caption is a third row in the TEXT
   column rather than a full-width row under the whole card. Before this, the
   shot was centred against rows 1-2 while .door__cap sat alone across the
   card's foot, 10 px under the picture and 27 px in from the card's edge —
   inside the box by measurement, detached from everything by eye, which is
   exactly how it was reported. 108x72 also went to 144x96: at 108 the face
   was a favicon and the seminar room was a brown smudge. */
/* `grid-row: 1 / -1` was tried here and silently did nothing: the door has no
   grid-template-rows, so line -1 IS line 1 in an implicit grid and the span
   collapsed, throwing .door__who down into column 1 under the picture. Every
   cell is placed by hand instead. */
.door__shot {
  grid-column: 1; grid-row: 1 / span 3; align-self: center;
  width: 144px; height: 96px; overflow: hidden;
  border-radius: var(--r);
}
.door__name, .door__who { grid-column: 2; }
.door__shot img { width: 144px; height: 96px; object-fit: cover; }
.door--alt .door__shot { box-shadow: inset 0 0 0 1px var(--line-ink); }
.door__cap {
  grid-column: 2;
  margin-top: 6px;
  font-size: var(--t-l2); letter-spacing: .10em; text-transform: uppercase;
}
.door--go .door__cap { color: rgba(255, 255, 255, .78); }
.door--alt .door__cap { color: var(--onink-mute); }

/* A 3:1 banner in the same full-bleed slot as .band-shot, held shorter so a
   wide frame does not eat a screen. */
.band-shot--wide img { height: clamp(150px, 21vw, 300px); object-position: 50% 45%; }

/* On a phone the two doors go full width and the fold is already tight, so the
   thumb drops to a square 56 px — 16 px wider than the icon it replaces.
   Emitting BOTH the icon and the photo and switching them with display:none
   was tried and reverted: HTMLImageElement.decode() never settles for a
   display:none image in this Chrome and it hung the screenshot gate. */
@media (max-width: 860px) {
  .door__shot, .door__shot img { width: 64px; height: 64px; }
}
