/* ══════════════════════════════════════════════════════════════════════
   Nivasik — tokens.css · ONE source of truth for colour, type and motion.

   Dark is the bare `:root` default: it is the brand, the TWA splash, the
   manifest colour, and the no-JS fallback. Light is ONE block selected by
   `:root[data-theme="light"]`, which the boot script in every page head
   sets — including when the stored preference is "system" and the OS is
   light. There is deliberately NO `@media (prefers-color-scheme)` twin:
   a duplicated palette is exactly the mechanism that produced three
   drifting forks of --muted in this file's previous life.

   Elevation ladder, both themes:  bg < surface < surface-2 < surface-3.

   Every contrast ratio in a comment below was computed with the WCAG
   relative-luminance formula against the ground named, not estimated.

   OLD NAMES ARE KEPT. --radius, --border-strong, --bg-elevated, --fs-tiny
   and friends are still defined, because ~335 declarations across 42 pages
   use them. New, better-named tokens sit alongside; old ones are aliases,
   not a second opinion.
   ══════════════════════════════════════════════════════════════════════ */

/* Registered so the SLA ring can tween instead of jumping. Engines that
   do not support @property ignore it and get today's behaviour. */
@property --p { syntax: '<number>'; inherits: false; initial-value: 0; }

/* ── Type faces ────────────────────────────────────────────────────────
   Self-hosted, same-origin, variable. Before this, tokens.css named
   'Clash Display' and 'Satoshi' while two login pages fetched them from
   Google Fonts — which does not carry either face and answered HTTP 400
   with 6.8 KB of error HTML, render-blocking, on every shift start. The
   product had no typeface at all: it rendered in SF on the founder's Mac
   and Roboto on the staff's phones.

   Both faces are from the Indian Type Foundry via fontshare.com and are
   licensed for commercial use, web included, under the ITF Free Font
   Licence. Provenance in public/assets/fonts/README.md.
   72 KB for the pair, cached forever, zero third-party DNS. ─────────── */
@font-face {
  font-family: 'Clash Display';
  src: url('/assets/fonts/clash-display-var.woff2') format('woff2-variations');
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('/assets/fonts/satoshi-var.woff2') format('woff2-variations');
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ── The top inset ────────────────────────────────────────────────────
     env(safe-area-inset-top) is correct on iOS and on the web. Inside the
     Android WebView it is exactly 0 — Capacitor does not enable the
     display-cutout plumbing that populates it — so js/native.js reads the
     real inset from the StatusBar plugin (which divides by density, i.e.
     gives CSS pixels) and publishes it as --nv-safe-top.

     max() of the two means neither platform has to know about the other,
     and on the web both are 0 so the layout is unchanged.

     --nv-safe-top IS DELIBERATELY NEVER DECLARED IN CSS, and this note is
     the only place that says so. It is a contract with the native shell:
     public/v2/js/native.js sets it on document.documentElement inside the
     Capacitor app and nowhere else, so on the web the var() falls through to
     its own 0px default. A grep for its declaration finds nothing, which
     looks exactly like a dangling pointer and is not one. Declaring it here
     would pin every platform to one number and make the app's real inset
     unreachable. */
  --safe-top: max(env(safe-area-inset-top, 0px), var(--nv-safe-top, 0px));

  /* Native controls — select popups, date pickers, scrollbars. */
  color-scheme: dark;

  /* ── Ground ──────────────────────────────────────────────────────────
     Cool graphite, per docs/design/DIRECTION.md. The surface before this
     was #161918, 1.072:1 against its bg — imperceptible on the cheap LCD a
     receptionist actually uses, so every "card" was invisible. The step is
     1.13:1 now, with the inset top highlight in --shadow-1 doing the rest:
     a hard lit edge reads at far lower contrast than an area fill.

     RE-MEASURED 2026-09-19. This comment used to say the surface had been
     lifted to #1B1F1E — a value that has not been in this file since the
     graphite ground landed — and every ratio annotated under it had been
     computed against that dead colour. Every number in this file is now the
     WCAG ratio against the hex actually declared beside it. A measurement
     that names a ground the file no longer has is worse than no measurement:
     it is the reason nobody re-checks. */
  --bg:          #0A0C0F;
  --surface:     #171B22;   /* 1.13:1 on bg */
  --surface-2:   #1E232B;   /* raised: modal, menu, popover */
  --surface-3:   #262C35;   /* row hover fill */
  --sunken:      #0F1216;   /* recessed band */
  /* Structure, not content. The sidebar painted the same --surface as the
     cards it frames, so the chrome and the thing inside it were literally
     the same material and neither could recede behind the other. A shade
     back from the content plane, so the frame reads as frame. */
  --surface-chrome: #10141A;
  --bg-elevated: #0F1216;   /* legacy alias of --sunken (it was misnamed) */

  /* ── Line ────────────────────────────────────────────────────────────
     Split by JOB. --border is decorative and may be subtle (WCAG 1.4.11
     does not apply to it). --border-control outlines a CONTROL and must
     clear 3:1 or the input is invisible to a low-vision user. */
  --border:         #262B33;
  /* 4.26 bg · 3.76 surface · 3.44 surface-2 · 3.06 surface-3. The old
     #636C69 is 2.60:1 on surface-3, i.e. an input inside a hovered row
     had a border a low-vision user could not see. */
  --border-control: #6E7681;
  --border-strong:  #6E7681;  /* legacy alias of --border-control */

  /* ── Text ─────────────── measured on --surface #171B22 ───────────── */
  --text:      #E8EAED;   /* 14.32:1 */
  --text-soft: #B7BDC6;   /*  9.13:1 — body copy. NOT --muted. */
  --muted:     #8B939F;   /*  5.57:1 — metadata only */
  --muted-2:   #7A828E;   /*  4.45:1 — placeholders, hover borders. NEVER text:
                              it is 3.62:1 on the row-hover ground. */

  /* ── Brand ───────────────────────────────────────────────────────────
     --accent-fill is the SAME #00C896 in both themes carrying the SAME
     dark ink (8.87:1 either way). The green button is the one object a
     buyer remembers; it must not change when the theme does.
     --accent is the INK role — text, icons, hairlines — and it differs. */
  --accent:       #00C896;   /* 9.05:1 on bg */
  --accent-fill:  #00C896;
  --accent-hover: #17D8A6;
  --on-accent:    #001210;   /* 8.87:1 on #00C896 */
  --primary:      #7C9CFF;   /* periwinkle ink, 7.51:1 on bg. It has not
                                been teal since DIRECTION.md moved info
                                off the accent's hue. */
  --primary-fill: #5B7CFA;

  /* ── Status ink ───────────────────────────────────────────────────────
     Measured against --surface-3 (#262C35), the ROW-HOVER ground, which
     is the worst one and which nothing had checked — not the original
     palette and not the design audit. On it, the old --danger #E04848
     measures 3.47:1 and --info #3D9DA8 measures 4.41:1, both under the
     4.5 floor, and both ARE text roles: `color: var(--danger)` appears
     26 times, and .btn:hover and an active table row both paint
     --surface-3 underneath it.
     Hue and saturation are held exactly; only lightness moves, so these
     read as the same red and the same teal. The single place --danger is
     a FILL is a 6px progress bar with no text on it, and it clears the
     3:1 non-text bar at 5.27. */
  --danger:  #F07C7C;   --warn: #E8AE55;   /* 5.27 · 7.11 on surface-3 */
  /* KNOWN, MEASURED, AND NOT FIXABLE HERE: --success is the same value as
     --accent, so a finished thing and an available action are the same
     colour. Every green tried against this accent measured 1.01–1.58:1 —
     the difference is hue, not lightness, and no amount of darkening
     crosses it. The fix is that the ACTION colour must leave green, which
     is a brand decision and is tracked in docs/design/PANEL.md, not a token
     tweak. Shipping a slightly different green here would look fixed and
     would not be. */
  --success: #00C896;   --info: #7C9CFF;   /* 6.49 · 5.39 on surface-3 */

  /* Alpha-of-hue is correct on a dark ground. Light inverts to opaque. */
  --danger-soft:  #F07C7C22;  --warn-soft:    #E8AE5522;
  --accent-soft:  #00C89622;  --primary-soft: #5B7CFA24;
  --danger-text:  #F58F8F;    --warn-text:    #F0BC6A;
  --success-text: #00C896;    --info-text:    #9DB4FF;

  /* ── Gauge: the SLA ring. Purpose-built, not reused status hues, and
     the track is a deliberate value rather than whatever hover happens
     to be. Measured against their own track: 6.49 / 7.11 / 5.27. ────── */
  --gauge-ok:   #00C896;  --gauge-warn:  #E8AE55;
  --gauge-late: #F07C7C;  --gauge-track: #262C35;

  /* ── Categorical ramp ────────────────────────────────────────────────
     utils.js category() colours a ticket-category badge — its TEXT and
     its BORDER — and it returned four raw hues (#9B6FE0 maintenance,
     #48A8E0 frontdesk, #E0C448 foodbev, #8DA88A amenity) with no light
     counterpart at all. Those four are the last off-palette colours that
     reach a live tenant's screen.
     Solved for 4.5:1 against the worst of the four grounds in each
     theme, hue and saturation held from the originals. Perceptual
     separation checked in Lab: the tightest pair is dE 15.7
     (cat-yellow vs warn, light), which is close but acceptable because
     every badge carries its category NAME as text — hue is the second
     cue here, not the only one. */
  --cat-violet: #B394F0;   /* maintenance · 5.63:1 worst ground */
  --cat-blue:   #5FBBFF;   /* frontdesk   · 6.72:1 */
  --cat-yellow: #E6CB57;   /* foodbev     · 8.73:1 */
  --cat-sage:   #93B08F;   /* amenity     · 5.92:1 */

  /* ── Tinted panels. Replace 25 hardcoded inline gradients that no
     stylesheet could override — and that would have gone white-on-white
     the moment light mode existed. */
  --panel-accent: linear-gradient(155deg, #0B2A24, var(--surface) 68%);
  --panel-warn:   linear-gradient(155deg, #2A1F0B, var(--surface) 68%);
  --panel-info:   linear-gradient(155deg, #12193A, var(--surface) 68%);
  --panel-danger: linear-gradient(155deg, #2A1414, var(--surface) 68%);
  --panel-violet: linear-gradient(155deg, #1B1433, var(--surface) 68%);
  /* Saturated brand fills. These carry --on-accent ink and are the same in
     both themes on purpose — the gradient IS the brand mark. */
  --grad-brand: linear-gradient(140deg, #00C896, #3D9DA8);
  --grad-risk:  linear-gradient(90deg, var(--warn), var(--danger));

  /* ── Elevation. In dark, LIGHT does the work: an inset top highlight
     is what makes a surface look lit from above. */
  --shadow-1: inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.35);
  --shadow-2: inset 0 1px 0 rgba(255,255,255,.06), 0 12px 32px -8px rgba(0,0,0,.6), 0 2px 6px rgba(0,0,0,.35);

  /* ── Scrims. Tint AND blur are tokens: light needs a HIGHER alpha and
     a wider blur than dark, which is what Apple ships. */
  --scrim:        rgba(10,12,15,.82);
  --scrim-filter: saturate(160%) blur(16px);
  --veil:         rgba(0,0,0,.62);

  --focus: #00C896;                                    /* 9.05:1 on bg */
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--focus);
  --focus-ring-inset: inset 0 0 0 1px var(--focus), 0 0 0 3px var(--accent-soft);

  /* ── Data ramp — five hues for a multi-series chart. One series is the
     accent; never four shades of green. Each ≥ 4.5:1 on the row-hover
     ground (#262C35): 6.49 · 5.55 · 7.55 · 6.02 · 6.72. ──────────────── */
  --data-1: #00C896;  --data-2: #8B9CFF;  --data-3: #F5B14F;  --data-4: #FF8496;  --data-5: #5FBBFF;
  /* Chrome is a material the content scrolls under, not a wall. */
  --chrome-glass: rgba(16,20,26,.78);
}

/* ══ LIGHT ══════════════════════════════════════════════════════════════
   Written once. The boot script sets data-theme="light" whenever the
   RESOLVED theme is light, so there is no media-query twin to keep in
   sync. Every value measured on all four grounds.

   The reason nobody had "just added light mode" before is real: the whole
   dark palette fails on white. #00C896 is 2.16:1 on #FFFFFF, #3D9DA8 is
   3.19:1, #E04848 is 4.05:1 — all below the 4.5:1 floor. Flipping the
   background alone would have shipped neon-green-on-white.
   ═════════════════════════════════════════════════════════════════════ */
:root[data-theme="light"] {
  color-scheme: light;

  --bg:          #F5F6F8;
  --surface:     #FFFFFF;   /* elevation from shadow, not fill */
  --surface-2:   #F0F2F5;
  --surface-3:   #E8EBEF;   /* row hover — the worst ground; everything
                               below is measured against it too */
  /* Structure, not content — see the dark block. */
  --surface-chrome: #F0F2F5;
  --sunken:      #EEF0F3;
  --bg-elevated: #EDF0EF;

  --border:         #E2E5EA;
  /* 4.37 page · 4.72 surface · 4.21 surface-2 · 3.95 surface-3. #7E8884
     is 3.06:1 on surface-3 — WCAG 1.4.11's floor exactly, with nothing left
     over for a cheap panel. Same row-hover miss as the dark side. */
  --border-control: #6B7482;
  --border-strong:  #6B7482;

  /*                     #FFF   page  surf-2  surf-3 */
  --text:      #111418;  /* 18.47  17.08  16.47  15.44 */
  --text-soft: #3E4551;  /*  9.66   8.93   8.61   8.07 */
  --muted:     #5C6572;  /*  5.90   5.46   5.26   4.93 */
  --muted-2:   #5C6572;  /* collapsed: there is no safe fifth grey here */

  --accent:       #00785C;  /* 5.47 · 5.06 · 4.87 · 4.57 — clears AA on
                               EVERY ground including row hover */
  --accent-fill:  #00C896;  /* UNCHANGED — the button a buyer remembers */
  --accent-hover: #00A87C;
  --on-accent:    #001210;  /* 8.87:1 on #00C896 */
  --primary:      #4757D6;  /* 5.83 · 5.39 · 5.20 · 4.88 */
  --primary-fill: #4757D6;  /* #FFF on it = 5.83 */

  --danger:  #B3261E;   /* #FFF on it = 6.54 */
  --warn:    #8A5A00;   /* #FFF on it = 5.93 */
  --success: #00785C;
  --info:    #4757D6;

  /* Opaque pastels, not alpha: the mechanism inverts with the ground. */
  --danger-soft:  #FDECEA;  --warn-soft:    #FCF3E0;
  --accent-soft:  #E2F6EF;  --primary-soft: #E9ECFC;
  --danger-text:  #9A2018;  /* 7.08:1 on its own tint */
  --warn-text:    #7A4F00;  /* 6.46:1 */
  --success-text: #00614A;  /* 6.64:1 */
  --info-text:    #3543B8;  /* 6.75:1 */

  --gauge-ok:   #00785C;  --gauge-warn:  #8A5A00;
  --gauge-late: #B3261E;  --gauge-track: #E1E6EA;

  /* Same four, re-solved downward for a light ground. */
  --cat-violet: #7D44D7;   /* 4.77:1 worst ground */
  --cat-blue:   #1A6C9C;   /* 4.79:1 */
  --cat-yellow: #786514;   /* 4.78:1 */
  --cat-sage:   #546D51;   /* 4.77:1 */

  --panel-accent: linear-gradient(155deg, #E2F6EF, var(--surface) 70%);
  --panel-warn:   linear-gradient(155deg, #FCF3E0, var(--surface) 70%);
  --panel-info:   linear-gradient(155deg, #E9ECFC, var(--surface) 70%);
  --panel-danger: linear-gradient(155deg, #FDECEA, var(--surface) 70%);
  --panel-violet: linear-gradient(155deg, #EFEAFA, var(--surface) 70%);
  --grad-brand: linear-gradient(140deg, #00C896, #3D9DA8);
  --grad-risk:  linear-gradient(90deg, var(--warn), var(--danger));

  /* Light alphas are an order of magnitude lower than dark, and tinted
     to the theme's ink rather than pure black. */
  --shadow-1: 0 1px 2px rgba(17,20,24,.05), 0 1px 3px rgba(17,20,24,.08);
  --shadow-2: 0 0 0 1px rgba(15,26,24,.07),
              0 8px 24px -6px rgba(15,26,24,.14),
              0 2px 6px rgba(15,26,24,.06);

  --scrim:        rgba(245,246,248,.9);
  --scrim-filter: saturate(180%) blur(20px);
  --veil:         rgba(15,26,24,.45);

  /* The old ring aliased --accent #00C896, which is 2.00:1 on the light
     page: keyboard navigation would have silently become invisible.
     #00785C is 5.06:1 there. */
  --focus: #00785C;
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--focus);
  --focus-ring-inset: inset 0 0 0 1px var(--focus), 0 0 0 3px var(--accent-soft);

  --data-1: #00785C;  --data-2: #4757D6;  --data-3: #8A5A00;  --data-4: #B8304A;  --data-5: #0F6FA8;
  --chrome-glass: rgba(240,242,245,.8);
}

/* ── Radius: three, no exceptions. Eleven were in use on the homepage
   alone, six of them within 7px of each other. A child inset by N inside
   radius R takes R−N. ─────────────────────────────────────────────────── */
:root {
  --r-sm: 10px;  --r-md: 16px;  --r-pill: 999px;

  /* ── Declared because they were already being USED ────────────────────
     Six names were referenced across the app and declared nowhere, so the
     declarations were silently inert. Two of them are in these very core
     stylesheets: the sidebar's module filter — the control that exists to
     make 43 modules navigable, present on all 42 dashboards — was rendering
     with square corners and no size of its own.
     `--t-fast` is the worst of the six: it sits inside a `transition:`
     shorthand, and one invalid component voids the WHOLE declaration, so
     four shipped pages had no transition at all rather than a fast one. */
  --fs-small:     var(--fs-caption);   /* alias; 4 sites in core CSS */
  --radius-md:    var(--r-md);         /* alias; the sidebar controls */
  --t-fast:       var(--dur-1);        /* 11 sites, inside shorthands */
  --fw-regular:   var(--fw-body);
  /* One call site and it is outside /v2: public/console/index.html's
     `.mod-row:hover`. Named here so the next sweep for dead tokens does not
     delete it — a grep of public/v2 alone finds nothing. */
  --surface-hover: var(--surface-3);

  /* LEGACY RADIUS ALIASES. Every one of them resolves to one of the three
     above; none of them is a fourth opinion.
     --radius-lg WAS 14px, a literal, declared twenty lines under a heading
     that says "three, no exceptions". It reached ten call sites — .card-elev,
     .modal, the mobile bottom sheet, nav.js's sheet, and five page-local
     rules — so a modal had a 14px corner while the card behind it had 16 and
     nothing said which was right. Aliased, not deleted: the name is in use
     and Render's publish is additive, so the file it is written in keeps
     serving. The 2px is the cost of having one scale. */
  --radius-sm: var(--r-sm);
  --radius:    var(--r-md);
  --radius-lg: var(--r-md);
  --radius-pill: var(--r-pill);
}

/* ── Space — 4px step. It was already right. ───────────────────────────── */
:root {
  --sp-1:4px;   --sp-2:8px;   --sp-3:12px;  --sp-4:16px;
  --sp-5:20px;  --sp-6:24px;  --sp-8:32px;  --sp-10:40px;
  --sp-12:48px; --sp-16:64px; --sp-20:80px;
  --sidebar-w:232px; --topbar-h:52px; --maxw:1180px; --pad:32px;
}

/* ── Type ───────────────────────────────────────────────────────────────
   Seven steps on a 1.2 minor third from a 15px body. Every value an
   integer px: fractional px blurs at the 1.5× DPR most budget Androids
   run at.

   WHY 15 AND NOT 16. This is an operations tool with nine-column tables
   read on a 360px screen. 16px body pushes a ticket row past the viewport.
   15px is the largest base the density survives, it is a 7% lift on the
   old 14px, and the answer to "the text feels small" is the CONTRAST —
   body up 1px, everything under 14px up 2px, and the display ceiling
   raised from 58px to 76px.

   Leading is absolute px paired to each step, not a multiplier: `1.7`
   appeared 36 times in this codebase applied identically to 11px metadata
   and 42px headings, which is how a heading ends up with a gap wider than
   its own cap height.

   Four weights. Only four. The old file declared 540/560/620/640/650/660/
   680 — against a static system face, CSS font-matching collapses all of
   those to 500 or 700, so seven declared steps rendered as two. They only
   ever existed on the author's Mac. ─────────────────────────────────── */
:root {
  --font-display: 'Satoshi', -apple-system, BlinkMacSystemFont,
                  'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-body:    'Satoshi', -apple-system, BlinkMacSystemFont, 'Segoe UI',
                  Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo,
                  Consolas, monospace;
}

/* ── Devanagari ──────────────────────────────────────────────────────────────
   Clash Display and Satoshi have no Devanagari glyphs, so a Hindi screen falls
   back to whatever the browser picks per character — and a fallback chosen one
   glyph at a time is how a heading ends up in two typefaces. Naming the
   platform faces puts the whole line in one: Kohinoor on Apple, Nirmala on
   Windows, Noto on Android and Linux. It is a font STACK, not a webfont: this
   costs nothing to download, which matters more on a hotel's 3G than the last
   five percent of typographic control. The Latin faces stay in front so an
   English word inside a Hindi sentence — Nivasik, GST, a room number — still
   sets in the product's own type. */
html[lang^="hi"] {
  --font-display: 'Satoshi', 'Kohinoor Devanagari', 'Nirmala UI',
                  'Noto Sans Devanagari', -apple-system, BlinkMacSystemFont,
                  'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-body:    'Satoshi', 'Kohinoor Devanagari', 'Nirmala UI',
                  'Noto Sans Devanagari', -apple-system, BlinkMacSystemFont,
                  'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo,
                  'Noto Sans Devanagari', Consolas, monospace;
}

:root {
  --fw-body: 400;  --fw-medium: 500;  --fw-semi: 600;  --fw-bold: 600;   /* 700 does not exist in the UI */

  /* step          size                    leading          tracking */
  --fs-micro:    13px;  --lh-micro:    18px;  --tr-micro:    0.08em;
  --fs-caption:  14px;  --lh-caption:  20px;  --tr-caption:  0;
  --fs-body:     15px;  --lh-body:     24px;  --tr-body:    -0.005em;
  --fs-subhead:  18px;  --lh-subhead:  26px;  --tr-subhead: -0.010em;
  --fs-heading:  22px;  --lh-heading:  30px;  --tr-heading: -0.012em;
  --fs-title:    28px;  --lh-title:    34px;  --tr-title:   -0.014em;
  --fs-hero:     clamp(30px, 4.6vw, 46px);
                        --lh-hero:     1.08;  --tr-hero:    -0.012em;
  --fs-display:  clamp(38px, 7vw, 76px);
                        --lh-display:  1.03;  --tr-display: -0.018em;

  /* Legacy names, remapped onto the new scale so the whole app gets the
     size lift without editing 335 declarations. Not a second opinion —
     each is exactly one of the steps above. */
  --fs-tiny: 14px;   /* = caption */
  --fs-sm:   14px;   /* = caption. Dense table cells stay here until the
                        mobile card list lands; 15px overflows a 9-column
                        row on a 360px screen. */
  --fs-md:   18px;   /* = subhead */
  --lh-tight: 1.2;  --lh-relaxed: 1.6;
}

/* Light-on-dark blooms roughly 10–15 weight units heavier than
   dark-on-light. Subpixel AA in light restores matched apparent weight. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
:root[data-theme="light"] body {
  -webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;
}

/* ── Motion ── four curves, six durations, each with a stated job ───────
   Apple's motion signature is almost entirely SEQUENCING, not effects: a
   panel opens and its contents arrive after it, which is what tells you
   the content belongs to the panel. Sequencing costs no bytes and no
   frames, which is why it is the right answer on a cheap Android on 3G. */
:root {
  --dur-0:  90ms;   /* colour-only swaps */
  --dur-1: 140ms;   /* press, hover */
  --dur-2: 200ms;   /* state change on a row */
  --dur-3: 280ms;   /* entering: modal, toast, dropdown, new row */
  --dur-4: 400ms;   /* large surfaces: sheet, page */
  --dur-5: 640ms;   /* exactly one thing: resolve */

  --ease-out:      cubic-bezier(0.32, 0.72, 0, 1);   /* workhorse. Fast
        start, long tail — reads as instant on a slow device. */
  --ease-in:       cubic-bezier(0.40, 0, 1, 1);       /* exits */
  --ease-emphasis: cubic-bezier(0.66, 0, 0.10, 1);    /* large surfaces */
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1); /* used ONCE */
  --ease-in-out:   cubic-bezier(0.66, 0, 0.10, 1);    /* legacy alias */
}

/* Someone who has asked their OS to stop animating things means it. */
/* index.html loads THIS FILE AND NOTHING ELSE, so for the marketing page this
   block is the whole of reduced motion — layout.css's fuller version never
   reaches it.

   The delays are the part that was missing. Zeroing duration without delay is
   the worst of both: no tween, but the stagger still runs, so a `both`-filled
   element holds its from-state for the entire delay and then pops into place.
   The homepage is built on exactly that pattern — 75 reveal elements on
   staggered delays.

   Transitions are gentled rather than zeroed, for the same reason as in
   layout.css: colour and opacity carry no vestibular risk and are what makes
   a state change legible. Movement goes by transform not being in the list. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  *, *::before, *::after {
    transition-property: opacity, color, background-color, border-color, fill, stroke, box-shadow !important;
    transition-duration: var(--dur-1) !important;
  }
}

/* ══ Base ══════════════════════════════════════════════════════════════ */

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

html {
  margin: 0; padding: 0;
  /* The document paints its own ground, edge to edge.
     ───────────────────────────────────────────────────────────────────────
     A WKWebView composites the page over ITS OWN backdrop, which is white.
     Any strip the document does not paint — above the topbar when the
     keyboard changes the content inset, or below the tab bar at the home
     indicator — showed as a white band across a dark app, with the status-bar
     clock white-on-white and unreadable. Measured on an iPhone 17 Pro: 62pt
     at the top with the keyboard up, 34pt at the bottom in dark mode. body
     already carries this; html did not, and html is what the webview sees. */
  background: var(--bg);
  /* `clip` contains horizontal overflow WITHOUT creating a scroll
     container. `hidden` here (and on body) made body the scrollport,
     which silently killed every `position: sticky` in the app — the
     topbar and sidebar scrolled away with the page. Ordered so old
     engines still get `hidden`. */
  overflow-x: hidden;
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--text);
  font: var(--fw-body) var(--fs-body)/var(--lh-body) var(--font-body);
  letter-spacing: var(--tr-body);
  /* Prevent pull-to-refresh breaking the app feel */
  overscroll-behavior-y: none;
}

/* ── The eight type steps, bound as classes. Size, leading, tracking and
   weight travel together so a step is syntactically impossible to
   half-apply. ─────────────────────────────────────────────────────────── */
.t-micro   { font: var(--fw-semi)   var(--fs-micro)/var(--lh-micro)     var(--font-body);
             letter-spacing: var(--tr-micro); text-transform: uppercase; }
.t-caption { font: var(--fw-medium) var(--fs-caption)/var(--lh-caption) var(--font-body);
             letter-spacing: var(--tr-caption); }
.t-body    { font: var(--fw-body)   var(--fs-body)/var(--lh-body)       var(--font-body);
             letter-spacing: var(--tr-body); }
.t-subhead { font: var(--fw-medium)   var(--fs-subhead)/var(--lh-subhead) var(--font-display);
             letter-spacing: var(--tr-subhead); }
.t-heading { font: var(--fw-medium)   var(--fs-heading)/var(--lh-heading) var(--font-display);
             letter-spacing: var(--tr-heading); }
.t-title   { font: var(--fw-semi)   var(--fs-title)/var(--lh-title)     var(--font-display);
             letter-spacing: var(--tr-title); }
.t-hero    { font: var(--fw-semi)   var(--fs-hero)/var(--lh-hero)       var(--font-display);
             letter-spacing: var(--tr-hero); text-wrap: balance; }
.t-display { font: var(--fw-bold)   var(--fs-display)/var(--lh-display) var(--font-display);
             letter-spacing: var(--tr-display); text-wrap: balance; }

/* App headings — dashboard chrome, not marketing. */
h1 { font: var(--fw-medium) var(--fs-title)/var(--lh-title)     var(--font-display);
     letter-spacing: var(--tr-title);   margin: 0; text-wrap: balance; }
h2 { font: var(--fw-medium) var(--fs-heading)/var(--lh-heading) var(--font-display);
     letter-spacing: var(--tr-heading); margin: 0; text-wrap: balance; }
h3 { font: var(--fw-medium) var(--fs-subhead)/var(--lh-subhead) var(--font-display);
     letter-spacing: var(--tr-subhead); margin: 0; }

/* Disable the tap flash everywhere — we draw our own press state. */
a, button, [role="button"], .btn, .nav-link, .tab {
  -webkit-tap-highlight-color: transparent;
}

a { color: var(--accent); text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
input, select, textarea { font-family: inherit; }

/* ── Utilities — restored 2026-09-12 ────────────────────────────────────────
   The design commit 776067f (11 Sep) deleted this block while rewriting the
   type scale. Thirty-two pages still write these classes, so from that day
   their spacing, stacking and muted text silently stopped applying — and
   .hidden stopped hiding, which is how the guest portal came to show all four
   of its panels at once. Restored as it was, in its original place, plus three
   classes pages use that the old block never had: breakable, ml-8, mr-8. No
   .wrap: the homepage and pricing page use that name for their page container,
   and in the app it only ever appears beside .row, which already wraps.

   ── 2026-09-19: THE VALUES ARE TOKENS NOW, AND THE SCALE HAS NO GAPS ──────
   This block used to write `gap: 4px` while --sp-1 IS 4px, and it shipped
   .gap-6, .gap-10 and .mb-10 — which made 6px and 10px first-class, blessed
   by the design system, on a 4px step. 6px (x87) and 10px (x55) are the #2
   and #4 most common raw px literals in the 42 dashboards and this block is
   where they were made legitimate. A utility that only exists off-step is an
   instruction to go off-step.
   .gap-10 and .mb-10 had ZERO and ONE call site. .gap-6 had four, all in
   settings.html and tickets.html, all moved to .gap-8 in the same commit.
   All three are gone. If a layout needs 6px, it needs 4 or 8.

   .num is new here and is not new to the product: 12 dashboards write it on
   a <th>/<td> that holds a figure, and it was defined on TWO of them — plus
   three more page-local variants that disagreed. Right-aligned, tabular,
   never wrapping is what all five meant. One definition, one behaviour. */
.mono  { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.soft  { color: var(--text-soft); }
.tiny  { font-size: var(--fs-tiny); line-height: 1.45; }
.row    { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.col    { display: flex; flex-direction: column; gap: var(--sp-3); }
.between{ justify-content: space-between; }
.grow   { flex: 1 1 0; min-width: 0; }
.center { display: flex; align-items: center; justify-content: center; }
.breakable { overflow-wrap: anywhere; }
.gap-4  { gap: var(--sp-1); }  .gap-8  { gap: var(--sp-2); }
.gap-12 { gap: var(--sp-3); }  .gap-16 { gap: var(--sp-4); }  .gap-24 { gap: var(--sp-6); }
.mt-4  { margin-top: var(--sp-1); }  .mt-8  { margin-top: var(--sp-2); }
.mt-12 { margin-top: var(--sp-3); }  .mt-16 { margin-top: var(--sp-4); }
.mt-24 { margin-top: var(--sp-6); }  .mt-32 { margin-top: var(--sp-8); }
.mb-4  { margin-bottom: var(--sp-1); }  .mb-8  { margin-bottom: var(--sp-2); }
.mb-12 { margin-bottom: var(--sp-3); }  .mb-16 { margin-bottom: var(--sp-4); }
.mb-24 { margin-bottom: var(--sp-6); }
.ml-8  { margin-left: var(--sp-2); } .mr-8 { margin-right: var(--sp-2); }
.text-center { text-align: center; }

/* ── The utilities that REPLACED an inline style, doubled ───────────────────
   Every one of these is new on 2026-09-19 and every one exists to beat a
   component default — which a single class cannot do: `.card h3` and
   `.table th` are (0,1,1) and a bare `.m0` or `.num` is (0,1,0), so the
   class loses and the utility silently does nothing. MEASURED: before this
   block, .m0 failed on six pages and .num on five, in real headless Chrome.

   `.x.x` is (0,2,0) and wins without !important, which is the whole point:
   !important is unanswerable, and the last thing this pass should do is
   replace one unanswerable declaration with another. The doubling is only
   on classes introduced in this pass — raising the specificity of .mt-*,
   .mb-* or .soft would change 32 pages that have relied on losing. */
.m0.m0             { margin: 0; }
.text-right.text-right { text-align: right; }
.num.num           { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.clickable.clickable   { cursor: pointer; }
/* Ink, by role. `style="color:var(--accent)"` was written 13 times and
   `color:var(--warn)` ten more; each one is a token used correctly through a
   mechanism — an inline style attribute — that no stylesheet can answer. */
.ink-accent.ink-accent { color: var(--accent); }
.ink-warn.ink-warn     { color: var(--warn); }
.ink-danger.ink-danger { color: var(--danger); }
/* Flex alignment, by name. `.row` centres and wraps, `.col` stacks; these are
   the five overrides the 42 dashboards actually write, 35 times between them.
   .minw0 is the one every flex layout needs and nobody remembers: a flex item
   will not shrink below its content's intrinsic width without it, which is
   how a long guest name pushes a row sideways off a phone. .grow carries it
   already; these are for the items that are not .grow. */
.minw0.minw0                 { min-width: 0; }
.items-start.items-start     { align-items: flex-start; }
.items-center.items-center   { align-items: center; }
.items-end.items-end         { align-items: flex-end; }
.justify-end.justify-end     { justify-content: flex-end; }
.justify-center.justify-center { justify-content: center; }
/* A roomier card. `style="padding:24px"` was written eleven times and
   `padding:20px` six more — the second is .card's own padding restated. */
.pad-24.pad-24 { padding: var(--sp-6); }
.hidden { display: none !important; }


/* Screen-reader-only, for the theme switch's group label and friends. */
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; }
