/* ==========================================================================
   USAWI BRAND LAYER  ·  usawi-brand.css
   --------------------------------------------------------------------------
   Single source of truth for the Usawi design system. Linked by all four
   documents:

       index.html                  <html data-app="hub">
       apps/usawi-biz.html         <html data-app="biz">
       apps/usawi-hr-wiz.html      <html data-app="hr">
       apps/usawi-gpu.html         <html data-app="gpu">

   Everything here is derived from design/branding.html (Brand Guidelines
   v1.0) - Ink Black #1A1A1A, Paper White #FFFFFF, Crimson #C8102E.

   HOW IT WORKS
   Each app keeps its own markup and logic; this file only remaps the CSS
   custom properties each app already uses onto the brand palette, and
   overrides the handful of hard-coded off-brand colours. Load it AFTER the
   app's own <style> block.

   Scoping is by html[data-app="…"], which outranks the :root and
   [data-theme] blocks the apps ship with, so order is not critical - but
   loading last is still the documented contract.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1 · TYPEFACES
   Fontsource via jsDelivr - the same source and the same versions the brand
   guide and Usawi HR Wiz already use, so the font files are shared from
   cache rather than downloaded twice. unicode-range sliced + font-display:
   swap, so only the subsets a page actually needs are fetched.
   -------------------------------------------------------------------------- */
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.2.5/400.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.2.5/500.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.2.5/600.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.2.5/700.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-jp@5.2.5/400.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-jp@5.2.5/600.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-jp@5.2.5/700.css");
@import url("https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-mono@5.2.5/400.css");

/* --------------------------------------------------------------------------
   2 · CANONICAL TOKENS
   The guide's names (--color-brand-*, --bg-*, --text-*) plus a --u-* set for
   internal use. These are the values every app should resolve to.
   -------------------------------------------------------------------------- */
:root,
[data-theme="light"] {
  /* core triad */
  --u-ink: #1A1A1A;
  --u-paper: #FFFFFF;

  /* crimson scale */
  --u-red-50: #FDF2F4;
  --u-red-100: #FCE4E8;
  --u-red-200: #F9C9D1;
  --u-red: #C8102E;
  --u-red-dark: #A00D24;
  --u-red-light: #E8354D;

  /* neutrals */
  --u-gray-50: #F9F9F9;
  --u-gray-200: #E0E0E0;
  --u-gray-400: #9E9E9E;
  --u-gray-600: #616161;
  --u-gray-800: #2C2C2C;
  --u-gray-900: #1A1A1A;

  /* surfaces + text (light) */
  --u-bg-primary: #FFFFFF;
  --u-bg-secondary: #F9F9F9;
  --u-bg-tertiary: #F0F0F0;
  --u-bg-card: #FFFFFF;
  --u-text-primary: #1A1A1A;
  --u-text-secondary: #616161;
  --u-text-tertiary: #9E9E9E;
  --u-border: #E0E0E0;
  --u-border-subtle: #F0F0F0;

  /* semantic - the guide's four, on Material 800 for light surfaces */
  --u-success: #2E7D32;
  --u-warning: #F57F17;
  --u-info: #1565C0;
  --u-error: #C8102E;

  /* radius scale (2 / 4 / 8 / 12 / full) */
  --u-r-sm: 2px;
  --u-r-md: 4px;
  --u-r-lg: 8px;
  --u-r-xl: 12px;
  --u-r-full: 9999px;

  /* spacing - 4px base grid */
  --u-s1: 4px;
  --u-s2: 8px;
  --u-s3: 16px;
  --u-s4: 24px;
  --u-s5: 32px;
  --u-s6: 48px;
  --u-s7: 64px;
  --u-s8: 96px;

  /* type */
  --u-font-ui: 'Noto Sans JP', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --u-font-display: 'Noto Serif JP', 'Noto Sans JP', Georgia, serif;
  --u-font-mono: 'Noto Sans Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* alias the guide's public token names */
  --color-brand-red: #C8102E;
  --color-brand-red-dark: #A00D24;
  --color-brand-red-light: #E8354D;
  --color-brand-red-50: #FDF2F4;
  --color-brand-red-100: #FCE4E8;
  --color-brand-red-200: #F9C9D1;
}

[data-theme="dark"] {
  --u-bg-primary: #0D0D0D;
  --u-bg-secondary: #1A1A1A;
  --u-bg-tertiary: #2C2C2C;
  --u-bg-card: #1A1A1A;
  --u-text-primary: #F0F0F0;
  --u-text-secondary: #BDBDBD;
  --u-text-tertiary: #757575;
  --u-border: #424242;
  --u-border-subtle: #2C2C2C;

  /* semantic lifted for contrast on near-black */
  --u-success: #66BB6A;
  --u-warning: #FFB74D;
  --u-info: #64B5F6;
  --u-error: #E8354D;
}

/* --------------------------------------------------------------------------
   3 · THE LOGO
   Two raster lockups exist: ink-on-transparent and white-on-transparent.
   The crimson circle is identical in both - it is never recoloured.
   Rendered as a background so only the variant in use is fetched.
   Size it with --logo-h; the width follows the 2.462:1 aspect automatically.
   -------------------------------------------------------------------------- */
.usawi-logo {
  display: inline-block;
  height: var(--logo-h, 40px);
  width: calc(var(--logo-h, 40px) * 2.462);
  background: url("assets/usawi-logo.png") no-repeat left center / contain;
  flex-shrink: 0;
}

[data-theme="dark"] .usawi-logo {
  background-image: url("assets/usawi-logo-reversed.png");
}

.usawi-mark {
  display: inline-block;
  height: var(--mark-h, 36px);
  width: calc(var(--mark-h, 36px) * 0.905);
  background: url("assets/usawi-mark.png") no-repeat center / contain;
  flex-shrink: 0;
}

[data-theme="dark"] .usawi-mark {
  background-image: url("assets/usawi-mark-reversed.png");
}

/* --------------------------------------------------------------------------
   Version chip
   Every application shows the same version next to its logo, single-sourced
   from window.USAWI_VERSION so the suite can never drift apart.
   -------------------------------------------------------------------------- */
.ver-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--u-r-full);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  background: rgba(200, 16, 46, .1);
  color: var(--color-brand-red);
  border: 1px solid rgba(200, 16, 46, .24);
}

[data-theme="dark"] .ver-chip {
  background: rgba(232, 53, 77, .14);
  color: var(--color-brand-red-light);
  border-color: rgba(232, 53, 77, .3);
}

/* --------------------------------------------------------------------------
   4 · HUB  ·  index.html
   The hub is almost entirely token-driven, so remapping six properties
   re-skins the whole page. Gradients that were cyan -> blue -> violet now
   resolve crimson -> crimson-dark, which reads as a flat crimson.
   -------------------------------------------------------------------------- */
html[data-app="hub"] {
  --accent: #C8102E;
  --accent-dark: #A00D24;
  --accent-soft: rgba(200, 16, 46, .09);
  --blue: #A00D24;
  --violet: #A00D24;
  --green: #2E7D32;
  --orange: #F57F17;
  --hero-glow-a: rgba(200, 16, 46, .08);
  --hero-glow-b: rgba(26, 26, 26, .045);
}

html[data-app="hub"][data-theme="dark"] {
  --accent: #E8354D;
  --accent-dark: #C8102E;
  --accent-soft: rgba(232, 53, 77, .14);
  --blue: #C8102E;
  --violet: #C8102E;
  --green: #66BB6A;
  --orange: #FFB74D;
  --hero-glow-a: rgba(200, 16, 46, .15);
  --hero-glow-b: rgba(232, 53, 77, .09);
}

html[data-app="hub"] body {
  font-family: var(--u-font-ui);
}

/* the guide's mono face, instead of the platform default */
html[data-app="hub"] .mono,
html[data-app="hub"] pre.code,
html[data-app="hub"] .inp.mono,
html[data-app="hub"] code.inl {
  font-family: var(--u-font-mono);
}

/* the guide reserves Noto Serif JP for Display / H1 / H2 */
html[data-app="hub"] .h1,
html[data-app="hub"] .sec-h h2,
html[data-app="hub"] .hub-h1,
html[data-app="hub"] .cta h2,
html[data-app="hub"] .mb-t {
  font-family: var(--u-font-display);
}

/* buttons: solid crimson primary, 2px ink outline secondary, crimson ghost */
html[data-app="hub"] .btn-primary {
  background: var(--color-brand-red);
  box-shadow: 0 4px 14px rgba(200, 16, 46, .22);
}

html[data-app="hub"] .btn-primary:hover {
  background: var(--color-brand-red-dark);
  box-shadow: 0 10px 26px rgba(200, 16, 46, .28);
}

html[data-app="hub"] .btn-outline {
  border: 1.5px solid var(--u-ink);
  color: var(--u-ink);
}

html[data-app="hub"] .btn-outline:hover {
  border-color: var(--u-ink);
  background: rgba(26, 26, 26, .05);
  color: var(--u-ink);
}

html[data-app="hub"][data-theme="dark"] .btn-outline {
  border-color: var(--u-text-primary);
  color: var(--u-text-primary);
}

html[data-app="hub"][data-theme="dark"] .btn-outline:hover {
  border-color: var(--u-text-primary);
  background: rgba(255, 255, 255, .07);
  color: var(--u-text-primary);
}

/* the headline no longer runs three hues */
html[data-app="hub"] .grad {
  background: none;
  -webkit-text-fill-color: currentColor;
  color: var(--color-brand-red);
}

html[data-app="hub"][data-theme="dark"] .grad {
  color: var(--color-brand-red-light);
}

/* app tiles: one crimson family instead of cyan / green / orange */
html[data-app="hub"] .i-biz,
html[data-app="hub"] .i-hr,
html[data-app="hub"] .i-gpu,
html[data-app="hub"] .i-sizing,
html[data-app="hub"] .avatar {
  background: linear-gradient(135deg, #C8102E, #A00D24);
}

html[data-app="hub"] .c-biz::after,
html[data-app="hub"] .c-hr::after,
html[data-app="hub"] .c-gpu::after,
html[data-app="hub"] .c-sizing::after {
  background: linear-gradient(90deg, #C8102E, #E8354D);
}

/* --------------------------------------------------------------------------
   5 · USAWI BIZ  ·  apps/usawi-biz.html
   Token names: --blue*, --ink*, --muted/--faint, --surface*, --green/--amber/--red
   Keeps Tajawal for Arabic - Noto Sans JP carries no Arabic glyphs.
   -------------------------------------------------------------------------- */
html[data-app="biz"] {
  --blue: #C8102E;
  --blue-strong: #A00D24;
  --blue-soft: #FDF2F4;
  --blue-ink: #7A0A1C;
  --ring: rgba(200, 16, 46, .28);
  --green: #2E7D32;
  --amber: #F57F17;
  --red: #C8102E;
  --grid-line: rgba(200, 16, 46, .05);
}

html[data-app="biz"][data-theme="dark"] {
  --blue: #E8354D;
  --blue-strong: #C8102E;
  --blue-soft: rgba(232, 53, 77, .13);
  --blue-ink: #F9C9D1;
  --ring: rgba(232, 53, 77, .38);
  --green: #66BB6A;
  --amber: #FFB74D;
  --red: #E8354D;
  --grid-line: rgba(232, 53, 77, .05);
}

/* Latin in Noto Sans JP; Arabic stays in Tajawal via glyph fallback. */
html[data-app="biz"] body {
  font-family: var(--u-font-ui);
}

html[data-app="biz"][data-lang="ar"] body {
  font-family: 'Tajawal', var(--u-font-ui);
}

/* Biz lays itself out with .shell rather than body, so it needs the dvh
   treatment too — otherwise the sidebar's full height runs under the phone's
   browser chrome. */
html[data-app="biz"] .shell {
  min-height: 100vh;
  min-height: 100dvh;
}

/* --------------------------------------------------------------------------
   6 · USAWI HR WIZ  ·  apps/usawi-hr-wiz.html
   Already built on the brand tokens (crimson + Noto + 4/8px radius), so it
   needs nothing but the shared logo and font guarantee.
   -------------------------------------------------------------------------- */
html[data-app="hr"] body {
  font-family: var(--u-font-ui);
}

html[data-app="hr"] {
  --ff-mono: var(--u-font-mono);
}

html[data-app="hr"] .uhw-hero-title,
html[data-app="hr"] h1,
html[data-app="hr"] h2 {
  font-family: var(--u-font-display);
}

/* --------------------------------------------------------------------------
   7 · USAWI GPU  ·  apps/usawi-gpu.html
   Was dark-locked navy + blue + teal. Now theme-aware.
     --secondary (actions/links/active)  -> crimson
     --accent    (decorative highlights) -> crimson-light
     stock/success states                -> brand success green
     low stock / notices                 -> brand warning
   -------------------------------------------------------------------------- */
html[data-app="gpu"][data-theme="light"] {
  --primary: #F0F0F0;
  --primary-light: #FFFFFF;
  --secondary: #C8102E;
  --secondary-hover: #A00D24;
  --accent: #A00D24;
  --accent-glow: rgba(200, 16, 46, .18);
  --text-light: #1A1A1A;
  --text-muted: #616161;
  --text-dark: #FFFFFF;
  --card-bg: #FFFFFF;
  --card-border: #E0E0E0;
  --bg-body: #F9F9F9;
  --shadow: 0 10px 15px rgba(0, 0, 0, .1);
  --radius: 12px;
  --radius-sm: 8px;
  --font: var(--u-font-ui);
  --danger: #C8102E;
  --warning: #F57F17;
  --u-stock-ok: #2E7D32;
  --u-stock-ok-bg: rgba(46, 125, 50, .12);
  --u-stock-ok-line: rgba(46, 125, 50, .3);
}

html[data-app="gpu"][data-theme="dark"] {
  --primary: #0D0D0D;
  --primary-light: #1A1A1A;
  --secondary: #C8102E;
  --secondary-hover: #A00D24;
  --accent: #E8354D;
  --accent-glow: rgba(200, 16, 46, .28);
  --text-light: #F0F0F0;
  --text-muted: #BDBDBD;
  --text-dark: #FFFFFF;
  --card-bg: #1A1A1A;
  --card-border: #424242;
  --bg-body: #0D0D0D;
  --shadow: 0 20px 40px -12px rgba(0, 0, 0, .8);
  --radius: 12px;
  --radius-sm: 8px;
  --font: var(--u-font-ui);
  --danger: #E8354D;
  --warning: #FFB74D;
  --u-stock-ok: #66BB6A;
  --u-stock-ok-bg: rgba(102, 187, 106, .14);
  --u-stock-ok-line: rgba(102, 187, 106, .3);
}

/* --- text that was hard-coded #fff for a dark-only app --- */
html[data-app="gpu"] .stats-bar .stat-item strong,
html[data-app="gpu"] .product-card .card-body .price,
html[data-app="gpu"] .no-results h4,
html[data-app="gpu"] .success-box h3,
html[data-app="gpu"] .admin-tabs .tab-btn:hover,
html[data-app="gpu"] .modal .modal-header .close:hover,
html[data-app="gpu"] .product-card .card-footer .qty-wrap button:hover {
  color: var(--text-light);
}

/* --- crimson that was hard-coded as a blue glow --- */
html[data-app="gpu"] .header-actions .search-wrap input:focus,
html[data-app="gpu"] .form-group input:focus {
  box-shadow: 0 0 0 3px rgba(200, 16, 46, .2);
}

html[data-app="gpu"] .btn-primary:hover {
  box-shadow: 0 0 24px rgba(200, 16, 46, .35);
}

html[data-app="gpu"] .btn-admin {
  background: rgba(200, 16, 46, .12);
  border-color: rgba(200, 16, 46, .3);
}

html[data-app="gpu"] .success-box .email-note {
  background: rgba(200, 16, 46, .08);
  border-color: rgba(200, 16, 46, .28);
}

html[data-app="gpu"] .login-box .lock-icon {
  background: rgba(200, 16, 46, .12);
}

/* --- danger kept distinct from the crimson primary action --- */
html[data-app="gpu"] .btn-danger {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
}

html[data-app="gpu"] .btn-danger:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: #FFF;
}

/* --- teal success states now read as brand success --- */
html[data-app="gpu"] .product-card .card-top .stock-badge.in,
html[data-app="gpu"] .order-card .order-header .order-status.done {
  background: var(--u-stock-ok-bg);
  border-color: var(--u-stock-ok-line);
  color: var(--u-stock-ok);
}

html[data-app="gpu"] .success-box .success-icon {
  background: var(--u-stock-ok-bg);
  border-color: var(--u-stock-ok);
  color: var(--u-stock-ok);
}

/* --- monospace details in the guide's mono face --- */
html[data-app="gpu"] .product-card .card-top .name .sku,
html[data-app="gpu"] .success-box .order-ref {
  font-family: var(--u-font-mono);
}

/* --- amber "attention" states that were hard-coded, not tokenised --- */
html[data-app="gpu"] .notice-bar,
html[data-app="gpu"] .login-hint {
  background: color-mix(in srgb, var(--warning) 8%, transparent);
  border-color: color-mix(in srgb, var(--warning) 28%, transparent);
}

html[data-app="gpu"] .product-card .card-top .stock-badge.low,
html[data-app="gpu"] .order-card .order-header .order-status {
  background: color-mix(in srgb, var(--warning) 15%, transparent);
  border-color: color-mix(in srgb, var(--warning) 32%, transparent);
  color: var(--warning);
}

/* --- hard-coded reds --- */
html[data-app="gpu"] .product-card .card-top .stock-badge.out {
  background: color-mix(in srgb, var(--danger) 15%, transparent);
  border-color: color-mix(in srgb, var(--danger) 32%, transparent);
  color: var(--danger);
}

html[data-app="gpu"] .form-group input.error {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent);
}

html[data-app="gpu"] .cart-item .ci-right .ci-remove:hover {
  color: var(--danger);
}

/* --- the placeholder letter tile is replaced by the real logo --- */
html[data-app="gpu"] .logo-icon {
  background: none;
  box-shadow: none;
  width: auto;
  height: auto;
}

html[data-app="gpu"] .logo-text {
  background: none;
  -webkit-text-fill-color: currentColor;
  color: var(--text-light);
  font-family: var(--u-font-display);
}

/* --------------------------------------------------------------------------
   8 · USAWI SIZING TOOL  ·  apps/usawi-sizingtool.html
   Built on its own token set, so this is a mapping rather than a rewrite:
      --brand*   (#DC2626 ramp)   -> crimson
      --ink*     (#1A1414 ramp)   -> Ink Black + neutral greys
      --bg / --surface*           -> Paper White
      --success / --warn / --danger -> brand semantic

   The app is unusually well tokenised — 268 token references against 40 hex
   literals in its stylesheet — which is why a mapping is enough. The handful
   of exceptions are handled explicitly below.

   LOCKED TO LIGHT, deliberately, and for the same reason as HR Wiz.
   Sixteen of its eighteen illustrations are inline SVG speaker cabinets whose
   fills are near-black (#1A1414 body, #0A0806 driver) drawn to sit on a light
   card. Forcing dark would put dark cabinets on dark cards and erase them.
   That is a re-skin of the artwork, not a token swap, so the app declares
   data-usawi-theme-lock="light" and the bridge reports its theme rather than
   imposing the hub's.
   -------------------------------------------------------------------------- */
html[data-app="sizing"] {
  --brand:        #C8102E;
  --brand-2:      #E8354D;
  --brand-3:      #F9C9D1;
  --brand-dark:   #A00D24;
  --brand-darker: #7A0A1C;

  --bg:        #FFFFFF;
  --surface:   #FFFFFF;
  --surface-2: #F7F7F7;
  --surface-3: #EFEFEF;

  --ink:   #1A1A1A;
  --ink-2: #3D3D3D;
  --ink-3: #616161;
  --muted: #8A8A8A;

  --line:   #E8E8E8;
  --line-2: #D6D6D6;

  --success: #2E7D32;
  --warn:    #F57F17;
  --danger:  #C8102E;
}

/* Type: the brand faces, replacing Inter and Space Grotesk. The app sets
   font-family on `*` and on h1/h2/h3/.display, so both need outranking. */
html[data-app="sizing"] * {
  font-family: var(--u-font-ui);
}

html[data-app="sizing"] h1,
html[data-app="sizing"] h2,
html[data-app="sizing"] h3,
html[data-app="sizing"] .display {
  font-family: var(--u-font-display);
}

/* These two carry literal gradient stops, so a token remap alone would leave
   the old #DC2626 showing through. They are re-declared instead. */
html[data-app="sizing"] .brand-grad {
  background: linear-gradient(135deg, #C8102E 0%, #E8354D 60%, #F9C9D1 100%);
}

html[data-app="sizing"] .brand-text {
  background: linear-gradient(135deg, #C8102E, #A00D24);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The app hard-codes its page background and a #ccc border on .surface, both
   of which would ignore the tokens above. */
html[data-app="sizing"] body {
  background: var(--bg);
  color: var(--ink);
}

html[data-app="sizing"] .surface {
  border-color: var(--line);
}

/* The active stepper glow is a literal rgba of the old red. */
html[data-app="sizing"] .stepper-dot.active {
  box-shadow: 0 4px 12px rgba(200, 16, 46, .32);
}

/* Focus rings, selection tints and the brand tag are painted with literal
   rgba() of the old red (#DC2626) and its darker shade (#B91C1C). A token
   remap cannot reach a literal, so crimson would have sat beside a pink-red
   tint — visible on every focused input and every selected tile. The hex
   sweep missed these because they are rgba(), not #hex, which is exactly the
   kind of miss that only shows up by looking at computed styles. */
html[data-app="sizing"] .tag-brand {
  background: rgba(200, 16, 46, .08);
  border-color: rgba(200, 16, 46, .22);
}

html[data-app="sizing"] .input:focus {
  box-shadow: 0 0 0 3px rgba(200, 16, 46, .12);
}

html[data-app="sizing"] .btn-brand {
  box-shadow: 0 1px 2px rgba(160, 13, 36, .30);
}

html[data-app="sizing"] .btn-brand:hover {
  box-shadow: 0 4px 12px rgba(160, 13, 36, .25);
}

html[data-app="sizing"] .product-card.selected {
  box-shadow: 0 0 0 2px var(--brand), 0 8px 24px rgba(200, 16, 46, .15);
}

html[data-app="sizing"] .type-tile.selected {
  background: rgba(200, 16, 46, .04);
}

html[data-app="sizing"] .app-shell {
  background:
    radial-gradient(1200px 600px at 0% 0%, rgba(200, 16, 46, .06), transparent 60%),
    radial-gradient(900px 500px at 100% 0%, rgba(232, 53, 77, .05), transparent 60%),
    var(--bg);
}

/* --------------------------------------------------------------------------
   9 · MOBILE SMOOTHNESS
   Shared by all documents so behaviour is identical everywhere rather than
   tuned app by app.
   -------------------------------------------------------------------------- */

/* Respect a reduced-motion preference — every transition and animation in the
   suite collapses, including the hub's theme cross-fade and the spark bars. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

html {
  /* no grey flash on tap */
  -webkit-tap-highlight-color: transparent;
  /* stop iOS inflating text when a phone is turned landscape */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* 100vh on a phone includes the browser chrome, so a full-height layout is
   either clipped or gains a phantom scroll. dvh tracks the height actually
   visible and degrades quietly where it is unsupported. The hub's viewer is
   already position:fixed/inset:0, so it needs nothing. */
body {
  min-height: 100vh;
  min-height: 100dvh;
}
