/* OrderLah Merchant Portal
 *
 * Hand-written, no framework, no build — the same approach admin-web/ and the
 * public site already take, and the reason the portal ships as static files
 * with nothing to compile.
 *
 * ## The design system
 *
 * The tokens here are the same vocabulary as the public site
 * (site/src/styles/tokens.css) and the app itself (lib/core/app_theme.dart):
 * AppBrand.primary #F97316 as the one brand accent, its accessible text
 * variants #C2410C / #FB923C, the navy #101E33 of the app icon as the
 * navigation rail, and warm off-white paper for the working surface. A
 * merchant who arrives from orderlah.app, or who runs the app on the counter
 * all day, is looking at the same product here.
 *
 * Three things drive everything else:
 *
 * 1. CLAUDE.md's product principle: user-friendly first. The people reading
 *    this run hawker stalls and small restaurants. Tap targets are generous,
 *    type is large enough to read on a phone in a bright shop, and nothing
 *    important is hidden behind a hover.
 *
 * 2. Colour is never the only signal. Every status has a word next to it, so
 *    the page works for the roughly 1 in 12 men who cannot separate red from
 *    green — and in a printout.
 *
 * 3. The Content-Security-Policy has no 'unsafe-inline', so there is not a
 *    single inline style attribute anywhere in this portal. Dynamic values
 *    (a bar's width, a heatmap cell's intensity) come through CSS custom
 *    properties set from JS, which the policy permits. Fonts are the site's
 *    own self-hosted subsets (assets/fonts/, 38 KB for all four), because the
 *    policy allows no external origin and should not need one.
 */

/* ── Fonts ─────────────────────────────────────────────────────────────────
 *
 * The exact families the app and the public site use, subset to Latin-1 +
 * general punctuation (the site README documents the command). Everything has
 * a real system fallback: a stall name in Chinese falls back mid-word and
 * must still sit on the same baseline at the same weight.
 */
@font-face {
  font-family: "Space Grotesk";
  src: url("assets/fonts/space-grotesk-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ── Overview performance command centre ──────────────────────────────────
 * This layer is intentionally page-scoped. The report shell below it still
 * owns scope, errors and permissions. It deliberately composes only the
 * existing OrderLah palette tokens: visual polish may change hierarchy,
 * spacing and effects, but it must not introduce or re-baseline colours. */
.overview-hero {
  position: relative;
  min-height: 390px;
  overflow: hidden;
  padding: 25px 27px 18px;
  border: 1px solid var(--rail-line);
  border-radius: 17px;
  color: var(--rail-ink);
  background: linear-gradient(135deg, var(--rail), var(--rail-2));
  box-shadow: var(--shadow-overlay);
}

.overview-hero::after {
  content: '';
  position: absolute;
  width: 230px;
  height: 230px;
  right: -80px;
  bottom: -115px;
  border-radius: 50%;
  background: var(--accent-soft);
  opacity: .22;
  filter: blur(25px);
  pointer-events: none;
}

.overview-hero-top { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; }
.overview-greeting p { margin: 0 0 2px; color: var(--rail-muted); font-size: 15px; }
.overview-greeting h2 { margin: 0; max-width: 520px; color: var(--rail-ink); font-size: clamp(24px, 2.2vw, 32px); line-height: 1.12; letter-spacing: -.03em; }
.overview-greeting > span { display: block; margin-top: 7px; color: var(--rail-muted); font-size: 14px; }
.overview-hero-switches { position: relative; z-index: 2; min-width: 270px; display: grid; gap: 8px; }

.premium-segmented { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; padding: 3px; border: 1px solid var(--rail-line); border-radius: 12px; background: var(--rail); }
.premium-segmented button { min-height: 35px; padding: 6px 15px; border: 0; border-radius: 9px; color: var(--rail-muted); background: transparent; cursor: pointer; font: inherit; font-size: 13px; }
.premium-segmented button.is-active { color: var(--rail-ink); background: var(--accent); box-shadow: var(--shadow); }
.premium-segmented button:focus-visible { outline: 2px solid var(--rail-ink); outline-offset: 2px; }

.hero-metrics { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, 1fr); max-width: 940px; margin: 22px 0 4px; }
.hero-metric { display: grid; grid-template-columns: 31px 1fr; grid-template-rows: auto auto auto; column-gap: 9px; padding: 0 20px; border-inline-end: 1px solid var(--rail-line); }
.hero-metric:first-child { padding-inline-start: 0; }
.hero-metric:last-child { border: 0; }
.hero-metric-icon { grid-row: 1 / 3; display: grid; place-items: center; width: 29px; height: 29px; border-radius: 8px; color: var(--rail-ink); background: linear-gradient(135deg, var(--series-1), var(--accent)); box-shadow: var(--shadow); }
.hero-metric-orders { background: linear-gradient(135deg, var(--series-2), var(--brand)); }
.hero-metric-average { background: linear-gradient(135deg, var(--series-3), var(--ok)); }
.hero-metric-items { background: linear-gradient(135deg, var(--series-4), var(--series-5)); }
.hero-metric-label { color: var(--rail-muted); font-size: 12px; }
.hero-metric-value { margin-top: 3px; color: var(--rail-ink); font-size: clamp(17px, 1.5vw, 23px); white-space: nowrap; }
.premium-change { display: flex; align-items: baseline; gap: 6px; margin-top: 4px; color: var(--ok); font-size: 11px; }
.premium-change strong { font-size: 12px; }
.premium-change.is-down { color: var(--bad); }
.premium-change.is-neutral { color: var(--rail-muted); }

.overview-hero-chart-slot { height: 190px; margin-top: 6px; }
.premium-chart { position: relative; height: 100%; }
.premium-chart svg { width: 100%; height: 100%; overflow: visible; }
.premium-gridline { stroke: var(--rail-line); stroke-width: 1; }
.premium-bar { fill: var(--series-1); filter: drop-shadow(0 0 6px var(--accent)); transform-box: fill-box; transform-origin: bottom; animation: overview-bar-in .55s ease-out both; }
.premium-bar.is-muted { opacity: .36; }
.premium-line { fill: none; stroke: var(--series-2); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 4px var(--brand)); vector-effect: non-scaling-stroke; }
.premium-line.is-strong { stroke-width: 4; }
.premium-axis-label { fill: var(--rail-muted); font-size: 10px; }
.premium-chart-target { position: absolute; inset-block: 0; border: 0; background: transparent; cursor: crosshair; }
.premium-chart-target:focus-visible { outline: 1px solid var(--accent); outline-offset: -2px; }
.premium-tooltip { position: absolute; z-index: 4; top: 10px; display: grid; gap: 4px; min-width: 150px; padding: 10px 12px; border: 1px solid var(--rail-line); border-radius: 9px; color: var(--rail-ink); background: var(--rail); box-shadow: var(--shadow-overlay); transform: translateX(-50%); font-size: 11px; pointer-events: none; }
.premium-tooltip strong { color: var(--rail-ink); font-size: 12px; }
.premium-empty { display: grid; place-content: center; height: 100%; text-align: center; color: var(--rail-muted); }
.premium-empty strong { color: var(--rail-ink); }
.premium-empty span { margin-top: 6px; }
.premium-legend { position: absolute; right: 26px; bottom: 13px; z-index: 2; display: flex; gap: 20px; color: var(--rail-muted); font-size: 11px; }
.premium-legend span::before { content: ''; display: inline-block; width: 9px; height: 9px; margin-inline-end: 6px; border-radius: 50%; background: var(--series-1); box-shadow: var(--shadow); }
.premium-legend .legend-orders::before { background: var(--series-2); }

.overview-health-strip { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 65px; padding: 10px 17px 10px 22px; border: 1px solid; border-radius: 14px; }
.overview-health-strip.is-healthy { border-color: var(--ok); background: var(--ok-soft); }
.overview-health-strip.is-warning { border-color: var(--warn); background: var(--warn-soft); }
.overview-health-copy { display: grid; grid-template-columns: 22px auto; align-items: center; column-gap: 10px; }
.overview-health-copy strong { color: var(--text); font-size: 14px; }
.overview-health-copy span { grid-column: 2; color: var(--text-muted); font-size: 12px; }
.overview-health-dot { grid-row: 1 / 3; width: 11px; height: 11px; border-radius: 50%; background: var(--ok); box-shadow: var(--shadow); }
.is-warning .overview-health-dot { background: var(--warn); }
.overview-health-link { flex: none; min-height: 39px; padding: 10px 15px; border: 1px solid var(--border); border-radius: 10px; color: var(--text); background: var(--surface); text-decoration: none; font-size: 12px; font-weight: 700; }

.premium-kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 13px; }
.premium-kpi-card { position: relative; display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; grid-template-rows: auto auto auto; gap: 3px 11px; min-height: 112px; padding: 16px; border: 1px solid var(--rail-line); border-radius: 13px; color: var(--rail-ink); background: var(--rail-2); text-decoration: none; transition: transform .16s ease, border-color .16s ease; }
.premium-kpi-card:hover { transform: translateY(-2px); border-color: var(--accent); }
.premium-kpi-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.premium-kpi-icon { grid-row: 1 / 3; display: grid; place-items: center; width: 39px; height: 39px; border-radius: 11px; color: var(--rail-ink); background: linear-gradient(135deg, var(--series-1), var(--accent)); box-shadow: var(--shadow); }
.premium-kpi-orders .premium-kpi-icon { background: linear-gradient(135deg, var(--series-2), var(--brand)); }
.premium-kpi-average .premium-kpi-icon { background: linear-gradient(135deg, var(--series-3), var(--ok)); }
.premium-kpi-items .premium-kpi-icon { background: linear-gradient(135deg, var(--series-4), var(--series-5)); }
.premium-kpi-label { color: var(--rail-muted); font-size: 12px; }
.premium-kpi-value { font-size: clamp(18px, 1.65vw, 24px); white-space: nowrap; }
.premium-kpi-card .premium-change { grid-column: 2; }
.premium-kpi-chevron { position: absolute; top: 14px; right: 15px; color: var(--rail-muted); font-size: 26px; }
.premium-spark { position: absolute; right: 15px; bottom: 15px; display: flex; align-items: end; gap: 2px; width: 64px; height: 29px; }
.premium-spark i { flex: 1; min-width: 2px; border-radius: 2px 2px 0 0; background: linear-gradient(var(--series-1), var(--accent)); }

.premium-analytics { display: grid; grid-template-columns: 1.08fr 1.18fr 1.1fr; gap: 14px; }
.premium-analytics-card { position: relative; min-width: 0; min-height: 230px; padding: 16px 17px; border: 1px solid var(--rail-line); border-radius: 13px; color: var(--rail-ink); background: var(--rail-2); }
.premium-card-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; min-width: 0; margin-bottom: 13px; }
.premium-card-heading h2 { min-width: 0; margin: 0; color: var(--rail-ink); font-size: 14px; line-height: 1.35; }
.premium-view-all { flex: none; color: var(--accent); font-size: 11px; line-height: 1.35; text-decoration: none; white-space: nowrap; }
.premium-channel-body { display: grid; grid-template-columns: minmax(105px, .8fr) 1.2fr; align-items: center; gap: 17px; }
.premium-donut { position: relative; aspect-ratio: 1; max-width: 145px; border-radius: 50%; }
.premium-donut::after { content: ''; position: absolute; inset: 22%; border-radius: 50%; background: var(--rail); }
.premium-donut > span { position: absolute; z-index: 1; inset: 28%; display: grid; place-content: center; text-align: center; }
.premium-donut strong { color: var(--rail-ink); font-size: 23px; }
.premium-donut small { color: var(--rail-muted); font-size: 10px; }
.premium-channel-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.premium-channel-list li { display: grid; grid-template-columns: 9px 1fr auto 30px; align-items: center; gap: 7px; color: var(--rail-muted); font-size: 11px; }
.premium-channel-list i { width: 8px; height: 8px; border-radius: 50%; }
.premium-channel-list strong { color: var(--rail-ink); }
.premium-channel-list small { text-align: right; }
.premium-product-list { display: grid; margin: 0; padding: 0; list-style: none; }
.premium-product-list li { display: grid; grid-template-columns: 23px 31px minmax(0, 1fr) auto auto; align-items: center; gap: 8px; min-height: 35px; border-bottom: 1px solid var(--rail-line); color: var(--rail-muted); font-size: 10px; }
.premium-product-list li > strong { overflow: hidden; color: var(--rail-ink); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.premium-product-list li > b { color: var(--rail-ink); font-size: 10px; }
.premium-product-rank { text-align: center; color: var(--rail-muted); }
.premium-product-list li:first-child .premium-product-rank { color: var(--series-2); font-weight: 700; }
.premium-product-image { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: var(--rail-ink); background: var(--rail); font-weight: 700; }
.premium-time-bars { display: flex; align-items: end; gap: clamp(2px, .45vw, 6px); height: 143px; padding-top: 10px; border-bottom: 1px solid var(--rail-line); background: repeating-linear-gradient(to bottom, transparent 0 35px, var(--rail-line) 36px); }
.premium-time-bars i { flex: 1; min-width: 2px; max-width: 13px; border-radius: 3px 3px 0 0; background: linear-gradient(var(--series-1), var(--accent)); box-shadow: var(--shadow); }
.premium-time-caption { margin: 10px 0 0; color: var(--rail-muted); font-size: 10px; }
.premium-card-empty { display: grid; place-content: center; min-height: 160px; margin: 0; color: var(--rail-muted); text-align: center; }

@keyframes overview-bar-in { from { transform: scaleY(0); opacity: .2; } }

@media (max-width: 1050px) {
  .hero-metrics { grid-template-columns: repeat(2, 1fr); row-gap: 16px; }
  .hero-metric:nth-child(2) { border: 0; }
  .premium-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .premium-analytics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .premium-time-card { grid-column: 1 / -1; }
}

@media (max-width: 680px) {
  .overview-hero { min-height: 520px; padding: 20px 15px 15px; }
  .overview-hero-top { display: grid; }
  .overview-hero-switches { width: 100%; min-width: 0; }
  .hero-metrics { margin-top: 17px; }
  .hero-metric { padding: 0 9px; }
  .hero-metric:nth-child(odd) { padding-inline-start: 0; }
  .hero-metric-value { font-size: 17px; }
  .hero-metric .premium-change span { display: none; }
  .overview-hero-chart-slot { height: 175px; margin-top: 10px; }
  .premium-axis-label { font-size: 8px; }
  .premium-legend { right: 15px; bottom: 10px; }
  .overview-health-strip { align-items: stretch; flex-direction: column; }
  .overview-health-link { text-align: center; }
  .premium-kpi-grid, .premium-analytics { grid-template-columns: 1fr; }
  .premium-time-card { grid-column: auto; }
}

@media (max-width: 430px) {
  .premium-kpi-grid { gap: 9px; }
  .premium-kpi-card { min-height: 105px; }
  .premium-card-heading { align-items: baseline; }
  .premium-channel-body { grid-template-columns: 120px 1fr; }
  .premium-product-list li { grid-template-columns: 21px 29px minmax(0, 1fr) auto; }
  .premium-product-list li > span:nth-of-type(3) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .premium-bar { animation: none; }
  .premium-kpi-card { transition: none; }
}
@font-face {
  font-family: "Plus Jakarta Sans";
  src: url("assets/fonts/jakarta-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Plus Jakarta Sans";
  src: url("assets/fonts/jakarta-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("assets/fonts/jetbrains-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ── Tokens ────────────────────────────────────────────────────────────────
 *
 * Light is the base; dark is a redefinition of the same names, so no rule
 * below ever needs to know which theme is active. `prefers-color-scheme`
 * only, with no toggle: a merchant checking takings at 6am and 11pm gets
 * whatever their phone already decided, which is a setting they have already
 * made.
 */
:root {
  /* Brand — constant across themes. Orange is a *fill and an accent*, never
   * a status: warnings are amber, and the two must stay tellable apart. */
  --brand: #f97316;
  --brand-ink: #3a1403; /* text on a brand fill — white on orange is ~2.2:1 */
  --brand-strong: #ea580c; /* pressed state of a brand fill */
  --brand-hover: #fb923c;
  --brand-text: #c2410c; /* brand as text/icon on the page — ≥4.5:1 */
  --brand-soft: #fff0e3;
  --brand-line: #fdba74;

  /* The navigation rail keeps the app icon's navy in both themes; only its
   * border against the page changes. */
  --rail: #101e33;
  --rail-2: #16273f;
  --rail-ink: #eef2f7;
  --rail-muted: #94a3b8;
  --rail-line: #24405f;
  --rail-active-ink: #fdba74;

  --bg: #f6f4f1; /* warm paper, not hospital grey */
  --surface: #ffffff;
  --surface-sunken: #efece7;
  --border: #e5e0d8;
  --border-strong: #c9c2b8;

  --text: #16202b;
  --text-muted: #5b6774;
  /* Small print (KPI sublines, table footnotes, the login divider) still has
   * to be READ — #7e8b98 measured 3.48:1 on white, below AA. This tone keeps
   * the faint tier visibly lighter than --text-muted while clearing 4.5:1 on
   * --surface and --bg (5.11:1 / 4.65:1, measured by the rendered audit). */
  --text-faint: #636f7f;

  /* The portal's working accent for links, focus and selection is the brand's
   * accessible text tone, so "the orange thing is the thing to press" holds
   * everywhere. One step darker than --brand-text: links also sit on the
   * sunken and soft-status grounds (report chips, alert insights), where
   * #c2410c measured 4.24-4.39:1; #b1400a clears 4.5:1 on all of them. */
  --accent: #b1400a;
  --accent-text: #ffffff;
  --accent-soft: #fff0e3;

  --ok: #117c39;
  --ok-soft: #ddf3e4;
  --warn: #9f6005;
  --warn-soft: #fdf2d0;
  --bad: #b91c1c;
  --bad-soft: #fbe3e3;
  --info: #2260e8;
  --info-soft: #e3edfd;
  --neutral: #52606f;
  --neutral-soft: #eceff3;

  /* Categorical series. Orange leads — it is the brand's chart voice, echoed
   * from the app's command strip — then hues ordered so that *neighbours*
   * stay apart for the common colour-vision deficiencies, because neighbours
   * are what touch in a donut or a stack. Six and no more: past that the tail
   * collapses into "Other".
   *
   * Validated rather than chosen by eye (OKLCH lightness band, chroma floor,
   * adjacent-pair ΔE under deutan/protan/tritan simulation, normal-vision
   * floor, 3:1 against the card): light passes every check, worst adjacent
   * colour-blind ΔE 8.2. The old order put magenta beside green (ΔE 6.8 for
   * deuteranopes) and its teal read as grey. Dark mode's worst pair is 7.2,
   * inside the band that is legal only with a second channel — which every
   * chart here has: a legend carrying the values, and a 2px surface gap
   * between touching marks. */
  --series-1: #ea580c;
  --series-2: #2260e8;
  --series-3: #be185d;
  --series-4: #0a9a82;
  --series-5: #7c3aed;
  --series-6: #4d7c0f;

  --font-display: "Space Grotesk", ui-sans-serif, system-ui, -apple-system,
    "Segoe UI", sans-serif;
  --font-body: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system,
    "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
    monospace;

  /* Corners, by role: --radius for cards and dialogs, --radius-sm for
   * controls and wells, --radius-pill for status pills and chips. */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-pill: 999px;
  /* Type, by step. The floor is 12px (0.75rem) — nothing an operator reads
   * is smaller. A size between two steps is a decision to write down beside
   * the rule, not a third value to invent (PORTAL_DESIGN §20.1). */
  --text-2xs: 0.75rem;
  --text-xs: 0.8rem;
  --text-sm: 0.85rem;
  --text-md: 0.9rem;
  --text-body: 0.95rem;
  --text-base: 1rem;
  --text-lg: 1.05rem;
  --text-xl: 1.15rem;
  --text-2xl: 1.25rem;
  --text-3xl: 1.5rem;
  --text-4xl: 1.75rem;
  --text-5xl: 2rem;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.04);
  --shadow-overlay: 0 4px 10px rgba(16, 24, 40, 0.08),
    0 12px 32px rgba(16, 24, 40, 0.14);
  --gap: 16px;

  /* One duration and one curve for every micro-interaction, so the portal
   * moves as one thing. Fast enough never to hold up a till. */
  --dur: 150ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);

  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    /* The site's own dark surfaces: deep slate-navy rather than pure black,
     * so the portal, the site and the app read as one product at night. */
    --bg: #0d1522;
    --surface: #162234;
    --surface-sunken: #101a2a;
    --border: #2a394f;
    --border-strong: #3d4f68;

    --rail: #0a111c;
    --rail-2: #101a2a;
    --rail-ink: #eef2f7;
    --rail-muted: #93a1b5;
    --rail-line: #22334a;
    --rail-active-ink: #fdba74;

    --text: #f1f5f9;
    --text-muted: #b8c4d4;
    --text-faint: #8a99ad;

    --accent: #fb923c;
    --accent-text: #3a1403;
    --accent-soft: #2a1708;
    --brand-text: #fb923c;
    --brand-soft: #2a1708;
    --brand-line: #7c3a12;

    --ok: #4ade80;
    --ok-soft: #17381f;
    --warn: #fbbf24;
    --warn-soft: #3a2d16;
    --bad: #f87171;
    --bad-soft: #402222;
    --info: #60a5fa;
    --info-soft: #14263d;
    --neutral: #a5b2c2;
    --neutral-soft: #1c2839;

    /* The same six hues, stepped down for the navy card: pastel steps sat
     * above the dark lightness band and read as glare, not as colour. */
    --series-1: #e2621f;
    --series-2: #3d7fe8;
    --series-3: #e0609a;
    --series-4: #0f8f78;
    --series-5: #8f68e6;
    --series-6: #5f9420;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-overlay: 0 4px 10px rgba(0, 0, 0, 0.4),
      0 12px 32px rgba(0, 0, 0, 0.5);

    color-scheme: dark;
  }
}

/* ── Base ─────────────────────────────────────────────────────────────────── */

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  /* 16px minimum throughout: anything smaller is unreadable on a tablet
   * propped behind a counter. */
  font-size: 16px;
  line-height: 1.5;
  /* The self-hosted Jakarta subset's space glyph is narrow; without this
   * compensation "Sign out" reads as "Signout" at small sizes. */
  word-spacing: 0.08em;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  margin: 0;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  text-decoration-thickness: 2px;
}

::selection {
  /* The ink flips with the scheme: the fixed #3a1403 was ~1.9:1 on dark
   * mode's #7c3a12 ground — selected text vanished. Light keeps the original
   * pair; dark gets a light peach ink on the same brand ground (~8:1). */
  background: var(--brand-line);
  color: #3a1403;
}

@media (prefers-color-scheme: dark) {
  ::selection {
    color: #ffedd5;
  }
}

/* A visible focus ring on everything, always. Keyboard users are not an edge
 * case here — an accountant working through the reports page uses Tab. The
 * ring is the brand's text tone, which clears 3:1 against both the paper and
 * the white cards in each theme. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  /* No border-radius here. An outline follows the element's own corners, and
   * a radius set on the pseudo-class beat every control's own rule, so a
   * keyboard-focused 8px button snapped to 4px corners the moment Tab
   * reached it. */
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--surface);
  color: var(--text);
  padding: 12px 18px;
  border-radius: 0 0 var(--radius-sm) 0;
  z-index: 60;
}

.skip-link:focus {
  left: 0;
}

/* ── The shell: navy rail on a desk, top bar in a pocket ──────────────────
 *
 * One navigation, two postures. From 1024px up it is a fixed navy rail down
 * the left — the professional-POS shape, and the app icon's own navy so the
 * portal is recognisably OrderLah before a single word is read. Below that it
 * is a sticky top bar: the brand, Find, the language chip and a ☰ button that
 * opens every destination and Sign out in one list (`.nav-menu`), the current
 * page marked. It replaced a row of sections that scrolled sideways and, on a
 * phone, showed three of twelve.
 *
 * The markup is identical in both postures (shell.js renders one <nav>);
 * only this stylesheet changes its shape.
 */

.app-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--rail);
  border-bottom: 1px solid var(--rail-line);
}

.nav {
  display: flex;
  align-items: center;
  gap: 4px 14px;
  padding: 10px 14px;
  flex-wrap: wrap;
}

.nav-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--rail-ink);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-xl);
  letter-spacing: -0.01em;
  border-radius: var(--radius-sm);
  padding: 4px 6px 4px 4px;
}

.brand-mark {
  width: 30px;
  height: 30px;
  flex: none;
  display: block;
}

.brand-lah {
  color: var(--brand);
}

.nav-sub {
  color: var(--rail-muted);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  align-self: flex-end;
  padding-bottom: 3px;
  white-space: nowrap;
}

.nav-links {
  display: flex;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
  order: 3;
  flex-basis: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.nav-links::-webkit-scrollbar {
  display: none;
}

/* In the top-bar posture the sections live in the ☰ menu instead. */
@media (max-width: 1023.9px) {
  .nav-links {
    display: none;
  }
}

.nav-links a,
.nav-menu-links a {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 12px;
  min-height: 44px; /* §18.2: every touch target keeps its 44px. */
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--rail-muted);
  font-weight: 600;
  font-size: var(--text-body);
  white-space: nowrap;
  transition: background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.nav-icon {
  width: 19px;
  height: 19px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.nav-links a:hover,
.nav-menu-links a:hover {
  background: rgba(255, 255, 255, 0.07);
  color: var(--rail-ink);
}

.nav-links a.current,
.nav-menu-links a.current {
  background: rgba(249, 115, 22, 0.16);
  color: var(--rail-active-ink);
}

/* ── The ☰ menu (phone and tablet) ─────────────────────────────────────────
 *
 * Never drawn on the rail. Below 1024px the button sits in the brand's row,
 * in the corner, with the language chip beside it, and the menu opens under
 * the whole header: every destination, then Sign out, scrolling inside itself
 * when twelve rows are taller than the phone. */
.nav-menu-toggle,
.nav-menu {
  display: none;
}

@media (max-width: 1023.9px) {
  .nav-account > .nav-signout {
    display: none;
  }

  /* Drawn 34px like the language chip beside it, answering a 46px tap — the
   * same smaller-to-look-at, never-smaller-to-hit rule as the chip. */
  .nav-menu-toggle {
    order: 1;
    flex: none;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    min-height: 34px;
    padding: 0;
    border: 1px solid var(--rail-line);
    border-radius: var(--radius-sm);
    background: var(--rail-2);
    color: var(--rail-ink);
    cursor: pointer;
  }

  .nav-menu-toggle::before {
    content: '';
    position: absolute;
    inset: -6px -2px;
  }

  .nav-menu-toggle:hover,
  .nav-menu-toggle[aria-expanded='true'] {
    border-color: var(--rail-muted);
  }

  /* A count inside the menu — an unread notice, an outlet that needs setting
   * up — is otherwise behind a closed button. The dot says there is one. */
  .nav:has(.nav-menu .nav-badge) .nav-menu-toggle[aria-expanded='false']::after {
    content: '';
    position: absolute;
    top: -3px;
    right: -3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--brand);
    border: 2px solid var(--rail);
  }

  /* The panel holds still and its list of pages scrolls inside it, so on a
   * short phone — twelve rows are taller than the screen — Sign out stays
   * pinned at the bottom rather than below the fold. The list fades at either
   * end to say there is more; its padding is as deep as the fade, so at rest
   * the fade touches no row, only what is scrolling past an edge. (It is the
   * fade the old scrolling row of sections had, turned on its side.) */
  .nav-menu:not([hidden]) {
    display: flex;
    flex-direction: column;
    gap: 4px;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    max-height: calc(100dvh - 100%);
    padding: 0 14px 14px;
    background: var(--rail);
    border-bottom: 1px solid var(--rail-line);
    box-shadow: var(--shadow-overlay);
  }

  .nav-menu-links {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 12px 0 26px;
    mask-image: linear-gradient(
      to bottom,
      transparent 0,
      #000 12px,
      #000 calc(100% - 26px),
      transparent 100%
    );
  }

  .nav-menu-links a {
    position: relative;
  }

  .nav-menu-links a > .nav-badge {
    margin-inline-start: auto;
  }

  .nav-menu-signout {
    flex: none;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 44px;
    padding: 8px 12px;
    font: inherit;
    font-size: var(--text-md);
    font-weight: 600;
    border-radius: var(--radius-sm);
    border: 1px solid var(--rail-line);
    background: var(--rail-2);
    color: var(--rail-ink);
    cursor: pointer;
  }

  .nav-menu-signout:hover {
    border-color: var(--rail-muted);
  }
}

.nav-account,
.nav-identity {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  min-width: 0;
}

/* Top-bar posture (phone and tablet): the language picker and Sign out sit
 * together in the top-right corner, on the brand's row, and who is signed in
 * drops to a row of its own under them. `display: contents` lets the parts
 * of the account block take their own places in the bar while the DOM — and
 * so the keyboard — keeps who-language-out. */
@media (max-width: 1023.9px) {
  .nav {
    column-gap: 8px;
  }

  .nav-account {
    display: contents;
  }

  .nav .language-picker {
    order: 1;
    margin-left: auto;
    flex: none;
  }

  .nav-signout {
    order: 1;
    flex: none;
  }

  .nav-identity {
    order: 2;
    flex-basis: 100%;
  }

  /* The brand is the one thing on that row that may give way. A zero basis
   * means its width never decides whether the row wraps — so Sign out cannot
   * be pushed off the top by a long translation of "Merchant" — and
   * max-content means it never grows into a home link across empty space. */
  .nav-brand {
    flex: 1 1 0;
    min-width: 0;
    max-width: max-content;
    overflow: hidden;
  }

  .nav-brand .nav-sub {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

.nav-email,
.nav-merchant-name {
  color: var(--rail-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav-merchant-name {
  font-weight: 600;
  color: var(--rail-ink);
}

/* The Merchant Reference Code (assets/reference-code.js): what it is called
 * and the code, pressed to copy. One control in three places — the rail, the
 * overview's greeting and the referral page — so it is one rule here, with the
 * rail's own colours where it sits on the rail. */
.reference-code-wrap {
  display: inline-flex;
  min-width: 0;
  max-width: 100%;
}

.reference-code {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  min-width: 0;
  max-width: 100%;
  min-height: 44px;
  padding: 6px 12px;
  font: inherit;
  font-size: var(--text-sm);
  text-align: start;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.reference-code:hover {
  background: var(--surface-sunken);
}

.reference-code-label {
  color: var(--text-muted);
}

.reference-code-value {
  font-weight: 700;
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
}

.nav .reference-code {
  color: var(--rail-ink);
  background: var(--rail-2);
  border-color: var(--rail-line);
}

.nav .reference-code:hover {
  background: var(--rail-2);
  border-color: var(--rail-muted);
}

.nav .reference-code-label {
  color: var(--rail-muted);
}

.nav-merchant,
.nav-signout {
  font: inherit;
  font-size: var(--text-md);
  font-weight: 600;
  padding: 8px 12px;
  min-height: 44px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--rail-line);
  background: var(--rail-2);
  color: var(--rail-ink);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
  max-width: 100%;
}

.nav-signout:hover,
.nav-merchant:hover {
  border-color: var(--rail-muted);
}

/* Focus on the navy rail: the page-level ring colour sits *on* navy here, so
 * the ring flips to the readable-on-navy brand tone. */
.nav a:focus-visible,
.nav button:focus-visible,
.nav select:focus-visible {
  outline-color: var(--rail-active-ink);
}

@media (min-width: 1024px) {
  body {
    display: grid;
    grid-template-columns: 236px minmax(0, 1fr);
    align-items: start;
  }

  /* Auth pages opt out of the rail grid — they have no rail. */
  body.login-page {
    display: grid;
    grid-template-columns: none;
  }

  .app-header {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
    border-bottom: 0;
    border-right: 1px solid var(--rail-line);
    overflow-y: auto;
    scrollbar-width: thin;
  }

  .nav {
    flex-direction: column;
    align-items: stretch;
    height: 100%;
    padding: 18px 12px 14px;
    gap: 0;
    flex-wrap: nowrap;
  }

  .nav-brand {
    margin: 0 4px 18px;
    font-size: 1.28rem;
    /* Two lines on the rail: the wordmark, then which surface this is —
     * side by side they overflow the 236px column. */
    flex-wrap: wrap;
  }

  .nav-brand .nav-sub {
    flex-basis: 100%;
    padding: 0 0 0 44px;
    margin-top: -6px;
    align-self: flex-start;
  }

  .brand-mark {
    width: 34px;
    height: 34px;
  }

  .nav-links {
    flex-direction: column;
    order: 0;
    flex-basis: auto;
    overflow: visible;
    gap: 2px;
  }

  .nav-links a {
    padding: 10px 12px;
  }

  /* The active page gets the brand bar the app's own navigation draws. */
  .nav-links a.current {
    box-shadow: inset 3px 0 0 var(--brand);
  }

  .nav-account,
  .nav-identity {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }

  .nav-account {
    margin-top: auto;
    padding-top: 14px;
    border-top: 1px solid var(--rail-line);
  }

  /* Who, then as-whom, then the way out — Sign out is always the last thing
   * in the rail. */
  .nav-email {
    padding: 0 4px;
  }

  .nav-signout {
    order: 1;
    width: 100%;
  }

  .nav-merchant-name {
    padding: 0 4px;
  }

  .nav-merchant {
    width: 100%;
  }
}

/* ── Layout ───────────────────────────────────────────────────────────────── */

.app-body {
  max-width: 1200px;
  margin: 0 auto;
  padding: 26px 24px 72px;
  width: 100%;
  min-width: 0;
}

/* A 24" and wider monitor: more of the page, not the same 1200px island in
 * a sea of gutter. Text measures stay capped by their own rules. */
@media (min-width: 1800px) {
  .app-body {
    max-width: 1440px;
  }
}

.page-title {
  font-family: var(--font-display);
  font-size: 1.65rem;
  letter-spacing: -0.02em;
  margin-bottom: 16px;
}

.controls[hidden] {
  display: none;
}

.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 14px;
  padding: 14px 16px;
  margin-bottom: var(--gap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.control {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 170px;
}

.control-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.control-input {
  font: inherit;
  /* 44px is the tap target a thumb reliably hits. */
  min-height: 44px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
}

.control-input:hover {
  border-color: var(--text-faint);
}

/* A .control that is really a checkbox choice (the payment-method boxes):
 * box and label on one comfortable row, the label in words rather than
 * small caps — PAYNOW in tracked caps is a heading, not a choice. */
.control:has(> input[type="checkbox"]) {
  flex-direction: row-reverse;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
  min-height: 44px;
}

.control:has(> input[type="checkbox"]) > input[type="checkbox"] {
  width: 22px;
  height: 22px;
  flex: none;
  accent-color: var(--brand);
}

.control:has(> input[type="checkbox"]) .control-label {
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--text-body);
  color: var(--text);
  cursor: pointer;
}

/* The save button under a grid of checkbox choices starts its own row. */
.inline-form:has(.control > input[type="checkbox"]) > .button {
  flex: 0 0 100%;
  max-width: 12rem;
}

.control-summary {
  margin: 0;
  flex: 1 1 100%;
  color: var(--text-muted);
  font-size: var(--text-md);
}

/* A merchant with one shop reads its name here, as plain words: there is
 * nothing to choose, so nothing that looks like a control. */
.control-summary-outlet {
  font-weight: 600;
  color: var(--text);
}

.main {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* A panel never outgrows its column: a nowrap table's min-content width
 * was pushing whole panels past the viewport on a phone. */
.main > * {
  min-width: 0;
  max-width: 100%;
}

/* Anchor targets (the reports index) land clear of the sticky top bar. */
[id] {
  scroll-margin-top: 76px;
}

.loading {
  color: var(--text-muted);
}

/* ── Small stacked rows (invoices) ────────────────────────────────────────── */

.stack,
.stack-tight {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.stack > .row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}

.stack > .row:last-child {
  border-bottom: 0;
}

.row-title {
  font-weight: 600;
  flex: 1 1 auto;
}

.row-value {
  font-variant-numeric: tabular-nums;
}

.row-note {
  flex: 1 1 100%;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.mono {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.88em;
}

/* ── KPI cards ────────────────────────────────────────────────────────────── */

.kpi-grid {
  display: grid;
  /* auto-fit + minmax is what makes desktop, tablet and phone one layout
   * rather than three: the cards reflow at whatever width is available. */
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: var(--gap);
}

/* The quiet tier under the headline figures: same cards, smaller numerals,
 * tighter columns — the hierarchy is the size difference, not a colour. */
.kpi-group-title {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 4px 0 10px;
}

.kpi-grid-compact {
  grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
  gap: 12px;
}

.kpi-grid-compact .kpi {
  padding: 13px 15px;
  gap: 5px;
}

.kpi-grid-compact .kpi-value {
  font-size: 1.2rem;
}

.kpi {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 16px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.kpi-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* The figure is the app's own voice: the mono numerals of the command strip,
 * tabular so a column of cards lines up digit for digit. */
.kpi-value {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 1.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}

.kpi-value-muted {
  font-family: var(--font-body);
  color: var(--text-faint);
  font-size: var(--text-xl);
  font-weight: 600;
}

.kpi-unavailable {
  border-style: dashed;
  box-shadow: none;
  background: none;
}

.kpi-reason {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.kpi-action,
.kpi-drill,
.panel-link,
.insight-action,
.health-action,
.attention-action {
  font-size: var(--text-sm);
  font-weight: 600;
  align-self: flex-start;
  /* A link is a tap target: 44px tall, the height a thumb reliably hits, with
   * the words centred in it. A 20px line of text was the only thing to aim
   * at, on the links that lead somewhere the merchant needs to go. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.kpi > .pill,
.kpi > .pill-wrap {
  align-self: flex-start;
}

.kpi-status {
  align-self: flex-start;
  font-size: var(--text-2xs);
  font-weight: 700;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--warn-soft);
  color: var(--warn);
}

.kpi-status-partial {
  background: var(--bad-soft);
  color: var(--bad);
}

.kpi-change {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.kpi-change-up {
  color: var(--ok);
}

.kpi-change-down {
  color: var(--bad);
}

.kpi-change-label {
  color: var(--text-faint);
  font-weight: 500;
}

.kpi-change-none {
  color: var(--text-faint);
  font-weight: 500;
}

/* ── Panels ───────────────────────────────────────────────────────────────── */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px;
}

/* Both forms a panel heading takes: inside the header the panel() helper
 * builds, and as a direct child on the hand-built panels — otherwise the
 * hand-built ones fall back to the browser's h2 and shout. */
.panel-header h2,
.panel > h2 {
  font-family: var(--font-display);
  font-size: 1.1rem;
  letter-spacing: -0.01em;
}

/* panel({ span: 'full' }) has emitted this class since panels were introduced,
   but nothing ever defined it. It is inert inside `.main`, which is a flex
   column where every panel already fills the width — which is why the omission
   went unnoticed. It is not inert inside `.panel-pair`: that is a real grid
   (auto-fit, with a floor of min(320px, 100%)), and
   `.panel-pair-host { display: contents }`
   promotes a nested panel to a grid item of it. A full-span panel placed there
   would silently take one auto-fit column and sit beside its neighbour instead
   of below it. Spanning every column is what the call sites have been asking
   for all along. */
.panel-full {
  grid-column: 1 / -1;
}

.panel-subtitle {
  margin: 4px 0 0;
  color: var(--text-muted);
  font-size: var(--text-md);
}

.panel-subhead {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin-top: 22px;
  margin-bottom: 8px;
}

.panel-body {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: flex-start;
}

/* Nothing in a panel body outgrows the panel: a nowrap table's min-content
 * width was carrying its wrapper past the viewport on a phone; capped, the
 * scroll container inside it scrolls instead. */
.panel-body > * {
  max-width: 100%;
  min-width: 0;
}

.panel-note {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-md);
  max-width: 78ch;
}

/* ── Help page ──────────────────────────────────────────────────────────────
 *
 * The question is the label and stays visible; the answer folds — the same
 * level-3 rule the app's AppDisclosure follows. The summary is a full-width
 * tap target because the reader is often on a phone in a shop. */
.faq {
  /* The panel body lays children out with align-items: flex-start, which
   * shrink-wraps this list; the dividers have to run the card's width. */
  align-self: stretch;
}

.faq details {
  border-bottom: 1px solid var(--border);
}

.faq details:last-child { border-bottom: 0; }

.faq summary {
  cursor: pointer;
  padding: 14px 4px;
  font-weight: 600;
  min-height: 44px;
  box-sizing: border-box;
}

.faq summary:hover { color: var(--brand); }

.faq details p {
  margin: 0 4px 12px;
  color: var(--text-muted);
  max-width: 78ch;
}

.faq details .panel-link { margin: 0 4px 14px; }

/* Guides from the help centre API, in the FAQ's fold. A guide's body reads as
 * body text rather than the muted answer tone: it is the thing the merchant
 * opened, often with steps to follow at the counter. */
.guide-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  align-self: stretch;
  margin-bottom: 8px;
}

.guide-tool {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1 1 220px;
  font-weight: 600;
}

.guide-tool input,
.guide-tool select { min-height: 44px; }

.faq .guide-body { margin: 0 4px 12px; max-width: 78ch; }
.faq .guide-body p,
.faq .guide-body li { color: var(--text); }
.guide-heading { margin: 14px 0 6px; font-size: var(--text-lg); }
.guide-list { margin: 0 0 12px; padding-inline-start: 22px; }
.guide-list li { margin: 4px 0; }
.faq details .guide-reason { color: var(--text); font-weight: 600; }

.guide-callout {
  border-inline-start: 3px solid var(--border);
  padding: 6px 10px;
}

.guide-warning { border-inline-start-color: var(--warn); }

.guide-ack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  margin: 0 4px 14px;
}

.guide-ack-ask { font-weight: 600; }

.whats-new {
  list-style: none;
  margin: 0;
  padding: 0;
  align-self: stretch;
}

.whats-new-item {
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

.whats-new-item:last-child { border-bottom: 0; }
.whats-new-head { margin: 0 0 6px; color: var(--text-muted); }

/* A page's own guide, under its title: a link, never a panel. */
.page-help {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  margin: -8px 0 12px;
}

.help-address {
  margin: 0 0 14px;
  font-size: var(--text-lg);
}

.help-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 14px;
}

/* A fold inside a panel: the label stays visible, the contents are one tap
 * away — the portal's level-3 shape, shared by the Help questions above. */
.panel-body > details {
  align-self: stretch;
}

.panel-body > details > summary {
  cursor: pointer;
  font-weight: 600;
  min-height: 44px;
  display: flex;
  align-items: center;
}

.panel-body > details > summary:hover { color: var(--brand); }

.panel-body > details > * + * { margin-top: 12px; }

/* ── Invoice document ───────────────────────────────────────────────────────
 *
 * A fiscal document, on screen and on paper. Quieter than a dashboard panel:
 * generous space, one rule between the parties and the figures, amounts down
 * a right edge. */
.invoice-parties {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
  align-self: stretch;
  padding: 14px 0;
  border-top: 1px solid var(--border);
}

.invoice-party-title {
  margin: 0 0 6px;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.invoice-party-line { margin: 2px 0; }

.invoice-lines {
  align-self: stretch;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.invoice-lines th,
.invoice-lines td {
  padding: 10px 4px;
  border-top: 1px solid var(--border);
  text-align: left;
  font-weight: 400;
}

.invoice-lines .cell-num { text-align: right; font-family: var(--font-mono); }

.invoice-lines .invoice-total th,
.invoice-lines .invoice-total td {
  font-weight: 700;
  border-top: 2px solid var(--text);
}

@media print {
  /* The document is the page: no card chrome, no shadow, full width. */
  .invoice-doc {
    border: 0 !important;
    box-shadow: none !important;
    padding: 0 !important;
  }
}

/* The zero-outlet welcome: three numbered steps, sized to be read once by
 * somebody who has never used a portal before. */
.first-run-steps {
  margin: 0 0 16px;
  padding-left: 26px;
  max-width: 68ch;
  font-size: var(--text-base);
}

.first-run-steps li {
  margin-bottom: 12px;
  line-height: 1.55;
}

/* An address meant to be copied verbatim into a Hub tablet. Boxed so the whole
   string - including the /__hub ending people drop - reads as one indivisible
   value rather than prose. Not a secret, so it is quieter than a code display;
   user-select:all makes one click take all of it, which is the difference
   between a Hub that connects and one that reports the kitchen offline. */
.wire-address {
  display: inline-block;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 13px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 5px 9px;
  word-break: break-all;
  user-select: all;
}

.panel-pair-host {
  display: contents;
}

.panel-pair {
  display: grid;
  /* `min(320px, 100%)` rather than a bare 320px floor.
   *
   * A minmax track cannot shrink below its own minimum, so on a 320px phone —
   * where this container is 292px after the page's 14px gutters — the track
   * stayed 320px and pushed the whole page 14px sideways. That takes the
   * navigation rail off screen with it, which is the failure the wide tables
   * are already careful about. The min() makes the floor the smaller of the
   * two, so the intent holds at every width that can afford it and the track
   * gives way on the ones that cannot. */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: var(--gap);
  /* Full width of the panel body it sits in — content-sized, the two
   * mini-panels stopped at half the card and clipped their own values. */
  width: 100%;
}

/* ── Banners ──────────────────────────────────────────────────────────────── */

.banner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 13px 16px;
  border-radius: var(--radius);
  border: 1px solid;
  font-size: var(--text-body);
}

/* "Checked 12 min ago · 3 of 4 outlets reporting · [Refresh]" — the top of
 * every report page. A quiet strip rather than a banner: it is read every
 * time, and it only raises its voice (.freshness-problem) when a refresh could
 * not finish. Wraps rather than scrolls, so at 360px the button drops under
 * the line instead of pushing the page sideways. */
.freshness {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.freshness-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  margin: 0;
  flex: 1 1 16rem;
  min-width: 0;
  font-size: var(--text-body);
}

.freshness-checked {
  font-weight: 600;
  color: var(--text);
}

.freshness-completeness {
  color: var(--text-muted);
}

/* Paper cannot tick: the printed page carries a clock time instead of an age. */
.freshness-printed {
  display: none;
}

.freshness-stale,
.freshness-problem {
  flex: 1 0 100%;
  margin: 0;
  font-size: var(--text-md);
}

.freshness-stale {
  color: var(--text);
}

.freshness-problem {
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--warn);
  background: var(--warn-soft);
  color: var(--text);
}

@media (max-width: 620px) {
  /* Refresh stays on the line it refreshes: the words wrap inside their own
   * column instead of pushing the button onto a row of its own. */
  .freshness {
    flex-wrap: wrap;
    gap: 8px 10px;
  }

  .freshness-line {
    flex: 1 1 0;
  }

  .freshness-refresh {
    flex: none;
  }
}

@media print {
  .freshness {
    border: 0;
    padding: 0;
  }

  .freshness-checked {
    display: none;
  }

  .freshness-printed {
    display: inline;
  }
}

/* A figure cell for an outlet whose figures are not in the totals: words, in
 * the muted tone, never a zero and never a bare dash. */
.cell-not-included {
  color: var(--text-muted);
  font-style: italic;
  white-space: nowrap;
}

.banner-warning {
  background: var(--warn-soft);
  border-color: var(--warn);
  color: var(--text);
}

.banner-error {
  background: var(--bad-soft);
  border-color: var(--bad);
  color: var(--text);
}

.banner-ok {
  background: var(--ok-soft);
  border-color: var(--ok);
  color: var(--text);
}

.alert {
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  margin-bottom: 14px;
  font-size: var(--text-body);
}

.alert-error {
  background: var(--bad-soft);
  color: var(--bad);
  border: 1px solid var(--bad);
}

/* Used for the one message in this portal that is good news arriving in the
 * same slot bad news uses — "check your email". Colour is not the only signal:
 * the sentence itself says what happened, and the two alerts are never on
 * screen together. */
.alert-ok {
  background: var(--ok-soft);
  color: var(--ok);
  border: 1px solid var(--ok);
}

/* ── Status pills ─────────────────────────────────────────────────────────
 *
 * A word in a soft chip with a dot. The word carries the meaning; the tint
 * and the dot make a column of them scannable. The detail half ("· last
 * connected 2 hours ago") stays quiet inside the same chip so the fact and
 * its status cannot drift apart in a table cell.
 */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  padding: 3px 10px 3px 8px;
  border-radius: var(--radius-pill);
  background: var(--neutral-soft);
  color: var(--neutral);
  max-width: 100%;
}

/* The chip's factual half ("last connected 2 hours ago") leaves the tint
 * and sits under the word — inside it, it wrapped into three cramped lines
 * and doubled the row height. */
.pill-wrap {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  max-width: 100%;
}

.pill-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.pill-ok {
  background: var(--ok-soft);
  color: var(--ok);
}

.pill-warn {
  background: var(--warn-soft);
  color: var(--warn);
}

.pill-bad {
  background: var(--bad-soft);
  color: var(--bad);
}

.pill-muted {
  background: var(--neutral-soft);
  /* --neutral, not --text-faint: on the grey chip ground the faint tier
   * measured 3.02:1, and these chips carry operational payment wording
   * ("Available but off", "Already used for another order"). */
  color: var(--neutral);
}

.pill-detail {
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--text-xs);
  white-space: normal;
}

/* ── Tables ───────────────────────────────────────────────────────────────── */

.table-wrap {
  width: 100%;
  /* Wide tables scroll inside their own box; the page body never scrolls
   * sideways, which is what makes a 9-column comparison usable on a phone.
   * The scroll shadows are the cue that there is more to the side — an
   * overflow with no scrollbar visible just looks like a broken table. */
  overflow-x: auto;
  background:
    linear-gradient(to right, var(--surface) 30%, transparent),
    linear-gradient(to left, var(--surface) 30%, transparent) 100% 0,
    radial-gradient(farthest-side at 0 50%, rgba(16, 24, 40, 0.14), transparent),
    radial-gradient(farthest-side at 100% 50%, rgba(16, 24, 40, 0.14), transparent) 100% 0;
  background-repeat: no-repeat;
  background-size: 44px 100%, 44px 100%, 16px 100%, 16px 100%;
  background-attachment: local, local, scroll, scroll;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-md);
}

.data-table caption {
  text-align: left;
  color: var(--text-muted);
  font-size: var(--text-sm);
  padding-bottom: 8px;
}

.data-table th,
.data-table td {
  text-align: left;
  padding: 11px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.data-table thead th {
  font-size: var(--text-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong);
}

.data-table tbody th {
  font-weight: 600;
  white-space: normal;
  min-width: 140px;
}

.data-table td {
  font-variant-numeric: tabular-nums;
}

.data-table tbody tr:last-child th,
.data-table tbody tr:last-child td {
  border-bottom: 0;
}

.data-table tbody tr:hover {
  background: var(--surface-sunken);
}

/* A key/value ledger carries sentences, not sums: its cells wrap rather
 * than clip mid-word at the card edge. dataTable adds the class when every
 * column label is empty. */
.data-table-ledger th,
.data-table-ledger td {
  white-space: normal;
  /* An email address or URL has no spaces to wrap at. */
  overflow-wrap: anywhere;
}

/* …except a row's label. `anywhere` also tells the table a cell can be one
 * letter wide, and on a phone the table believed it: the Refer page printed
 * its steps as "1. Shar / e" and "3. We / reme / mber" in a sliver of a
 * column. `break-word` still breaks a word that genuinely cannot fit, but no
 * longer offers that as the column's width, so a label keeps its words whole
 * and wraps between them. `pages.browser.mjs` checks every page at 390px. */
.data-table-ledger tr > :first-child {
  overflow-wrap: break-word;
}

/* A label reads beside the first line of its value. Centred, a one-line label
 * floated halfway down a three-line answer — "3. We remember" sat level with
 * the middle of "Their signup is recorded…" — and nothing lined up. Baseline
 * rather than top, so a label beside a button or a pill lines up with that
 * control's words, not with its border. */
.data-table-ledger th,
.data-table-ledger td {
  vertical-align: baseline;
}

/* A cell carrying an address meant to be copied verbatim wraps it — the
 * /__hub ending is exactly the part a nowrap cell cut off on a phone. */
.data-table td:has(.wire-address),
.data-table th:has(.wire-address) {
  white-space: normal;
}

/* A column of figures reads down its decimal points, not down its left edge.
 * dataTable marks money/number columns with this class. */
.data-table th.cell-num,
.data-table td.cell-num {
  text-align: right;
}

/* The refund control and a payment's timeline, stacked in one ledger cell. */
.cell-stack {
  display: grid;
  gap: 6px;
  justify-items: start;
}

/* A payment's recorded history — folded until opened, and fetched then. */
.payment-timeline summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
}

.timeline-list {
  margin: 4px 0 0;
  padding-left: 1.2em;
  display: grid;
  gap: 4px;
  min-width: 18rem;
}

.cell-unavailable {
  color: var(--text-faint);
  font-style: italic;
  white-space: normal;
  display: inline-block;
  max-width: 34ch;
}

/* A method row sitting under its provider in the payment-methods matrix. A
 * class, because the literal leading spaces it used to be drawn with collapse
 * to nothing in HTML. */
.data-table .cell-indent {
  padding-left: 32px;
  font-weight: 400;
}

/* The quiet second line inside a table cell — an attention row's note. */
.cell-note {
  display: block;
  font-size: var(--text-sm);
  color: var(--text-muted);
  white-space: normal;
  max-width: 44ch;
}

/* ── Stacked tables on a phone ─────────────────────────────────────────────
 *
 * A table five or more columns wide (dataTable adds the class) collapses to
 * label/value cards below 620px, because a horizontally scrolled table hides
 * the money column exactly when it matters. The markup stays a real table —
 * assistive tech keeps rows, headers and scopes — only the rendering folds.
 */
@media screen and (max-width: 620px) {
  .data-table-stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .data-table-stack tbody tr {
    display: block;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    margin-bottom: 10px;
    background: var(--surface);
  }

  .data-table-stack tbody tr:hover {
    background: var(--surface);
  }

  .data-table-stack tbody th,
  .data-table-stack tbody td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    border-bottom: 0;
    padding: 3px 0;
    white-space: normal;
    text-align: right;
  }

  .data-table-stack tbody th {
    display: block;
    text-align: left;
    font-size: var(--text-base);
    padding-bottom: 6px;
  }

  .data-table-stack tbody td::before {
    content: attr(data-label);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    text-align: left;
    flex: none;
  }

  .data-table-stack tbody td:not([data-label])::before {
    content: none;
  }
}

/* ── Charts ───────────────────────────────────────────────────────────────── */

.chart-host {
  width: 100%;
  overflow-x: auto;
}

.chart {
  width: 100%;
  height: auto;
  display: block;
}

.chart-grid {
  stroke: var(--border);
  stroke-width: 1;
}

.chart-axis {
  fill: var(--text-muted);
  font-size: var(--text-2xs);
  font-family: var(--font-body);
}

.chart-axis-strong {
  fill: var(--text);
  font-weight: 600;
}

.chart-line {
  fill: none;
  stroke: var(--series-1);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart-area {
  fill: var(--series-1);
  opacity: 0.1;
  stroke: none;
}

@media (prefers-color-scheme: dark) {
  /* At 10% the orange washes to grey over the navy card; a touch more
   * alpha keeps the fill readably warm. */
  .chart-area {
    opacity: 0.16;
  }
}

.chart-dot {
  fill: var(--series-1);
  /* A ring in the card's own colour, so a dot sitting on the line reads as a
   * point rather than a bump in it. */
  stroke: var(--surface);
  stroke-width: 2;
}

.chart-bar {
  fill: var(--series-1);
}

.chart-slice {
  stroke: var(--surface);
  stroke-width: 2;
}

/* The baseline (zero) is one step stronger than the other guides. */
.chart-baseline {
  stroke: var(--border-strong);
}

.chart-end-label {
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 4px;
  stroke-linejoin: round;
}

/* ── Hover layer ─────────────────────────────────────────────────────────────
 * charts.js's hit targets are invisible and larger than the marks they stand
 * for; the crosshair and focus ring appear only while a pointer is over the
 * plot. The mark being read keeps its colour and the rest recede, so nothing
 * changes hue under the pointer. */
.chart-hit {
  fill: transparent;
  cursor: pointer;
}

.chart-crosshair {
  stroke: var(--text-muted);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  pointer-events: none;
}

.chart-focus {
  fill: var(--series-1);
  stroke: var(--surface);
  stroke-width: 2.5;
  pointer-events: none;
}

.chart-bar,
.chart-slice,
.waterfall-bar,
.heat-cell,
.chart-legend li {
  transition: opacity var(--dur) var(--ease);
}

.chart.has-active .chart-bar:not(.is-active),
.chart.has-active .waterfall-bar:not(.is-active),
.chart-donut-wrap.has-linked .chart-slice:not(.is-linked),
.chart-donut-wrap.has-linked .chart-legend li:not(.is-linked),
.heatmap.has-active .heat-cell:not(.is-active) {
  opacity: 0.45;
}

.chart-slice.is-linked {
  stroke-width: 3;
}

.chart-slice,
.waterfall-bar {
  cursor: pointer;
}

/* One card for every chart on the page, moved by charts.js. Positioned from
 * custom properties because the CSP permits no inline style attribute. */
.chart-tip:not([hidden]) {
  position: absolute;
  left: var(--tip-x, 0);
  top: var(--tip-y, 0);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 11px;
  max-width: 240px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-overlay);
  pointer-events: none;
}

.chart-tip-label {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.chart-tip-value {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The tap answer to the hover tooltip — filled by charts.js's readout when a
 * dot, bar or heat cell is tapped. `:not([hidden])` because a display rule
 * here would beat the browser's default `[hidden] { display: none }`. */
.chart-readout:not([hidden]) {
  display: block;
  margin: 8px 0 0;
  padding: 6px 10px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  width: fit-content;
}

.chart-empty {
  margin: 0;
  padding: 22px;
  width: 100%;
  text-align: center;
  color: var(--text-muted);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
}

.chart-donut-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  align-items: center;
}

.chart-donut {
  width: 176px;
  flex: none;
}

.chart-donut-centre {
  fill: var(--text);
  font-size: 17px;
  font-weight: 700;
  font-family: var(--font-mono);
}

.chart-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1 1 200px;
}

.chart-legend li {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: var(--text-md);
}

.chart-swatch {
  width: 13px;
  height: 13px;
  border-radius: 4px;
  background: var(--swatch);
  flex: none;
}

.chart-legend-name {
  flex: 1 1 auto;
}

.chart-legend-value {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.sparkline {
  width: 96px;
  height: 28px;
  margin-top: 2px;
}

.sparkline-path {
  fill: none;
  stroke: var(--series-1);
  stroke-width: 2;
  stroke-linejoin: round;
}

.progress-ring {
  width: 132px;
}

.ring-track {
  fill: none;
  stroke: var(--surface-sunken);
  stroke-width: 12;
}

.ring-fill {
  fill: none;
  stroke: var(--series-1);
  stroke-width: 12;
  stroke-linecap: round;
}

.ring-value {
  fill: var(--text);
  font-size: 26px;
  font-weight: 700;
  font-family: var(--font-mono);
}

/* The P&L bridge. What arrives and what remains share the calm blue; what
 * is taken out is a muted red; only the brand-orange stays out of it, so
 * the one money chart on the page does not fight the buttons. */
.waterfall-bar {
  fill: var(--series-2);
}

.waterfall-add {
  fill: var(--series-4);
}

.waterfall-subtract {
  fill: color-mix(in srgb, var(--bad) 78%, var(--surface));
}

.waterfall-total {
  fill: var(--series-2);
}

.waterfall-connector {
  stroke: var(--border-strong);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

/* ── Ranking bars ─────────────────────────────────────────────────────────── */

.ranking {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.ranking li {
  display: grid;
  /* The value column gets its full content width and is never elided — a
   * truncated 'SGD 6,3' reads as six dollars thirty. The label is the
   * column that gives way, and the bar after it. */
  grid-template-columns: minmax(90px, 1.3fr) minmax(50px, 2fr) max-content;
  align-items: center;
  gap: 12px;
  font-size: var(--text-md);
}

.ranking-label {
  /* Two lines before eliding, not one: a dish name is what the merchant is
   * ranking, and at phone widths one nowrap line ate half of most names.
   * The clamp keeps rows bounded so the bars stay scannable; the full name
   * rides in the title attribute (charts.js). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.ranking-track {
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}

.ranking-fill {
  display: block;
  height: 100%;
  width: var(--fill);
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--series-1), var(--brand));
}

.ranking-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  white-space: nowrap;
}

/* ── Heatmap ──────────────────────────────────────────────────────────────── */

.heatmap {
  border-collapse: collapse;
  font-size: var(--text-2xs);
  width: 100%;
}

.heatmap th {
  color: var(--text-muted);
  font-weight: 600;
  padding: 3px 6px;
  text-align: right;
  white-space: nowrap;
}

.heat-cell {
  min-width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--surface);
  border-radius: 4px;
  /* One hue, varying intensity: "busier" reads as a scale, where six unrelated
   * colours would read as six categories. */
  background: color-mix(
    in srgb,
    var(--series-1) calc(var(--heat) * 100%),
    var(--surface-sunken)
  );
}

/* ── Attention: what needs the owner, above the figures ───────────────────── */

/* Drawn only while something is true. The calm state is one clause in the status
   header above rather than a block of its own — a green panel every morning is a
   green panel nobody reads, and the whole value of this one is that its presence
   means something. */
.attention {
  width: 100%;
}

.attention-critical,
.attention-important,
.attention-unknown {
  padding: 14px 16px;
  border-radius: var(--radius);
  border: 1px solid;
}

.attention-critical {
  background: var(--bad-soft);
  border-color: var(--bad);
}

.attention-important,
section.attention-unknown {
  background: var(--warn-soft);
  border-color: var(--warn);
}

.attention-title {
  margin: 0 0 10px;
  font-size: var(--text-lg);
}

.attention-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.attention-item {
  display: grid;
  /* The severity word is a fixed rail so two items line up, and it wraps to
     its own row on a phone rather than squeezing the title into three
     characters a line. */
  grid-template-columns: auto 1fr;
  column-gap: 10px;
  row-gap: 2px;
  align-items: baseline;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--border);
}

.attention-severity {
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--warn);
}

.attention-item.attention-critical {
  /* Already inside a tinted panel, so the row itself stays on the surface
     colour and only its rail changes — two nested tints read as a bug. */
  background: var(--surface);
  border-color: var(--bad);
  padding: 10px 12px;
}

.attention-item.attention-critical .attention-severity {
  color: var(--bad);
}

.attention-item-title {
  margin: 0;
  font-weight: 700;
}

.attention-item-detail,
.attention-action {
  grid-column: 2;
}

.attention-item-detail {
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.attention-action {
  justify-self: start;
  font-size: var(--text-md);
}

.attention-unknown {
  margin: 10px 0 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

/* "Try again", under the sentence that says what could not be checked. */
.attention-retry {
  margin-top: 10px;
}

/* A panel that is only the unknown sentence has no list above it, so the
   paragraph carries the padding instead of sitting inside it twice. */
section.attention-unknown > .attention-unknown {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

@media (max-width: 520px) {
  .attention-item {
    grid-template-columns: 1fr;
  }

  .attention-item-detail,
  .attention-action {
    grid-column: 1;
  }
}

/* ── Insights ─────────────────────────────────────────────────────────────── */

.insights {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px;
  width: 100%;
}

.insight {
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.insight-kind {
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.insight-alert {
  border-color: var(--bad);
  background: var(--bad-soft);
}

.insight-alert .insight-kind {
  color: var(--bad);
}

.insight-data {
  border-color: var(--warn);
  background: var(--warn-soft);
}

.insight-data .insight-kind {
  color: var(--warn);
}

.insight-trend .insight-kind {
  color: var(--brand-text);
}

.insight-title {
  margin: 0;
  font-weight: 700;
}

.insight-detail {
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.insight-empty {
  color: var(--text-muted);
}

/* ── Health ───────────────────────────────────────────────────────────────── */

.health {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  align-items: center;
  width: 100%;
}

.health-ring {
  flex: none;
}

.health-checks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1 1 240px;
}

.health-checks li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: var(--text-body);
}

.health-icon {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: var(--text-xs);
}

.health-ok .health-icon {
  background: var(--ok-soft);
  color: var(--ok);
}

.health-missing .health-icon {
  background: var(--warn-soft);
  color: var(--warn);
}

.health-label {
  flex: 1 1 auto;
}

/* ── Standouts, definition lists, empty states ────────────────────────────── */

.standouts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}

.standouts li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

.standout-label {
  grid-column: 1 / -1;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-weight: 600;
}

.standout-name {
  font-weight: 600;
}

.standout-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.definition-list {
  margin: 0;
  display: grid;
  gap: 6px;
}

.definition-list dt {
  font-weight: 700;
  margin-top: 10px;
}

.definition-list dd {
  margin: 0;
  color: var(--text-muted);
  max-width: 80ch;
}

/* An empty state answers two questions — what happened, and what can I do —
 * and then stops. The ticket edge along the top is the one piece of
 * decoration: the perforated top of an order slip, drawn in CSS, quiet
 * enough to print. */
.empty-state {
  position: relative;
  width: 100%;
  padding: 26px;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  border: 1px dashed var(--border-strong);
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-start;
}

.empty-title {
  margin: 0;
  font-weight: 700;
}

.empty-detail {
  margin: 0;
  color: var(--text-muted);
  max-width: 62ch;
}

.report-index {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.report-index a {
  display: inline-block;
  padding: 9px 14px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  text-decoration: none;
  font-weight: 600;
  font-size: var(--text-md);
  transition: background-color var(--dur) var(--ease);
}

.report-index a:hover {
  background: var(--accent-soft);
}

/* ── Buttons and forms ────────────────────────────────────────────────────── */

.button {
  /* inline-flex, not the default inline.
   *
   * A `<button>` is inline-block already, so the vertical padding below works
   * there. On an `<a class="button">` — and there are five — the element stays
   * `display: inline`, where vertical padding does not add to the line box: it
   * paints outside it and lands on whatever follows. Download CSV on the
   * payments page overlapped its own note by 155×9px because of this. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-weight: 600;
  min-height: 44px;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease), transform 80ms var(--ease);
}

.button:hover {
  background: var(--surface-sunken);
}

.button:active {
  transform: translateY(1px);
}

.button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* The primary action wears the brand: orange fill, dark ink, because white
 * on this orange is ~2.2:1 and a button label is not decoration. */
.button-primary {
  width: 100%;
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}

/* Inside the signed-in portal, from tablet width up, a primary action is a
 * button rather than a bar: full width, a Save in a 900px panel was a
 * 900px orange stripe that read as a banner. Sign-in, sign-up and the other
 * single-card pages keep the full-width bar, and so does a phone, where a
 * wide target is the point. */
@media (min-width: 600px) {
  .app-body .button-primary {
    width: auto;
    min-width: 12rem;
  }
}

.button-primary:hover:not(:disabled) {
  background: var(--brand-hover);
  border-color: var(--brand-hover);
}

.button-primary:active:not(:disabled) {
  background: var(--brand-strong);
}

.button-quiet {
  min-height: 44px;
  padding: 6px 12px;
  font-size: var(--text-sm);
}

.button-danger {
  border-color: var(--bad);
  color: var(--bad);
}

.button-danger:hover {
  background: var(--bad-soft);
}

.inline-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.inline-form > .control-input {
  /* Direct children only: flex-basis runs along the container's axis, so on
   * an input inside a column-direction .control this became 140px of
   * *height* — the giant selects the payments page grew. */
  flex: 1 1 140px;
  min-width: 0;
}

/* A field that has no placeholder standing in for its label — a password box —
 * and the "show password" tick under it. Both take the row's whole width: the
 * password and its confirmation sit side by side, and a label floating between
 * them would read as belonging to the wrong one. */
.inline-form > .form-field-label,
.inline-form > .form-reveal {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.form-status {
  flex: 1 1 100%;
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

/* Mark all read, and — only when it could not — the sentence saying so, on
 * the line under it rather than over the list. */
.mark-all {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
}

.mark-all > .form-status:empty {
  display: none;
}

.skeleton {
  height: 96px;
  border-radius: var(--radius);
  background: linear-gradient(
    90deg,
    var(--surface-sunken) 25%,
    var(--border) 37%,
    var(--surface-sunken) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
}

@keyframes shimmer {
  0% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0 50%;
  }
}

/* Sections settle in rather than pop, one beat, distance of a hairline. */
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.main > .panel,
.main > .kpi-grid,
.main > .banner {
  animation: rise 220ms var(--ease) backwards;
}

/* Motion is decoration here. Anyone who has asked their system to stop it
 * gets a static placeholder rather than nothing, so the layout is unchanged. */
@media (prefers-reduced-motion: reduce) {
  .skeleton,
  .main > .panel,
  .main > .kpi-grid,
  .main > .banner {
    animation: none;
  }

  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ── Signing in: one product from orderlah.app onwards ────────────────────
 *
 * The auth pages are the seam between the marketing site and the portal, so
 * they wear both halves: the site's navy, wordmark and warm paper on one
 * side, the portal's form vocabulary on the other. From 920px up the navy
 * brand panel stands beside the card; below that it becomes a compact band
 * above it, because a phone screen belongs to the form.
 */

.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 24px;
  /* A whisper of brand warmth behind the card, in both themes. */
  background:
    radial-gradient(
      1000px 480px at 85% -10%,
      color-mix(in srgb, var(--brand) 7%, transparent),
      transparent 70%
    ),
    var(--bg);
}

.login-split {
  display: grid;
  width: 100%;
  max-width: 880px;
  border-radius: calc(var(--radius) + 4px);
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-overlay);
  background: var(--surface);
}

/* The navy half. Everything on it is either the wordmark or one sentence —
 * it is a doorplate, not a brochure. */
.login-brand {
  position: relative;
  background:
    linear-gradient(160deg, var(--rail) 0%, var(--rail-2) 100%);
  color: #eef2f7;
  padding: 26px 28px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}

.login-brand-logo {
  /* A <p>, so the browser gives it 1em top and bottom unless told otherwise —
   * and it was not, while both its siblings below zero theirs. That is 48px of
   * space in the doorplate nobody asked for, on every page in this shell, in
   * both postures; on a phone it was most of what put the sign-in page past
   * the fold. The gap under the wordmark is a decision now rather than a
   * default, and the panel's padding is what it says it is. */
  margin: 0 0 10px;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-3xl);
  letter-spacing: -0.01em;
}

.login-brand-logo .brand-mark {
  width: 38px;
  height: 38px;
}

.login-brand-surface {
  margin: 0;
  color: #94a3b8;
  font-weight: 600;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.login-brand-line {
  margin: auto 0 0;
  color: #cbd5e1;
  font-size: var(--text-base);
  max-width: 30ch;
  line-height: 1.55;
  position: relative;
  z-index: 1;
}

/* The decoration is the logo's own motif — the three speed lines and the
 * bowl — echoed large and faint in the panel's corner. Fully contained: a
 * shape half-clipped at the panel edge read as an accident, not a motif. */
.login-brand-decor {
  position: absolute;
  right: 16px;
  bottom: 14px;
  width: 132px;
  height: auto;
  opacity: 0.5;
  pointer-events: none;
}

.login-card {
  width: 100%;
  padding: 30px 28px;
  background: var(--surface);
}

/* A login card that is *not* inside the split (the index redirect stub) keeps
 * the old standalone shape. */
.login-page > .login-card {
  max-width: 400px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-overlay);
}

/* In the compact band posture the motif has no corner to live in, and the
 * panel is a header over the form rather than a column beside it — so it takes
 * the room a header needs. Every pixel it gives back is a pixel of the form a
 * merchant can see without scrolling, which on a phone is the whole argument:
 * this is the posture where an auth page most often cannot fit on the screen. */
@media (max-width: 919.9px) {
  .login-brand-decor {
    display: none;
  }

  .login-brand {
    padding: 18px 22px;
    gap: 6px;
  }

  .login-brand-line {
    font-size: var(--text-body);
  }
}

@media (min-width: 920px) {
  .login-split {
    grid-template-columns: 5fr 7fr;
    min-height: 460px;
  }

  /* Room for the corner motif, which is what this reserve is for and was not
   * doing. The motif is 132px wide and sits 16px off the panel's right edge,
   * so it owns the last 148px of the panel — about 116px of the content box
   * once the padding is off. Reserving 76 left the sentence running 23-38px
   * into it on four of the five pages in this shell, at every desktop width,
   * with an emoji landing squarely on the orange bars.
   *
   * It lives in here rather than on the rule itself because the motif does:
   * below 920px there is no corner for it and it is `display: none`, and a
   * reserve kept for a shape that is not drawn is a sentence wrapped for
   * nothing — which on a phone is the one posture that cannot spare the
   * lines. */
  .login-brand-line {
    max-width: min(30ch, calc(100% - 120px));
  }

  .login-brand {
    padding: 34px 32px;
  }

  .login-card {
    padding: 38px 40px;
    align-self: center;
  }
}

.login-card h1 {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  letter-spacing: -0.02em;
}

.login-sub {
  margin: 4px 0 22px;
  color: var(--text-muted);
}

.login-card label {
  display: block;
  margin-bottom: 6px;
  font-weight: 600;
  font-size: var(--text-md);
}

.login-card input[type="email"],
.login-card input[type="password"],
.login-card input[type="text"],
.login-card select {
  font: inherit;
  width: 100%;
  min-height: 46px;
  padding: 11px 12px;
  margin-bottom: 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  transition: border-color var(--dur) var(--ease);
}

.login-card input:hover,
.login-card select:hover {
  border-color: var(--text-faint);
}

.login-note {
  margin: 20px 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ── Forgotten passwords ──────────────────────────────────────────────────
 *
 * The reset page reuses .login-card wholesale — it is the same card, in the
 * same frame, which is what makes a merchant arriving from their email
 * recognise where they are. What follows is only the handful of things the
 * login form does not already have.
 */

.login-forgot {
  margin: 14px 0 0;
  font-size: var(--text-md);
}

/* The sentence above each form. Slightly darker than .login-note's muted grey
 * on its own, because here it is an instruction being followed rather than a
 * footnote being skimmed. */
.login-lead {
  margin: 0 0 18px;
  color: var(--text);
}

/* ── Signing up ───────────────────────────────────────────────────────────
 *
 * The signup page reuses .login-card wholesale, the same way reset.html does.
 * What follows is only what the sign-in form does not already have: a select,
 * and two words of labelling.
 */

/* Matches the inputs above rather than the browser's default control, which is
 * shorter than 46px on every platform and lands under the 44px tap target the
 * counter UI holds itself to. `appearance: none` would also remove the arrow,
 * so the arrow is drawn back on — a select with no arrow reads as a disabled
 * text field. */
.login-card select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
    linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 38px;
}

/* "(optional)" beside a label. Quieter than the label it sits in, because it
 * is the answer to "must I?" and not part of the question. */
.login-optional {
  font-weight: 400;
  color: var(--text-muted);
}

.login-signup {
  margin-top: 18px;
}

/* ── The signup card is one screenful ────────────────────────────────────
 *
 * This is the only auth page with six fields on it, and in one column it ran
 * about 1,020px tall — past the bottom of every laptop window at 100%. A form
 * that scrolls hides its own submit button, and the merchant who cannot see
 * the end of a signup is the one who abandons it halfway. So the card is wider
 * than the sign-in card and pairs its fields up.
 *
 * Wider on this page only: `.login-split`'s 880px is right for a form with two
 * fields in it, and stretching that card for every page would leave the
 * sign-in form's inputs the width of a table.
 */
.signup-split {
  max-width: 1080px;
}

@media (min-width: 920px) {
  /* A little more of the row for the form than the sign-in split gives it,
   * because two columns of fields need the room and a doorplate does not. */
  .signup-split {
    grid-template-columns: 4fr 8fr;
  }

  .signup-split .login-card {
    padding: 30px 34px;
  }
}

/* auto-fit rather than a breakpoint: the second column exists exactly when
 * there is room for it. A phone, a narrow window and a merchant running a
 * large system font all get one column back without anybody having to have
 * guessed the width at which that becomes true. */
.signup-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  column-gap: 20px;
  align-items: start;
}

/* A pair of fields has to agree on where its inputs start.
 *
 * The ⓘ is 26px tall against a 22px label, so the header carrying one is 4px
 * taller than a plain one. Stacked, nobody could see that — which is why
 * `.login-card .info-label` further down can say its job is to leave "the
 * label, the icon and the field exactly where they were before the icon
 * existed" and be believed. Side by side it is a visible step between the
 * Email input and the Password input beside it.
 *
 * So a plain label takes the icon's height as a floor. Above it the label
 * wins and both headers grow together, which is what happens at a large
 * system font — the fix is a floor rather than a fixed height for exactly
 * that reason. Flex with a gap because the floor needs the text centred in
 * it, and centring a block would leave "(optional)" resting on the baseline
 * of a label 4px taller than its own text. */
.signup-field > label {
  display: flex;
  align-items: center;
  gap: 0.3em;
  min-height: 26px;
}

.custom-field {
  margin-top: 8px;
}

.custom-field[hidden] { display: none; }

/* The button is the end of the form, not a seventh field. */
.signup-submit {
  grid-column: 1 / -1;
  margin-top: 2px;
}

/* The tighter rhythm the two columns buy back. Scoped to this card: the
 * sign-in and reset forms are short already and their spacing is not the
 * problem this solves. */
.signup-split .login-sub {
  margin: 4px 0 14px;
}

.signup-split .login-divider {
  margin: 12px 0;
}

.signup-split .login-card input[type="email"],
.signup-split .login-card input[type="password"],
.signup-split .login-card input[type="text"],
.signup-split .login-card select {
  margin-bottom: 14px;
}

.signup-split .login-hint {
  margin: -4px 0 14px;
}

/* "Show password" sits on the field's own header line, beside the label.
 *
 * It was under the input, on one line with "At least 10 characters." — which
 * fits at a wide column and stops fitting at about 300px, where the two stack
 * and the field grows 25px. That is 25px the tightest window did not have: at
 * 1024x640 the filled form cleared the fold by 4px, a margin tuned to one
 * renderer's font metrics rather than a property that holds. Beside the label
 * there is always room, because "Password ⓘ" is short.
 *
 * Both still stay on the screen: the rule because a hidden minimum is a
 * rejected submit, and the tick box because a thumb on a counter tablet needs
 * a real target. It is placed rather than reordered — the markup keeps the box
 * after the input, so tabbing still reaches the field before the control that
 * reveals it.
 *
 * `:not([hidden])` because a Google signup hides this whole field, and an
 * author `display` beats the browser's own `[hidden] { display: none }` —
 * the same guard .login-google-row above needs, for the same reason. */
.signup-password:not([hidden]) {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "label reveal"
    "field field"
    "rule  rule";
}

.signup-password .info-label {
  grid-area: label;
}

.signup-password input[type="password"] {
  grid-area: field;
}

.signup-password .login-hint {
  grid-area: rule;
}

/* Two classes deep rather than one, and deliberately: `.login-reveal`'s own
 * rule sits further down this stylesheet, so a single class here loses to it
 * on source order and the field silently keeps the 16px margin meant for a
 * tick box standing on its own under a field. */
.signup-password .login-reveal {
  grid-area: reveal;
  justify-self: end;
  /* Level with the label it shares the line with. */
  margin: 0 0 6px;
  font-size: var(--text-sm);
}

/* The doorplate's three lines sit together in the middle of the panel here
 * rather than pinned top and bottom. On the sign-in page the panel is about as
 * tall as its card; the signup card is wider and shorter, so the same
 * arrangement left a sentence marooned in the bottom corner, wrapped around
 * the motif that lives there. */
.signup-split .login-brand {
  justify-content: center;
}

.signup-split .login-brand-line {
  margin: 6px 0 0;
  max-width: 32ch;
}

/* Whose account this is about to be, once Google has vouched for the address.
 * It replaces the button and the divider above the form rather than sitting
 * after them, so it carries their spacing rather than .login-note's, which is
 * all top margin and would leave the sentence resting on the first label. */
.signup-identity {
  margin: 0 0 14px;
}

/* The way onward and the small print share the last line. */
.signup-footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 16px;
  margin-top: 14px;
}

/* ── A phone, everywhere in the auth shell ────────────────────────────────
 *
 * The same argument as the shallow window below, at the other end: a merchant
 * signing in behind the till is holding a phone, and the shorter the phone the
 * more of the form has to be on it. The gaps come in on the screens that are
 * short of them.
 *
 * Bounded by height as well as width so a tablet standing in portrait — which
 * has 1024px and no shortage — is not given a phone's spacing. And the tap
 * targets are not in here, on any screen: the fields, the checkbox and both
 * buttons keep their full height, because a thumb on a counter phone is the
 * one thing this whole shell cannot economise on.
 */
@media (max-width: 919.9px) and (max-height: 900px) {
  .login-page {
    padding: 10px;
  }

  .login-split .login-card {
    padding: 18px 22px;
  }

  .login-split .login-brand {
    padding: 12px 22px;
  }

  .login-split .login-sub {
    margin: 2px 0 12px;
  }

  .login-split .login-divider {
    margin: 10px 0;
  }

  .login-split .login-card input[type="email"],
  .login-split .login-card input[type="password"],
  .login-split .login-card input[type="text"],
  .login-split .login-card select {
    margin-bottom: 12px;
  }

  .login-split .login-hint {
    margin: -4px 0 12px;
  }

  .login-split .login-reveal {
    margin-bottom: 12px;
  }

  /* 1.6 rather than the 1.75 a desktop gets. Still above the 1.5 that
   * readable body copy is held to, and on a phone it is 20-30px of the one
   * page in this shell whose whole point is the button underneath it. */
  .login-split .login-steps {
    margin-bottom: 12px;
    line-height: 1.6;
  }

  .login-split .login-forgot {
    margin-top: 10px;
  }

  .login-split .login-note,
  .login-split .login-signup {
    margin-top: 12px;
  }

  .login-split .alert {
    padding: 9px 12px;
    margin-bottom: 10px;
  }
}

/* On a small phone the doorplate keeps its wordmark and gives up its sentence.
 *
 * The band is brand reassurance — this is OrderLah, this is the merchant door
 * — and the wordmark says all of that on its own. The sentence underneath is
 * the part that is nice to have, and on a 667pt screen it is 70px standing
 * between a merchant and the password field. Above this height it stays: the
 * common phone is tall enough to have both.
 */
@media (max-width: 460px) and (max-height: 800px) {
  .login-split .login-brand-line {
    display: none;
  }
}

/* ── A shallow window, everywhere in the auth shell ───────────────────────
 *
 * A 1366x768 laptop has about 640px of page left once the browser has taken
 * its chrome, and the arithmetic does not care what the screen says. These
 * pages are the seam a merchant arrives at from the site, and every one of
 * them is meant to be read without scrolling; at 640px the sign-in page ran
 * 17px past the fold and the signup page rather more.
 *
 * So the padding and the gaps come in on exactly the windows that are short of
 * them, rather than everywhere: a merchant on a roomy display should not be
 * given a cramped page to pay for one who is not.
 *
 * The tap targets are not in here. Every input, every checkbox and every
 * button keeps its full height at every window — this takes the air around
 * them, never the thing a thumb has to hit.
 */
@media (min-width: 920px) and (max-height: 760px) {
  .login-page {
    padding: 10px;
  }

  .login-split .login-card {
    padding: 16px 30px;
  }

  .login-split .login-brand {
    padding: 24px 30px;
  }

  .login-split .login-sub {
    margin: 2px 0 10px;
  }

  .login-split .login-divider {
    margin: 6px 0;
  }

  .login-split .login-card input[type="email"],
  .login-split .login-card input[type="password"],
  .login-split .login-card input[type="text"],
  .login-split .login-card select {
    margin-bottom: 12px;
  }

  .login-split .login-hint {
    margin: -4px 0 10px;
  }

  .login-split .signup-footer {
    margin-top: 12px;
  }

  /* A refusal banner is the one thing on these pages that arrives late and
   * unasked for, so a shallow window is exactly where it has to cost least.
   * The sentence keeps its size; the box around it gives up the air. */
  .login-split .alert {
    padding: 9px 12px;
    margin-bottom: 10px;
  }
}

.login-hint {
  margin: -6px 0 18px;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The get-the-app handoff's three steps (§P1-6). Its own class rather than an
   inline style, which the CSP would silently drop. */
.login-steps {
  margin: 0 0 18px;
  padding-left: 1.2em;
  line-height: 1.75;
}

/* "Show password". The label is part of the target, not decoration beside it:
 * a 16px checkbox alone is not something a thumb finds on a counter tablet. */
.login-reveal {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 16px;
  font-size: var(--text-md);
}

.login-reveal input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--brand);
}

.login-reveal label {
  margin: 0;
  font-weight: 500;
}

/* The way onward once the password is changed. A link that has to look and
 * behave exactly like the submit button above it — same height, same width —
 * because it is the only thing left to press on the page. */
.login-signin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  text-decoration: none;
}

/* ── Continue with Google ─────────────────────────────────────────────────
 *
 * Both blocks below are hidden in the markup and unhidden by login.js only
 * when the server says an OAuth client is configured, so `[hidden]` has to
 * actually hide them — `display: flex` on the element itself would override
 * the browser's default `[hidden] { display: none }`, which is the standard
 * way this pattern breaks. Hence the `:not([hidden])`.
 *
 * The mark keeps Google's own four colours on a neutral surface, which is what
 * their branding guidelines ask for and also what makes the button instantly
 * recognisable to somebody who is not reading the label carefully. It is the
 * one place in this portal where colour is not a status.
 */

.login-google-row {
  margin: 0 0 4px;
}

.login-google-row:not([hidden]) {
  display: block;
}

.login-google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  /* Taller than the .button floor: this is the first thing a merchant reaches
   * for on a phone, often one-handed at a counter. */
  min-height: 48px;
  text-decoration: none;
}

.login-google-mark {
  width: 18px;
  height: 18px;
  flex: none;
}

.login-divider {
  margin: 18px 0;
  color: var(--text-faint);
  font-size: var(--text-sm);
  text-align: center;
}

.login-divider:not([hidden]) {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* The rules either side of the words. Empty pseudo-elements rather than a
 * border on the container, so the label sits *in* the line rather than above
 * it at large system font sizes. */
.login-divider::before,
.login-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ── Licence ──────────────────────────────────────────────────────────────
 *
 * §41: this page is read on a phone at a hawker stall, on the 8-inch tablet
 * that runs the till, and on the owner's laptop. It is the same content in
 * all three, laid out for each — not one layout stretched.
 *
 * The prices are the reason the grid is `auto-fit` rather than a fixed three
 * columns. Three cards forced side by side on a 7-inch tablet put RM1,400 in
 * a size nobody reads across a counter, and a merchant deciding how much to
 * spend must be able to see what they are choosing between.
 */

/* The outlet chooser, on a licence page reached with more than one shop.
 *
 * One row per outlet, full width and left-aligned: `.button` centres its label,
 * and four shop names of four lengths otherwise make four differently sized tap
 * targets down the page, of which the shortest is the hardest to hit. */
.licence-outlet-choice {
  justify-content: flex-start;
  width: 100%;
  text-align: left;
  padding: 12px 16px;
}

.licence-status .panel-header {
  align-items: center;
}

.licence-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px 22px;
  margin: 16px 0 0;
}

.licence-fact {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.licence-fact-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.licence-fact-value {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
}

.licence-promo-banner {
  color: var(--brand-text);
  letter-spacing: 0.02em;
}

.plan-grid {
  display: grid;
  /* 250px is the width at which a struck-through price and the price beside it
   * both stay on one line at the largest system font a merchant is likely to
   * have set. Narrower and the two prices wrap into each other, which reads as
   * one confusing number. */
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: var(--gap);
  margin-top: 16px;
  /* Three cards and no more, however wide the monitor. A licence chooser
   * stretched across an ultrawide is three cards a metre apart. */
  max-width: 1000px;
}

.plan-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 22px 18px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: center;
}

.plan-card-best {
  border-color: var(--brand);
  border-width: 2px;
}

.plan-badge {
  position: absolute;
  top: -11px;
  padding: 3px 12px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--brand-ink);
  font-size: var(--text-2xs);
  font-weight: 800;
  letter-spacing: 0.04em;
}

.plan-term {
  margin: 0;
  font-size: var(--text-body);
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.plan-standard {
  /* A real strikethrough on a real <s>, kept legible rather than faded to the
   * point where the merchant cannot read what they are being saved. */
  font-size: var(--text-xl);
  color: var(--text-muted);
}

.plan-charged {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-5xl);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.plan-promo-label {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--brand-text);
}

.plan-saving {
  margin: 0;
  font-weight: 700;
  color: var(--ok);
}

.plan-footnote {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  max-width: 24ch;
}

.plan-choose {
  width: 100%;
  margin-top: 12px;
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}

.plan-choose:hover:not(:disabled) {
  background: var(--brand-hover);
  border-color: var(--brand-hover);
}

/* The call to action after a plan is chosen. Brand-bordered rather than a
 * plain panel, because it is the one thing on the page a merchant who has just
 * pressed "Choose Plan" is looking for. */
.licence-checkout {
  border-color: var(--brand);
  border-width: 2px;
}

.licence-checkout .plan-choose {
  width: auto;
  min-width: 16rem;
  display: inline-block;
  text-align: center;
  text-decoration: none;
}

.licence-reference {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* A purchase row's way to pay and its way to put the purchase down, side by
 * side, wrapping under each other in a narrow stacked row. */
.licence-row-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.button-small {
  min-height: 44px;
  padding: 7px 14px;
  font-size: var(--text-md);
  display: inline-block;
  text-decoration: none;
}

/* ── Responsive ───────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
  .page-title {
    font-size: 1.45rem;
  }

  .kpi-grid {
    /* The base 215px column keeps a five-figure mono amount inside its
     * card; the old 180px packed four cards where three fit and sliced
     * the last digit off the takings. */
    grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  }

  .kpi-value {
    font-size: 1.45rem;
  }

  .control {
    flex: 1 1 100%;
    min-width: 0;
  }

  /* Period and Compare are a word each: two to a row, so the first figure
   * on a phone is not pushed below the fold by two full-width selects. */
  .control.control-pair {
    flex-basis: calc(50% - 7px);
  }

  .plan-charged {
    font-size: 1.8rem;
  }

  .licence-facts {
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  }
}

@media (max-width: 620px) {
  .app-body {
    padding: 16px 14px 60px;
  }

  .kpi-grid {
    grid-template-columns: 1fr;
  }

  /* The quiet tier's values are short; two-up halves a very long scroll. */
  .kpi-grid-compact {
    grid-template-columns: 1fr 1fr;
  }

  .kpi-grid-compact .kpi-value {
    font-size: var(--text-lg);
  }

  .kpi-value {
    font-size: var(--text-4xl);
  }

  .panel {
    padding: 16px;
  }

  .ranking li {
    /* The bar is decoration once space is tight; the label and the number are
     * the content, so the bar is what gets dropped. */
    grid-template-columns: 1fr auto;
  }

  .ranking-track {
    display: none;
  }

  .nav-email {
    display: none;
  }

  .plan-grid {
    /* One card at a time on a phone. Two 150px cards side by side would put a
     * three-year price in a size that has to be zoomed to read. */
    grid-template-columns: 1fr;
  }

  .licence-facts {
    grid-template-columns: 1fr 1fr;
  }
}

/* ── Message preferences ──────────────────────────────────────────────────
 *
 * A checkbox row, sized for a thumb. The label is tied to the box with
 * htmlFor, so the whole line is the target rather than the 16px square — a
 * stall owner reading this on a phone should not have to aim.
 */

/* `:not([hidden])` because a display rule here would beat the browser's
 * default `[hidden] { display: none }` — the People panel hides its outlet
 * checkboxes until "Chosen outlets" is picked. */
.preference-list:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}

.preference-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 4px;
  border-bottom: 1px solid var(--border);
}

.preference-row:last-child { border-bottom: none; }

.preference-row input[type="checkbox"] {
  width: 22px;
  height: 22px;
  margin: 2px 0 0;
  flex: 0 0 auto;
  accent-color: var(--brand);
}

.preference-row input[type="checkbox"]:disabled { cursor: not-allowed; }

.preference-text { flex: 1 1 auto; min-width: 0; }

.preference-label {
  display: block;
  font-weight: 600;
  cursor: pointer;
  /* A comfortable target on a phone, which is where a stall owner reads this. */
  padding: 2px 0;
}

.preference-row input[type="checkbox"]:disabled + .preference-text .preference-label {
  cursor: not-allowed;
}

.preference-detail {
  margin: 2px 0 0;
  color: var(--text-muted);
  font-size: var(--text-md);
}

/* ── Contextual help: the ⓘ and its panel ─────────────────────────────────
 *
 * The portal used to explain itself in prose — a paragraph under the heading,
 * a sentence under every field. This is what replaced it: the label stays, the
 * explanation is one tap away, and the page becomes scannable again. Same idea
 * and same look as the Flutter app's `InfoHint`, so an operator who learns it
 * on the tablet already knows it here.
 *
 * Quiet by design. It must never compete with a primary button, and it must
 * never look like a warning — nothing that needs attention lives behind it.
 * `assets/info.js` drives the behaviour and sets --info-x/--info-y; there is
 * no style attribute in any markup, because the CSP forbids one.
 */
.info-hint {
  /* A 32px target inside a text line: big enough for a thumb on a tablet in a
     wet kitchen, small enough that a row of labels does not grow a column of
     buttons. */
  flex: none;
  width: 32px;
  height: 32px;
  margin: -6px 0 -6px 2px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
  vertical-align: middle;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  -webkit-tap-highlight-color: transparent;
}

/* The glyph is drawn, not typed: a real "ⓘ" character renders at a different
   weight in every font stack the five languages pull in, and an <img> would be
   one more request per row. */
.info-hint::before {
  content: 'i';
  width: 17px;
  height: 17px;
  border: 1.5px solid currentColor;
  border-radius: var(--radius-pill);
  font: 600 11px/15px ui-sans-serif, system-ui, sans-serif;
  font-style: italic;
  text-align: center;
}

.info-hint:hover,
.info-hint[aria-expanded='true'] {
  color: var(--brand-text);
  background: var(--brand-soft);
}

.info-panel {
  position: absolute;
  left: var(--info-x, 0);
  top: var(--info-y, 0);
  /* Above the sticky chrome (50), below the skip link (60): a panel the
   * user just opened must not slide beneath the top bar as they scroll. */
  z-index: 55;
  width: min(300px, calc(100vw - 24px));
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-overlay);
  padding: 14px 16px;
}

.info-panel[hidden] {
  display: none;
}

.info-panel-title {
  margin: 0 0 4px;
  font-size: var(--text-body);
  font-weight: 700;
}

.info-panel-title[hidden] {
  display: none;
}

.info-panel-body {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--text-muted);
}

/* A label carrying a ⓘ has to keep the two on one baseline without the icon
   pushing the text into a second line. */
.info-label {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

/* A ⓘ on a form label, on the sign-in card's stacked fields.
 *
 * `.login-card label` is a block with 6px under it, and the field it names is
 * the next block down. Wrapping it inline-flex would leave that 6px inside the
 * wrapper and the 32px icon would push the field a line further from its own
 * label — so the wrapper takes the spacing and the label gives it up. The
 * result is the label, the icon and the field exactly where they were before
 * the icon existed. */
.login-card .info-label {
  display: flex;
  margin-bottom: 6px;
}

.login-card .info-label label {
  margin-bottom: 0;
}

/* ── A report on paper ──────────────────────────────────────────────────────
 *
 * Printing a report page — or "Save as PDF" from the same dialog, which is how
 * the portal makes its PDFs (report-print.js says why) — must produce a
 * document, not a picture of a web page: OrderLah's header with the business,
 * scope, period, currency and completeness; the figures; a footer; and none of
 * the navigation, buttons, filters, popovers or card chrome around them.
 *
 * Four rules carry most of it:
 *
 * - **A4 portrait, real margins**, and a page number in the margin where the
 *   browser supports margin boxes (Chrome does; elsewhere the browser's own
 *   header and footer carry the title and page).
 * - **Short things stay whole; tables run on.** A KPI card, a chart or a small
 *   panel never splits across a page. A table must be free to — holding a
 *   long one together is how a printed report gets a blank page and a clipped
 *   ledger — so its header row repeats instead, and only a row stays whole.
 * - **Nothing is clipped.** Scroll boxes become plain blocks, a stacked phone
 *   table is a table again, sticky cells stop sticking.
 * - **Honesty prints.** The completeness banner, every footnote and the
 *   "Estimated" chips stay; only what is interactive goes.
 *
 * Colours are the palette's own tokens: print-color-adjust keeps the brand
 * accents and status chips on paper, and nothing here is a new colour. */

.print-only {
  display: none;
}

@page {
  size: A4 portrait;
  margin: 14mm 12mm 16mm;

  @bottom-right {
    content: counter(page) " / " counter(pages);
    font-size: 8pt;
  }
}

@media print {
  .app-header,
  .controls,
  .button,
  .inline-form,
  .skip-link,
  .report-index,
  .info-hint,
  .info-panel,
  .print-hidden,
  .freshness-refresh,
  .chart-tooltip,
  [role="tooltip"] {
    display: none !important;
  }

  .print-only {
    display: block !important;
  }

  /* The rail grid collapses so the content column is the page again, on
     paper's own white. */
  body {
    display: block;
    background: #fff;
    color: #000;
  }

  .app-body {
    max-width: none;
    padding: 0;
  }

  /* The print header names the report; the screen's own page heading would
     say "Reports" above "Business Performance Report". */
  .page-title {
    display: none;
  }

  /* A panel whose whole body is chrome — an index of anchor links, a print
     button — and whose explanation lives behind a ⓘ, which the rule above has
     just hidden. Without this it reaches paper as an empty captioned box. */
  .panel-screen-only {
    display: none !important;
  }

  /* One column, in reading order: a grid of half-width cards is a screen
     layout, and a grid row is the one thing a printer cannot break inside. */
  .main,
  .panel-pair {
    display: block;
  }

  .main > * + *,
  .panel-pair > * + * {
    margin-top: 7mm;
  }

  /* Sections, not cards: the card chrome goes and a ruled heading takes its
     place. Panels may run across pages — a long one held together is a blank
     half page — and what must stay whole says so below. */
  .panel {
    box-shadow: none;
    border: 0;
    border-radius: 0;
    padding: 0;
    background: none;
  }

  .panel-header {
    border-bottom: 2px solid var(--brand);
    padding-bottom: 1.5mm;
    margin-bottom: 3mm;
  }

  .panel-header,
  .panel-subhead,
  caption {
    break-after: avoid;
  }

  .kpi,
  .chart-host,
  .panel-note,
  .banner,
  .print-header,
  .data-table tr {
    break-inside: avoid;
  }

  /* A panel with no table in it is short; it moves to the next page whole. */
  .panel:not(:has(.data-table)) {
    break-inside: avoid;
  }

  .kpi-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 3mm;
  }

  /* A KPI card keeps a hairline on paper: it is the one card that is still a
     card, because each one is a figure to be read on its own. */
  .kpi {
    box-shadow: none;
    border-color: #999;
  }

  /* Nothing scrolls on paper, so nothing may be hidden inside a scroll box. */
  .table-wrap,
  .table-wrap-tall,
  .chart-host {
    overflow: visible !important;
    max-height: none !important;
  }

  .chart,
  .chart-host svg {
    max-width: 100%;
    height: auto;
  }

  .data-table {
    width: 100%;
  }

  /* Repeated at the top of every page a table runs onto, so a column of
     figures on page four still says what it is. */
  .data-table thead {
    display: table-header-group;
  }

  .data-table tfoot {
    display: table-footer-group;
  }

  .data-table th,
  .data-table td {
    border-color: #999;
    position: static !important;
  }

  .kpi-status,
  .pill,
  .banner,
  .chart-host,
  .print-brand-dot,
  .panel-header {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* One report out of a page that holds several (report-print.js, `only`). */
  html[data-print-scope="only"] .main > :not([data-print-include]):not(.print-header):not(.print-footer):not(.banner) {
    display: none !important;
  }
}

/* The header a printed report opens with. */
.print-header {
  margin-bottom: 6mm;
  padding-bottom: 4mm;
  border-bottom: 1px solid var(--border-strong);
}

.print-brand {
  margin: 0 0 3mm;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 13pt;
  letter-spacing: -0.01em;
}

.print-brand-dot {
  color: var(--brand);
}

.print-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 20pt;
  font-weight: 700;
  line-height: 1.2;
}

.print-business {
  margin: 1mm 0 3mm;
  font-size: 11pt;
  font-weight: 600;
}

.print-meta {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2mm 6mm;
  margin: 0;
  padding: 3mm 4mm;
  background: var(--surface-sunken);
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

.print-meta-item dt {
  font-size: 7.5pt;
  color: var(--text-muted);
}

.print-meta-item dd {
  margin: 0;
  font-size: 9pt;
  font-weight: 600;
}

/* Partial data is in the header itself, in words, where nobody skips it. */
.print-partial {
  margin: 3mm 0 0;
  padding: 2mm 3mm;
  border-left: 3px solid var(--warn);
  background: var(--warn-soft);
  font-size: 9pt;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

.print-footer {
  margin-top: 8mm;
  padding-top: 2mm;
  border-top: 1px solid var(--border);
  font-size: 8pt;
  color: var(--text-muted);
}

.print-footer p {
  margin: 0;
}

/* The export panel's two groups: the report itself, then its raw data. */
.export-group + .export-group {
  margin-top: 16px;
}

.export-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
}

/* ── The sign-in button's three states ────────────────────────────────────
 *
 * §27. All three were one state before this: the button looked and behaved
 * identically whether the form was empty, ready or already sent, so the only
 * feedback a merchant got for pressing it on an empty form was
 * "that email or password did not match" — about a password they had not
 * typed — after a round trip to a rate-limited endpoint.
 *
 * `opacity: .55` on `.button:disabled` above is not enough on its own here.
 * On the brand fill, a 55% wash still reads as a live primary button, and on
 * a phone in daylight it reads as one at full strength. So the primary
 * button's disabled state changes *hue*, not just alpha: it leaves the brand
 * entirely and becomes a flat, bordered, muted control that nothing about
 * looks pressable. Contrast is kept above the 4.5:1 floor deliberately — a
 * disabled control still has to be readable, or "Sign in" becomes a grey
 * smear nobody can identify. */
.button-primary:disabled,
.button-primary[aria-disabled="true"] {
  opacity: 1;
  background: var(--surface-sunken);
  border-color: var(--border);
  color: var(--text-muted);
}

/* Sending. The label already changes to "Signing in…", which is the part a
 * screen reader gets; this is the part somebody glancing at the screen gets —
 * a striped bar that is visibly not the same object as the button they just
 * pressed, so a slow connection reads as "working" rather than "nothing
 * happened".
 *
 * A gradient rather than an animation: `prefers-reduced-motion` covers a real
 * need on this page — a busy indicator that moves is exactly the kind of thing
 * that should hold still for somebody who asked for that — and a static
 * pattern needs no exception written for it. */
form[aria-busy="true"] .button-primary:disabled {
  background: repeating-linear-gradient(
    -45deg,
    var(--surface-sunken),
    var(--surface-sunken) 10px,
    var(--border) 10px,
    var(--border) 20px
  );
  color: var(--text);
  cursor: progress;
}

/* A method's name in the sign-in list is a heading, not a control. It borrows
 * `.preference-label`'s weight and rhythm so the two lists look like one idea,
 * but a pointer cursor over something nothing happens on is a small lie the
 * message switches above do not tell — theirs really are <label>s. */
span.preference-label { cursor: default; }

/* ── Confirmation dialog ───────────────────────────────────────────────────
 *
 * Native <dialog>, so focus trapping, Esc and the top layer are the
 * platform's job. Replaces window.confirm(), which could not carry a reason,
 * could not style a destructive action, and is suppressible in some browsers
 * — the wrong failure mode for a revoke.
 */
.confirm-dialog {
  width: min(30rem, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 18px 48px rgb(16 30 51 / 22%);
}

.confirm-dialog::backdrop { background: rgb(16 30 51 / 44%); }

.confirm-dialog form { padding: 22px; }

.confirm-dialog h2 { margin: 0 0 8px; font-size: 18px; }

.confirm-dialog p {
  margin: 0 0 16px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-muted);
}

.confirm-field { margin-bottom: 14px; }

.confirm-field label {
  display: block;
  margin-bottom: 5px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.confirm-field input {
  width: 100%;
  box-sizing: border-box;
}

.confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
}

@media (max-width: 480px) {
  .confirm-actions { flex-direction: column-reverse; }
  .confirm-actions button { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .confirm-dialog { animation: none; }
}

.confirm-hint {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--text-muted);
}

/* ── Legal ────────────────────────────────────────────────────────────
 *
 * Two lists and a form, and the form is the part that matters: an agreement
 * is one tick and one button, with the button disabled until the tick — so a
 * return keypress meant for the page underneath cannot enter into a contract.
 */
.legal-agree { margin: 14px 0 0; }
.legal-agree .button { margin-top: 6px; }

.legal-history { list-style: none; margin: 0; padding: 0; }
.legal-history li {
  padding: 10px 0;
  border-top: 1px solid var(--border);
  font-size: 15px;
}
.legal-history li:first-child { border-top: none; }

/* The footer every signed-in page carries. Deliberately quiet and
 * deliberately always there: a merchant looking for the terms they agreed to
 * should not have to know which screen keeps them. */
.app-footer {
  margin: 40px 0 0;
  padding: 18px 0 8px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  align-items: baseline;
  font-size: 14px;
}
.app-footer a { color: inherit; }
.app-footer .app-footer-note { color: var(--text-muted); }

/* The interstitial an outstanding agreement puts in front of the account
 * screens. It replaces the page's content, never the navigation — a merchant
 * who lands here must still be able to reach Help and sign out. */
.legal-gate { max-width: 44rem; }
.legal-gate h2 { margin-top: 0; }

/* ── Finishing setup ──────────────────────────────────────────────────────
 *
 * `get-the-app.html` used to be three printed steps and a store button, which
 * fitted in the same card the sign-in form uses. It is a checklist now — five
 * derived steps, a download block and a QR code — so it is deliberately *not*
 * one screenful, and `login.browser.mjs` no longer asks it to be. What it
 * still holds is the primary action on the first screen of a 320pt phone,
 * which is the thing a merchant who has just signed up is looking for.
 */
.setup-split {
  max-width: 980px;
}

@media (min-width: 920px) {
  .setup-split {
    grid-template-columns: 4fr 8fr;
  }
}

/* The primary action keeps `.login-signin`'s own shape — full width, 48px of
 * height, centred — because it is the same thing that class already means on
 * every other page in this shell: the one button the page exists for, sized
 * for a thumb. This only gives it its air. */
.setup-primary {
  margin: 0 0 20px;
}

.setup-steps {
  list-style: none;
  margin: 0 0 22px;
  padding: 0;
  display: grid;
  gap: 2px;
}

/* A row per step: the mark, then the label with its detail under it. The
 * mark column is fixed so five labels start on one line down the list — a
 * ragged left edge is what makes a checklist read as a paragraph. */
.setup-step {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 10px;
  align-items: start;
  padding: 10px 12px;
  border-radius: var(--radius);
  border: 1px solid transparent;
}

.setup-step-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid var(--border);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1;
  color: var(--text-muted);
}

.setup-step-body {
  display: block;
}

.setup-step-label {
  display: block;
  font-weight: 600;
  line-height: 1.45;
}

/* The detail is a value, not an explanation — which outlet, how many devices,
 * how many days are left — so it is on the screen rather than behind a ⓘ.
 * There is no ⓘ anywhere on this page for that reason. */
.setup-step-detail {
  display: block;
  margin-top: 2px;
  font-size: var(--text-md);
  color: var(--text-muted);
  line-height: 1.55;
}

/* Done is quiet. A wall of green ticks would make the one step that is *not*
 * done the hardest thing on the page to find. */
.setup-step-done {
  background: var(--ok-soft);
}

.setup-step-done .setup-step-mark {
  border-color: var(--ok);
  color: var(--ok);
}

/* The outstanding step is the one the page is about, so it is the one with an
 * edge around it. The word beside the tick carries the same meaning for a
 * reader who cannot see either — see `.sr-only` in `stepItem`. */
.setup-step-todo {
  border-color: var(--border);
}

.setup-step-todo .setup-step-label {
  color: var(--text);
}

.setup-step-na .setup-step-label,
.setup-step-waiting .setup-step-label {
  color: var(--text-muted);
  font-weight: 500;
}

/* The one step nothing can check. A real tick box with its label as part of
 * the target, the same choice "Show password" makes across this shell. */
.setup-declare {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  font-size: var(--text-md);
}

.setup-declare input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--brand);
}

.setup-declare label {
  margin: 0;
  font-weight: 500;
}

.setup-tablet {
  border-top: 1px solid var(--border);
  padding-top: 18px;
}

.setup-heading {
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-size: 1.08rem;
  font-weight: 700;
}

.setup-line {
  margin: 0 0 12px;
  font-size: var(--text-body);
  line-height: 1.6;
  color: var(--text);
}

/* The sentence that says there is nothing to download yet. It carries the
 * weight a missing button would have carried, so it is not muted into a
 * footnote. */
.setup-line-quiet {
  font-weight: 600;
}

.setup-download {
  margin: 0 0 14px;
}

/* The code beside its explanation, and stacked on a phone — where the tablet
 * being scanned is usually the screen you are already holding, so the code
 * matters less and the sentence more. */
.setup-qr {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin: 0 0 14px;
}

.setup-qr .qr-code {
  width: 132px;
  height: 132px;
  flex: none;
  border-radius: var(--radius-sm);
  /* The quiet zone is inside the SVG's own white ground; this is just the
   * edge, so the code does not float on a dark theme with nothing to say
   * where it ends. */
  border: 1px solid var(--border);
}

.setup-qr-beside {
  flex: 1 1 16rem;
}

.setup-qr-beside .setup-line {
  margin-bottom: 6px;
}

/* The address in text as well as in the code, because a QR nobody can scan is
 * a dead end unless what it encodes is readable. Wraps anywhere: a URL with no
 * spaces in it will otherwise push the card sideways on a phone. */
.setup-address {
  margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* The documents line in the signup footer. Two links where OrderLah has
 * published them, one where it has not — on the row that was already there,
 * because the card has about twelve pixels of headroom left. */
.signup-documents {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}

/* ── Notices ───────────────────────────────────────────────────────────────
 *
 * The merchant's inbox. Every rule here is about one thing being scannable:
 * an owner opens this page to find out whether anything needs them, and the
 * answer has to be readable without reading.
 */

/* The unread count on the Notices link in the rail. A count and not a dot,
 * because "three things" and "something" are different amounts of hurry. */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--brand-ink);
  font-size: var(--text-2xs);
  font-weight: 700;
  line-height: 1;
}

.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 16px;
}

/* A filter, not a button: it changes what is listed rather than doing
 * something. Sized to the 44px touch target the rest of the product uses, so
 * it stays tappable on the phone an owner reads this on. */
.chip {
  min-height: 44px;
  padding: 10px 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.chip:hover {
  border-color: var(--brand-line);
}

.chip-on {
  background: var(--brand-soft);
  border-color: var(--brand-line);
  color: var(--brand-text);
}

.notice-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.notice {
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* Unread carries a brand-coloured edge rather than a tinted background: a
 * tint on the whole card competes with the urgency pill inside it, which is
 * the thing that actually says how much this matters. */
.notice-unread {
  border-left: 3px solid var(--brand);
}

.notice-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
}

.notice-category {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-muted);
}

.notice-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand);
}

.notice-title {
  margin: 8px 0 0;
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--text);
}

.notice-meta {
  margin: 4px 0 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.notice-action {
  display: inline-block;
  margin-top: 10px;
  min-height: 44px;
  padding: 11px 0 0;
  font-weight: 600;
  color: var(--brand-text);
}

/* ── Language ─────────────────────────────────────────────────────────────
 *
 * The self-hosted faces are Latin subsets: they carry no Vietnamese tone
 * marks, no Chinese, no Myanmar and no Thai. Left alone, a browser falls back
 * glyph by glyph, so "Tiếng Việt" is drawn in two fonts at once. So each of
 * those languages takes the system's own face for the whole page instead —
 * one face per line, and one the device already knows how to shape.
 *
 * Myanmar and Thai stack their vowel signs and tone marks above and below the
 * line, so they need more leading than Latin text or the marks of one line
 * touch the next, and a heading's tight line-height clips them. */
:root:lang(vi) {
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans",
    "Helvetica Neue", Arial, sans-serif;
  --font-display: var(--font-body);
}

:root:lang(zh) {
  --font-body: system-ui, -apple-system, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", "Noto Sans SC", "Noto Sans CJK SC", "Source Han Sans SC",
    sans-serif;
  --font-display: var(--font-body);
}

:root:lang(my) {
  --font-body: "Noto Sans Myanmar", "Myanmar Text", Pyidaungsu, Padauk,
    "Noto Sans Myanmar UI", system-ui, sans-serif;
  --font-display: var(--font-body);
}

:root:lang(my) body {
  line-height: 1.8;
}

:root:lang(my) :is(h1, h2, h3, h4, .page-title, .kpi-value, .panel-title, button, .button, label, th) {
  line-height: 1.6;
}

/* Thai: Windows' Leelawadee UI, Apple's Thonburi, Android's and Linux's Noto
 * Sans Thai — every one already on the device, so nothing is fetched. Thai
 * marks stack less deeply than Myanmar's, so it takes a little less leading. */
:root:lang(th) {
  --font-body: "Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI", Thonburi,
    Tahoma, system-ui, -apple-system, sans-serif;
  /* Spelled out, not var(--font-body): tool/colour_freeze.dart reads a custom
   * property that points at another as a colour, and there is none here. */
  --font-display: "Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI",
    Thonburi, Tahoma, system-ui, -apple-system, sans-serif;
}

:root:lang(th) body {
  line-height: 1.7;
}

:root:lang(th) :is(h1, h2, h3, h4, .page-title, .kpi-value, .panel-title, button, .button, label, th) {
  line-height: 1.5;
}

/* Thai is not tracked: the letter-spacing small Latin labels carry pulls a
 * Thai word apart into its letters. */
:root:lang(th) body * {
  letter-spacing: normal;
}

/* A word with no spaces in it (Myanmar and Thai have none between words,
 * Chinese none at all) must still wrap rather than push a card sideways. A
 * browser already breaks Thai at word boundaries from its own dictionary; this
 * is only the fallback for a run it cannot break. */
:root:is(:lang(my), :lang(zh), :lang(th)) :is(p, li, td, th, h1, h2, h3, .banner, .panel-note) {
  overflow-wrap: anywhere;
}

.language-picker {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.language-picker-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
}

.language-picker-glyph {
  font-size: var(--text-base);
  line-height: 1;
}

.language-picker-select {
  font: inherit;
  font-size: var(--text-body);
  font-weight: 600;
  min-height: 44px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  max-width: 100%;
}

.language-picker-select:disabled {
  cursor: progress;
  opacity: 0.7;
}

.language-picker-note {
  margin: 0;
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--text-muted);
}

/* On the navy rail. */
.nav .language-picker-label,
.nav .language-picker-note {
  color: var(--rail-muted);
}

.nav .language-picker-select {
  border-color: var(--rail-line);
  background: var(--rail-2);
  color: var(--rail-ink);
}

.nav .language-picker-select:hover {
  border-color: var(--rail-muted);
}

.language-picker-code {
  display: none;
}

/* In the nav the picker is one tight row — the globe and the select, the
 * same 44px as Sign out beside it — rather than a label stacked over a
 * tall select. The word "Language" stays the select's accessible name; only
 * the globe is drawn. */
.nav .language-picker {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.nav .language-picker-label > span:not(.language-picker-glyph) {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.nav .language-picker-select {
  font-size: var(--text-md);
  min-height: 44px;
  padding: 6px 8px;
}

.nav .language-picker-note {
  flex-basis: 100%;
}

/* In the top bar the picker shares a row with Sign out, and a sentence under
 * it would stretch that row across the screen. The select still points at
 * the note through aria-describedby, and the desktop rail still draws it. */
@media (max-width: 1023.9px) {
  .nav .language-picker-note {
    display: none;
  }
}

@media (min-width: 1024px) {
  .nav .language-picker {
    padding-left: 4px;
  }

  .nav .language-picker-select {
    flex: 1 1 auto;
    min-width: 0;
  }
}

/* The top bar: the brand's row has to hold the wordmark, the language picker
 * and Sign out side by side, and a select is as wide as its longest option —
 * "Bahasa Melayu". So the picker becomes a chip: the globe and a short name,
 * with the real select stretched invisibly over it, so a tap still opens the
 * device's own language list and a screen reader still hears the select.
 *
 * Both corner buttons are drawn small — 34px, so they sit quietly beside the
 * wordmark rather than outweighing it — but each still answers a tap across
 * 44px: the chip's select and Sign out's ::after reach 6px above and below
 * what is drawn. Smaller to look at, never smaller to hit. */
@media (max-width: 1023.9px) {
  .nav-signout {
    position: relative;
    min-height: 34px;
    padding: 4px 10px;
    font-size: var(--text-xs);
  }

  .nav-signout::after {
    content: '';
    position: absolute;
    inset: -6px 0;
  }

  .nav .language-picker {
    position: relative;
    flex-wrap: nowrap;
    gap: 4px;
    min-height: 34px;
    padding: 0 8px;
    border: 1px solid var(--rail-line);
    border-radius: var(--radius-sm);
    background: var(--rail-2);
  }

  .nav .language-picker:hover {
    border-color: var(--rail-muted);
  }

  .nav .language-picker:has(.language-picker-select:focus-visible) {
    outline: 2px solid var(--rail-active-ink);
    outline-offset: 2px;
  }

  .nav .language-picker-glyph {
    font-size: var(--text-md);
  }

  .nav .language-picker-code {
    display: inline;
    color: var(--rail-ink);
    font-size: var(--text-xs);
    font-weight: 600;
    white-space: nowrap;
  }

  .nav .language-picker-select {
    position: absolute;
    inset: -6px 0;
    width: 100%;
    height: auto;
    min-height: 0;
    padding: 0;
    opacity: 0;
  }
}

/* The "Merchant" tag beside the wordmark gives way first, then the short
 * name on the chip, leaving the globe — which is still the select, and still
 * announces the language. Burmese gives it up sooner: its name is the widest
 * of the six and its script draws the wordmark row taller and wider. */
@media (max-width: 419.9px) {
  .nav-brand .nav-sub {
    display: none;
  }

  :root:lang(my) .nav .language-picker-code {
    display: none;
  }
}

@media (max-width: 359.9px) {
  .nav .language-picker-code {
    display: none;
  }
}

/* The signed-out pages carry it on the navy doorplate, so it adds nothing to
 * the height of a form that has to fit on one laptop screen. */
.language-slot {
  position: relative;
  z-index: 1;
  margin-top: 18px;
  max-width: 260px;
}

.login-brand .language-picker-label {
  color: var(--rail-muted);
}

.login-brand .language-picker-select {
  border-color: var(--rail-line);
  background: var(--rail-2);
  color: var(--rail-ink);
}

.login-brand .language-picker-note {
  color: var(--rail-muted);
}

@media print {
  .language-picker,
  .language-slot {
    display: none !important;
  }
}

/* In the phone band the doorplate is a header over the form, and a picker
 * stacked under the wordmark cost the sign-in page ~90px — past the fold on a
 * 667pt phone (login.browser.mjs measures it). So there the picker shares the
 * wordmark's row: the band grows by the few pixels a 44px select is taller
 * than the logo, and the form below does not move. The word "Language" stays
 * the select's accessible name; only the globe is drawn beside it, and the
 * select narrows on the smallest phones so the wordmark keeps its room. */
@media (max-width: 919.9px) {
  .login-brand {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 8px;
  }

  .login-brand > :not(.language-slot):not(.login-brand-decor) {
    grid-column: 1 / -1;
  }

  .login-brand > .login-brand-logo {
    grid-column: 1;
    grid-row: 1;
    margin-bottom: 0;
  }

  .login-brand .language-slot {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    max-width: none;
  }

  .login-brand .language-picker {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
  }

  .login-brand .language-picker-label > span:not(.language-picker-glyph) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .login-brand .language-picker-select {
    font-size: var(--text-sm);
    padding: 6px 8px;
    max-width: min(160px, calc(100vw - 270px));
  }

  .login-brand .language-picker-note {
    flex-basis: 100%;
    text-align: right;
  }
}

@media (max-width: 399.9px) {
  .login-brand .language-picker-glyph {
    display: none;
  }

  .login-brand .language-picker-select {
    max-width: calc(100vw - 248px);
  }
}

@media (max-width: 339.9px) {
  .login-split .login-brand {
    padding-left: 14px;
    padding-right: 14px;
  }

  .login-brand .language-picker-select {
    max-width: calc(100vw - 216px);
  }
}

/* Permanent Offline: the conditions are a list a merchant reads before the
   tick box, so they get body-size type and room between lines rather than
   footnote styling. */
.licence-permanent-heading {
  margin: 16px 0 6px;
  font-size: var(--text-lg);
}
.licence-permanent-conditions {
  margin: 0 0 12px;
  padding-left: 1.25rem;
  line-height: 1.5;
}
.licence-permanent-conditions li + li {
  margin-top: 4px;
}
.licence-permanent-ack {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin: 8px 0 12px;
  font-weight: 600;
}
.licence-permanent-ack input {
  width: 22px;
  height: 22px;
  flex: none;
}

/* ── Sectioned pages (Settings): tabs, search, danger zone ───────────────── */

/* A page of forms does not stretch across a wide monitor: the line an
 * operator reads should not be longer than their eye can follow. */
.main-narrow {
  max-width: 960px;
}

.page-intro {
  margin: -8px 0 18px;
  max-width: 70ch;
  color: var(--text-muted);
}

.section-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0;
}

.section-tabs {
  display: flex;
  flex: 1 1 auto;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: thin;
}

.section-tab {
  min-height: 44px;
  padding: 10px 14px;
  border: 0;
  border-bottom: 3px solid transparent;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.section-tab:hover {
  color: var(--text);
}

.section-tab[aria-selected='true'] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.section-tab:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}

.section-search {
  position: relative;
  flex: 0 1 280px;
  margin-bottom: 8px;
}

.section-search-input {
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.section-results {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}

.section-results[hidden] {
  display: none;
}

.section-result {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
}

.section-result:hover,
.section-result:focus-visible {
  background: var(--accent-soft);
}

.section-result-where {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.section-result-empty {
  padding: 10px 12px;
  color: var(--text-muted);
}

.section-group,
.section-pane {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
}

.section-pane > *,
.section-bar > * {
  /* The rule .main > * gives a panel, one level further down. */
  min-width: 0;
  max-width: 100%;
}

.section-pane[hidden] {
  display: none;
}

/* The one irreversible act on a page is set apart, never beside Save. */
.panel-danger {
  border-color: var(--bad);
  margin-top: 16px;
}

.danger-label {
  margin: 0 0 4px;
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--bad);
}

@media (max-width: 620px) {
  .section-bar {
    flex-direction: column-reverse;
    align-items: stretch;
  }

  .section-search {
    flex-basis: auto;
    margin: 0;
  }
}

/* ── Icons ──────────────────────────────────────────────────────────────
 * Drawn by assets/icons.js wherever markup says `data-icon="…"`. Inline and
 * sized in `em`, so an icon follows its label's size and colour and never
 * changes the display of the element it sits in (an author `display` would
 * beat `[hidden]`). Decorative by contract: the label is always beside it. */
.ui-icon {
  display: inline-block;
  width: 1.15em;
  height: 1.15em;
  margin-inline-end: 0.42em;
  vertical-align: -0.2em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.page-title > .ui-icon { color: var(--brand-strong); width: 1em; height: 1em; vertical-align: -0.14em; }
h2 > .ui-icon { color: var(--text-muted); }
h3 > .ui-icon { color: var(--text-muted); }
button > .ui-icon,
.button > .ui-icon { width: 1.05em; height: 1.05em; margin-inline-end: 0.36em; vertical-align: -0.18em; }

/* ── The operational layer ────────────────────────────────────────────────
 *
 * Five things added together, and they share one idea: the portal should be
 * able to say what needs a person and get them to it, without a second screen
 * and without the dashboard growing another band of furniture.
 *
 *   1. The status header — one line at the top that answers "is everything all
 *      right", where a whole calm panel used to sit on every good morning.
 *   2. The rail's Find button and its attention badges.
 *   3. The contextual drawer — a summary of the thing that was clicked, over
 *      the page it was clicked on.
 *   4. The fast-find palette.
 *   5. Quick actions, the customise sheet, and the compact density.
 *
 * Nothing here introduces a new colour, a new radius or a new shadow: every
 * value is one of the tokens at the top of this file. A new surface that
 * invented its own would be the thing that makes two screens stop looking like
 * one product.
 */

/* ── Status header ─────────────────────────────────────────────────────────
 *
 * Deliberately not a panel. It introduces the page rather than being one of its
 * sections, so it carries no card, no border and no tint — the verdict chip is
 * the only coloured thing in it, which is what makes the colour mean something.
 */
.status-header {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: 4px 16px;
  padding: 0 0 4px;
}

.status-greeting {
  grid-column: 1;
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.status-line {
  grid-column: 1;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  letter-spacing: -0.01em;
}

.status-facts {
  grid-column: 1;
  margin: 4px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  font-size: var(--text-md);
  color: var(--text-muted);
}

/* The separator is a middle dot drawn by CSS rather than a character in a
 * translated sentence: where a dot goes between two clauses is the language's
 * business, and a string that carried one would be a fragment. */
.status-facts > * + *::before {
  content: "·";
  margin-inline-end: 10px;
  color: var(--border-strong);
}

/* The one part of the header that carries a tone. The word is always there —
 * "Everything looks good", "2 things need you" — so the tint only makes it
 * findable while scanning, exactly as the status pills do. */
.status-verdict {
  font-weight: 600;
  color: var(--text);
}

.status-verdict-clear {
  color: var(--ok);
}

.status-verdict-important {
  color: var(--warn);
}

.status-verdict-critical {
  color: var(--bad);
}

/* Not known is not a warning: it is quieter than both, because the honest
 * statement is "we could not check", and a red chip there would report a
 * failure of ours as a problem of the merchant's. */
.status-verdict-unknown {
  color: var(--text-muted);
}

/* A shop that has not opened yet. Warm rather than alarming: nothing is wrong,
 * there is simply something left to do, and the banner under it says what. */
.status-verdict-setup {
  color: var(--brand-text);
}

.status-updated {
  font-variant-numeric: tabular-nums;
}

/* The referral page's headline: the business's code, large enough to read
 * out over a counter. */
.referral-code {
  margin: 4px 0;
  font-size: var(--text-4xl);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* The business's reference code, under its name: the one thing on the header
 * a merchant might want to take away with them. */
.status-header > .reference-code-wrap {
  grid-column: 1;
  justify-self: start;
  margin-top: 4px;
}

.status-customise {
  grid-column: 2;
  grid-row: 1 / span 3;
  align-self: start;
  white-space: nowrap;
}

/* The count of what is hidden, inside the button. Without it a merchant who put
 * a panel away in March has no reason to look in here in June. */
.status-customise-count {
  margin-inline-start: 6px;
  font-weight: 400;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

@media (max-width: 599.9px) {
  /* On a phone the button drops under the facts rather than squeezing the
   * business's own name into two words a line. */
  .status-header {
    grid-template-columns: 1fr;
  }

  /* "Good morning" is a courtesy, and on a phone it is a line of the screen
   * the takings are waiting under. The name and the verdict stay. */
  .status-greeting {
    display: none;
  }

  .status-customise {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
    margin-top: 6px;
  }
}

/* ── Quick actions ─────────────────────────────────────────────────────────
 *
 * A row of destinations at the foot of the page, wrapping rather than
 * scrolling: a hidden control on a row nobody thought to swipe is a control
 * that does not exist. Sized to the product's 44px touch target.
 */
.quick-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.quick-action {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  font-size: var(--text-body);
  text-decoration: none;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.quick-action:hover {
  border-color: var(--brand);
  color: var(--accent);
  text-decoration: none;
}

.quick-action > .ui-icon {
  color: var(--text-muted);
}

.quick-action:hover > .ui-icon {
  color: var(--accent);
}

/* ── The rail's Find button ────────────────────────────────────────────────
 *
 * Shaped like a rail link because it belongs to the rail, with the keys printed
 * on the right where a desktop reader expects them. The hint is hidden below the
 * rail's own breakpoint: a phone has no ⌘ and no Ctrl, and printing one there
 * would name a key that is not on the device.
 */
.nav-find {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  margin: 0 0 8px;
  padding: 10px 12px;
  border: 1px solid var(--rail-line);
  border-radius: var(--radius-sm);
  background: var(--rail-2);
  color: var(--rail-ink);
  font: inherit;
  font-size: var(--text-md);
  font-weight: 600;
  text-align: start;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease);
}

.nav-find:hover {
  border-color: var(--rail-muted);
}

.nav-find > .ui-icon {
  color: var(--rail-muted);
}

.nav-find-key {
  margin-inline-start: auto;
  padding: 2px 6px;
  border: 1px solid var(--rail-line);
  border-radius: 5px;
  background: var(--rail);
  color: var(--rail-muted);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  line-height: 1.5;
}

@media (max-width: 1023.9px) {
  /* In the top-bar posture the button sits in the brand's row rather than taking
   * a line of its own above the sections.
   *
   * `order: 1` puts it in the same group as the language picker and Sign out,
   * *before* both — and that is load-bearing rather than tidy. At `order: 2` it
   * shared a group with the identity row, which has a 100% basis, so the
   * browser placed it on the first row after Sign out and pushed Sign out 67px
   * in from the right-hand corner where a thumb looks for it.
   * `language.browser.mjs` measures exactly that, at seven widths in five
   * languages, which is how this was caught. The picker keeps its own
   * `margin-left: auto`, so the two of them stay flush right and this sits
   * beside the brand. */
  .nav-find {
    order: 1;
    width: auto;
    margin: 0;
    flex: none;
  }

  .nav-find .nav-label,
  .nav-find-key {
    display: none;
  }

  .nav-find > .ui-icon {
    margin-inline-end: 0;
  }
}

/* A badge on a rail link sits at the end of the row, after the label, so it
 * never covers a word. */
.nav-links a {
  position: relative;
}

.nav-links a > .nav-badge {
  margin-inline-start: auto;
}

/* ── Saved views ──────────────────────────────────────────────────────────── */
.control-views .control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.control-views .control-input {
  min-width: 11rem;
}

/* ── The contextual drawer ────────────────────────────────────────────────
 *
 * One `<dialog>`, two postures. From 600px up it is a panel against the right
 * edge, full height, because that is where a details pane belongs beside a
 * table. Below that it is a sheet up from the bottom edge, which is where a
 * thumb is. The markup is identical in both — `drawer.js` has no branch — so
 * the two cannot come to contain different things.
 */
.drawer {
  /* `<dialog>`'s own defaults centre it and cap it; all three have to go before
   * it can be an edge panel. */
  margin: 0 0 0 auto;
  padding: 0;
  width: min(420px, 100%);
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  border: 0;
  border-inline-start: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-overlay);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.drawer::backdrop {
  background: rgba(16, 24, 40, 0.42);
}

.drawer-header {
  display: flex;
  align-items: start;
  gap: 12px;
  padding: 18px 18px 12px;
  border-bottom: 1px solid var(--border);
}

.drawer-titles {
  min-width: 0;
}

.drawer-title {
  margin: 0;
  font-size: 1.1rem;
  /* A merchant's own name for a shop can be long and has no spaces to break
   * at in every language. */
  overflow-wrap: anywhere;
}

.drawer-subtitle {
  margin: 2px 0 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.drawer-close {
  margin-inline-start: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.drawer-close:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.drawer-close > .ui-icon {
  margin: 0;
  width: 1.2em;
  height: 1.2em;
}

.drawer-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 16px 18px 20px;
  display: grid;
  gap: 12px;
  align-content: start;
}

.drawer-group {
  margin: 6px 0 0;
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

/* Labelled facts down a column. A label and its value on one row from 380px up,
 * which is every phone this product has ever been opened on. */
.drawer-facts {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(6rem, auto) 1fr;
  gap: 6px 14px;
  font-size: var(--text-body);
}

.drawer-facts dt {
  color: var(--text-muted);
}

.drawer-facts dd {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.drawer-fact-detail {
  display: block;
  margin-top: 2px;
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.drawer-waiting,
.drawer-failed {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-body);
}

.drawer-footer {
  flex: none;
  padding: 12px 18px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

.drawer-footer .button {
  width: 100%;
  justify-content: center;
}

@media (max-width: 599.9px) {
  .drawer {
    margin: auto 0 0;
    width: 100%;
    height: auto;
    max-height: 88vh;
    border-inline-start: 0;
    border-top: 1px solid var(--border);
    border-start-start-radius: var(--radius);
    border-start-end-radius: var(--radius);
  }
}

/* ── The fast-find palette ────────────────────────────────────────────────── */
.palette {
  margin: 12vh auto auto;
  padding: 0;
  width: min(560px, calc(100% - 32px));
  max-width: 100%;
  max-height: min(70vh, 560px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-overlay);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.palette::backdrop {
  background: rgba(16, 24, 40, 0.42);
}

.palette-field {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}

.palette-icon {
  color: var(--text-muted);
  margin: 0;
  flex: none;
}

/* `input.palette-input` for the reason the console's copy gives: a portal that
 * later styles `input[type="search"]` by attribute would outrank a bare class
 * however far down the file it sits, and this field has to stay chrome-free. */
input.palette-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--text-base);
  /* A 16px field is what stops iOS zooming the page when it is focused, which
   * on a phone reads as the portal jumping. */
  min-height: 32px;
}

input.palette-input:focus {
  outline: none;
}

/* The browser's own clear button, beside the panel's own close control: two
 * crosses a few pixels apart, one of which closes the palette and one of which
 * does not. */
input.palette-input::-webkit-search-cancel-button { display: none; }

.palette-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.palette-close:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.palette-close > .ui-icon {
  margin: 0;
}

.palette-list {
  flex: 1 1 auto;
  overflow-y: auto;
  margin: 0;
  padding: 6px;
  list-style: none;
}

.palette-group {
  padding: 10px 10px 4px;
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.palette-item {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 44px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.palette-item > .ui-icon {
  color: var(--text-muted);
  flex: none;
}

.palette-item-label {
  min-width: 0;
  overflow-wrap: anywhere;
}

.palette-item-hint {
  margin-inline-start: auto;
  padding-inline-start: 12px;
  color: var(--text-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* The active row is the one Return would open. It carries a left rail as well
 * as a tint, because a tint alone is the kind of difference a bright shop
 * window washes out. */
.palette-item-active {
  background: var(--surface-sunken);
  box-shadow: inset 3px 0 0 var(--brand);
}

.palette-empty {
  padding: 18px 12px;
  color: var(--text-muted);
}

.palette-footer {
  flex: none;
  margin: 0;
  padding: 8px 14px 10px;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

@media (max-width: 599.9px) {
  .palette {
    margin: 0 auto auto;
    width: 100%;
    max-height: 100%;
    height: 100%;
    border: 0;
    border-radius: 0;
  }

  /* The keyboard hints name keys a phone does not have. */
  .palette-footer {
    display: none;
  }
}

/* ── The customise sheet ──────────────────────────────────────────────────── */
.customise-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
}

.customise-label {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 4px 2px;
  cursor: pointer;
  font-size: var(--text-base);
}

.customise-label input {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--brand);
}

.customise-density {
  display: grid;
  gap: 2px;
}

/* ── A table cell that opens a drawer ─────────────────────────────────────
 *
 * A button, not a link: it stays on this page. Styled as the row's own text
 * with a quiet underline, so a column of them does not read as a column of
 * orange — the same decision the console made about row links.
 */
.cell-drawer {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  font-weight: inherit;
  color: var(--text);
  text-align: start;
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 2px;
  cursor: pointer;
}

.cell-drawer:hover {
  color: var(--accent);
  text-decoration-color: var(--accent);
}

/* ── Hiding one observation ───────────────────────────────────────────────
 *
 * Only ever on the reading list, and only on a row that is an observation —
 * `insightList` is told which rows may carry it and draws none by default.
 */
.insight-dismiss {
  grid-column: 2;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-faint);
  cursor: pointer;
}

.insight-dismiss:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.insight-dismiss > .ui-icon {
  margin: 0;
}

/* ── A long table keeps its headings ──────────────────────────────────────
 *
 * §10's rule is that a wide table scrolls inside its own card and the page never
 * scrolls sideways — which also means the card is the scroll container, so a
 * heading row cannot stick to the viewport. A *tall* table gets a bounded box
 * instead: `dataTable` adds this class past two dozen rows, so a three-row
 * summary is untouched and a ledger of two hundred payments stops pushing its
 * own headings off the top of the screen.
 *
 * The heading's rule is drawn with an inset shadow rather than a border, because
 * a sticky cell's own border scrolls away from it.
 */
.table-wrap-tall {
  max-height: min(68vh, 620px);
}

/* Paper cannot scroll, so a ceiling here would cut the table off at whatever
 * row happened to be at 68vh. */
@media print {
  .table-wrap-tall { max-height: none; }
}

.table-wrap-tall .data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 var(--border-strong);
}

/* The row's own identity stays put while the table scrolls sideways. Only on the
 * grid form: the phone's stacked-card form has no columns to scroll past. */
@media (min-width: 600px) {
  .table-wrap > .data-table:not(.data-table-stack) tbody th:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--surface);
  }

  .table-wrap > .data-table:not(.data-table-stack) tbody tr:hover th:first-child {
    background: var(--surface-sunken);
  }
}

/* ── Compact density ──────────────────────────────────────────────────────
 *
 * A device-local preference (`prefs.js`), applied as one attribute on the root
 * so no rule has to be told about it. It takes **air**, never a control: every
 * touch target here keeps its 44px, the type does not shrink below what the
 * portal's own contrast audit was run at, and the figures keep their size. The
 * same rule `CounterDisplayPreferences` follows in the app — density may tighten
 * the spacing around a button and may not shrink the button.
 */
:root[data-density="compact"] .main {
  gap: 12px;
}

:root[data-density="compact"] .panel-body {
  padding: 12px 14px;
  gap: 10px;
}

:root[data-density="compact"] .panel-header {
  padding: 12px 14px 0;
}

:root[data-density="compact"] .kpi {
  padding: 12px 14px;
}

:root[data-density="compact"] .kpi-grid {
  gap: 10px;
}

:root[data-density="compact"] .data-table th,
:root[data-density="compact"] .data-table td {
  padding: 7px 10px;
}

:root[data-density="compact"] .status-header {
  padding-bottom: 0;
}

/* ── Motion ───────────────────────────────────────────────────────────────
 *
 * The drawer and the palette are the only two surfaces in this portal that
 * arrive rather than simply being there, and both animate for one reason: a
 * panel that appears instantly at the edge of the screen reads as a layout
 * change rather than as something opening. It is 150ms — the portal's one
 * duration — and it never delays the content, which is already in the DOM and
 * focused before the animation ends.
 */
@media (prefers-reduced-motion: no-preference) {
  .drawer[open] {
    animation: drawer-in var(--dur) var(--ease);
  }

  .palette[open] {
    animation: palette-in var(--dur) var(--ease);
  }
}

@keyframes drawer-in {
  from {
    transform: translateX(16px);
    opacity: 0.4;
  }
}

@keyframes palette-in {
  from {
    transform: translateY(-8px);
    opacity: 0.4;
  }
}

/* ── Remote Assist history (Help) ───────────────────────────────────────── */
.support-history {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 12px;
}

.support-history-item {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}

.support-history-item p {
  margin: 0 0 4px;
}

.support-history-head {
  font-weight: 700;
}

.support-history-end {
  color: var(--text-muted);
}

/* ── Ask OrderLah ─────────────────────────────────────────────────────────
 *
 * A thread, a box to type in, and the earlier conversations. Every colour is
 * an existing token — this section adds shapes, not a palette. Each answer
 * component reuses the portal's own pieces where one exists (KPI cards, the
 * data table, banners, chips), so a figure here looks like the same figure on
 * Overview; what is below is only the thread's own layout. Phone first: the
 * question box and every button are full-width targets at 320px, and nothing
 * here sets a width a phone cannot hold. */
.ask-thread {
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-self: stretch;
}

.ask-thread:empty,
.ask-suggestions:empty {
  display: none;
}

.ask-question {
  align-self: flex-end;
  max-width: min(100%, 60ch);
  margin: 0;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--brand-soft);
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.ask-answer {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: flex-start;
  align-self: stretch;
  padding-inline-start: 14px;
  border-inline-start: 3px solid var(--brand-line);
  /* break-word, not anywhere: it breaks only a word that cannot fit (a long
   * link in a model's sentence), and leaves a KPI card's own min-content
   * alone — `anywhere` let the grid squeeze "Yesterday" into "Yesterda / y". */
  overflow-wrap: break-word;
}

.ask-answer > *,
.ask-preview > * {
  max-width: 100%;
  min-width: 0;
  margin: 0;
}

.ask-answer > .ask-metrics,
.ask-answer > .table-wrap,
.ask-answer > .banner,
.ask-answer > .ask-preview,
.ask-answer > .ask-narrative {
  align-self: stretch;
}

.ask-block {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.ask-block-title {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 700;
}

.ask-text {
  margin: 0;
  max-width: 78ch;
}

.ask-note,
.ask-pending,
.ask-chips-title,
.ask-history-meta {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.ask-steps,
.ask-notes,
.ask-names,
.narrative-observations {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding-inline-start: 1.4em;
}

.ask-checklist {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ask-check-mark {
  display: inline-block;
  width: 1.6em;
  font-weight: 700;
}

.ask-check-passed .ask-check-mark { color: var(--ok); }
.ask-check-failed .ask-check-mark { color: var(--bad); }
.ask-check-unread .ask-check-mark { color: var(--text-faint); }
.ask-check-failing { font-weight: 600; }

.ask-actions,
.ask-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.ask-chips-block {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* A prepared change is not a change: dashed, like a figure that is not
 * there yet, until somebody confirms it on the tablet. */
.ask-preview {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 16px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

.ask-preview-change {
  font-weight: 700;
}

.ask-preview-cancelled .ask-preview-change {
  text-decoration: line-through;
  color: var(--text-muted);
}

.ask-composer {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-self: stretch;
}

.ask-input {
  width: 100%;
  min-height: 88px;
  resize: vertical;
}

.ask-composer-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.ask-history {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-self: stretch;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ask-history-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  text-align: start;
  overflow-wrap: anywhere;
  cursor: pointer;
}

.ask-history-item:hover {
  border-color: var(--brand-line);
}

.ask-history-item[aria-current='true'] {
  border-color: var(--brand-line);
  background: var(--brand-soft);
}

.ask-history-title {
  font-weight: 600;
}
