/* ============================================================================
 * AI-Fleet Typography Foundation — typography.css
 * The shared low-level typesetting layer ALL output surfaces inherit
 * (HTML reports / PPT slides / documents / web / app-reference).
 *
 * Distilled from 10 canonical works (Müller-Brockmann, Lupton, Bringhurst,
 * Ruder, Samara, Tschichold, Vignelli, Craig-Scala, Tondreau, Butterick) +
 * a CJK layer the Western canon omits. Swarm-built, adversarially verified.
 *
 * COMPOSE, don't replace: genre/brand layers (html-style-router) override only
 * --accent, color, and the brand font-family. They must NOT redefine scale,
 * rhythm, measure, or the CJK correctness rules below.
 *
 * Single source of truth for values: tokens/typography.json (this file is the
 * CSS projection). Enforced by scripts/validate-typography.py.
 * Source: knowledge facts rule 24-typography-foundation; SKILL.md.
 * ==========================================================================*/

:root {
  /* --- SCALE: one base × one ratio. Never introduce off-scale sizes. ------ */
  --type-scale-ratio: 1.25;          /* major third; doc/web/app default */
  --type-scale-ratio-slide: 1.333;   /* perfect fourth; slides/hero drama */
  --type-base-web: 16px;             /* web/app body base + fluid floor */
  --type-base-doc: 18px;             /* long-form screen body base */
  --type-base-doc-print: 11pt;       /* pandoc/PDF print body (Butterick 10-12pt) */

  --fs-body: clamp(1rem, 0.95rem + 0.3vw, 1.25rem); /* fluid 16->20px; scale n=0 */
  --fs-sm: 0.8rem;                   /* n=-1; captions, meta */
  --fs-xs: 0.64rem;                  /* n=-2; fine print, labels */
  --fs-h3: 1.25rem;                  /* n=+1 (20px) */
  --fs-h2: 1.5625rem;                /* n=+2 (25px) */
  --fs-h1: 1.953rem;                 /* n=+3 (~31px) */
  --fs-display: clamp(2.44rem, 2rem + 2vw, 3.05rem); /* n=+4..+5; hero/opener */
  --fs-slide-title: clamp(40px, 5vw, 64px);          /* 1.333 slide title */
  --fs-slide-body: clamp(22px, 2.4vw, 28px);         /* slide body; >=22px Han floor */
  --max-sizes-per-view: 4;           /* distinct sizes visible in one view/slide/screen */
  --max-sizes-per-artifact: 8;       /* across the whole artifact; 6 = tighter Swiss */

  /* --- RHYTHM: leading + the 8px baseline. CJK leads looser than Latin. --- */
  --lh-body-latin: 1.5;              /* Latin body (1.4-1.5) */
  --lh-body-cjk: 1.75;              /* CJK body (1.6-1.8); Songti long-form up to 1.9 */
  --lh-body-cjk-slide: 1.5;          /* CJK slide body (short measure) */
  --lh-heading-latin: 1.15;          /* Latin display/heading (1.05-1.25) */
  --lh-heading-cjk: 1.3;             /* CJK heading (1.25-1.4) */
  --baseline: 8px;                   /* vertical rhythm unit; BODY line-box snaps to multiples */
  --space-paragraph: 1em;            /* paragraph gap = one line; use space OR indent, not both */
  --space-section: 48px;             /* between major sections (6 baseline units) */
  --group-to-intra-gap-ratio: 1.5;   /* between-group space >= 1.5x within-group space */
  --ls-body-cjk: 0.02em;             /* slight Han body tracking (0.02-0.05em); 0 on Latin runs */
  --ls-display-cjk: 0.1em;           /* CJK heading/label tracking (0.1-0.3em) */
  --ls-allcaps-latin: 0.06em;        /* Latin all-caps/small-caps tracking; 0 on lowercase body */
  --ls-display-latin: -0.02em;       /* large Latin display slight negative (-0.01..-0.025em) */

  /* --- MEASURE: cap line length. In mixed text, size by CJK; Latin flows in. */
  --measure-latin: 66ch;             /* Latin body target; bound 45-75ch */
  --measure-latin-min: 45ch;
  --measure-latin-max: 75ch;
  --measure-latin-clamp: clamp(45ch, 90vw, 75ch);
  --measure-cjk: 38em;               /* CJK body ~38 Han (1 Han ≈ 1em); bound 25-45 */
  --measure-cjk-min: 25em;
  --measure-cjk-max: 45em;
  --measure-cjk-slide: 30em;         /* slide/mobile CJK (~25-35 Han) */
  --measure-caption: 48ch;           /* captions/asides run shorter (40-50ch) */

  /* --- GRID: column counts per surface + the canonical content frame. ----- */
  --grid-cols-web: 12;
  --grid-cols-slide: 6;              /* × 12 rows on 16:9 */
  --grid-cols-doc: 1;               /* single text column at measure */
  --grid-cols-app: 4;               /* handset; 8 = tablet */
  --gutter-web: 24px;               /* 3 units */
  --gutter-slide: 32px;             /* 4 units */
  --field-gap: 8px;                 /* modular-grid field gap (1 baseline) */
  --page-max: 1040px;               /* canonical content frame; body never exceeds */
  --page-gutter: clamp(16px, 3vw, 48px);
  --page-gutter-total: clamp(32px, 6vw, 96px);
  --margin-web: clamp(16px, 4vw, 64px); /* outer margin >= 1.5x gutter & >= 2x line-height */
  --margin-gutter-ratio: 2;
  --slide-canvas-w: 1280px;
  --slide-canvas-h: 720px;
  --slide-safe-inset: 64px;          /* critical content stays in 1152×592 */
  --ios-margin: 16px;                /* SwiftUI 16pt content inset */
  --ios-gutter: 8px;

  /* --- FONT: <=2 families + mono. Mixed stacks are LATIN-FIRST (critical). - */
  /* CANONICAL body stacks — Latin family FIRST so Latin/digits use the Latin
     face; named CJK after; generic last. Use THESE for any CJK-bearing text. */
  --font-stack-mixed-sans: 'Inter', 'Source Han Sans SC', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  --font-stack-mixed-serif: 'Source Serif 4', 'Source Han Serif SC', 'Noto Serif SC', 'Songti SC', 'STSong', serif;
  /* Optional playful CJK display stack. The font face itself is opt-in via
     assets/fonts/black-sugar-plum-candy.css or the content-pipeline helper.
     Keep Inter first so Latin/digits do not render in the CJK display face. */
  --font-stack-playful-cjk-display: 'Inter', 'Black Sugar Plum Candy', '黑糖话梅', 'Source Han Sans SC', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  /* Optional Latin pixel display stack. Link assets/fonts/geist-pixel-google.css
     or load the npm `geist/font/pixel` variants before using it. Short tech
     covers/product moments only; never body copy or CJK prose. */
  --font-stack-pixel-display: 'Geist Pixel', 'Geist Mono', 'JetBrains Mono', 'SF Mono', monospace;
  /* Pure-Latin stacks (no CJK content) */
  --font-latin-sans: 'Inter', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --font-latin-serif: 'Source Serif 4', Georgia, 'Times New Roman', serif;
  /* CJK-only FRAGMENTS — do NOT use as a standalone body stack (they put a CJK
     face at index 0, which renders Latin/digits in the CJK face). Compose only
     after a Latin family, or just use --font-stack-mixed-* above. */
  --font-cjk-sans-fragment: 'Source Han Sans SC', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei';
  --font-cjk-serif-fragment: 'Source Han Serif SC', 'Noto Serif SC', 'Songti SC', 'STSong', 'SimSun';
  --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', Consolas, monospace; /* only allowed 3rd family */

  --font-weight-body: 400;
  --font-weight-medium: 500;
  --font-weight-bold: 700;           /* load the REAL Bold/Heavy instance (esp. CJK) */
  --font-synthesis: none;            /* ban faux bold/italic/small-caps */
  --font-feature-mixed: 'kern' 1, 'liga' 1; /* kerning + common ligatures */
  --font-feature-tabular: 'tnum' 1, 'lnum' 1; /* tabular lining figures for data */

  /* CJK correctness — corrected values (critic fix: 'normal' was a no-op). */
  --cjk-text-autospace: ideograph-alpha ideograph-numeric; /* ~1/8em Han<->Latin gap (pangu) */
  --cjk-text-spacing-trim: space-first; /* trim redundant fullwidth-punct side-bearing */
  --cjk-line-break: strict;          /* kinsoku 禁则: forbidden leading/trailing punctuation */
  --cjk-word-break: keep-all;        /* never break-all on Han */
  --cjk-hyphens: none;
  /* quote conventions: Chinese 「」『』《》 ; Latin curly “” ‘’ ; straight only in code */

  /* --- SPACING: the 8pt ramp is the ONLY allowed set. -------------------- */
  --space-unit: 8px;
  --space-half: 4px;                 /* only sub-unit; tight inline gaps */
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;                   /* = web gutter */
  --space-4: 32px;                   /* = slide gutter */
  --space-6: 48px;                   /* = section spacing */
  --space-8: 64px;                   /* = slide safe inset */

  /* --- COLOR (neutral foundation): one accent, ink/paper, semantic states. */
  --ink: #1A1A1A;                    /* body on light; near-black, not #000 */
  --ink-strong: #0A0A0A;             /* headings / max prominence */
  --ink-muted: #6B6B6E;              /* captions, meta; AA on paper */
  --ink-faint: #9B9B9D;              /* footer / disabled (matches footer color) */
  --paper: #FFFFFF;
  --paper-alt: #F7F7F5;              /* off-white section/card ground */
  --rule: #E5E5E3;                   /* hairline dividers */
  --ink-inverse: #F5F5F5;
  --paper-inverse: #141414;
  /* --accent: define ONE per artifact (brand/genre layer owns it). */
  --state-red: #C0392B;
  --state-amber: #D97706;
  --state-green: #059669;
}

/* ============================================================================
 * BASE RULES — link/inline this file and correct typesetting is the default.
 * Low specificity + custom-property driven so genre/brand layers override
 * cleanly. These encode the red-lines as MECHANISM, not advice.
 * ==========================================================================*/

html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  font-family: var(--font-stack-mixed-sans);
  font-size: var(--fs-body);
  font-weight: var(--font-weight-body);
  line-height: var(--lh-body-latin);
  color: var(--ink);
  background: var(--paper);
  font-synthesis: var(--font-synthesis);
  font-kerning: normal;
  font-feature-settings: var(--font-feature-mixed);
  text-autospace: var(--cjk-text-autospace);
  text-spacing-trim: var(--cjk-text-spacing-trim);
  line-break: var(--cjk-line-break);
  word-break: var(--cjk-word-break);
  overflow-wrap: break-word;         /* let long Latin/URLs wrap without break-all on Han */
  hyphens: var(--cjk-hyphens);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* CJK paragraphs lead looser — automatic via the lang attribute, not a reminder. */
:lang(zh), :lang(ja), :lang(ko) { line-height: var(--lh-body-cjk); letter-spacing: var(--ls-body-cjk); }

/* Bound the measure: body text never runs full-viewport. These caps are
   LOW-SPECIFICITY (:where → specificity 0) DEFAULTS, not walls: measure bounds
   READING TEXT, not boxes. Any layout container (card, nav item, grid cell) that
   happens to be an <li>/<p> overrides the cap with a single normal class rule —
   the foundation is a default, never a straitjacket. (A measure cap on the BOX
   strands an <li>-based card at 38em inside a full-width frame; that is a layout
   bug, not a reading width.) */
:where(p, li, blockquote, dd) { max-inline-size: var(--measure-latin); }
:where(:lang(zh) p, :lang(zh) li, :lang(zh) blockquote) { max-inline-size: var(--measure-cjk); }
/* Opt-IN (raise measure on a non-default element): keep normal class specificity. */
.measure { max-inline-size: var(--measure-latin); }
.measure-cjk { max-inline-size: var(--measure-cjk); }
/* Opt-OUT the comment above always promised but never shipped (CLAIM-VS-CODE):
   layout containers used as cards / nav / grid items are NOT body text — never
   measure-cap their box. `.cards > li` covers card-list components by convention. */
.full-bleed, :where(ul, ol).cards > li, nav li { max-inline-size: none; }

/* Modular scale + semantic hierarchy (display leads tight, unitless — exempt
   from the 8px baseline snap which applies to BODY only, per critic fix). */
h1 { font-size: var(--fs-h1); line-height: var(--lh-heading-latin); font-weight: var(--font-weight-bold); color: var(--ink-strong); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-heading-latin); font-weight: var(--font-weight-bold); color: var(--ink-strong); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-heading-latin); font-weight: var(--font-weight-medium); }
:lang(zh) h1, :lang(zh) h2, :lang(zh) h3 { line-height: var(--lh-heading-cjk); letter-spacing: var(--ls-display-cjk); }
small, .text-sm { font-size: var(--fs-sm); }
.text-xs { font-size: var(--fs-xs); }
.display { font-size: var(--fs-display); line-height: var(--lh-heading-latin); letter-spacing: var(--ls-display-latin); }
.display-playful-cjk, .font-playful-cjk {
  font-family: var(--font-stack-playful-cjk-display);
  font-weight: var(--font-weight-bold);
}
.display-pixel, .font-pixel-display {
  font-family: var(--font-stack-pixel-display);
  font-weight: 400;
  letter-spacing: 0;
  font-synthesis: none;
}

/* Spacing: vertical rhythm on the baseline. Paragraphs separated by space OR
   indent, not both (suppress indent after a heading). */
p { margin-block: 0 var(--space-paragraph); }
h1, h2, h3 { margin-block: var(--space-6) var(--space-2); }
h1 + p, h2 + p, h3 + p { text-indent: 0; }
section + section { margin-block-start: var(--space-section); }

/* Code/data: the only mono family; no ligatures; tabular figures in tables.
   overflow-wrap:anywhere — a long inline token (URL, html-style-router, package
   id) must break to fit a narrow phone column instead of overflowing its box and
   getting clipped at the viewport edge. `anywhere` (not break-word) also shrinks
   the token's min-content so grid/flex/table tracks size correctly on mobile. */
code, pre, kbd, samp { font-family: var(--font-mono); font-variant-ligatures: none; overflow-wrap: anywhere; }
table { font-feature-settings: var(--font-feature-tabular); }

/* All-caps must be tracked; lowercase body must not. */
.allcaps, [style*="uppercase"], .uppercase { text-transform: uppercase; letter-spacing: var(--ls-allcaps-latin); }

/* The canonical content frame (composes with html-style-router layout guard).
   box-sizing:border-box is REQUIRED, not cosmetic: the inline-size formula
   (100% - gutter) is the frame's TOTAL footprint, so any horizontal padding a
   consumer adds must be absorbed INTO it. Under the content-box default the
   padding is added on top and the frame overflows the viewport by the padding
   width at narrow widths (audit 2026-06-26: a padded main.html-frame overflowed
   ~8px <=768px — invisible on desktop, a horizontal-scroll sliver on mobile). */
.html-frame, .doc-frame, .report-frame {
  box-sizing: border-box;
  inline-size: min(var(--page-max), calc(100% - var(--page-gutter-total)));
  margin-inline: auto;
}

/* Text must never carry gradient/shadow/bevel/outline/texture (red-line 11). */
h1, h2, h3, p, li { text-shadow: none; }

/* Strong emphasis uses a real weight; never faux. Italic allowed on Latin only. */
strong, b { font-weight: var(--font-weight-bold); }
:lang(zh) em, :lang(zh) i { font-style: normal; font-weight: var(--font-weight-medium); } /* no oblique on Han */

/* ============================================================================
 * v2 ELEVATION LAYER (升维) — composes on top of v1, never breaks it.
 * 5 dimensions shipped past the anti-bloat critic: variable-font · fluid ·
 * adaptive-color (highest value) · perceptual-a11y · motion. (i18n: only 3
 * laws shipped, no token group — tategaki/ruby/per-script dropped as
 * future-proofing for scripts this house never emits.)
 * Every token carries a graceful degradation path; v1 output is the floor.
 * ==========================================================================*/

:root {
  /* --- VARIABLE FONT: weight is an axis position, not a static face. ------ */
  --vf-wght-body: 380;        /* <400: Han reads heavier than Latin at equal nominal weight */
  --vf-wght-caption: 420;
  --vf-wght-emph: 520;
  --vf-wght-display: 680;
  --vf-cjk-emph-wght: 560;    /* CJK emphasis tier — impossible with one static CJK face */
  --vf-cjk-bold-wght: 700;    /* real CJK heavy via wght axis, NOT synthesis */
  --font-cjk-vf-stack: "Source Han Sans SC VF", "Noto Sans CJK SC", "PingFang SC", system-ui;
  --vf-opsz-display: 48;      /* slide/hero optical size, decoupled from layout px (projected large) */
  --vf-opsz-caption: 9;
  --vf-grad-dark: 48;         /* GRAD bump on dark ground offsets light-on-dark optical thinning (metric-invariant) */
  --vf-grad-light: 0;
  --font-display-body: swap;     /* FOUT over FOIT — body text never invisible */
  --font-display-brand: optional;
  --fallback-size-adjust: 105%;  /* on the metric-matched fallback @font-face so VF swap causes no reflow */

  /* --- FLUID: one systematic type+space scale across a min/max viewport. -- */
  --fluid-min-vw: 320; --fluid-max-vw: 1280;   /* max = slide width, so PPT+web share one ceiling */
  --fluid-min-ratio: 1.2; --fluid-max-ratio: 1.333;
  --step--2: clamp(0.694rem, 0.78rem + -0.2vw, 0.633rem);  /* legal/footnote */
  --step--1: clamp(0.833rem, 0.8rem + 0.16vw, 0.844rem);   /* caption/meta */
  --step-0:  clamp(1rem, 0.96rem + 0.2vw, 1.125rem);       /* body anchor (min in rem: no px-lock) */
  --step-1:  clamp(1.2rem, 1.05rem + 0.75vw, 1.5rem);      /* h4 / lead */
  --step-2:  clamp(1.44rem, 1.2rem + 1.2vw, 2rem);         /* h3 */
  --step-3:  clamp(1.728rem, 1.34rem + 1.95vw, 2.665rem);  /* h2 */
  --step-4:  clamp(2.074rem, 1.45rem + 3.1vw, 3.553rem);   /* h1 */
  --step-5:  clamp(2.488rem, 1.5rem + 4.95vw, 4.737rem);   /* section display */
  --step-6:  clamp(2.986rem, 1.45rem + 7.7vw, 6.314rem);   /* hero/slide-title display */
  --space-3xs: clamp(0.25rem, 0.23rem + 0.1vw, 0.3125rem);
  --space-xs:  clamp(0.5rem, 0.43rem + 0.35vw, 0.75rem);   /* 8->12px */
  --space-s:   clamp(1rem, 0.86rem + 0.7vw, 1.5rem);       /* 16->24px */
  --space-m:   clamp(1.5rem, 1.29rem + 1.05vw, 2.25rem);   /* 24->36px */
  --space-l:   clamp(2rem, 1.71rem + 1.45vw, 3rem);        /* 32->48px */
  --space-xl:  clamp(3rem, 2.57rem + 2.15vw, 4.5rem);      /* 48->72px section padding */
  --measure-fluid-cap: 66ch;   /* critic fix: cap container measure at v1 TARGET (was 75ch) */

  /* --- PERCEPTUAL A11Y: APCA Lc targets (advisory) over a WCAG2.2 floor. -- */
  --contrast-lc-body: 75;      /* APCA Lc target, body <=18px / weight<=400 (advisory) */
  --contrast-lc-large: 60;     /* APCA Lc target, large/heading */
  --contrast-lc-ui: 45;        /* APCA Lc target, non-text UI */
  --contrast-floor-wcag: 4.5;  /* WCAG2.2 AA hard floor (3:1 large) — ENFORCED, APCA is the target above it */
  --focus-ring: currentColor;
  --focus-ring-width: 2px;     /* WCAG2.2 2.4.13 */
  --focus-ring-offset: 2px;
  /* opt-in high-legibility reading mode (NEVER default) */
  --reading-tracking: 0.02em; --reading-leading: 1.8; --reading-measure: 60ch; --reading-para-gap: 1.4em;
  --reading-font-stack: 'Atkinson Hyperlegible', system-ui, 'PingFang SC', sans-serif;

  /* --- MOTION: temporal typography, all reduced-motion gated. ------------- */
  --motion-safe-properties: opacity, transform;   /* the ONLY props type motion may touch (compositor, no reflow) */
  --dur-instant: 80ms; --dur-fast: 160ms; --dur-base: 240ms; --dur-slow: 360ms; --dur-slide: 500ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);     /* entrances — decelerate into place (default) */
  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --reveal-distance: 12px;     /* <=16px so it never implies layout shift */
  --reveal-stagger: 60ms;
  --reveal-budget-max: 1000ms; /* stagger×count + dur-slide must stay <= this */
}

/* --- ADAPTIVE COLOR (highest-value dimension): light/dark/auto via light-dark().
 * hex fallback FIRST (old engines), light-dark() enhancement SECOND. Dark
 * operands are RE-DERIVED for contrast on #141414, not naive inversions
 * (critic near-miss guard). color-scheme opts the page into dual mode. ----- */
:root {
  color-scheme: light dark;
  --ink: #1A1A1A;        --ink: light-dark(#1A1A1A, #F5F5F5);
  --ink-strong: #0A0A0A; --ink-strong: light-dark(#0A0A0A, #FFFFFF);
  --ink-muted: #6B6B6E;  --ink-muted: light-dark(#6B6B6E, #A8A8AB);   /* brightened, not reused */
  --ink-faint: #9B9B9D;  --ink-faint: light-dark(#9B9B9D, #7C7C7E);
  --paper: #FFFFFF;      --paper: light-dark(#FFFFFF, #141414);
  --paper-alt: #F7F7F5;  --paper-alt: light-dark(#F7F7F5, #1F1F1F);   /* one step up from --paper, not pure black */
  --rule: #E5E5E3;       --rule: light-dark(#E5E5E3, #333335);
  --state-red: #C0392B;   --state-red: light-dark(#C0392B, #F2766B);  /* dark operand lifted, same hue */
  --state-amber: #D97706; --state-amber: light-dark(#D97706, #F0A742);
  --state-green: #059669; --state-green: light-dark(#059669, #34D399);
}
/* Scoped pins: an always-light or always-dark subtree (hero/slide) — tokens
   auto-resolve, no hand-painted inverse needed (AC-INVERSE folded into light-dark). */
.force-light { color-scheme: light; }
.force-dark  { color-scheme: dark; }
/* Forced-colors / Windows High Contrast: bind to system colors so the hairline
   and focus ring survive background stripping. */
@media (forced-colors: active) {
  :root { --ink: CanvasText; --ink-strong: CanvasText; --paper: Canvas; --paper-alt: Canvas;
          --rule: CanvasText; --focus-ring: Highlight; }
  .force-light, .force-dark { forced-color-adjust: auto; }
}

/* --- BASE RULES (v2): make the elevations the default, degrade gracefully. - */
/* Optical sizing auto everywhere it is supported (no-op otherwise). */
body { font-optical-sizing: auto; }
/* Headings adopt the fluid scale (v1 --fs-* now point at the computed steps). */
:root { --fs-body: var(--step-0); --fs-h3: var(--step-2); --fs-h2: var(--step-3);
        --fs-h1: var(--step-4); --fs-display: var(--step-6); }
/* Variable-weight: drive weight via the axis, mirrored to numeric font-weight
   (keyword bold/normal is banned on a VF — validator enforces). */
h1, h2 { font-variation-settings: 'wght' var(--vf-wght-display); }
h3 { font-variation-settings: 'wght' var(--vf-wght-emph); }
:lang(zh) strong, :lang(zh) b { font-variation-settings: 'wght' var(--vf-cjk-bold-wght); } /* real CJK heavy, not faux */

/* Focus is always visible (never suppress outline without a replacement). */
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }

/* BALANCE (居中/对齐/平衡): balanced line-breaking is a foundation default, not a
   per-artifact afterthought. Headings BALANCE — even ragged edges, no lone short
   tail line (the "02 · 路由原则" / "06 · CLAIM-VS-CODE" heading-wrap defect).
   Body PRETTY — the engine avoids orphans and too-short last lines (the "Quality-
   Gated（…）" short-second-line defect). Centered blocks (footers, hero meta, pull
   quotes) MUST also balance + bound their measure so a centered ragged column does
   not strand a lone word on its own line (the footer "（访问 / 2026-07-01)" defect).
   All degrade gracefully to normal wrapping where text-wrap is unsupported. */
h1, h2, h3, .display, .hero h1, .cover h1, figcaption, caption { text-wrap: balance; }
p, li, blockquote, dd, .lead, .sub { text-wrap: pretty; }
/* A centered text block is only balanced if its measure is bounded AND it balances.
   .center-balance is the reusable primitive genres apply to footers / hero meta. */
.center-balance { text-align: center; text-wrap: balance; max-inline-size: var(--measure-cjk); margin-inline: auto; }

/* A bare footer element is centered PAGE CHROME by default, not a reading column — so
   it gets the .center-balance behavior AUTOMATICALLY (no per-artifact class needed).
   Root cause this fixes (CLAIM-VS-CODE, found 2026-07-06): footer paragraphs inherited
   the left-anchored measure cap (margin-inline:0) → each became a 38em box
   stranded at the inline-start, so text-align:center centered text WITHIN a left box
   → the whole footer rendered left-of-page-center on wide screens. Bounding the
   measure is correct (a footer line should not run full-viewport); the miss was not
   CENTERING that bounded box. margin-inline:auto centers it → footer sits at page
   center on PC, and at mobile widths the box is full-width so it centers there too
   (responsive by construction). Genres may still opt a footer OUT with a class. */
footer { text-align: center; }
footer :where(p, li, .sub, small) { margin-inline: auto; text-wrap: balance; }

/* v2.1 SOTA micro-typography (2026-07-01). The fine details that separate "clean"
   from "SOTA" — optical margin alignment. Progressive enhancement: unsupported
   browsers ignore each property, zero risk.
   1. hanging-punctuation: leading/trailing punctuation ("「」『』（）·—) hangs into
      the margin so the reading column's edge is OPTICALLY straight, not just
      mathematically flush. Highest impact for CJK where brackets/quotes create a
      false visual indent. */
:where(.html-frame, .doc-frame, .report-frame, article, section, main) { hanging-punctuation: first last; }
/* 2. Optical sizing on for large display type (variable fonts render heavier/finer
      stroke contrast at display sizes). */
h1, h2, .display, .hero h1, .cover h1 { font-optical-sizing: auto; }
/* 3. Data figures align in a column: any table cell / .num renders tabular +
      slashed-zero so columns of numbers line up digit-for-digit (SOTA for the
      pricing/benchmark tables this foundation sets). */
th, td, .num, [data-numeric] { font-variant-numeric: tabular-nums slashed-zero; }

/* Container-aware measure (OPT-IN): elements inside .cq size measure to their
   OWN width, capped at the v1 target. Degradation: without container-query
   support, the v1 static measure on p/li still applies. */
.cq { container-type: inline-size; }
.cq p, .cq li, .cq .measure { max-inline-size: clamp(45ch, 60cqi, var(--measure-fluid-cap)); }

/* Opt-in high-legibility reading mode (never default). */
.reading-mode { font-family: var(--reading-font-stack); line-height: var(--reading-leading); letter-spacing: var(--reading-tracking); }
.reading-mode p { max-inline-size: var(--reading-measure); margin-block-end: var(--reading-para-gap); }

/* Type motion: entrance reveal + scroll emphasis — compositor-only, gated. */
.reveal { opacity: 0; transform: translateY(var(--reveal-distance)); animation: type-reveal var(--dur-slide) var(--ease-out) both; }
@keyframes type-reveal { to { opacity: 1; transform: none; } }
/* The non-negotiable guard: reduced-motion collapses ALL motion to end-state. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
  .reveal { opacity: 1; transform: none; }
}

/* i18n (shipped laws only): logical properties already used above so layout
   flips correctly under dir=rtl; locale-aware quotes via :lang(). */
:lang(en) { quotes: '\201C' '\201D' '\2018' '\2019'; }   /* curly “” ‘’ */
:lang(zh) { quotes: '\300C' '\300D' '\300E' '\300F'; }   /* corner 「」『』 */
:lang(fr) { quotes: '\00AB' '\00BB' '\2039' '\203A'; }   /* guillemets « » */

/* --- RESPONSIVE TABLE REFLOW: below 560px a wide data table becomes labelled
 * stacked key→value blocks so COLUMN IDENTITY survives on a phone — no
 * horizontal overflow, no header-less wall. CONTRACT: the generator MUST emit
 * data-label="<column header>" on every <td>; the ::before reintroduces it.
 * Opt out per-table with class="no-reflow" (genuinely single-column tables).
 * Enforced by validate_typography.py (stacked-table-no-label). The foundation
 * OWNS this so each report stops re-inventing (or omitting) mobile table reflow.
 * Spacing on the 8pt ramp. --------------------------------------------------- */
@media (max-width: 560px) {
  table:not(.no-reflow) thead { display: none; }
  table:not(.no-reflow),
  table:not(.no-reflow) tbody,
  table:not(.no-reflow) tr,
  table:not(.no-reflow) td { display: block; width: auto; }
  table:not(.no-reflow) tr { padding-block: 16px; border-block-end: 1px solid var(--rule); }
  /* [label | value] GRID, both LEFT-aligned — no space-between "river of void".
     Label = narrow column, value = 1fr and left-aligned so long values wrap
     cleanly in their own column instead of stranding right. (2026-07-01: replaced
     justify-content:space-between, which pushed label far-left / value far-right
     with an unscannable gap and mangled long CJK values on a phone.) */
  table:not(.no-reflow) td { display: grid; grid-template-columns: minmax(4em, 34%) 1fr;
    gap: 4px 12px; align-items: baseline; border: none; padding-block: 4px; text-align: left; }
  table:not(.no-reflow) td::before { content: attr(data-label); font-weight: 600;
    color: var(--ink-muted); font-size: var(--fs-sm); }   /* AA muted, never the faint footer tier */
  table:not(.no-reflow) td[data-label=''] { display: block; }
  table:not(.no-reflow) td[data-label='']::before { content: none; }
}

/* Responsive component grids — the foundation OWNS these so reports/decks stop
 * hand-rolling fixed montage images and cramped 6-column text tables. auto-fit +
 * minmax(min(track,100%),1fr) makes the grid FILL the frame width on desktop (no
 * right-blank imbalance) and STACK on a phone (adaptive) with zero media queries;
 * min(track,100%) means a single card never overflows a narrow viewport. Children
 * opt out of the body measure-cap (they are layout boxes, not reading columns) and
 * get min-inline-size:0 so long unbreakable tokens can't blow the track out.
 *   .card-grid   — finding / comparison / feature cards (min ~320px)
 *   .screen-grid — app/web screen thumbnails, tighter track (min ~180px)
 * Gap on the 8pt ramp. Use these instead of a wide data table when a cell carries
 * a paragraph of text, and instead of one baked montage when showing N screens. */
.card-grid, .screen-grid { display: grid; margin-block: var(--space-3); }
.card-grid   { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); gap: var(--space-2); }
.screen-grid { grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--space-3) var(--space-2); }
.card-grid > *, .screen-grid > * { max-inline-size: none; min-inline-size: 0; }
