/* 禹門 · Ulysses — Site_Rebuild_Spec_2026-08-17.md
   Direction A, "Aperture". Tokens first, then the eleven modules in the
   order they appear down a page. */

/* ---- tokens ------------------------------------------------------- */
:root{
  /* Locked 15 Aug 2026. Derived values are tints and shades of these
     three only — no new hue enters the palette. */
  --green:#1C3B32;
  --paper:#F6F5F1;
  --ink:#14201C;
  --green-deep:#0E211C;
  --green-mid:#2A5347;
  --paper-dim:#D9D6CC;
  --body-ink:#2C3A35;

  --latin:"Ulysses Latin",-apple-system,BlinkMacSystemFont,sans-serif;
  --mono:"Ulysses Mono",ui-monospace,Menlo,monospace;
  --cjk:"Ulysses Latin","Ulysses CJK",sans-serif;

  --t-hero:clamp(44px,6.6vw,104px);
  --t-claim:clamp(28px,2.9vw,40px);
  --t-h2:26px;
  --t-body:19px;
  --t-cell:17px;
  --t-label:11px;

  --s1:8px; --s2:16px; --s3:24px; --s4:32px;
  --s5:48px; --s6:64px; --s7:96px; --s8:112px;

  --page:1560px;
  --gut:var(--s4);
  --labelcol:236px;
}

/* ---- faces -------------------------------------------------------- */
/* Instrument Sans and JetBrains Mono are variable: one file each, declared
   across the range, so 400 and 500 cost nothing extra. Each range is the
   font's real wght axis — Instrument Sans 400-700, JetBrains Mono 100-800 —
   and not a generous 100 900. Overstating the axis is not harmless: a
   request above the real maximum silently clamps to the top master and
   stops there, so the browser believes it has the weight it asked for and
   never applies synthetic bold. Declared honestly, it knows the weight is
   out of range and can synthesise. */
@font-face{font-family:"Ulysses Latin";src:url(/assets/fonts/latin.woff2) format("woff2");
  font-weight:400 700;font-style:normal;font-display:swap}
@font-face{font-family:"Ulysses Mono";src:url(/assets/fonts/mono.woff2) format("woff2");
  font-weight:100 800;font-style:normal;font-display:swap}
@font-face{font-family:"Ulysses CJK";src:url(/assets/fonts/tc-regular.woff2) format("woff2");
  font-weight:400;font-style:normal;font-display:swap;unicode-range:U+2E80-9FFF,U+F900-FAFF,U+FF00-FFEF,U+3000-303F}
@font-face{font-family:"Ulysses Brand";src:url(/assets/fonts/brand-zh.woff2) format("woff2");
  font-weight:400;font-style:normal;font-display:swap}
/* The wordmark's Latin, and only the wordmark's. Source Serif 4, cut to seven
   characters with both variable axes intact — .brand-en pins 'opsz' 20 and
   the axis has to survive the subset for that to mean anything. The
   font-weight descriptor is the file's real wght axis, 200-900; check/fonts.sh
   fails the build if it is not. */
@font-face{font-family:"Ulysses Wordmark";src:url(/assets/fonts/wordmark.woff2) format("woff2");
  font-weight:200 900;font-style:normal;font-display:swap}
/* The 简体 pages point --cjk at a differently named face rather than
   redeclaring "Ulysses CJK". Two @font-face rules sharing one family name
   and an overlapping unicode-range resolve in document order, not per
   page, so the 繁體 file would win everywhere and the 简体 pages would
   render Traditional glyph forms. */
@font-face{font-family:"Ulysses CJK SC";src:url(/assets/fonts/sc-regular.woff2) format("woff2");
  font-weight:400;font-style:normal;font-display:swap;unicode-range:U+2E80-9FFF,U+F900-FAFF,U+FF00-FFEF,U+3000-303F}
html[lang="zh-Hans"]{--cjk:"Ulysses Latin","Ulysses CJK SC",sans-serif}

/* ---- base --------------------------------------------------------- */
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
/* The column flex is what holds .site-foot to the bottom of a short page.
   Without it nothing pushes the footer down: the dark band ended wherever
   the copy ran out and paper continued below it — measured 17 Aug 2026 at
   1440x900, 351px of paper under the footer on People, 154px on Contact,
   150px on Notices, and half again as much at 1440x1080. main takes
   flex:1 0 auto rather than flex:1 so a page longer than the viewport is
   never shrunk to fit it. The homepage is long enough that the footer
   already sat at the bottom and is unaffected, as is the hero: its
   height:100vh is an absolute length, not a percentage of a parent. */
body{background:var(--paper);color:var(--ink);font-family:var(--latin);
  font-size:var(--t-body);line-height:1.62;-webkit-font-smoothing:antialiased;
  min-height:100vh;display:flex;flex-direction:column}
main{flex:1 0 auto}
html[lang^="zh"] body{font-family:var(--cjk)}
img{max-width:100%;display:block}
a{color:inherit}
:focus-visible{outline:2px solid var(--green-mid);outline-offset:3px}
/* The global ring is 1.94:1 on --green-deep, below the 3:1 WCAG 1.4.11
   floor for non-text contrast. --paper is 15.36:1 there. Scoped rather
   than global because --green-mid is correct on the paper ground, where
   it measures 7.93:1. No check tests contrast, so this rule is the only
   thing standing between a keyboard user and an invisible focus ring. */
.site-head :focus-visible,.site-foot :focus-visible{outline-color:var(--paper)}

.wrap{max-width:var(--page);margin:0 auto;padding:0 var(--gut)}
/* Column assignment is explicit in both directions. Without the second
   selector a third child added to a .row lands in row 2 column 1 by the
   grid's default auto-flow — silently occupying the label column. Modules
   that need something else override it. */
.row{display:grid;grid-template-columns:var(--labelcol) 1fr;gap:var(--s6)}
.row>.label{grid-column:1}
.row>:not(.label){grid-column:2}
.label{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.18em;
  text-transform:uppercase;color:var(--body-ink);opacity:.55;padding-top:10px}

/* ---- 1. header ---------------------------------------------------- */
.site-head{position:absolute;top:0;left:0;right:0;z-index:3;color:var(--paper)}
.site-head.solid{position:static;background:var(--green-deep)}
.site-head .wrap{display:flex;align-items:center;justify-content:space-between;height:84px;gap:var(--s4)}
/* The lockup. Every number here is derived from a measured font metric rather
   than chosen, and the derivations are worth keeping because none of them is
   recoverable by eye:

   Latin cap height 0.667em, so an 11px cap needs font-size 11/0.667 = 16.49.
   Tracking is specified as 0.20 of cap height, = 0.20 x 0.667 = 0.1334em, so
   it survives a change of size. text-indent cancels the trailing letter-space
   that would otherwise push the run left of its own box.

   The Chinese is sized by its ideograph BLOCK, not its font-size: 禹門資本 in
   思源宋體 occupies -0.085 to +0.844em, a block of 0.929em. Setting that block
   to 0.88 of the Latin cap gives font-size 0.88 x 11 / 0.929 = 10.42px.

   ALIGNMENT. align-items was `center` until 18 Aug 2026 and that was the bug:
   flex centres LINE BOXES, which are positioned by ascent and descent, and
   those are not comparable between the two families (Instrument Sans declares
   970/-250, Noto CJK 1160/-288). The Chinese was being placed by a number
   with no relationship to where its glyphs are, and the offset would change
   if either font were swapped.

   Baselines are no better: a Latin capital sits ON the baseline and an
   ideograph straddles it, so 禹門資本's foot hangs 0.085em below the line.
   What has to match is the OPTICAL CENTRES — the cap band's at 0.5 x cap, the
   ideograph block's at 0.3795 of the CJK font-size. The difference is
   0.1405 x cap = 0.148em of the Chinese's own size, hence the raise below.
   In em, not px, so it holds if the header size ever changes; position
   rather than transform, so it creates no compositing layer on every page.
   繁 and 简 differ by a tenth of a pixel here and share one value. */
.brand{display:flex;align-items:baseline;gap:14px;text-decoration:none}
.brand-en{font-family:"Ulysses Wordmark",serif;font-size:16.49px;font-weight:500;
  font-variation-settings:'opsz' 20;letter-spacing:.1334em;text-indent:.1334em}
.brand-zh{font-family:"Ulysses Brand",serif;font-size:10.42px;opacity:.88;
  letter-spacing:.22em;position:relative;top:-.148em}
.site-nav,.lang-nav{display:flex;gap:var(--s4);font-family:var(--mono);
  font-size:var(--t-label);letter-spacing:.14em;text-transform:uppercase}
.site-nav a,.lang-nav a{text-decoration:none;opacity:.72}
.site-nav a:hover,.lang-nav a:hover{opacity:1}
.site-nav a[aria-current],.lang-nav a[aria-current]{opacity:1;border-bottom:1px solid currentColor;padding-bottom:2px}

/* ---- 2. hero ------------------------------------------------------ */
.hero{position:relative;height:100vh;min-height:560px;overflow:hidden;
  background:var(--green-deep);color:var(--paper);display:flex;align-items:flex-end}
@supports(height:100svh){.hero{height:100svh}}
/* Only the img needs this: object-fit does nothing on <picture>, which is
   not a replaced element, and the img's own inset:0 resolves to the same
   box with or without the wrapper being positioned. */
.hero img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* Four stops, not one. A flat tint kills the photograph; a single gradient
   leaves the eyebrow on mid-tone with no contrast guarantee. */
.hero::after{content:"";position:absolute;inset:0;z-index:1;background:linear-gradient(
  180deg,rgba(12,27,23,.52) 0%,rgba(12,27,23,.10) 34%,
  rgba(12,27,23,.62) 78%,rgba(12,27,23,.88) 100%)}
.hero .wrap{position:relative;z-index:2;width:100%;padding-bottom:clamp(56px,7vh,104px)}
.eyebrow{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.2em;
  text-transform:uppercase;opacity:.72;margin-bottom:var(--s3)}
/* Every Latin measure on this stylesheet — 11ch here, 19ch on .claim, 52ch
   on .prose .txt, 45ch on .contact .txt — is denominated in the Latin face's
   "0" and therefore moves when that face changes. They are not round design
   numbers: they were re-derived 17 Aug 2026 for Instrument Sans (ch =
   0.666em, against Archivo's 0.573em) to hold the RENDERED WIDTH of each
   block, not its character count. On any Latin face swap, convert rather
   than keep: new = old x ch_old / ch_new, then re-check the line breaks by
   eye at the breakpoints — the conversion preserves width, not where a
   particular word lands. */
.hero-line{font-size:var(--t-hero);font-weight:500;line-height:1;
  letter-spacing:-.028em;max-width:11ch;text-wrap:balance}
.hero-line span{opacity:.62}
html[lang^="zh"] .hero-line{letter-spacing:.02em;max-width:none;font-weight:400}
.cue{position:absolute;right:var(--gut);bottom:clamp(56px,7vh,104px);z-index:2;
  font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  opacity:.55;display:flex;align-items:center;gap:10px}
.cue i{display:block;width:38px;height:1px;background:currentColor;opacity:.7}

/* ---- 3. page opener ----------------------------------------------- */
.opener{padding:var(--s8) 0 var(--s5)}
.opener h1{grid-column:2;font-size:var(--t-claim);font-weight:500;
  line-height:1.2;letter-spacing:-.014em}

/* ---- 4. claim ----------------------------------------------------- */
.claim{padding:var(--s8) 0 var(--s7)}
.claim h1{grid-column:2;font-size:var(--t-claim);font-weight:400;
  line-height:1.24;letter-spacing:-.014em;max-width:19ch}
/* em, not ch, on the Chinese claim: every glyph on this line is one em wide,
   so 15em is literally fifteen characters, whereas ch denominates the
   measure in the Latin "0" and made a Chinese line reflow when the Latin
   face changed. html[lang^="zh"] .prose .txt below has always done it this
   way — note that the plain .prose .txt rule beside it is the Latin one and
   is correctly in ch. */
html[lang^="zh"] .claim h1{letter-spacing:0;max-width:15em;line-height:1.4}

/* ---- 5. prose ----------------------------------------------------- */
.prose{padding:0 0 var(--s7)}
.prose .txt{grid-column:2;max-width:52ch}
.prose p{color:var(--body-ink);margin-bottom:var(--s3)}
.prose p:last-child{margin-bottom:0}
html[lang^="zh"] .prose .txt{max-width:38em}

/* ---- 6. focus grid ------------------------------------------------ */
.focus{padding:0 0 var(--s7)}
.focus .grid{grid-column:2;display:grid;grid-template-columns:repeat(4,1fr);
  gap:1px;background:var(--paper-dim)}
.focus .cell{background:var(--paper);padding:var(--s3) var(--s3) var(--s4);
  min-height:150px;display:flex;flex-direction:column;justify-content:space-between}
.focus .cell .caption{font-family:var(--cjk);font-size:12px;letter-spacing:.14em;opacity:.5}
/* Keyed off the caption's own language, not the page's. The caption holds
   whichever language the heading does not, and marking that explicitly
   means the styling follows the content instead of a convention that
   would break silently the first time a caption held same-language text. */
.focus .cell .caption:lang(en){font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase}
.focus .cell h2{font-size:var(--t-cell);font-weight:500;line-height:1.3;letter-spacing:-.005em}

/* ---- 7. plate ----------------------------------------------------- */
.plate{padding:0 0 var(--s7)}
.plate figure{grid-column:2;max-width:660px}
.plate figcaption{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.12em;
  text-transform:uppercase;color:var(--body-ink);opacity:.55;margin-top:12px}

/* ---- 8. strip ----------------------------------------------------- */
.strip{background:var(--green-deep)}
.strip .inner{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;height:220px}
.strip picture{overflow:hidden}
.strip img{width:100%;height:100%;object-fit:cover;opacity:.92}

/* ---- 9. people ---------------------------------------------------- */
.people{padding:0 0 var(--s7)}
.people .grid{grid-column:2;display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s5) var(--s4)}
.person img{aspect-ratio:4/5;object-fit:cover;width:100%;background:var(--paper-dim)}
.person h2{font-size:var(--t-cell);font-weight:500;margin-top:0}
.person img+h2{margin-top:var(--s2)}
.person .name-zh{font-weight:500}
.person .role{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.14em;
  text-transform:uppercase;opacity:.55;margin-top:6px}
.person p{font-size:16px;line-height:1.55;color:var(--body-ink);margin-top:var(--s2)}

/* ---- 10. contact -------------------------------------------------- */
.contact{padding:0 0 var(--s7)}
.contact .txt{grid-column:2;max-width:45ch}
.contact dt{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.16em;
  text-transform:uppercase;opacity:.55;margin-top:var(--s4)}
.contact dt:first-child{margin-top:0}
.contact dd{margin-top:6px}

/* ---- 11. footer --------------------------------------------------- */
.site-foot{background:var(--green-deep);color:var(--paper);padding:var(--s5) 0 var(--s6)}
.site-foot .wrap{display:flex;justify-content:space-between;gap:var(--s4);
  font-family:var(--mono);font-size:var(--t-label);letter-spacing:.1em;opacity:.62}
.site-foot a{text-decoration:none}
.site-foot a:hover{text-decoration:underline}

/* ---- motion ------------------------------------------------------- */
@media(prefers-reduced-motion:no-preference){
  .hero .eyebrow,.hero .hero-line{animation:rise .9s cubic-bezier(.2,.7,.2,1) both}
  .hero .hero-line{animation-delay:.12s}
  /* .armed is added by assets/js/reveal.js at runtime. Hiding is opt-in:
     if that script 404s, is blocked, or JS is off, nothing is ever hidden
     and every section renders normally. The previous form hid .reveal in
     CSS and depended on JS to un-hide it, which meant a blocked script
     left the notices page's regulatory text permanently invisible. */
  .reveal.armed{opacity:0;transform:translateY(8px);
    transition:opacity .7s cubic-bezier(.2,.7,.2,1),transform .7s cubic-bezier(.2,.7,.2,1)}
  .reveal.armed.seen{opacity:1;transform:none}
}
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}

/* ---- responsive --------------------------------------------------- */
@media(max-width:1080px){
  .focus .grid{grid-template-columns:repeat(2,1fr)}
  .people .grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:900px){
  :root{--gut:var(--s3)}
  .row{grid-template-columns:1fr;gap:var(--s2)}
  .label{padding-top:0}
  /* Same specificity as the two base rules above, so source order decides.
     Listing individual descendants here instead — .opener h1 is (0,1,1) —
     let the (0,2,0) base rule win and left the heading stranded in the
     second column on every page below 900px. */
  .row>.label,.row>:not(.label){grid-column:1}
  .cue{display:none}
  /* Three nav labels plus the three-item language switcher cannot share
     one row with the brand lockup below 900px without overflowing — the
     English labels measured ~78px over at 390px. Wrapping the header
     onto two rows (brand alone, then both navs together) removes the
     overflow at every width down to 320px in all three languages and
     needs no fixed height, so the transparent variant over the hero
     photograph still works. */
  /* The Chinese name stayed hidden below 900px while the header also carried
     a 22px mark and a 12px gap. Retiring the mark returned 34px and the name
     is now the firm's actual Chinese name rather than a two-character
     ornament, so it is shown at every width. Verified at 320, 390 and 768. */
  .site-head .wrap{height:auto;flex-wrap:wrap;padding-top:var(--s2);padding-bottom:var(--s2);row-gap:var(--s2)}
  .brand{flex:0 0 100%}
  .site-nav{gap:var(--s3)}
  .lang-nav{gap:var(--s2)}
  .strip .inner{grid-template-columns:1fr;height:180px}
  .strip picture:nth-child(n+2){display:none}
  .people .grid{grid-template-columns:1fr}
}
