/* ── The Latin display face, self-hosted ──────────────────────────────────
 *
 * v8.5.4. Until now this was linked from fonts.googleapis.com while the site's
 * OWN Content-Security-Policy (server.py: `default-src 'self'`, `font-src
 * 'self' data:`) blocked both the stylesheet and the font file. So
 * "Cormorant Garamond" in --font-latin below never resolved to anything —
 * every portal has been rendering Georgia since the CSP shipped, silently,
 * while still paying for two blocked requests per page load.
 *
 * One variable file per subset covers 300-700, so five declared weights cost
 * two downloads rather than five. `display: swap` keeps first paint on the
 * fallback; `unicode-range` means a CJK-only page never fetches either file.
 *
 * SIL OFL 1.1 — the licence is shipped alongside at fonts/OFL.txt, which the
 * OFL requires and which is also how the next person knows it may stay.
 *
 * The URLs carry no ?v= — a font binary is content-addressed by its own name
 * and does not change between releases, unlike the CSS and JS.
 */
@font-face {
  font-family: "Cormorant Garamond";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fonts/cormorant-garamond-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fonts/cormorant-garamond-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fonts/cormorant-garamond-latin-italic.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fonts/cormorant-garamond-latin-ext-italic.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* StudioSaaS public-surface palette — the single source of truth for the
 * tenant portal (index.html) and the standalone register page.
 *
 * Both pages used to declare their own :root (clay/paper/serif/2px vs
 * blue/cold-grey/system/8px), so a parent clicking "报名" on the portal
 * landed on what looked like another company's site. Everything visual now
 * starts here; brand-system.css reads these to build the --brand-* roles,
 * and /brand overrides them per tenant at runtime.
 *
 * Load order: ui-tokens.css → portal-theme.css → brand-system.css
 *
 * The default values below are the "vintage-press" light theme from
 * backend/studiosaas/presets.py. They are what an unbranded page renders
 * before /brand resolves — keep the two in step.
 *
 * Every colour here was solved for a measured WCAG target by
 * docs/design/palette_gen.py, which asserts 61 pairs per theme-mode. Do not
 * hand-edit a hex without re-running it.
 *
 * v8.4.0: "keep the two in step" was an instruction with nothing enforcing it,
 * and seven of the twenty-one had drifted — --warning was #8D6426 against a
 * generated #5B421F and --danger #B6483A against #76332A, which are not
 * near-misses but different colours. So the unbranded state of every public
 * page carried a warning and a danger belonging to no theme in the product.
 * test_portal_theme_contract.py now asserts the whole set against
 * style_theme(DEFAULT_STYLE_ID, "light").
 */
:root {
  /* ── Surfaces ────────────────────────────────────────────────────────
   * Three layers, not two. page → card sits at ~1.13:1 so a card reads as
   * an actual layer instead of relying on its border to exist. */
  --bg: #F3EFEA;
  --bg2: #EAE3DB;
  --panel: #FDFDFD;
  --surface: #FDFDFD;

  /* ── Ink ─────────────────────────────────────────────────────────────
   * Each level is solved against --bg2, the hardest surface it can land on,
   * so no text token fails on the darker band. */
  --ink: #221E1A;      /* 13:1  — headings, primary copy   */
  --ink2: #46403A;     /*  8:1  — secondary copy           */
  --muted: #6C635A;    /*  4.6:1 — captions, helper text   */

  /* ── Borders ─────────────────────────────────────────────────────────
   * A1. One token could not serve both jobs: a divider wants to be quiet,
   * an input boundary must clear WCAG 1.4.11 at 3:1. They are now separate.
   *   --line        decorative dividers, section rules
   *   --line-strong interactive boundaries: inputs, selects, outline buttons
   */
  --line: #E0D8CF;
  --line-strong: #8D7F70;

  /* ── Accents ─────────────────────────────────────────────────────────
   * hover/pressed move in the same direction (darker in light mode, lighter
   * in dark) so the two states are never mistaken for each other. */
  --clay: #835D33;
  --clay-hover: #6D4D2A;
  --clay-pressed: #573E22;
  --clay-d: #4C6877;
  --brand: var(--clay);
  --brand-2: var(--clay-d);
  --on-accent: #FFFFFF;

  /* ── Semantic ────────────────────────────────────────────────────────
   * A2. Fixed hue anchors (152 / 36 / 6) nudged 4% toward the theme, with
   * lightness re-solved per surface, so contrast is identical in all eight
   * themes rather than drifting per palette. */
  --success: #2F7856;
  --warning: #5F4319;
  --danger: #7B2F27;
  /* Ink on a success fill. Defaults to white the same way --on-accent does;
   * a theme that lightens --success must re-solve this via palette_gen.py
   * like every other pair. */
  --on-success: #FFFFFF;

  /* ── Interaction states ──────────────────────────────────────────────
   * Required in both light and dark. A theme that defines these for only one
   * mode is the single most common dark-mode failure. */
  --focus-ring: #A3743F;
  --disabled-surface: #E6DED5;
  --disabled-text: #857E76;   /* ~3:1 — legible, but clearly unavailable */
  --scrim: rgba(0, 0, 0, 0.5);
  /* The lightbox always sits on the scrim, never on a theme surface,
   * so its ink is fixed rather than theme-derived. */
  --on-scrim: #EDE9E2;

  /* ── Shape ───────────────────────────────────────────────────────────
   * A five-step soft scale, replacing the two hard values (2px and 4px) the
   * whole public site used until 2026-08-06. This is not decoration: a 2px
   * corner reads as a form and a 20-36px corner reads as a gallery, and the
   * public site is the surface that has to feel like the second one. Measured
   * against the reference studio site, which carries the same five steps, it
   * was the single largest non-colour difference between the two.
   *
   * Every step has a job. Adding a sixth means one of these has two.
   */
  --radius-xs: 10px;    /* chips, tags, icon buttons */
  --radius: 14px;       /* buttons and inputs — the default */
  --radius-card: 20px;  /* ordinary cards */
  --radius-lg: 28px;    /* large panels */
  --radius-xl: 36px;    /* gallery-scale containers and the hero */
  --radius-pill: 999px; /* toggles, filters, small badges */

  /* ── Elevation ───────────────────────────────────────────────────────
   * Two, and only two. Anything needing more separation than `float` should
   * change SURFACE, not add a third shadow — which is also why dark mode does
   * not scale these: black on black is not elevation, and the dark palette
   * carries its layering in the surface lightnesses instead.
   */
  --shadow-soft: 0 18px 55px color-mix(in srgb, var(--ink) 9%, transparent);
  --shadow-float: 0 24px 70px color-mix(in srgb, var(--ink) 14%, transparent);

  /* ── Type ────────────────────────────────────────────────────────────
   * CJK runs on the system stack so mainland visitors never wait on
   * fonts.googleapis.com; only the Latin display face is a web font. */
  --font-latin: "Cormorant Garamond", "Iowan Old Style", Georgia, serif;
  --font-body: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
    "Source Han Sans SC", "Noto Sans SC", system-ui, -apple-system, sans-serif;
  --font-display: "Songti SC", "Source Han Serif SC", "Noto Serif SC",
    "Times New Roman", serif;

  /* ── Motion ──────────────────────────────────────────────────────────
   * One rhythm for the whole product. Exits run at ~65% of entrances so an
   * interface feels responsive rather than sluggish. */
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in: cubic-bezier(0.5, 0, 0.85, 0.4);
  --dur-enter: var(--ui-duration-enter, 233ms);
  --dur-exit: var(--ui-duration-exit, 144ms);

  /* Golden-ratio rhythm shared by Portal and Quick Registration. */
  --space-phi-xs: var(--ui-space-2, 8px);
  --space-phi-sm: var(--ui-space-3, 13px);
  --space-phi-md: var(--ui-space-4, 21px);
  --space-phi-lg: var(--ui-space-5, 34px);
  --space-phi-xl: var(--ui-space-6, 55px);
  --space-phi-2xl: var(--ui-space-7, 89px);
  --golden-columns: var(--ui-golden-columns, minmax(0, 1.618fr) minmax(0, 1fr));
  --golden-columns-reverse: var(--ui-golden-columns-reverse, minmax(0, 1fr) minmax(0, 1.618fr));

  /* ── Touch targets ───────────────────────────────────────────────────*/
  --tap-min: 44px;
}

/* Interactive boundaries and focus, applied once so every surface inherits
 * the same behaviour instead of each page re-declaring it. */
:where(input, select, textarea) {
  border-color: var(--line-strong);
}

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

:where(button, [role="button"], a.btn):disabled,
:where(button, [role="button"], a.btn)[aria-disabled="true"] {
  background: var(--disabled-surface);
  color: var(--disabled-text);
  border-color: var(--line);
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-enter: 0ms;
    --dur-exit: 0ms;
  }
}
