/* Genie Dashboard — mid-fidelity kit (v2)
   Purpose: clear content, clear interaction, clear contrast, correct spacing &
   sizing. Type sized up for 40+ readers. Colour is applied lightly on top of the
   greyscale structure: a blue-tinted neutral ramp, one indigo brand accent, and
   the three semantic hues. Structure and weights are unchanged from greyscale. */

@import url('https://fonts.googleapis.com/css2?family=Urbanist:wght@400;500;600;700;800&display=swap');

:root{
  --bg:#f8faff; --card:#ffffff;
  /* OUTLINES, quieter (user, 2026-08-03: "reduce all components' outline contrast — make the lines less
     obvious"). --line was #dfe3f0, 1.28:1 on white; it is now 1.17:1, which takes about 40% out of the
     contrast the eye actually sees (the ratio's distance above 1.0 is the part that reads as "line").
     --line2, the internal hairline for table rows, metric strips and section rules, goes to 1.11:1.
     They are floors, not preferences: a panel border separates white from a #f8faff page, so at 1.10 the
     panel edge stops resolving at all on a laptop screen and the layout loses its structure. Both are
     tokens, so this is 100 borders moving from one place — nothing hardcodes an outline colour except the
     two dark surfaces (the rail edge) and the deliberate accent states. */
  --line:#eaedf6; --line2:#f2f4fa;
  --ink:#111827;         /* primary text — very high contrast */
  --ink2:#3f4657;        /* secondary — still strong contrast */
  --ink3:#646b78;        /* tertiary/labels — 4.7:1 on the TINTED surfaces, not just on white:
                            labels sit on locked pills (#eef0f7) and zebra rows, so white is the
                            wrong thing to qualify against. 5.3:1 on white. The dark rail is NOT in
                            this ramp's remit — it has its own, see --rail-ink3 below. */
  /* One step below --ink3, for METADATA about a figure rather than a label of it: the period a KPI card
     covers and the period it is compared against. Both are answers to "when", printed on every card in
     the deck, and at --ink3 they read as content competing with the label beside them. 4.7:1 on white —
     still AA, and the floor for how quiet a piece of real text is allowed to get in this file. Nothing
     larger than 11px uses it, and nothing on a tinted surface does. */
  --ink4:#6d7484;
  --brand:#3a31d8;       /* deep indigo — identity, chart bars, primary action */
  --brand-bright:#1f4dff;/* bright blue — meters, small emphasis marks */
  --accent:#3a31d8;      /* primary action / active state */
  --accent-soft:#eef0ff;
  /* DATA marks (bars, plotted lines, the paler "previous period" series) are VIOLET, not the brand
     indigo (user, 2026-08-03: blue is reserved for clickable links, green/red/amber for judgements). The
     brand indigo keeps every INTERACTIVE surface — links, active tab, the rail's selected item — so the one
     blue on screen always means "you can click this". --spark stays neutral grey: a sparkline takes the
     semantic green/red of its own direction. */
  --spark:#98a0b8; --bar:#7c3aed; --bar2:#d8b4fe; --line-c:#c7cad6;   /* --line-c: a chart BASELINE and a few control borders — neutral, never data */
  /* ── DATA PALETTE ─────────────────────────────────────────────────────────────
     Violet alone was doing every job (user, 2026-08-04: "all charts are now purple and pink — it's
     too flat if all charts share the same colour. Pair other non-semantic colour palettes"), with
     references UI 1 and UI 2 named for the chart colours: both run a violet/purple primary against
     a cyan–teal secondary and drop to grey for the residual category. That is the pairing adopted
     here, and it is the widest hue spread available — blue, green, red and amber are all spoken for,
     so the palette moves AROUND them: violet 270° → cyan 190° → fuchsia 290° → teal 195° → slate.
     Cyan is held at hue ≥190 on purpose: at 170 it starts reading as the success green, and above
     230 it becomes the interactive blue. Both fences matter more than the exact hue.
     --d1..--d5 are the categorical slots in first-use order (they mirror CAT in widgets.js, which is
     what colours SVG fills — the same values in both places, JS for marks and CSS for legends).
     --bar/--bar2 stay the primary pair, and --barc/--barc2 are the cyan pair a second SERIES takes.
     Two shades of one hue mean "the same metric, two periods" (this year/last year); two HUES mean
     two different metrics. That distinction is the whole reason for a second pair. */
  --d1:#6d28d9; --d2:#0891b2; --d3:#c026d3; --d4:#155e75; --d5:#64748b;
  --barc:#0891b2; --barc2:#a5f3fc;
  /* semantic colours — used ONLY where meaning matters (severity / concern); the rest stays grey */
  /* Semantic hues come from the Figma colour library ("Colorful option"):
     Error #E11D48 · Warning #fbbf24 · Success #10c795. Those are the AREA values —
     see --*-fill below. The three tokens here are the TEXT weights of the same hues.
     Error is used verbatim because it already clears 4.5:1 on white; Warning and
     Success are the Figma hues darkened along their own hue until they clear it too
     (#fbbf24 is 1.7:1 and #10c795 is 2.2:1 as text — unusable at 13px). */
  /* Figma Error #E11D48 is 4.70:1 on white — it passes, but with only 0.2 of headroom, so it
     drops to 4.31:1 the moment it sits on a tinted surface (a hovered table row, #f3f5fc).
     Red values live in exactly those rows, so the text token is the Figma hue taken down one
     step to 5.0:1, which holds on white AND on every hover/zebra tint in the file. The AREA
     value --danger-fill below is still #E11D48 exactly — that's the one that has to match. */
  --danger:#e11d48;      /* Figma Error, one step down — 5.0:1 white, 4.6:1 on hover tint */
  --warn:#b25c00;        /* Figma Warning darkened — 4.6:1 */
  --good:#0a8259;        /* Figma Success darkened — 4.8:1 */
  /* MARK weights: dots, rules, small swatches. Non-text, so they need 3:1 rather than
     4.5:1 — bright enough to still read as the Figma hue, dark enough to be visible. */
  --danger-mark:#e11d48; --warn-mark:#e8a50b; --good-mark:#0fb07a;
  /* AREA fills — the Figma library values exactly as authored. Used for the ops-health
     traffic light, legends and any other shape big enough to carry its own colour.
     NOTE they do not share a lightness: Error is dark and needs white text on it,
     Warning and Success are light and need --ink. That is handled per-segment rather
     than by flattening the palette, so the ops light matches Figma. */
  --danger-fill:#e11d48; --warn-fill:#fbbf24; --good-fill:#10c795;
  --brand-tint:#e4e7ff;  /* Figma Brand Tint */
  /* ── LEFT RAIL: a dark zone (user, 2026-08-03) ──────────────────────────────
     The rail is now the one inverted surface in the deck, which is what makes it read as chrome
     rather than as another panel — the data surfaces are white on #f8faff, so the navigation stops
     competing with them for attention. It gets its OWN ink ramp: --ink/--ink2/--ink3 are tuned for
     contrast on white and would fail on a dark fill (--ink3 #646b78 is 1.9:1 here), so every rail
     rule below reads from these tokens instead. Same blue-tinted hue family as the #eef1fa it
     replaces, so the rail still belongs to the palette.
     Contrast on --rail-bg: ink 16.6:1 · ink2 10.3:1 · ink3 5.9:1 (10px uppercase labels) ·
     white on --rail-on-bg 6.9:1. */
  --rail-bg:#161c30;
  --rail-ink:#f2f4fb;    /* active + primary names */
  --rail-ink2:#c3c9dc;   /* nav labels at rest */
  --rail-ink3:#8e97b0;   /* eyebrows, role line, Genie sign-off */
  --rail-line:rgba(255,255,255,.10);   /* lowered with --line, same instruction, same job on dark */
  --rail-hover:rgba(255,255,255,.07);
  --rail-fill:rgba(255,255,255,.06);   /* raised surfaces on the rail (profile button, tags) */
  --rail-on-bg:#3a45db;  /* active tab / avatar — the brand indigo lifted for a dark ground */
  /* Radii on a 4px grid (user, 2026-08-03: every corner rounded UP to the nearest multiple of 4). The
     deck used to run 5,6,7,8,9,10,11,13,14,17px — nine values for what is really three jobs. It is now
     8 / 12 / 16 / 20 plus the 999px pill and 50% circle, and the two tokens land on the same 8px: --r2
     was 6px, which rounds up to --r's value. Both names stay, because they still mark a DIFFERENT
     intent — a panel versus a chip inside it — and the day one of the two moves, every use of it moves
     together rather than being hunted down. */
  --r:8px; --r2:8px;
  /* ═══ TYPE SCALE — three steps, one meaning each (user, 2026-08-03, CEO desktop) ═══
     Reviewed across every CEO component and collapsed: the deck was running 9.5, 10, 10.5, 11, 11.5,
     12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5 and 16px, which is fourteen sizes for four jobs — the same
     role read at 12px in one panel and 14.5px in the next, and a small panel silently shrank its labels
     where a wide one did not.
       --t-title  the component's title. Largest thing in its component. 800.
       --t-body   table headers (600), labels (600) and body (500) — ONE size, weight tells them apart.
       --t-cap    captions: qualifiers, footnotes, sub-labels. Smallest, 500, always dimmed ink.
     A heading INSIDE a component (an h4 over a sub-block) is a header, so it takes --t-body at 800 —
     distinguished by weight, not by a fourth size, which is what keeps the scale at three steps.
     Two things are deliberately outside it, and neither is a typographic role:
       • FIGURES — a KPI value, a hero number, a donut centre. They are data, sized by prominence.
       • TEXT PLOTTED INSIDE A CHART — axis ticks, bar and bubble labels, map and sankey labels, the
         heat grid's hour row. Those are positioned by coordinate and held at real pixels by chartAbs()
         (--ax-fs and friends); forcing 13.5px into them lands labels on top of the marks they name. */
  --t-title:16px; --t-body:13.5px; --t-cap:12px;
}
*{box-sizing:border-box}
/* font-weight:500 IS THE FLOOR (user, 2026-08-03: "anything below that is too light to pass"). It sits on
   body so the ~600 elements that never state a weight inherit it instead of the browser's 400 — Urbanist
   at 400 is a thin face, and at 12–13.5px on white it stops holding its own. Every explicit 400/normal in
   the deck was raised with it, so the scale now runs 500 (body · caption) → 600 (label · table header) →
   700/800 (figures · titles). One consequence, stated because it is a real cost: "regular" and "semibold"
   are now one step apart rather than two, so weight alone separates a label from body text less strongly
   than it did — size and ink still do the rest. */
body{margin:0;font-family:Urbanist,system-ui,sans-serif;background:var(--bg);color:var(--ink);
  font-size:16px;font-weight:500;line-height:1.5;-webkit-font-smoothing:antialiased}

/* shell — fixed height: rail + top bar stay put, only .content scrolls
   (so Settings + Log out are always in the viewport) */
.app{display:flex;height:100vh;overflow:hidden}
.rail{width:194px;flex:0 0 194px;height:100vh;overflow-y:auto;background:var(--rail-bg);color:var(--rail-ink2);padding:16px 12px;display:flex;flex-direction:column;border-right:1px solid #0f1425}
/* brand is its own zone — a divider separates the identity from the actions/nav below.
   The identity here is the CLIENT (Hungry House), not the product; Genie signs off at the bottom
   in .genieby. `min-width:0` on the name lets it ellipsis rather than push the toggle off the rail. */
.rail .brand{display:flex;align-items:center;gap:7px;padding-bottom:16px;margin-bottom:11px;border-bottom:1px solid var(--rail-line)}
/* ══════ The client logo ══════
   Hungry House (logo.png, 800x320) replaced the Space Burgers lockup on 2026-08-07. It is 2.50:1 and
   contains its own wordmark, so it takes the whole brand row rather than sitting in a 26px tile.
   126px, measured: the rail is 194px with 12px padding each side, and the collapse toggle takes 28px
   plus the row's 7px gap — 127px is what is left, so the logo is capped one pixel under it and its
   height follows the aspect (50.4px — the row is 10px shallower than the old 2.12:1 lockup left it).
   width/height attributes are on the <img> as well, so the row reserves its space before the file
   decodes and the nav below it does not jump on load.

   IT IS A KNOCKOUT, AND IT HAD TO BE. The brand supplies the wordmark as black on transparency, which
   measures 1.24:1 against --rail-bg #161c30 — a straight file swap renders a brand row that occupies
   its 126px and shows nothing. logo.png is therefore a REVERSED lockup derived from the brand artwork
   by scripts/knockout-logo.py: the wordmark repainted to --rail-ink (15.37:1), the orange lens left
   exactly as drawn (7.29:1). Replace it wholesale if Hungry House ever supplies an official reversed
   file. The untouched positive lives beside it as logo-positive.png and is what prints.

   Still no tile and no background, but for a NEW reason. The old file carried its own navy plate, so a
   panel behind it would have drawn a box around a box; this one is a true cut-out, so a plate would be
   the only box — and a white one would be the loudest object in the rail, competing with the nav for
   first read. The knockout sits directly on --rail-bg because that is the ground it was made for.
   THE STROKED MARK IS STILL THERE, for the collapsed rail only (see the collapsed block): 40px of usable
   width cannot hold a horizontal lockup, and scaling this one to fit would render the wordmark at ~3px. */
.rail .brand .blogo{display:block;width:100%;max-width:126px;height:auto;flex:0 1 auto;min-width:0}
.rail .brand .logo{display:none}                      /* collapsed rail only */
.rail .brand .logo svg{width:17px;height:17px;display:block}
/* 15px, not the old 18px: a client name is longer than the 5 characters of "Genie", and the rail keeps
   its 194px (the content column is measured off it deck-wide). MEASURED at 1440: the row leaves
   101px once tile, two gaps and the toggle are taken, and the wordmark renders at 98.3px — one line,
   no ellipsis. A longer client name truncates rather than reflowing the rail. */
.rail .nm{font-weight:800;font-size:15px;color:var(--rail-ink);line-height:1.1;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* collapse / expand toggle, sits at the end of the brand row */
.rail .navtoggle{margin-left:auto;flex:0 0 auto;width:28px;height:28px;padding:0;display:grid;place-items:center;border:none;background:none;color:var(--rail-ink3);border-radius:8px;cursor:pointer}
.rail .navtoggle:hover{background:var(--rail-hover);color:var(--rail-ink)}
.rail .navtoggle svg{width:18px;height:18px}
/* small section label above the nav list */
.rail .nav-label{font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--rail-ink3);padding:0 12px;margin-bottom:5px}
.rail nav{display:flex;flex-direction:column;gap:3px}
.rail a{color:var(--rail-ink2);text-decoration:none;padding:8px 12px;border-radius:8px;font-weight:600;font-size:14px}
.rail a:hover{background:var(--rail-hover);color:var(--rail-ink)}
/* Active tab: a solid indigo pill with WHITE text. On the light rail the active state was a pale
   tint with indigo text; inverted, that reads as a disabled row, so the fill carries the brand and
   the text takes the contrast (6.9:1). Fill + white + 700 weight all move together — the state is
   never signalled by the fill alone. */
.rail a.on{background:var(--rail-on-bg);color:#fff;font-weight:700}
/* nav rows: icon + label paired as one unit — icon inherits the label colour so weight/contrast match */
.rail nav a{display:flex;align-items:center;gap:11px}
.rail nav a .ico{flex:0 0 auto;width:18px;height:18px;display:grid;place-items:center;color:inherit}
.rail nav a .ico svg{width:18px;height:18px;display:block;stroke-width:1.75}
/* Roadmap status pill on a nav item (e.g. Inventory · Later). Sits at the rail's right edge, in
   white so it stays legible on the hover and active fills as well as the rail. The item itself is
   NOT dimmed — the tab is still a working, clickable destination; only its status is annotated.
   `gap:11px` above would double the label→pill spacing, so the auto margin owns it instead. */
.rail nav a .navtag{margin-left:auto;flex:0 0 auto;background:var(--rail-fill);border:1px solid var(--rail-line);
  color:var(--rail-ink2);font-size:9.5px;font-weight:700;letter-spacing:.03em;line-height:1.6;
  padding:0 8px;border-radius:20px;white-space:nowrap}
/* on the active pill the tag sits on indigo, so it borrows white at low alpha instead of the rail's */
.rail nav a.on .navtag{background:rgba(255,255,255,.18);border-color:rgba(255,255,255,.42);color:#fff}
/* Ask Genie: its own section below the nav — styled exactly like a nav tab */
.rail .nav-sec{margin-top:10px}
.railbottom{margin-top:auto;display:flex;flex-direction:column;gap:10px;padding-top:12px}
.nav-sec{display:flex;flex-direction:column;gap:3px;border-top:1px solid var(--rail-line);padding-top:12px}
.rail .foot{font-size:12.5px;color:var(--rail-ink3);line-height:1.5}
.main{flex:1;min-width:0;height:100vh;display:flex;flex-direction:column;overflow:hidden}

/* global top bar — single row: title + filters, then equal-height action controls */
/* gap 8, not 10: the bar carries a 4th filter pill as of 2026-08-03 and the pills grew with the 4px
   padding grid, which pushed the Insights toggle onto a second row at 1440. 8px is on the grid and buys
   back 12px across the row's six gaps. */
.top{background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:8px;padding:12px 20px;flex-wrap:wrap;row-gap:12px}
.top .crumb{font-weight:800;font-size:18px;line-height:1.15;white-space:nowrap}
.top .spacer{flex:1}
.filters{display:flex;gap:8px;flex-wrap:wrap}
.fpill{display:inline-flex;align-items:center;gap:6px;height:34px;border:1px solid var(--line);background:#fff;border-radius:20px;
  /* 12px of side padding, the one place the 4px sweep rounds DOWN (as the table cells do): four pills at
     16px a side, plus the crumb, the sync note, Export and the Insights toggle, came to 1258px of content
     in a 1210px bar at 1440 — the toggle wrapped to a second row. The pills give the 32px back. */
  padding:0 12px;font-size:var(--t-body);color:var(--ink2);font-weight:600;cursor:pointer;white-space:nowrap}
.fpill b{color:var(--ink);font-weight:700}
.fpill .fico{width:16px;height:16px;flex:0 0 auto;color:var(--ink3);margin-right:-1px}
.fpill .cmpval{color:var(--ink3);font-weight:500;font-size:13px}
/* one-click exit from an active Location scope. It sits inside the pill, whose click opens the
   modal, so its handler stops propagation. Only rendered when a scope is actually set. */
.fpill .fclear{display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;
  margin:0 -3px 0 1px;color:var(--ink3);font-size:11px;font-weight:700;line-height:1}
.fpill .fclear:hover{background:var(--line2);color:var(--ink)}
.fpill.lock{background:#eef0f7;color:var(--ink3);cursor:default}
.fresh{font-size:12.5px;color:var(--ink3);display:inline-flex;align-items:center;gap:6px;font-weight:600;white-space:nowrap;margin-right:2px;border:1px solid transparent;background:none;font-family:inherit;cursor:pointer;padding:8px 8px;border-radius:8px}
.fresh:hover{background:#f3f5fc;color:var(--ink2);border-color:var(--line)}
.fresh:disabled{cursor:default}
.fresh svg{width:14px;height:14px;opacity:.7;transform-origin:center}
.fresh.syncing{color:var(--ink2)}
.fresh.syncing svg{opacity:1;animation:fresh-spin .8s linear infinite}
@keyframes fresh-spin{to{transform:rotate(360deg)}}
.top-actions{display:flex;align-items:center;gap:9px}
.iconbtn{width:34px;height:34px;border:1px solid var(--line);border-radius:12px;background:#fff;display:grid;place-items:center;cursor:pointer;color:var(--ink2);position:relative;padding:0}
.iconbtn:hover{background:#f3f5fc;color:var(--ink)}
.iconbtn svg{width:18px;height:18px}
.iconbtn .badge{position:absolute;top:-6px;right:-6px;min-width:18px;height:18px;padding:0 4px;background:var(--accent);color:#fff;border-radius:12px;font-size:11px;font-weight:800;display:grid;place-items:center;border:2px solid #fff}
.askbtn{display:inline-flex;align-items:center;gap:7px;height:34px;border:1px solid var(--line);background:#fff;border-radius:12px;padding:0 16px;font-weight:700;font-size:13.5px;color:var(--ink);cursor:pointer;white-space:nowrap}
.askbtn:hover{background:#f3f5fc}
.askbtn svg{width:15px;height:15px;color:var(--accent)}
/* Export holds the button while it walks the DOM and encodes the PNG — a second or two on a long
   page, so the button has to say it is working and refuse a second click. */
.askbtn:disabled{cursor:default;opacity:.72}
.askbtn.busy{background:#f3f5fc}
.askbtn.busy svg{animation:expup 1s ease-in-out infinite}
@keyframes expup{0%,100%{transform:translateY(1px)}50%{transform:translateY(-2px)}}

/* Genie sign-off — the product credit, last thing in the rail. Deliberately quiet: 11.5px, the rail's
   tertiary ink, a 12px sparkle. It replaces the old "Workspace · Space Burgers" block, which said the
   same thing the top-left identity now says. Not a link and not focusable — attribution only. */
.rail .genieby{display:flex;align-items:center;gap:6px;padding-top:12px;border-top:1px solid var(--rail-line);
  font-size:11.5px;font-weight:600;color:var(--rail-ink3);letter-spacing:.01em;white-space:nowrap}
.rail .genieby .gb-mark{flex:0 0 auto;display:grid;place-items:center;color:var(--rail-ink2);opacity:.85}
.rail .genieby .gb-mark svg{width:12px;height:12px;display:block}
.rail .genieby b{font-weight:800;color:var(--rail-ink2)}

/* profile now lives at the bottom of the left nav, opening a Profile / Settings / Log out menu.
   On the dark rail it is a raised translucent surface rather than a white card: a white button here
   would out-weigh the active nav tab and read as the loudest thing in the chrome. */
.railprofile-wrap{position:relative;margin-top:8px}
.railprofile{display:flex;align-items:center;gap:9px;width:100%;border:1px solid var(--rail-line);border-radius:12px;padding:8px 12px;background:var(--rail-fill);cursor:pointer;text-align:left;font-family:inherit;font-weight:500;color:var(--rail-ink)}
.railprofile:hover{background:rgba(255,255,255,.11)}
.railprofile .av{width:32px;height:32px;border-radius:8px;background:var(--rail-on-bg);color:#fff;display:grid;place-items:center;font-weight:700;font-size:14px;flex:0 0 auto}
.railprofile .who-txt{display:flex;flex-direction:column;line-height:1.15;min-width:0;flex:1}
.railprofile .nm{font-weight:700;font-size:13.5px;color:var(--rail-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.railprofile .rl{font-size:11.5px;color:var(--rail-ink3)}
.railprofile>svg{width:15px;height:15px;color:var(--rail-ink3);flex:0 0 auto}
.profmenu{position:absolute;bottom:calc(100% + 8px);left:0;right:0;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.16);padding:8px;display:none;z-index:50}
.profmenu.open{display:block}
.profmenu a{display:flex;align-items:center;gap:11px;padding:12px 12px;border-radius:8px;text-decoration:none;color:var(--ink);font-weight:600;font-size:14px}
.profmenu a:hover{background:#f1f4fb}
.profmenu a svg{width:16px;height:16px;color:var(--ink2)}
.profmenu .sep{height:1px;background:var(--line2);margin:5px 6px}

.content{padding:24px 24px;overflow-y:auto;flex:1}
.grid-main{display:grid;grid-template-columns:1fr 320px;gap:12px;align-items:start}

/* Data-provenance footer (reference #1/#3): the last thing on every data screen — the figures are
   live, so they move, and every amount is ringgit. Appended by Shell() rather than pasted into 26
   pages, so the wording can never drift between screens. Centred, muted and unboxed: it is a
   standing caveat, not a panel competing with the data above it. */
.datafoot{margin:22px 0 4px;text-align:center;font-size:12px;font-weight:600;color:var(--ink3)}

/* ═══ KPI cards — ONE standard, set by My Dashboard ═══
   These values ARE the My Dashboard card. They used to live behind a ".compact" modifier that only
   three pages opted into, so the same metric rendered at two densities depending on which screen you
   were on. The density is now the DEFAULT — there is no opt-in, and no way to drift off it.
   Change a number here and every KPI card in the deck moves together.

   The card was redesigned on My Dashboard (2026-07-27) and promoted here to the whole deck. What it
   fixes and why, so none of it gets "tidied" back:

   1. TWO AXES, not one. The old card was a single left-aligned stack of near-equal-weight lines, so
      nothing led and the eye had no path through it. Now the label sits left with a tinted icon
      badge pinned hard right (the reference card's own device), and the comparison row puts the
      named baseline left — sharing the value's left edge, so the two read as a pair — against the
      movement pill hard right. Badge top-right and pill bottom-right give the card a diagonal.
   2. TYPE CONTRAST. The scale was 12.5 → 24 (1.9×, value and delta within 7px of each other). It is
      now 9.5 → 12 → 12.5 → 13.5 → 27 — near 3×, so the value is unambiguously the headline.
   3. WEIGHT RANGE. Value and delta were both 800, so neither won. Only the value is 800; the delta
      earns prominence from colour and a tinted pill instead, and support steps 500/600/700.

   The structure (.khead wrapper, .kicon badge, .dpct pill, .dprev baseline) is applied at runtime by
   upgradeKpiCards() in shell.js rather than pasted into 76 cards across 17 pages — same reasoning as
   the nav icons and the sparkline pass. Author a card as label / .val / .delta and it upgrades. */
/* FOUR FIXED TRACKS — every KPI card is 25% of the content column, on every page (user, 2026-08-05:
   "for the KPI cards in Rewards and Points, use 25% width. This applies to all KPI cards"). Twelve pages
   had their own `.kpis` override, most of them `auto-fit minmax(200–250px,1fr)`, which let the track
   COUNT float with the window: the same row of six read 3+3 at 1280 and 5+1 at 1920, and a card was a
   different width on every screen. All twelve are gone; this is the only rule that sets the tracks.
   minmax(0,1fr), not 1fr: a card holding a long unbroken figure would otherwise widen its own track past
   its share and hand the rest a narrower one.
   Rows of 5, 6 or 7 cards now wrap at four — 4+2 for Sales' six, 4+3 for Operations' seven. That REVERSES
   an earlier call on Sales ("six KPI cards want to break 3+3, never 4+2"), which is the cost of one card
   width everywhere and is the trade the instruction asks for.
   The narrow-screen step-downs on Performance, My Dashboard and rm-overview stay: those are 3-up and 2-up
   below 900px / 799px, where a quarter column is 170px, and they are about a different problem. */
.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.kcard{background:var(--card);border:1px solid var(--line);border-radius:var(--r);min-height:96px;padding:16px 16px;position:relative;display:flex;flex-direction:column;
  box-shadow:0 1px 2px rgba(20,24,40,.04)}   /* hairline lift: surfaces, not outlines, on a tinted page */
/* WRAPS, and the row gap is zero: every card carries a period qualifier next to its title
   ("Total Sales (Today)"), and the longest titles leave it no room on line 1. The qualifier and the
   `i` badge are each nowrap, so what moves is the whole token, never a broken date. */
.kcard .lab{font-size:var(--t-body);color:var(--ink2);font-weight:600;display:flex;align-items:center;
  flex-wrap:wrap;column-gap:7px;row-gap:0}
/* Header. The badge is OUT of flow, pinned to the card's top-right: as a flex sibling its 30px
   height set the row height, so the label's 17px of text left ~13px of dead air that pushed the
   value down. top/right match .kcard's own 13px/15px padding. padding-right keeps a long label
   clear of the badge. */
/* min-height RESERVES whatever the TALLEST header in this row of cards needs, on every card in it.
   The period qualifier rides next to the title and wraps on the longest titles; if that cost only the
   cards that wrap, their value, subvalue and sparkline would sit a line below their neighbours' and the
   row would lose its rhythm (the exact reason customers.html once dropped a qualifier). The figure is
   MEASURED per card group by kpiHeadSync() in shell.js and set as --khead-min, because it depends on
   the column width, the titles on that page and the length of the current period name — 35px (two
   lines of a 12px label) is only the fallback. */
.kcard .khead{padding-right:36px;min-height:var(--khead-min,35px)}
/* top/right TRACK the card's padding — they are not padding themselves, so the 4px-grid sweep left them
   at 13/15 and the badge stopped lining up with the label beside it. */
/* NEUTRAL, not the indigo accent (user, 2026-08-04: "if it doesn't symbolise any semantics, it
   doesn't need to be that vibrant — icons in KPI cards, display only, not clickable, can be more
   neutral"). The badge names the metric; it carries no state and nothing happens if you press it, so
   in the accent it was the one place the deck's interactive blue appeared on something inert — 76 of
   them, one per card. Grey glyph on a grey tint: 4.7:1, well past the 3:1 a non-text mark needs. */
.kcard .kicon{position:absolute;top:16px;right:16px;width:30px;height:30px;border-radius:12px;
  background:#eef1f8;color:var(--ink3);display:grid;place-items:center}
.kcard .kicon svg{width:17px;height:17px;display:block}
/* KPI label qualifier — same bracketed treatment as a panel title's, lighter than the label.
   margin-top is reset: it inherits 1px from `.kcard .sub` (the footer-note rule), which drops the
   qualifier off the label's baseline. */
/* font-size stated here, not inherited: this used to pick up 11px from a `.kcard .sub` rule that
   also styled the card's footer note. That rule is gone with the one-layout sweep.
   QUIETER as of 2026-08-03 (user: the periods "stand out too much"). It was 11px/600/--ink3 — the same
   weight as the 12px label it sits beside, so a qualifier that answers "when" was reading as part of the
   metric's name. 10.5px/500/--ink4 keeps it legible and puts it a clear step below the label. The size
   feeds kpiHeadSync(), which measures each row's header and holds every card to the tallest, so the
   change cannot leave one card's value sitting lower than its neighbours'. */
.kcard .lab .sub{font-size:var(--t-cap);font-weight:500;color:var(--ink4);margin-top:0;white-space:nowrap}
/* Period qualifier + tooltip badge as ONE wrapping unit, so the badge can never end up alone on the
   label's second line. */
.kcard .lab .labtail{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;flex:0 0 auto}
/* Same job on a panel TITLE, but as a plain inline run: the title's qualifier keeps its own margin and the
   spaces between the parts have to survive, and an inline-FLEX wrapper would drop both (flex discards
   whitespace-only children). nowrap is the whole point — see titleTail() in shell.js. */
.panel h3 .labtail,.panel h4 .labtail,.charthead h3 .labtail{white-space:nowrap}
.kcard .lab .sub::before{content:"("}
.kcard .lab .sub::after{content:")"}
.i{width:16px;height:16px;border-radius:50%;border:1px solid var(--ink3);color:var(--ink3);font-size:10px;display:inline-grid;place-items:center;font-style:normal;vertical-align:middle;position:relative;top:-1px}
/* Values run to "RM 5,109,289" on the money pages, so the headline is 27px rather than My
   Dashboard's 29 — it is the largest size that still holds 12 characters inside the narrowest
   (190px) card without wrapping. It never wraps mid-figure: "RM" must not break onto its own line. */
/* 24px, one size for every KPI value in the deck (user, 2026-08-03: "reduce them from 29px to 24px, use
   this consistently"). It was 27px here and 29px on My Dashboard — the same figure two sizes depending on
   the page. 24 also lands on the 4px grid, and it buys back the headroom the 27px value was spending:
   "RM 5,109,289" is 12 characters and had to fit a 190px card without wrapping, which is why the money
   pages ran 2px smaller than My Dashboard in the first place. At 24px there is room to spare on both. */
.kcard .val{font-size:24px;font-weight:800;letter-spacing:-.025em;margin:5px 0 0;line-height:1;white-space:nowrap}
.kcard .val small{font-size:14px;color:var(--ink3);font-weight:600}
/* .delta is the card's THIRD ROW and it is always the two-column composition: the subvalue left,
   the movement pill right. It used to be a plain full-width line by default and only became the
   two-column row when it held a movement (`.has-move`), which is why the deck ended up with four
   different card shapes. Every card is normalised into this row by upgradeKpiCards() now — whatever
   the page authored (a movement, a bare phrase, a .foot / .kcmp / .sub support line) is composed
   into .dprev on the left — so the row layout belongs on the base class. `.has-move` survives only
   as the marker meaning "this row carries a pill", for the sentiment colouring. */
.kcard .delta{font-size:var(--t-body);font-weight:700;color:var(--ink);margin-top:9px;
  display:flex;align-items:center;justify-content:space-between;gap:10px}
.kcard .delta .sub2{color:var(--ink3);font-weight:500}
/* Movement pill. Prominence from colour and shape, not from weight or size. */
/* margin-left:auto, not just space-between: a card whose subvalue slot is empty has the pill as the
   row's ONLY child, and space-between then parks it at the START — which is how 35 cards across six
   pages ended up with the delta under the value instead of on the right. auto keeps it hard right
   whether or not anything sits beside it. */
.kcard .delta .dpct{flex:0 0 auto;margin-left:auto;font-size:var(--t-body);font-weight:700;line-height:1.5;
  padding:4px 8px;border-radius:20px;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
/* The pill's ink is one step darker than the --good/--danger text tokens, for the same reason .pill.good
   and .pill.bad carry their own: those tokens clear 4.5:1 on WHITE, and a tinted backing eats the
   headroom — the vivid emerald measured 4.29:1 on its own mint tint, the alert red 4.03:1 on its pink.
   Same two hues, one weight down. */
.kcard .delta .dpct.goodv{background:#def7ee;color:#066b49}
.kcard .delta .dpct.warnv{background:#fde3e9;color:#b01238}
.kcard .delta .dpct.flatv{background:var(--line2);color:var(--ink3)}
/* Direction marker, DRAWN not typed. ▲/▼ (U+25B2/BC) are font glyphs and Urbanist has no
   geometric-shapes block, so they came from a fallback face: a wide, blunt ~70° wedge sized to the
   glyph em, bigger and softer than the type beside it and different on another machine. A border
   triangle is exact and font-independent — 6px base on a 6px rise is a 53° apex — and inherits the
   semantic colour through currentColor. Widths are WHOLE pixels: Chrome rounds fractional
   border-widths down, so 2.5px silently becomes 2px and turns the mark into a 37° needle. */
.kcard .delta .dpct .tri{display:inline-block;width:0;height:0;margin-right:5px;
  border-left:3px solid transparent;border-right:3px solid transparent;position:relative;top:-1px}
.kcard .delta .dpct .tri.up{border-bottom:6px solid currentColor}
.kcard .delta .dpct .tri.dn{border-top:6px solid currentColor}
/* SUBVALUE slot, left of the pill. Two forms, both of them stacked in this one box:
     • a named baseline — .dpl label over a .b figure (My Dashboard's "YESTERDAY / RM 170,856");
     • a .dpx context phrase, which is what the deck's old .foot / .kcmp / .sub lines become.
   A card may carry both, which is why this is a column and not a single line. */
.kcard .delta .dprev{min-width:0;text-align:left;display:flex;flex-direction:column;gap:1px}
.kcard .delta .dprev:empty{display:none}
/* SENTENCE CASE, not the uppercase this carried until 2026-08-03 (user: the periods "stand out too
   much"). It was 9.5px/600/.07em tracking/uppercase/--ink3 — the loudest small text a card has, because
   caps plus tracking plus a bold figure directly beneath it read as a HEADING for the value rather than a
   caption on it. Caps and tracking are the two dials that were doing that, so they are the two that go;
   weight drops to 500 and the ink one step to --ink4 (4.7:1).
   Size goes UP, 9.5 → 10px, and that is not a contradiction: this file sizes type for 40+ readers, so the
   subtlety has to come from weight, case and ink rather than from shrinking real text — and mixed case at
   10px is easier to read than caps at 9.5 while taking up less visual space. The words themselves were
   always mixed case in the DOM ("vs Yesterday"); only the transform shouted.
   It does step off the deck's uppercase micro-label idiom (.nav-label, .ws-eyebrow, table headers) — on
   purpose: those label a COLUMN or a SECTION, where this one is a caption inside a card, sitting against
   a number. The card's weights now step 500 → 700 → 800, caption to figure to value. */
.kcard .delta .dprev .dpl{font-size:var(--t-cap);font-weight:500;letter-spacing:normal;
  color:var(--ink4);line-height:1.15;white-space:nowrap}
.kcard .delta .dprev b{font-size:var(--t-body);font-weight:700;color:var(--ink2);line-height:1.1;
  font-variant-numeric:tabular-nums;white-space:nowrap}
/* .dpw — the same line carrying a short PHRASE instead of a figure ("Iced Lemon Tea", "Critical",
   "given away"): it keeps the figure's weight, because on those cards it is the second thing read,
   but it must be allowed to break, where a figure must never break mid-number. */
.kcard .delta .dprev b.dpw{white-space:normal;font-variant-numeric:normal;line-height:1.2}
/* Context phrase. Same type as the .foot line it replaces (11px / 500 / ink3) rather than the
   figure's 12.5px bold: these are sentences, not values, and setting a sentence at the figure's
   weight would put three elements on 700 and flatten the card again. It WRAPS — .dprev b above is
   nowrap because a figure must never break mid-number, but a phrase sharing its row with the pill
   has ~115px and has to be allowed two lines. */
.kcard .delta .dprev .dpx{font-size:var(--t-cap);font-weight:500;color:var(--ink3);line-height:1.25;
  white-space:normal;overflow-wrap:break-word}
/* A phrase directly under a figure needs air; against the .dpl label alone it does not. */
.kcard .delta .dprev b + .dpx{margin-top:2px}
/* RETIRED with the one-layout sweep: .kcard .sub, .kcard .kcmp, .kcard .foot, .kcard .ctx and
   .kcard .delta .cmpnote. Those were four ways to set the SAME line — the card's one piece of
   supporting context — in three different slots, and .foot sat below the sparkline while the other
   two sat above it. upgradeKpiCards() now folds whatever a page authors in any of them into the
   .dprev subvalue above, so a card cannot have a fifth row to style. The class names are still
   ACCEPTED as input (a page may write .foot and have it land in the right place); they just no
   longer have a rendered position of their own. `.foot` outside a card is untouched — the rail
   footer and the table footers are different components with their own rules.
   .kbar (the greyscale progress bar) went the same way: it was a card's floor graphic on two cards
   where every other card carries a sparkline, and both printed the proportion in the value already. */
.kcard .spark{margin-top:auto;padding-top:12px;width:100%;display:block;height:26px}   /* full card width (ref) — no max-width cap */
.kcard svg.spark{height:26px}
/* A sparkline in a TABLE CELL — Performance summary vs target's "Trend (7 days)" column, and any
   other table that draws one. 26px tall, the KPI card's height, because the card is the standard
   (user, 2026-08-05: the scorecard's sparkline "is visually thicker than the ones in the KPI card").
   It was not a thicker stroke — both are the same 2px --ln-w — it was a 5px-tall box: only .kcard
   set a height, so out here the svg took its width from the collapsed column (35px) and its height
   from the authored 160x24 aspect, giving 35x5.27, in which a 2px line covers 38% of the box and
   reads as a bar. Same stroke over a 26px box reads as the same weight, which is what was asked.
   Sized in BOTH axes on purpose: the enhance pass in shell.js re-derives the viewBox from the
   measured box, so a fixed box is what keeps its scale at 1 and its pen undistorted. */
.tbl svg.spark{width:64px;height:26px;display:block;margin-left:auto}
/* ---- Net-sales chart: KPI-card header + enlarged interactive graph (sales + rm-sales) ---- */
.nc-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:6px}
.nc-hkpi .lab{font-size:12.5px;color:var(--ink2);font-weight:600;display:flex;align-items:center;gap:7px}
.nc-val{font-size:24px;font-weight:800;letter-spacing:-.01em;line-height:1.05;color:var(--ink);margin:5px 0 3px}
.nc-delta{font-size:12.5px;font-weight:700}
.nc-delta .s{color:var(--ink3);font-weight:500}
.nc-legend{display:flex;gap:14px;padding-top:4px}
.nc-lg{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--ink2)}
.nc-lg i{width:16px;height:0;border-top:var(--ln-w) solid var(--accent);flex:0 0 auto}
.nc-lg.prev i{border-top:var(--ln-w) dashed var(--spark)}
/* ══════════════════ LINE GRAPHS — one width, one layout ══════════════════
   User, 2026-07-28: "For ALL line graphs, keep the styling consistent. 1. Same thin line width.
   2. Same layout (title, tooltip, legend, filter)."

   The deck had FIVE stroke widths in use (1.1, 1.35, 1.8, 2, 2.5), three legend treatments
   (.nc-legend chips, widgets' .leg row UNDER the plot, and text labels painted inside the plot),
   three tooltip mechanisms (a crosshair + floating .nc-tip, native SVG <title>, and none at all),
   and the chrome classes were defined per-page — .trendhead/.tlegend/.tchip existed twice, once in
   overview.html scoped to .ov and once in sales.html, with near-identical rules free to drift.
   Everything a line graph needs now lives HERE, once.

   LAYOUT, top to bottom:
     .charthead   title left (h3 + its `i`), the filter right, on one row
     .tlegend     legend on its own row, ABOVE the plot — you learn what the marks mean before you
                  look at them, the same argument .phead makes below
     .chartw      the plot, with a crosshair and a floating .nc-tip on hover

   WIDTH: --ln-w, one value for every data series. It was tempting to keep a comparison line
   thinner, but dash + muted colour already say "previous period"; a third signal spent on the same
   distinction just made the primary line of one chart heavier than another chart's.

   A DECLARED width is not a RENDERED width (user, 2026-07-29: "the one in sales Sales trend looks
   thicker compared to the one in My Dashboard"). stroke-width is in the SVG's own user units, so it
   is multiplied by the viewBox->box scale: one shared 1.8 rendered anywhere from 1.03px on My
   Dashboard's Sales Trend (a 720-unit viewBox in a 411px column, scale 0.571) to 2.05px on
   Operations' prep chart (760 units in an 866px panel, scale 1.139) — a 2x spread from a single
   number. vector-effect:non-scaling-stroke takes the stroke out of that transform, so --ln-w is a
   real CSS pixel value on every chart no matter how big its box or how odd its viewBox. It fixes the
   dash pattern the same way, and it is what lets a chart keep whatever viewBox suits its geometry.

   2px, up from 1.1px — and --dot-sw with it, so the ring around a point is never lighter than the
   line it sits on (user, 2026-08-05: "for all sparklines, and line charts, make the lines thicker:
   --ln-w and --dot-sw should both be 2px", after 1.5px on the Sales Trend line). 1.1px was My
   Dashboard's rendered weight, which is why every chart had come DOWN to it; a hairline that thin
   reads as tentative, and one token is the whole point of this block — so the standard moves and
   every series in the deck moves with it, sparklines and My Dashboard's trend included.

   EVERY SIZE IN A CHART IS ABSOLUTE, not just the stroke (user, 2026-07-29: "all the elements sizes
   should match in absolute terms — the only difference is the component's container size"). The
   tokens below are real CSS pixels, and each kind of size reaches them by a different route:
     - strokes            vector-effect:non-scaling-stroke, handled here in CSS
     - font-size and r    NO CSS property exempts them from the viewBox transform (SVG 2's
                          vector-effect:non-scaling-size is not shipped by Chrome), so W.chartAbs()
                          in widgets.js divides them by each chart's measured scale at mount, on
                          resize and after any re-render. Authored values stay at the target, which
                          is what they mean at scale 1 — the pass only corrects for scale. */
:root{--ln-w:2px;     /* every data series */
      --gr-w:1px;     /* gridlines, baseline, crosshair */
      --dot-sw:2px;   /* the ring around a data point */
      --ax-fs:12px;   /* axis tick labels — the CAPTION size (--t-cap), read by W.chartAbs() */
      --dot-r:2px;    /* data point radius — ditto, and r cannot be set from CSS at all */
      --dot-lr:3px;   /* the emphasised last point */
      --hot-r:4px}    /* the dot the crosshair parks on the hovered point */
.ln,.ln-prev,.ln-over,.nc-line,.nc-prev,.sparkln{vector-effect:non-scaling-stroke}
/* The KPI-card sparkline (kpiSpark in shell.js). Same token as every other series — it is a line
   chart, just a small decorative one, and it was the last series in the deck carrying its width as a
   hardcoded attribute. Neutral grey by policy: a sparkline is context, not a judgement. */
.sparkln{fill:none;stroke:var(--spark);stroke-width:var(--ln-w);stroke-linecap:round;stroke-linejoin:round}
/* Title row. Shared with the two charts that already had it (Sales Trend, the net-sales chart);
   `align-items:center` keeps a select vertically centred against the title rather than hanging. */
.charthead{display:flex;justify-content:space-between;align-items:center;gap:10px 12px;
  flex-wrap:wrap;margin-bottom:6px}
.charthead > h3{margin:0}
.charthead .psub{margin:3px 0 0;font-size:var(--t-cap);white-space:nowrap}
/* Legend row. Chips, above the plot. */
.tlegend{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-start;margin-bottom:8px}
/* The window the chart actually drew, pushed to the far end of the legend row — metadata about the
   axis, so it sits with the legend rather than under the title (where a subtitle would be). Was
   page-local to sales.html, which left the SAME panel on rm-sales.html printing its span at body
   size, inline against the chips, because the rule wasn't there (2026-08-05). */
.ncspan{margin-left:auto;align-self:center;font-size:var(--t-cap);font-weight:500;color:var(--ink3);
  white-space:nowrap}
/* neutral pill, same grey as the KPI badge tile: a legend is a label, not a control, and it was
   sitting on the interactive indigo tint (user, 2026-08-04 — vibrancy only where it means something).
   The swatch inside carries the series colour; the pill behind it must not compete with it. */
.tchip{display:inline-flex;align-items:center;gap:5px;font-size:var(--t-cap);font-weight:600;color:var(--ink);
  background:#eef1f8;border:1px solid var(--line);border-radius:20px;padding:4px 8px;white-space:nowrap}
/* The swatch DRAWS THE MARK it stands for: a line chart's key is a line segment, not a filled
   square. Sales Trend's legend had "Today" as a 7px square beside "Yesterday" as a dashed rule —
   two different swatch shapes for two lines in the same chart. Series that really are areas or bars
   keep the filled block (.sw-fill). */
.tchip i{width:15px;height:0;border-radius:0;border-top:var(--ln-w) solid var(--accent);flex:0 0 auto}
.tchip i.sw-prev{border-top-style:dashed;border-top-color:var(--spark)}
.tchip i.sw-alt{border-top-color:var(--bar)}
.tchip i.sw-ink{border-top-color:var(--ink)}
.tchip i.sw-fill{width:9px;height:9px;border:0;border-radius:4px;background:var(--accent)}
.tchip i.sw-fill.sw-alt{background:var(--bar)}
.tchip i.sw-fill.sw-alt2{background:var(--bar2);border:1px solid var(--bar)}
/* Plot box. position:relative anchors the floating .nc-tip; the SVG scales with the panel and
   NEVER with preserveAspectRatio="none" — a non-uniform scale stretches the pen, so one declared
   stroke-width renders at two different thicknesses along its own length. Four charts did that. */
.chartw{position:relative;width:100%}
.chartw svg{display:block;width:100%;height:auto;overflow:visible}
/* The series strokes. One width for all of them. */
/* stroke reads from the token rather than repeating its value, so a panel that plots a second,
   different metric can set --ln-c on its own wrapper (see .tlegend swatch .sw-c) without a new class. */
.ln{fill:none;stroke:var(--ln-c,var(--bar));stroke-width:var(--ln-w);stroke-linecap:round;stroke-linejoin:round}
.ln-prev{fill:none;stroke:var(--spark);stroke-width:var(--ln-w);stroke-dasharray:4 3.5;
  stroke-linecap:round;stroke-linejoin:round}
.ln-alt{stroke:var(--bar)}
.ln-ink{stroke:var(--ink)}
/* trend()'s overlay line over its bars: same width and joins, but the COLOUR stays the caller's,
   since it has to hold against whatever the bars are. */
.ln-over{fill:none;stroke-width:var(--ln-w);stroke-linecap:round;stroke-linejoin:round}
/* Chart furniture. UNSCOPED on purpose: these were `.chartw ...`, which left every chart that isn't
   a .chartw — My Dashboard's Sales Trend in its .trendwrap aspect box, the net-sales chart in
   .netchart — to restate the same furniture inline and drift. The .ln-* and .nc-* names are kept as
   aliases of one rule so no call site has to be renamed. */
.grid,.ln-grid,.nc-grid{stroke:var(--line2);stroke-width:var(--gr-w);vector-effect:non-scaling-stroke}
.ln-base,.nc-base{stroke:var(--line-c);stroke-width:var(--gr-w);vector-effect:non-scaling-stroke}
.ln-ax,.nc-ax,.tr-ax{font-size:var(--ax-fs);fill:var(--ink3);font-weight:600}
/* One marker for the whole deck: a ring, so a dot on a line still reads as a point ON the line
   rather than a bead of it. My Dashboard drew solid accent dots instead — the only chart that did. */
.ln-dot,.nc-dot{fill:var(--card);stroke:var(--accent);stroke-width:var(--dot-sw);vector-effect:non-scaling-stroke}
.ln-dot.last,.nc-dot.last{fill:var(--accent)}
.ln-cross,.nc-cross{stroke:var(--ink3);stroke-width:var(--gr-w);stroke-dasharray:3 3;
  vector-effect:non-scaling-stroke;opacity:0;transition:opacity .1s}
.ln-hot,.nc-hot{fill:var(--accent);stroke:var(--card);stroke-width:1.6px;
  vector-effect:non-scaling-stroke;opacity:0;transition:opacity .1s}
.ln-ov{fill:transparent;cursor:crosshair}

.netchart{position:relative;width:100%}
.netchart svg{display:block;width:100%;height:auto;overflow:visible}
/* Axis labels, gridlines, baseline, dots, crosshair and hot dot are NOT restated here: they are the
   shared .nc-* aliases in the furniture block above, at the deck's absolute sizes. This block used
   to hold its own copies, tuned by hand for "a 900-wide viewBox that renders at ~866px, so these
   numbers are very nearly their on-screen size" — true only of this one chart at one window width,
   which is how the deck ended up with a chart whose labels and dots were half again the size of My
   Dashboard's. */
/* Was 2 / 1.35. Both now take the deck's single --ln-w; the previous-period line stays
   distinguishable by its dash and its muted ink, which is all it needed. */
.nc-line{fill:none;stroke:var(--accent);stroke-width:var(--ln-w);stroke-linecap:round;stroke-linejoin:round}
.nc-prev{fill:none;stroke:var(--spark);stroke-width:var(--ln-w);stroke-dasharray:4 3.5;stroke-linecap:round;stroke-linejoin:round}
/* Two-tier x axis (see axisTiers in netchart.js). The tick marks the bucket; the band names the
   month or year the run of buckets sits in, so it reads as the group header it is — one step darker
   and heavier than the tick labels, at the same absolute size. */
.nc-tick{stroke:var(--line-c);stroke-width:var(--gr-w);vector-effect:non-scaling-stroke}
.nc-band{fill:var(--ink2);font-weight:700}
.nc-tip{position:absolute;transform:translate(-50%,-100%);margin-top:-12px;background:var(--ink);color:#fff;border-radius:12px;padding:12px 12px;font-size:12px;line-height:1.4;pointer-events:none;opacity:0;transition:opacity .1s;white-space:nowrap;z-index:5;box-shadow:0 8px 22px rgba(20,24,30,.22)}
.nc-tip .t-wk{font-weight:800;margin-bottom:4px;font-size:12.5px}
.nc-tip .t-row{display:flex;justify-content:space-between;gap:18px}
.nc-tip .t-row .k{color:#c8cfe2}
.nc-tip .t-row .v{font-weight:700;font-variant-numeric:tabular-nums}
.nc-tip .up{color:#6ee7b7}.nc-tip .dn{color:#fda4af}

/* panels */
/* DENSITY. These four numbers are the app-wide base and My Dashboard is where they were
   validated — the same move already made for .kcard. Panels were running 18px padding + an 18px
   title everywhere except My Dashboard, which overrode both down; keeping the looser figures as
   the base meant every new panel started loose and the validated screen was the exception.
   Change them HERE, not per page. */
.panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:16px}
.panel h3{margin:0 0 3px;font-size:var(--t-title);font-weight:800}
/* Qualifier in a panel title ("Top 10", "Average", "By Value"). It had no rule at all, so it
   rendered at full title weight and read as part of the name — "Sales by outlet Top 10".
   The PARENTHESES are drawn by CSS, not typed into the markup: every reference panel that
   qualifies its title wraps the qualifier in brackets — "Sales by Outlet (Top 10)", "Queue &
   Wait Time (Average)" — so making it automatic means a new qualifier cannot be added without
   them, and none of them can drift to a different bracket style. */
.panel h3 .sub{font-size:var(--t-cap);font-weight:500;color:var(--ink3);margin-left:7px;letter-spacing:0}
.panel h3 .sub::before{content:"("}
.panel h3 .sub::after{content:")"}
/* Small in-panel select (chart granularity). Shared: Sales trend and Operations prep-by-hour. */
.selsm{font:inherit;font-size:12px;font-weight:600;color:var(--ink2);background:var(--card);
  border:1px solid var(--line);border-radius:8px;padding:4px 8px;cursor:pointer}
.selsm:hover{border-color:var(--ink3)}
.panel .psub{font-size:var(--t-cap);color:var(--ink3);margin-bottom:11px}

/* ---- Panel header (.phead) — title left, legend right, on one row.
   Every legend that predates this component sits at the top (the net-sales chart's .nc-head and
   the Overview trend's .trendhead both right-align one there); every legend that had drifted to
   the BOTTOM was one added later. Top-right is also the better reading order — you learn what
   the marks mean before you look at them. This exists so the next chart can't drift again. ---- */
.phead{display:flex;justify-content:space-between;align-items:flex-start;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.phead > h3{margin:0}
.phead .plegend{display:flex;align-items:center;flex-wrap:wrap;gap:7px 16px;font-size:12px;
  color:var(--ink3);font-weight:600;padding-top:4px}
.phead .plegend i{width:11px;height:11px;border-radius:50%;flex:0 0 auto}
.phead .plegend span{display:inline-flex;align-items:center;gap:7px}
/* a heading inside .phead already carries the row's margin — don't double it up */
.phead > h3:not(:has(+ .psub)){margin-bottom:0}

/* ---- "How to use" — deliberately secondary and at the FOOT of the panel: the chart has to
   work without it. It sits after the content, in muted link-weight, so it reads as a way out of
   confusion rather than a prerequisite for reading the chart. ---- */
.howto-btn{display:inline-flex;align-items:center;gap:7px;margin-top:14px;padding:8px 0;border:none;
  background:none;font:inherit;font-size:12.5px;font-weight:600;color:var(--ink3);cursor:pointer}
.howto-btn:hover{color:var(--ink)}
.howto-btn .hq{display:inline-grid;place-items:center;width:16px;height:16px;border-radius:50%;
  border:1.3px solid currentColor;font-size:10px;font-weight:800;line-height:1}

/* ---- How-to-use drawer — follows the deck's existing .notif-drawer shape (right-side, header +
   close). A slide-in, not a modal: it's a temporary reference you dismiss, not a decision you
   must resolve before continuing. Read-only, so there's no Apply — just a way out. ---- */
.hwrap{position:fixed;inset:0;z-index:70;display:none}
.hwrap.open{display:block}
.hwrap .h-backdrop{position:absolute;inset:0;background:rgba(20,24,30,.30);opacity:0;transition:opacity .22s}
.hwrap.in .h-backdrop{opacity:1}
.hdrawer{position:absolute;top:0;right:0;width:430px;max-width:94vw;height:100%;background:#fff;
  border-left:1px solid var(--line);box-shadow:-10px 0 34px rgba(0,0,0,.10);display:flex;
  flex-direction:column;transform:translateX(100%);transition:transform .26s cubic-bezier(.2,.8,.3,1)}
.hwrap.in .hdrawer{transform:translateX(0)}
.hdrawer .hh{display:flex;align-items:center;justify-content:space-between;padding:20px 20px;
  border-bottom:1px solid var(--line);flex:0 0 auto}
.hdrawer .hh h3{font-size:18px;margin:0}
.hdrawer .hclose{border:none;background:none;font-size:24px;cursor:pointer;color:var(--ink2);line-height:1;padding:0 4px}
.hdrawer .hclose:hover{color:var(--ink)}
.hbody{overflow:auto;padding:20px 20px 24px;flex:1;font-size:14px;line-height:1.62;color:var(--ink2)}
.hbody h4{margin:20px 0 6px;font-size:13px;font-weight:800;color:var(--ink);letter-spacing:.02em}
.hbody h4:first-child{margin-top:0}
.hbody p{margin:0 0 10px}
.hbody b{color:var(--ink);font-weight:800}
.hbody .hkey{display:flex;align-items:flex-start;gap:9px;padding:8px 0;border-top:1px solid var(--line2)}
.hbody .hkey:first-of-type{border-top:none}
.hbody .hkey i{width:11px;height:11px;border-radius:50%;flex:0 0 auto;margin-top:5px}
.hbody .hkey div{flex:1}
/* A heading sitting DIRECTLY on its content — no description beneath — has to carry the gap
   the description used to provide (3px + the psub's 11px), or the chart starts 3px under the
   title. Scoped to `.panel > h3` so it only hits panels whose heading is the direct child and
   is followed by content; the table panels nest their heading inside a padded header row and
   are unaffected. */
.panel > h3:not(:has(+ .psub)){margin-bottom:12px}
/* ROW-FIT STANDARD (deck-wide). Panels sharing a row are EQUAL width (1fr 1fr) and EQUAL height
   (align-items:stretch — the row stretches every card to the tallest so bottoms line up; ragged
   bottoms read as messy). The gap between ALL components — row and column — is 12px. Where a
   stretch leaves a shorter card half-empty, trim the taller card's content rather than reverting
   to ragged heights. Validated first on My Dashboard (see .ov rules). */
.two{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px;align-items:stretch}
/* A row that keeps BOTH tracks whatever it holds, so a panel left alone in it stays at half width
   instead of stretching across. .two on its own can't: pages that widen it to auto-fit collapse the
   empty track (Sales), and a lone panel then spans the row. Three pages use it now — Customers,
   Loyalty and Sales — which is what brings it out of their local <style> blocks and in here. */
.two.half{grid-template-columns:repeat(2,minmax(0,1fr))}
/* Bottom margin for a panel with another row under it. NOT for a last child, where it doubles the gap
   the section edge already sets. */
.rowgap{margin-bottom:12px}
/* Four fixed tracks, so two or three cards land at 25% each instead of spreading to fill. */
.kpis-few{grid-template-columns:repeat(4,minmax(0,1fr))}
/* Section rule INSIDE a tab — a labelled hairline that opens a section, the next one closing it.
   Used by Customers' Segments and Lifecycle tabs and by every section of Loyalty. */
.seam{display:flex;align-items:center;gap:14px;margin:26px 0 16px}
.seam span{font-size:12.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--ink3);white-space:nowrap}
.seam:before,.seam:after{content:'';height:1px;background:var(--line);flex:1}
.seam:before{flex:0 0 4px}
/* A seam that OPENS a tab or a page needs no room above it — the tab edge is the gap. */
.ltab > .seam:first-child,.content > .seam:first-child{margin-top:6px}
/* Tab panel. Customers and Loyalty share the class and the ltab() switcher (each page defines its own
   switcher, because the two tab bars are different lengths), so the display rule belongs here rather
   than in one of them. Other pages name their own — Sales has .stab — which is why this is not the
   deck's only tab rule; it is the one two pages were about to duplicate. */
.ltab{display:none}.ltab.on{display:block}

/* performers */
.perf{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px}   /* minmax(0,…) so a wider column can't steal width — columns stay equal */
.perf h4{margin:0 0 8px;font-size:var(--t-body);color:var(--ink2);font-weight:800}
.prow{display:flex;align-items:center;gap:11px;padding:12px 0;border-top:1px solid var(--line2)}
.prow .rk{color:var(--ink3);font-size:var(--t-body);width:18px}
.prow .nm{font-weight:600;font-size:var(--t-body)}
.prow .meta{font-size:var(--t-cap);color:var(--ink3)}
/* nowrap: the value is an atom. Without it a narrow .perf column breaks "27 ▼" across two lines
   — the ops score on one, its ▼ on the next — and a wrapping value is never the thing that
   should give way when a column runs out of room; the name beside it is. */
.prow .d{margin-left:auto;text-align:right;font-weight:800;font-size:var(--t-body);color:var(--ink);
  white-space:nowrap;flex:0 0 auto}
.prow .d small{display:block;color:var(--ink3);font-weight:500;font-size:var(--t-cap)}

/* insights rail */
.rail-r{display:flex;flex-direction:column;gap:12px}
.rail-r .rh{display:flex;align-items:center;gap:8px;font-weight:800;font-size:17px;margin-bottom:2px}
.ins{background:var(--card);border:1px solid var(--line);border-radius:var(--r2);padding:16px 16px}
.ins .tag{font-size:var(--t-cap);font-weight:600;letter-spacing:.05em;display:flex;align-items:center;gap:7px;color:var(--danger)}   /* CRITICAL */
.ins .tag .dt{width:9px;height:9px;border-radius:50%;background:var(--danger-mark)}     /* CRITICAL = filled red */
.ins .tag.watch{color:var(--warn)}                                                      /* WATCH = amber */
/* the ring is a mark, not text, so it takes the brighter mark weight — the 4.5:1 text ochre
   reads as brown at 2px and stops looking like the same amber as the label */
.ins .tag.watch .dt{background:transparent;border:2px solid var(--warn-mark)}            /* WATCH = amber outline */
.ins .ttl{font-weight:700;font-size:var(--t-body);margin:6px 0 4px;line-height:1.25}
.ins .bd{font-size:var(--t-body);color:var(--ink2);line-height:1.5}
/* Genie Insight rail cards are clickable → open that insight's drill-down breakdown.
   (KPI cards are NOT clickable — see the removed .kcard:hover.) */
a.ins{display:block;text-decoration:none;color:inherit;position:relative;padding-right:32px;cursor:pointer;transition:border-color .12s,box-shadow .12s}
a.ins:hover{border-color:var(--ink3);box-shadow:0 4px 14px rgba(20,24,30,.08)}
a.ins::after{content:'›';position:absolute;top:50%;right:14px;transform:translateY(-50%);color:var(--ink3);font-size:20px;font-weight:700;line-height:1}
a.ins:hover::after{color:var(--ink)}
.rail-r .more{font-size:13px;color:var(--ink3);padding:4px 4px;line-height:1.5}
.rail-r .more a{color:var(--accent);font-weight:700;text-decoration:none;white-space:nowrap}
.rail-r .more a:hover{text-decoration:underline}

/* ---- global Insights rail: opens BELOW the header (content | rail row), so the header
   keeps full page width; collapsible; built in shell.js ---- */
.mainrow{display:flex;flex:1;min-height:0;overflow:hidden}
.mainrow>.content{min-width:0}
.rail-g{flex:0 0 300px;width:300px;min-height:0;overflow-y:auto;background:var(--bg);border-left:1px solid var(--line);padding:16px 16px;display:flex;flex-direction:column;gap:9px}
.rg-head{display:flex;align-items:center;gap:8px;margin-bottom:1px}
.rg-title{flex:1;font-weight:800;font-size:16px;color:var(--ink)}
.rail-g a.ins{padding:12px 28px 12px 16px}       /* compact insight cards in the rail */
.rail-g .ins .ttl{font-size:14px;margin:5px 0 3px}
.rail-g .ins .bd{font-size:12.5px}
.rg-toggle{width:30px;height:30px;flex:0 0 auto;border:none;background:none;color:var(--ink3);border-radius:8px;cursor:pointer;display:grid;place-items:center;padding:0}
.rg-toggle svg{width:19px;height:19px}
.rg-toggle:hover{background:#e3e8f6;color:var(--ink2)}
.rg-body{display:flex;flex-direction:column;gap:9px}
.rg-body .more{font-size:13px;color:var(--ink3);padding:4px 4px;line-height:1.5}
.rg-body .more a{color:var(--accent);font-weight:700;text-decoration:none}
.rg-body .more a:hover{text-decoration:underline}
.rail-g.collapsed{display:none}          /* collapsed → only the header button remains */
/* header Insights toggle button (uses .iconbtn base; always present). Two modes:
   inactive = default iconbtn (panel closed); active = accent-filled (panel open). */
.insbtn{position:relative}
.insbtn svg{width:21px;height:21px;display:block}
.insbtn .badge{min-width:20px;height:20px;padding:0 8px;font-size:12px;font-weight:800;line-height:1;border-radius:12px;top:-7px;right:-7px;border:2px solid #fff;display:grid;place-items:center}
.insbtn.on{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
.insbtn.on .badge{border-color:var(--accent-soft)}
@media (max-width:1080px){ .rail-g,.rail-g.collapsed{display:none} .insbtn{display:none} }

.viewall{margin-top:14px;text-align:center;padding:12px;border:1px solid var(--line);border-radius:8px;color:var(--ink);font-weight:700;font-size:14.5px}
/* `svg .grid` was a SECOND gridline rule — darker ink (--line, not --line2), plain stroke-width, and
   more specific than the shared .grid above, so it quietly won everywhere and My Dashboard's grid
   came out heavier and darker than the net-sales chart's. Gone: one .grid rule, in the chart block. */
svg text{font-family:Urbanist,sans-serif;fill:var(--ink3);font-size:12px}

/* region rollup strip */
.rollup{display:flex;gap:12px;margin-bottom:12px}
.rup{flex:1;background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:12px 16px}
.rup .rn{font-size:var(--t-body);color:var(--ink2);font-weight:600}
.rup .rv{font-size:22px;font-weight:800;margin-top:3px}
.rup .rd{font-size:var(--t-cap);color:var(--ink3);margin-top:1px}
.rup .rb{height:6px;border-radius:4px;background:var(--line2);margin-top:9px;overflow:hidden}
.rup .rb i{display:block;height:100%;background:var(--bar)}

/* table */
.tbl{width:100%;border-collapse:collapse;font-size:var(--t-body)}
/* Cells are the ONE place the 4px sweep rounds a value DOWN, and only on the horizontal axis: 10px → 8px
   rather than 12px. Rounding up cost ~4px per column, which on a 13-column table is 52px — it pushed five
   tables that fitted their panel at 1280 into horizontal scrolling and made every table that already
   scrolled worse by 30–50px. Vertical padding still rounds UP (9 → 12), so rows are airier, which is the
   direction the rest of this sweep goes anyway. 8 and 12 are both on the grid; the grid is what was asked
   for, and a table that has to be dragged sideways to read a number is not a rounder corner. */
.tbl th{text-align:left;font-size:var(--t-body);font-weight:600;color:var(--ink3);padding:8px 8px;border-bottom:1px solid var(--line)}
.tbl td{padding:12px 8px;border-bottom:1px solid var(--line2)}
/* Only the OUTER cells stretch to the panel's content edge, so a table lines up with the heading
   above it instead of sitting inside it. Interior columns keep their tighter padding, so the
   change buys alignment without loosening the grid. Tied to .panel's padding above — move one,
   move the other. */
.tbl th:first-child,.tbl td:first-child{padding-left:16px}
.tbl th:last-child,.tbl td:last-child{padding-right:16px}
.tbl tr{cursor:pointer}.tbl tbody tr:hover td{background:#f3f5fc}
.tbl .nm{font-weight:700}
.tbl .r{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}   /* full-ringgit figures stay on one line */
.tbl .muted{color:var(--ink3)}
/* Rank column — a position, not a measure, so it recedes: tertiary ink, tabular, narrow enough
   to read as a gutter beside the name rather than a column competing with it. */
.dt-rank{color:var(--ink3);font-variant-numeric:tabular-nums;font-size:var(--t-body);font-weight:500}
/* Opt-in nowrap for a label cell. The rank column took width from Region, which wrapped
   "Kuala Lumpur" onto two lines and left rows at uneven heights — the exact unevenness that
   already had to be designed out of the movers chart's labels. */
.tbl .nw{white-space:nowrap}
.warnv{font-weight:700;color:var(--danger)}   /* bad / concern — semantic red + weight */
.goodv{font-weight:700;color:var(--good)}     /* good / healthy — semantic green + weight */
.det-head .flag.warnv{border-color:var(--danger)}   /* detail-page flag pill picks up the sentiment */
.det-head .flag.goodv{border-color:var(--good)}
.sc{font-weight:800;text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
.sc.sc-crit{color:var(--danger)}                 /* ops health score — the two exceptional bands */
.sc.sc-healthy{color:var(--good)}
/* .sc-watch is deliberately NOT coloured. The band is 35–50, which is 53 of the network's 78
   outlets — scores cluster tight (range 24–64, median 45), so "middling" is simply what an
   ordinary outlet looks like. Painting two-thirds of the estate amber made the column read as
   an emergency and left the 8 genuinely critical outlets with nothing to stand out against.
   Same rule this file already applies to concern thresholds: don't colour the normal majority,
   it floods. Red and green now mark only the exceptions; the pack reads as plain ink.
   Bands themselves are unchanged (<35 / 35–50 / >50) — operations.html prints them in a legend.
   The .rstack distribution bar keeps all three fills: every segment of a stacked bar needs one. */
.sc.sc-crit::after{content:" ▼";font-size:11px;color:var(--danger)}
.tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}   /* wide table scrolls inside its card, not clipped at viewport */

/* ---- scalable data table (search · sort · filter · scrolling body with sticky header) ---- */
/* 18px sides to match the panel header and the table's outer cells — was 16px, which put the
   search box 2px out of line with everything above and below it */
.dt-controls{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:16px 20px 12px}
.dt-search{flex:0 1 260px;min-width:150px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}
.dt-filter{padding:8px 12px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink2);font-weight:600;cursor:pointer}

/* ---- .segsw — segmented switch, for a SMALL set of mutually exclusive views ----
   Lives with the controls rather than on a page because it is chrome, not layout, and the deck will
   want it again the next time a panel can be read four ways.
   Chosen over a <select> for Top Members' Rank by (user, 2026-07-31: the dropdown "doesn't look very
   convenient"): a select costs two clicks and a menu, and hides the other three choices until opened,
   so the reader cannot see that the panel HAS four rankings. Four pills cost one click and state the
   whole set. The trade is width — about 230px of panel header — which is why this is for four or five
   options, and a select is still right past that.
   Real <button>s in a role=group: four tab stops, Enter/Space, and a focus ring, none of it hand-rolled. */
/* The noun beside the switch — "Colour by", "Rank by", "Group by". It sits OUTSIDE the pills rather
   than being folded into them ("Type: All", the DataTable select's trick): that works for a filter,
   whose default is All, but a segmented switch has no null state — every option is a real view, so
   the noun cannot be one of the choices. Was page-local to customers.html until Sales Trend's
   granularity control became a switch too (2026-08-05); the second use is what brings it in here. */
.rankby{display:flex;align-items:center;gap:9px;font-size:var(--t-body);font-weight:600;color:var(--ink3);
  white-space:nowrap}
.segsw{display:inline-flex;align-items:center;gap:2px;padding:4px;background:var(--line2);
  border:1px solid var(--line);border-radius:999px}
.segsw button{font:inherit;font-size:var(--t-body);font-weight:600;line-height:1.2;color:var(--ink2);
  background:transparent;border:0;border-radius:999px;padding:8px 12px;cursor:pointer;
  white-space:nowrap;transition:background-color .12s,color .12s}
.segsw button:hover{background:#fff;color:var(--ink)}
/* Selected is a filled pill — the same accent that marks an active nav item and the chosen RFM block,
   so "this one is live" looks the same wherever the deck says it. */
.segsw button.on{background:var(--accent);color:#fff}
.segsw button.on:hover{background:var(--accent);color:#fff}
.segsw button:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
.dt-count{margin-left:auto;font-size:var(--t-cap);color:var(--ink3);font-weight:500;white-space:nowrap}
/* Result count relocated into a panel title (countInto) — wrapped in parentheses, subdued. */
.dt-title-count{color:var(--ink3);font-weight:600;font-size:.82em}
.dt-title-count:not(:empty)::before{content:"("}
.dt-title-count:not(:empty)::after{content:")"}
.dt-scroll{max-height:62vh;overflow:auto}
.dt-scroll.dt-flow{max-height:none;overflow:visible}   /* flow list: no inner scroll, grows with the page */
/* …unless the table is wider than the card, which overflow:visible would let it spill out of. Set by
   fitX() in datatable.js, which measures the table against its card — see the note there for why the
   horizontal scroll is not simply on all the time. */
.dt-scroll.dt-flow.dt-xscroll{overflow-x:auto;overflow-y:visible}
.dt-scroll thead th{position:sticky;top:0;background:var(--card);z-index:3;box-shadow:0 1px 0 var(--line),0 5px 8px -3px rgba(20,24,30,.16)}
.dt-scroll.dt-flow thead th{top:-22px}   /* page-scroll flow list: offset .content's 22px top padding so the frozen header sits flush under the top bar (no row peeking above) */
.dt-end{text-align:center;color:var(--ink3);font-size:var(--t-cap);font-weight:500;letter-spacing:.02em;padding:16px 12px;border-top:1px solid var(--line2)}
/* The sort affordance. It is on EVERY column now (user, 2026-08-05), which changed what it may cost:
   one icon on the two or three sortable columns of a DataTable was free, sixteen of them across the
   deck's static tables were not. Measured at the old 11px glyph + 5px margin it added 121px to Top
   Members' ten columns and 34px to Sales' Promotions table — enough to push three tables that fitted
   into .tblwrap's silent side-scroll.
   So the glyph is 9px with a 3px margin (~8px a column against ~13px), and `white-space:nowrap` is NOT
   restated: several tables wrap their headers deliberately to collapse a column to its value width, and
   nowrap here would have overridden that and cost far more than the icon does. The active column's
   arrow is the same width as the resting glyph, so sorting never reflows the table. */
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable::after{content:'⇅';opacity:.3;margin-left:3px;font-size:9px}
.tbl th.sortable.asc::after{content:'▲';opacity:.7;font-size:9px}
.tbl th.sortable.desc::after{content:'▼';opacity:.7;font-size:9px}
.tbl th.sortable:hover::after{opacity:.75}
/* Column-header menu (sort + multi-select filter) — trigger in the header + its popover. */
.tbl th.has-menu{white-space:nowrap}
.th-menu{border:none;background:none;cursor:pointer;color:var(--ink3);font-size:11px;line-height:1;
  margin-left:5px;padding:4px 4px;border-radius:8px;vertical-align:middle;font-family:inherit}
.th-menu:hover{background:var(--accent-soft);color:var(--accent)}
.th-menu.active{color:var(--accent)}
.dt-colmenu{background:#fff;border:1px solid var(--line);border-radius:12px;padding:8px;min-width:186px;
  box-shadow:0 10px 28px rgba(17,24,39,.16);font-size:13px;color:var(--ink)}
.dt-cm-sort{display:flex;flex-direction:column}
.dt-cm-item{text-align:left;border:none;background:none;padding:8px 12px;border-radius:8px;cursor:pointer;
  font:inherit;font-weight:600;color:var(--ink)}
.dt-cm-item:hover{background:var(--accent-soft)}
.dt-cm-item.on{color:var(--accent)}
.dt-cm-hd{font-size:10.5px;font-weight:700;color:var(--ink3);text-transform:uppercase;letter-spacing:.03em;
  padding:12px 12px 4px;margin-top:4px;border-top:1px solid var(--line2)}
.dt-cm-list{max-height:230px;overflow-y:auto}
.dt-cm-opt{display:flex;align-items:center;gap:9px;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:500}
.dt-cm-opt:hover{background:var(--accent-soft)}
.dt-cm-opt input{cursor:pointer;accent-color:var(--accent)}
.dt-cm-foot{display:flex;justify-content:flex-end;padding:8px 8px 4px;margin-top:4px;border-top:1px solid var(--line2)}
.dt-cm-clear{border:none;background:none;color:var(--accent);font-weight:700;cursor:pointer;font:inherit;padding:4px}
.dt-cm-clear:hover{text-decoration:underline}
.dt-empty td{text-align:center;color:var(--ink3);font-weight:600;padding:32px 12px}
.dt-empty:hover td{background:transparent}
/* menu-engineering class summary chips (filter the products table) */
.clstrip{display:flex;flex-wrap:wrap;gap:10px}
.clschip{flex:1 1 150px;min-width:150px;border:1px solid var(--line);border-radius:12px;background:#fff;padding:12px 16px;cursor:pointer;text-align:left;font:inherit;color:var(--ink)}
.clschip:hover{background:#f3f5fc}
.clschip.on{border-color:var(--accent);background:var(--accent-soft)}
.clschip:disabled{opacity:.5;cursor:default}
.clschip:disabled:hover{background:#fff}
.clschip .cl{font-size:13.5px;font-weight:700}
.clschip .cn{font-size:24px;font-weight:800;line-height:1.1;margin:2px 0}
.clschip .ca{font-size:12px;color:var(--ink3);font-weight:600}
.rgcell b{font-weight:700}.rgcell .u{display:block;font-size:var(--t-cap);color:var(--ink3);font-weight:500;margin-top:1px}
.dt-showall{display:block;width:100%;margin-top:8px;padding:12px;border:1px dashed var(--line);border-radius:8px;background:#fff;font:inherit;font-weight:700;font-size:13.5px;color:var(--ink2);cursor:pointer}
.dt-showall:hover{background:#f3f5fc}

/* ---- grouped Genie-insights feed (dedup: many outlet-level signals collapse into one) ---- */
.grp-toggle{border:none;background:none;color:var(--ink);font-weight:700;cursor:pointer;font-size:13px;padding:8px 0 4px;font-family:inherit}
.grp-list{margin-top:6px;border-top:1px solid var(--line2);padding-top:8px}
.grp-item{display:flex;justify-content:space-between;gap:12px;padding:8px 4px;font-size:13px;border-bottom:1px solid var(--line2);cursor:pointer}
.grp-item:last-child{border-bottom:none}
.grp-item:hover{background:#f5f7fd}
.grp-item .sc{font-weight:800}
.feed-empty{padding:40px;text-align:center;color:var(--ink3)}

/* ---- global outlet selector (region → outlet popover), opened from the Location filter ---- */
.omodal-wrap{position:fixed;inset:0;z-index:60;display:none}
.omodal-wrap.open{display:block}
.omodal-backdrop{position:absolute;inset:0;background:rgba(20,24,31,.34)}
.omodal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:680px;max-width:94vw;height:600px;max-height:88vh;background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.28);display:flex;flex-direction:column;overflow:hidden}
.omodal-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--line)}
.omodal-head h3{margin:0;font-size:18px;font-weight:700;line-height:1.2}
.omodal-head .nclose{border:none;background:none;font-size:24px;cursor:pointer;color:var(--ink2);line-height:1}
.omodal-search{position:relative;padding:12px 20px;border-bottom:1px solid var(--line)}
/* left = the row's own padding (20px) + the input's 13px inset; it tracks the padding above, so it moved
   with it when the paddings went onto the 4px grid. */
.omodal-search svg{position:absolute;top:50%;left:34px;transform:translateY(-50%);opacity:.5}
.omodal-input{width:100%;padding:12px 16px 12px 40px;border:1px solid var(--line);border-radius:12px;font:inherit;background:#fff;color:var(--ink)}
.omodal-selectall{display:flex;align-items:center;gap:11px;padding:16px 20px;border-bottom:1px solid var(--line);font-weight:700;cursor:pointer;user-select:none}
.omodal-mid{display:flex;min-height:0;flex:1}
.omodal-regions{width:44%;border-right:1px solid var(--line);overflow-y:auto;padding:12px}
.omodal-outlets{flex:1;overflow-y:auto;padding:12px 20px}
.oregion{display:flex;align-items:center;gap:10px;padding:12px 12px;border-radius:12px;cursor:pointer}
.oregion:hover{background:#f3f5fc}.oregion.active{background:var(--accent-soft)}
.oregion .orn{font-weight:600;color:var(--ink);line-height:1.2;flex:1;font-size:14.5px}
.oregion.active .orn{color:var(--accent)}
.oregion .orc{font-size:12.5px;color:var(--ink3);font-weight:700;white-space:nowrap}
.oregion .orx{opacity:.5;font-size:13px}
.orow{display:flex;align-items:center;gap:11px;padding:12px 4px;cursor:pointer;border-bottom:1px solid var(--line2);font-size:14.5px}
.orow.res{gap:12px}.orow .osub{font-size:12.5px;color:var(--ink3)}
.orow .obadge{margin-left:auto;font-size:var(--t-cap);font-weight:800;padding:4px 12px;border-radius:12px}
.obadge.new{background:#d7f7ec;color:var(--good)}.obadge.closed{background:#fde3e9;color:var(--danger)}
.orow.disabled{opacity:.45;cursor:default}
/* ── Design-system checkbox — one canonical control used everywhere (filters, login, …). ── */
input[type=checkbox]{appearance:none;-webkit-appearance:none;margin:0;flex:0 0 auto;cursor:pointer;width:19px;height:19px;border:1px solid var(--line-c);border-radius:8px;background:#fff;vertical-align:middle;transition:background-color .12s,border-color .12s}
input[type=checkbox]:checked{border-color:var(--accent);background:var(--accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5 10 17.5 19 7'/%3E%3C/svg%3E") center/70% no-repeat}
input[type=checkbox]:indeterminate{border-color:var(--accent);background:var(--accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M6 12 18 12'/%3E%3C/svg%3E") center/66% no-repeat}
input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.omodal-back{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--line)}
.omodal-back a{color:var(--accent);font-weight:700;cursor:pointer}.omodal-back span{color:var(--ink3);font-size:13.5px}
.omodal-empty{text-align:center;padding:44px 24px;color:var(--ink3);font-size:14px;line-height:1.55}
.omodal-empty .eicon{width:66px;height:66px;border-radius:16px;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:28px;margin:0 auto 16px}
.omodal-empty b{display:block;color:var(--ink);font-size:16px;margin-bottom:6px}
.omodal-foot{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-top:1px solid var(--line)}
.omodal-foot .omodal-count{color:var(--ink3);font-size:14px;margin-right:14px}
.omodal-foot .btn.pri[disabled]{opacity:.5;cursor:default}

/* ---- channel multi-select popover (anchored under its pill) ---- */
.filterpop{position:fixed;z-index:60;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 14px 40px rgba(0,0,0,.18);display:none;padding:8px}
.filterpop.open{display:block}
.fp-sa{display:flex;align-items:center;gap:10px;padding:12px 12px;border-bottom:1px solid var(--line);font-weight:700;cursor:pointer;user-select:none;margin-bottom:4px}
.fp-row{display:flex;align-items:center;gap:11px;padding:12px 12px;border-radius:8px;cursor:pointer;font-size:14.5px;min-width:230px}
.fp-row:hover{background:#f1f4fb}
.filterpop input[type=checkbox]{width:18px;height:18px;pointer-events:none}

/* ---- period picker modal ---- */
.pmodal{width:640px;height:auto;max-height:none}
.pmodal-top{display:flex;align-items:stretch;gap:10px;padding:16px 20px;border-bottom:1px solid var(--line)}
.pfield{flex:1;border:1px solid var(--line);border-radius:12px;padding:12px 16px;cursor:pointer}
.pfield.on{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.pfield.on.cmpon{border-color:#7c84a0;box-shadow:0 0 0 2px #eef1fa}
.pcmp-arrow{flex:0 0 auto;align-self:center;display:flex;color:var(--ink3)}
.pcmp-arrow svg{width:20px;height:20px}
.pfield .pfl{font-size:12px;color:var(--ink3);font-weight:700;display:flex;align-items:center;gap:6px}
.pfield .pfl .pd{width:8px;height:8px;border-radius:50%;background:var(--accent);display:inline-block}
.pfield .pfl .pd.cmp{background:#7c84a0}
.pfield .pfv{font-weight:700;font-size:14.5px;margin-top:3px}
/* 308px was the 6-row calendar's natural height, pinned so the modal doesn't jump between a 5-week
   and a 6-week month. As a fixed HEIGHT it also pinned the preset column, which the list had already
   outgrown: seven presets need 329px, so "Custom range" was rendering outside the column, across the
   footer rule and under the Cancel button. min-height keeps the calendar steady and lets the taller
   side — the presets, now eleven of them — set the modal's height instead of spilling out of it. */
.pmodal-main{display:flex;min-height:308px}
.presets{width:38%;border-right:1px solid var(--line);padding:8px;display:flex;flex-direction:column;gap:1px}
/* Denser than the 12px it was: eleven rungs of a duration ladder are meant to be read as a set, and
   at 45px a row the set was taller than the calendar it sits beside. 8px holds a 34px row, which is
   what the deck's other list rows (.set-navitem, .fp-row) run at. */
.preset{padding:8px 12px;line-height:1.25;border-radius:8px;cursor:pointer;font-weight:600;font-size:14px}
.preset:hover{background:#f3f5fc}.preset.on{background:var(--accent);color:#fff}
/* Custom range — in the list but not of it. See PRESETS in shell.js: it is a state readout and a
   pointer at the calendar, not a duration, so the ladder above it stays a ladder. */
/* The rule is drawn in the gap ABOVE the row, not as the row's own border-top: this row is the one
   most often selected (the deck's default period matches no preset), and a selected row's indigo fill
   paints over its own border — the separator would vanish exactly when the row is highlighted. */
.preset.psep{margin-top:11px;position:relative}
.preset.psep::before{content:"";position:absolute;left:0;right:0;top:-6px;height:1px;background:var(--line)}
.cal{flex:1;padding:12px 16px}
.cal-head{display:flex;align-items:center;margin-bottom:8px;gap:6px}
.cal-head .cal-nav:first-child{margin-right:auto}
.cal-head .cal-nav:last-child{margin-left:auto}
.cal-head select{appearance:none;-webkit-appearance:none;border:1px solid var(--line);border-radius:8px;padding:8px 24px 8px 12px;font:inherit;font-size:13px;font-weight:600;color:var(--ink);cursor:pointer;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3.5 8 3.5 5 7Z' fill='%2369707c'/%3E%3C/svg%3E") no-repeat right 8px center;background-size:9px;transition:border-color .12s,box-shadow .12s}
.cal-head select:hover{border-color:var(--ink2)}
.cal-head select:focus-visible{outline:none;border-color:var(--ink2);box-shadow:0 0 0 2px var(--accent-soft)}
.cal-nav{border:none;background:none;cursor:pointer;font-size:18px;color:var(--ink2);padding:4px 8px;border-radius:8px}
.cal-nav:hover{background:#f1f4fb}
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;text-align:center}
.cal-dow{font-size:12px;color:var(--ink3);font-weight:700;padding:8px 0}
.cal-day{padding:8px 0;font-size:13.5px;border-radius:8px;cursor:pointer}
.cal-day:hover{background:#f1f4fb}
.cal-day.muted{color:var(--ink3);opacity:.45}
.cal-day.inrange{background:var(--accent-soft);border-radius:0}
.cal-day.edge{background:var(--accent);color:#fff}
.cal-day.cinrange{background:#eef1fa;border-radius:0}
.cal-day.cedge{background:#3a31d8;color:#fff}
.cal-day.ctx{background:var(--accent-soft);opacity:.55}
.tblwrap .foot,.tbl+.foot,.tblwrap+.foot{padding:12px;text-align:center;color:var(--ink3);font-size:var(--t-cap)}

/* ---- Settings modal (Claude-style: left icon sub-nav + row-based content panes) ---- */
.setmodal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:940px;max-width:94vw;height:640px;max-height:88vh;background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.28);display:flex;overflow:hidden}
.set-nav{width:250px;flex:0 0 250px;background:#f5f7fd;border-right:1px solid var(--line);padding:20px 12px;overflow-y:auto}
.set-navgrp{font-size:12.5px;font-weight:700;color:var(--ink3);padding:8px 12px 12px}
.set-navitem{display:flex;align-items:center;gap:11px;padding:12px 12px;border-radius:12px;color:var(--ink2);font-weight:600;font-size:15px;cursor:pointer;text-decoration:none;margin-bottom:2px}
.set-navitem:hover{background:#e8ecf8}
.set-navitem.on{background:#dde4f4;color:var(--ink);font-weight:700}
.set-navitem svg{width:18px;height:18px;flex:0 0 auto;color:var(--ink3)}
.set-navitem.on svg{color:var(--ink)}
.set-main{flex:1;position:relative;overflow-y:auto;padding:28px 36px 32px}
.set-close{position:absolute;top:20px;right:22px;border:none;background:none;font-size:26px;line-height:1;color:var(--ink2);cursor:pointer;padding:4px 8px;border-radius:8px}
.set-close:hover{background:#f1f4fb;color:var(--ink)}
.set-panel[hidden]{display:none}
.set-h{font-size:22px;font-weight:800;margin:0 0 4px}
.set-sub{color:var(--ink3);font-size:14px;margin:0 0 10px}
.set-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 0;border-bottom:1px solid var(--line2)}
.set-lbl{font-size:15.5px;color:var(--ink);font-weight:600}
.set-input{width:320px;max-width:52%;padding:12px 16px;border:1px solid var(--line);border-radius:12px;font:inherit;font-size:14.5px;background:#fff;color:var(--ink)}
.set-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.set-static{font-size:14.5px;color:var(--ink3);font-weight:600}
.set-err{color:var(--danger);font-size:13.5px;font-weight:600;margin-top:15px;display:none}
.set-foot{display:flex;align-items:center;justify-content:flex-end;gap:16px;margin-top:26px}
.set-saved{color:var(--good);font-weight:700;font-size:14px;opacity:0;transform:translateY(2px);transition:opacity .2s,transform .2s}
.set-saved.show{opacity:1;transform:none}
.set-back{border:none;background:none;color:var(--accent);font-weight:700;font-size:14px;cursor:pointer;padding:0;margin-bottom:14px}
.set-back:hover{text-decoration:underline}
.otp-row{display:flex;gap:10px;margin:8px 0 4px}
.otp-box{width:52px;height:60px;text-align:center;font-size:24px;font-weight:700;border:1px solid var(--line);border-radius:12px;background:#fff;color:var(--ink);font-family:inherit}
.otp-box:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.otp-demo{font-size:12.5px;color:var(--ink3);margin-top:12px;font-style:italic}
.otp-resend{font-size:13.5px;color:var(--ink3);margin-top:14px}
.otp-resend a{color:var(--accent);font-weight:700;cursor:pointer}
.otp-resend a:hover{text-decoration:underline}
.otp-resend a.disabled{color:var(--ink3);cursor:default;font-weight:600;opacity:.7;text-decoration:none}
.pw-forgot{margin-top:16px}
.pw-forgot a{color:var(--accent);font-weight:700;font-size:14px;cursor:pointer}
.pw-forgot a:hover{text-decoration:underline}

/* ---- global toast: cute pop-in at top-centre, auto-dismisses, manual × close ---- */
.toast-host{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:200;display:flex;flex-direction:column;align-items:center;gap:10px;pointer-events:none;width:max-content;max-width:92vw}
.toast{pointer-events:auto;display:flex;align-items:center;gap:11px;background:#1b1f26;color:#fff;padding:12px 12px 12px 16px;border-radius:16px;box-shadow:0 14px 38px rgba(0,0,0,.30);font-weight:600;font-size:14.5px;opacity:0;transform:translateY(-18px) scale(.94);transition:opacity .3s cubic-bezier(.2,.9,.3,1.35),transform .3s cubic-bezier(.2,.9,.3,1.35)}
.toast.in{opacity:1;transform:none}
.toast .tk{width:22px;height:22px;border-radius:50%;background:var(--good);color:#fff;display:grid;place-items:center;flex:0 0 auto;font-size:13px;font-weight:800}
.toast .tk.err{background:var(--danger)}
.toast .tm{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70vw}
.toast .tact{background:none;border:none;color:#fff;font:inherit;font-size:14px;font-weight:800;cursor:pointer;padding:4px 8px;border-radius:8px;flex:0 0 auto;text-decoration:underline;text-underline-offset:2px}
.toast .tact:hover{background:rgba(255,255,255,.14)}
.toast .tx{background:none;border:none;color:rgba(255,255,255,.6);cursor:pointer;font-size:19px;line-height:1;padding:4px 8px;border-radius:8px;flex:0 0 auto}
.toast .tx:hover{color:#fff;background:rgba(255,255,255,.12)}

/* ---- Ask Genie: full-page chat (deliberately NOT a slide-in — needs room, users
   reference it against the data pages, so it gets its own full workspace) ---- */
.content.ask{display:flex;flex-direction:column;height:100%;padding:0;overflow:hidden}
.ask-page{flex:1;display:flex;flex-direction:column;min-height:0;width:100%;max-width:820px;margin:0 auto}
.ask-conv{flex:1;overflow-y:auto;padding:28px 24px 16px;display:flex;flex-direction:column;gap:16px}
.ask-empty{margin:auto;text-align:center;max-width:560px;padding:20px 0}
.ask-empty .ask-spark{font-size:34px;color:var(--accent);line-height:1}
.ask-empty h1{font-size:26px;font-weight:800;margin:10px 0 6px}
.ask-empty p{color:var(--ink3);font-size:15px;margin:0 0 8px;line-height:1.55}
.ask-block{max-width:460px;margin:22px auto 0}
.ask-block-label{font-size:12px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--ink3);margin-bottom:10px;text-align:left}
.ask-suggest{display:flex;flex-direction:column;gap:10px}
.ask-suggest button{display:flex;align-items:center;gap:10px;border:1px solid var(--line);background:#fff;border-radius:12px;padding:16px 16px;font:inherit;font-size:14.5px;color:var(--ink);text-align:left;cursor:pointer}
.ask-suggest button:hover{background:#f3f5fc;border-color:var(--line-c)}
/* daily brief — the recurring, personalised prompt; the hero suggestion above the generic starters */
.ask-brief{display:flex;align-items:center;gap:14px;width:100%;max-width:460px;margin:22px auto 0;text-align:left;
  border:1px solid var(--line-c);background:#fff;border-radius:16px;padding:16px 16px;cursor:pointer;font:inherit;
  box-shadow:0 1px 2px rgba(0,0,0,.04)}
.ask-brief:hover{background:#fafbff;border-color:var(--ink3)}
.ask-brief-ico{flex:0 0 auto;width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:var(--accent-soft);color:var(--accent)}
.ask-brief-ico svg{width:22px;height:22px}
.ask-brief-body{display:flex;flex-direction:column;gap:3px;min-width:0}
.ask-brief-lab{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--accent)}
.ask-brief-q{font-size:14.5px;font-weight:600;color:var(--ink);line-height:1.35}
.ask-brief-hint{font-size:12px;color:var(--ink3);line-height:1.35;margin-top:1px}
.ask-brief-go{flex:0 0 auto;align-self:center;color:var(--ink3);font-size:18px;font-weight:700}
/* subtle clear affordance — a muted link by the composer, not a prominent top-right pill */
.ask-actions{display:flex;justify-content:flex-end;padding:8px 24px 0}
.ask-actions[hidden]{display:none}   /* keep the toggle honest — class display was overriding [hidden] */
.ask-clearlink{border:none;background:none;color:var(--ink3);font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;padding:4px 8px;border-radius:8px}
.ask-clearlink:hover{color:var(--ink2);text-decoration:underline}
/* small confirm / warning dialog (reusable) */
.confirmbox{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:420px;max-width:92vw;background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.28);padding:24px 24px 20px}
.confirmbox h3{font-size:18px;font-weight:800;margin:0 0 8px}
.confirmbox p{color:var(--ink2);font-size:14px;line-height:1.55;margin:0 0 20px}
.confirm-actions{display:flex;justify-content:flex-end;gap:10px}
.ask-msg{display:flex;max-width:100%}
.ask-msg.u{justify-content:flex-end}
.ask-bubble{padding:12px 16px;border-radius:16px;font-size:14.5px;line-height:1.55;max-width:80%}
.ask-msg.g .ask-bubble{background:#eef1fa;color:var(--ink);border-top-left-radius:4px}
.ask-msg.u .ask-bubble{background:var(--ink);color:#fff;border-top-right-radius:4px}
/* contextual follow-up prompts under the latest answer (keeps the discussion going) */
.ask-follow{display:flex;flex-wrap:wrap;gap:8px;max-width:80%;margin-top:-4px}
.ask-follow button{border:1px solid var(--line);background:#fff;border-radius:16px;padding:8px 16px;font:inherit;font-size:13px;font-weight:600;color:var(--ink2);cursor:pointer}
.ask-follow button:hover{background:#f3f5fc;border-color:var(--line-c);color:var(--ink)}
.ask-composer{display:flex;gap:10px;padding:16px 24px 24px;border-top:1px solid var(--line2);background:var(--bg)}
.ask-composer input{flex:1;padding:16px 16px;border:1px solid var(--line);border-radius:12px;font:inherit;font-size:15px;background:#fff;color:var(--ink)}
.ask-composer input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.ask-composer .btn.pri{padding:0 20px}
@media (max-width:860px){
  .setmodal{flex-direction:column;width:96vw;height:90vh}
  .set-nav{width:auto;flex:0 0 auto;display:flex;gap:6px;overflow-x:auto;padding:12px 12px;border-right:none;border-bottom:1px solid var(--line)}
  .set-navgrp{display:none}
  .set-navitem{margin-bottom:0;white-space:nowrap}
  .set-main{padding:24px 20px 28px}
  .set-input{width:auto;flex:1;max-width:none;min-width:0}
  .set-row{gap:14px}
  .otp-row{gap:7px}
  .otp-box{width:44px;height:54px;font-size:21px}
}

/* outlet detail */
.det-head{display:flex;flex-direction:column;gap:9px;margin-bottom:12px}
.back{align-self:flex-start;color:var(--ink2);text-decoration:none;font-size:14.5px;font-weight:700}
.det-titlerow{display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:8px 16px}
.det-head h2{margin:0;font-size:26px}
.det-head .rg{color:var(--ink3);font-size:15px}
.det-head .flag{flex:0 0 auto;margin-top:3px;font-size:12.5px;font-weight:800;color:var(--ink);border:1.5px solid var(--ink);border-radius:16px;padding:8px 16px;white-space:nowrap}
.det-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:stretch}   /* see .two */

/* .fillbars — a bar-list (hbar / rankBars / divergeBar) that FILLS its stretched panel instead of
   leaving whitespace below: the panel becomes a flex column and the rows grow to distribute evenly
   over the height its taller row-mate sets. Add the class to the mount div. Part of the row-fit
   standard (equal-height rows shouldn't leave a shorter component half-empty). */
.panel:has(> .fillbars){display:flex;flex-direction:column}
.fillbars{flex:1;min-height:0;display:flex;flex-direction:column}
.fillbars > .rankbars,.fillbars > .divbars{flex:1;min-height:0;display:flex;flex-direction:column}
.fillbars > .hbar,.fillbars .rb-row,.fillbars .dv-row{flex:1}
.linkcta{display:inline-block;margin-top:6px;padding:12px 20px;border:1.5px solid var(--accent);border-radius:8px;color:#fff;font-weight:700;font-size:14.5px;text-decoration:none;background:var(--accent)}
.statline{display:flex;justify-content:space-between;align-items:baseline;padding:12px 0;border-top:1px solid var(--line2);font-size:var(--t-body)}
.statline b{font-weight:800}
.statline .muted{color:var(--ink3);font-size:var(--t-cap);font-weight:500}

/* misc layout helpers for deeper screens */
.section-h{font-size:20px;font-weight:800;margin:4px 0 12px}
.bars-h{display:flex;align-items:stretch;gap:18px;height:190px;padding:0 8px}
.barcol{flex:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:8px}
.barcol .bk{width:62%;min-height:4px;background:var(--bar);border-radius:4px 4px 0 0}
.barcol .bl{font-size:var(--t-body);color:var(--ink2);font-weight:600}
/* Section tabs — a SEGMENTED control (a soft track holding the tabs, with a raised white pill for
   the active one). The old underlined bar blended into the page and didn't read as its own thing;
   the enclosed track gives the switcher a clear boundary and the lifted pill makes the active tab
   unmistakable. inline-flex shrinks it to its content (a discrete control, not a full-width rule),
   and overflow-x lets the many-tab pages scroll rather than blow out the column. */
/* The TRACK is neutral (user, 2026-08-04: "tabination background doesn't need to be purple, remain
   neutral — only words can be vibrant once active"). It was --bar2, the pale violet that also fills a
   chart's comparison series, which put a data colour behind a navigation control and made the switcher
   the most saturated object on several pages. The active state is carried by the white pill, its lift
   and the accent WORD — which is legitimate: the tab is the one thing in this row you can click. */
.tabs{display:inline-flex;max-width:100%;gap:3px;margin-bottom:20px;padding:4px;
  background:#e9edf7;border:1px solid var(--line);border-radius:12px;overflow-x:auto}
.tabs a{flex:0 0 auto;padding:8px 16px;border-radius:8px;text-decoration:none;
  color:var(--ink2);font-weight:600;font-size:13.5px;white-space:nowrap}
.tabs a.on{background:var(--card);color:var(--accent);font-weight:700;
  box-shadow:0 1px 2px rgba(17,24,39,.06),0 2px 5px rgba(17,24,39,.09)}
/* max-width for the same reason as .rankbars: .ht is flex:1, so in a full-width panel a
   six-region share list stretched its bars past 640px, where lengths stop being comparable
   and the panel reads as far wider than its content. */
.hbar{display:flex;align-items:center;gap:12px;padding:8px 0;max-width:640px}
.hbar .hl{width:150px;font-weight:600;font-size:var(--t-body)}
.hbar .ht{flex:1;height:14px;background:var(--line2);border-radius:8px;overflow:hidden}
.hbar .ht i{display:block;height:100%;background:var(--bar)}
.hbar .hv{width:52px;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
/* clickable mix rows/bars — drill from a share into that slice's detail */
a.hbar-link{text-decoration:none;color:inherit;cursor:pointer;border-radius:8px;margin:0 -8px;padding-left:8px;padding-right:8px;transition:background .12s}
a.hbar-link:hover{background:var(--line2)}
a.hbar-link:hover .hl{text-decoration:underline}
a.hbar-link:hover .ht i{background:var(--accent)}
a.barcol-link{text-decoration:none;color:inherit;cursor:pointer;border-radius:8px 8px 8px 8px;transition:background .12s}
a.barcol-link:hover{background:var(--line2)}
a.barcol-link:hover .bk{background:var(--accent)}
a.barcol-link:hover .bl{color:var(--ink);text-decoration:underline}

/* ---- Conflict empty state — shown when a page-local drill and the global Location filter
   exclude each other. Names the contradiction and carries the exit, rather than leaving a bare
   empty table to be read as "broken". ---- */
.dt-conflict{padding:36px 20px 40px;text-align:center;color:var(--ink2);font-size:14.5px;line-height:1.7}
.dt-conflict b{color:var(--ink);font-weight:800}
.dt-conflict a{color:var(--ink);font-weight:700;text-decoration:underline;cursor:pointer}

/* ---- Data bar in a table cell (.dbar) — the fix for "I can't cross-view two columns".
   A single-sort table can only rank one measure at a time, so comparing "top by net sales" with
   "top by penetration" meant sorting twice and remembering. A bar in each cell lets the eye scan
   ACROSS columns on one pass: a long net-sales bar beside a stub of a penetration bar is a
   finding you can see without touching the sort. Fixed track width so bars align down the
   column; the number stays for precision. ---- */
.dbar{display:inline-flex;align-items:center;gap:9px;justify-content:flex-end;width:100%}
.dbar .dbt{flex:0 0 52px;height:6px;border-radius:4px;background:var(--line2);overflow:hidden}
.dbar .dbt i{display:block;height:100%;background:var(--bar);border-radius:4px}
.dbar .dbv{flex:0 0 auto;min-width:50px;text-align:right;font-variant-numeric:tabular-nums}
/* a measure that's bad when LOW reads with the danger tone, per the semantic policy — colour by
   good/bad, not by direction */
/* FILL takes the mark weight, the NUMBER beside it the text weight: the same split as the outlet
   status dots (user, 2026-08-04 — a status indicator can be vibrant; the type next to it still has
   to clear 4.5:1, and that is what --danger/--good are darkened for). A 6px bar in the mark hue is
   what makes an ops score of 31 findable while scrolling. */
.dbar.low .dbt i{background:var(--danger-mark)}
.dbar.low .dbv{color:var(--danger);font-weight:800}
/* .high is .low's mirror, for a column whose measure has two live ends rather than one concern
   threshold — Ops score, where >50 is a genuine finding and not just "not bad". Deliberately not
   applied to Member %: greening the ordinary majority of a dense table floods colour. */
.dbar.high .dbt i{background:var(--good-mark)}
.dbar.high .dbv{color:var(--good);font-weight:800}
/* .narrow: the default 52px track and 50px value slot are sized for ringgit ("RM152k"). An ops
   score is two digits, so those defaults waste ~36px — enough to push a full table over its
   container and pick up a scrollbar. */
.dbar.narrow .dbt{flex:0 0 40px}
.dbar.narrow .dbv{min-width:24px}

/* ---- Drill chip (.dchip) — a page-local narrowing that came from the URL, not the global
   Location filter. It reads as a removable breadcrumb rather than a filter pill on purpose:
   the two must not be confusable, since one is app-wide state and one dies with the page. ---- */
.dchip{display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 8px 0 16px;
  margin-bottom:14px;border:1px solid var(--line);background:#fff;border-radius:16px;
  font-size:13.5px;color:var(--ink2);font-weight:600;white-space:nowrap}
.dchip b{color:var(--ink);font-weight:800}
.dchip a{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;
  color:var(--ink3);text-decoration:none;font-size:var(--t-cap);font-weight:700;line-height:1}
.dchip a:hover{background:var(--line2);color:var(--ink)}

/* ---- Ranked region bars (.rbar) — one measure, one grey, identity in the row label, so no
   categorical palette and no legend box. Used for the total and per-outlet charts, each ranked
   highest-to-lowest by its OWN measure so each reads as a clean list. That means a region's
   rank change between them (Johor: 1st by total, 3rd per outlet) has to be read off the
   labels rather than showing up as jaggedness — the per-outlet chart carries `.ref` and its
   ±% deviation labels to make its own point stand without the comparison.
   `.ref` is the network-average marker; its top/bottom bleed matches .rbar's vertical padding
   so adjacent rows join into one continuous line across the chart. ---- */
.rbar{display:flex;align-items:center;gap:12px;padding:8px 0;text-decoration:none;color:inherit}
.rbar .rl{width:104px;flex:0 0 auto;font-weight:600;font-size:13.5px;color:var(--ink2)}
/* the track STRETCHES to the row's content height (which the taller label sets, not the
   16px bar) — that's what lets .ref span the full row and join up between rows. The visible
   track and fill are centred inside it rather than being the box itself. */
.rbar .rt{flex:1;position:relative;align-self:stretch}
.rbar .rt::before{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
  height:16px;background:var(--line2);border-radius:8px}
.rbar .rt i{position:absolute;left:0;top:50%;transform:translateY(-50%);height:16px;
  background:var(--bar);border-radius:8px;transition:background .12s}
.rbar .rv{width:62px;flex:0 0 auto;text-align:right;font-weight:800;font-size:13.5px;
  font-variant-numeric:tabular-nums;color:var(--ink)}
.rbar .rs{width:46px;flex:0 0 auto;text-align:right;font-weight:500;font-size:var(--t-cap);
  color:var(--ink3);font-variant-numeric:tabular-nums}
a.rbar{cursor:pointer;border-radius:8px}
a.rbar:hover .rt i{background:var(--accent)}
a.rbar:hover .rl{color:var(--ink);text-decoration:underline}
/* -7px matches .rbar's vertical padding exactly, so each row's segment meets its neighbour's
   and the six read as one continuous line down the chart */
.rbar .ref{position:absolute;top:-7px;bottom:-7px;width:2px;background:var(--ink3);opacity:.55;
  border-radius:4px;pointer-events:none;z-index:1}
/* the reference line's swatch, shown in the panel header legend beside its value — the line is
   meaningless unless something says what it is */
/* `i.refswatch`, not `.refswatch`: .phead .plegend i (a round 11px dot, for series keys) scores
   one higher on specificity, so this rule lost and every reference-line legend in the deck drew
   a CIRCLE to explain a LINE — a key that doesn't resemble the mark it keys is worse than none.
   Matching the type selector lets it win on source order. */
.plegend i.refswatch{width:2px;height:12px;background:var(--ink3);opacity:.55;border-radius:4px;flex:0 0 auto}

/* ---- Contribution to change (.cbar) — each region's share of the network's movement in RM.
   Length carries magnitude, the label carries direction. The bars are GREY, not red: every
   region is negative today, so painting all six red discriminated nothing and read as six
   alarms when only one region is exceptional (Selangor is ~4x the next worst; the rest is
   ordinary drift). Red stays on the label, which is where the rest of the deck keeps it.
   Green is kept for the bar because a region moving AGAINST a declining network is the real
   exception — that one deserves to jump out. ---- */
.cbar{display:flex;align-items:center;gap:12px;padding:8px 0;text-decoration:none;color:inherit}
.cbar .cl{width:104px;flex:0 0 auto;font-weight:600;font-size:13.5px;color:var(--ink2)}
/* The track STRETCHES to the row's content height rather than being the 16px bar itself; the
   visible track and the fill are centred inside it. (This once carried a .zero line for the
   Outlets movers chart, which needed the full-height track to join up between rows. That chart
   is now two ranked columns and .zero is gone — the stretch is kept because it's also what keeps
   a wrapped region label from squashing its own bar.) */
.cbar .ct{flex:1;position:relative;align-self:stretch}
.cbar .ct::before{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
  height:16px;background:var(--line2);border-radius:8px}
/* Absolute, a holdover from the diverging variant that could start a bar away from the left
   edge. The region contribution bars set only a width and no left, so they fall back to their
   static position — flush left — which is what they want. */
.cbar .ct i{position:absolute;top:50%;transform:translateY(-50%);height:16px;border-radius:8px}
.cbar .ct i.neg{background:var(--bar)}
.cbar .ct i.pos{background:var(--good)}
/* .movers-sep is gone. It named the omitted middle ("62 outlets between") in the stacked
   worst-above-best lists — an honest device that only existed to hold two lists together in one
   column. Those lists are now side-by-side .perf columns, which need nothing between them, and
   the count each column shows is stated in its panel's subtitle instead. */
.cbar .cv{width:74px;flex:0 0 auto;text-align:right;font-weight:800;font-size:13.5px;
  font-variant-numeric:tabular-nums}
.cbar .cp{width:52px;flex:0 0 auto;text-align:right;font-weight:500;font-size:var(--t-cap);
  color:var(--ink3);font-variant-numeric:tabular-nums}
a.cbar{cursor:pointer;border-radius:8px}
a.cbar:hover .cl{color:var(--ink);text-decoration:underline}
a.cbar:hover .ct i{filter:brightness(.86)}
/* total line — the bars are a decomposition, so the sum has to be visible to be checkable */
.ctot{display:flex;align-items:center;gap:12px;padding:12px 0 0;margin-top:6px;
  border-top:1px solid var(--line2);font-size:var(--t-body)}
.ctot .cl{width:104px;flex:0 0 auto;font-weight:800;color:var(--ink)}
.ctot .ct{flex:1}
.ctot .cv{width:74px;flex:0 0 auto;text-align:right;font-weight:800;font-variant-numeric:tabular-nums}
.ctot .cp{width:52px;flex:0 0 auto;text-align:right;font-weight:600;color:var(--ink3);
  font-variant-numeric:tabular-nums}

/* ---- Channel mix — a single 100% stacked track.
   At full width, three separate bars read as three unrelated measures; one track reads as
   a composition that sums to 100%, and takes a third of the vertical space. Segments use
   the deck's categorical grey steps (same family as BSHADE on the price-band bars), are
   split by a 2px surface gap, and are identified by the legend — never by shade alone. ---- */
.mixbar{display:flex;height:26px;border-radius:8px;overflow:hidden;background:var(--line2);margin:12px 0 14px}
.mixbar a{display:block;height:100%;text-decoration:none;transition:filter .12s}
.mixbar a + a{border-left:2px solid var(--card)}
.mixbar a:hover{filter:brightness(.86)}
.mixleg{display:flex;flex-wrap:wrap;gap:8px 26px}
.mixleg a{display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:inherit}
.mixleg i{width:11px;height:11px;border-radius:4px;flex:0 0 auto}
.mixleg .ml{font-weight:600;font-size:var(--t-body);color:var(--ink2)}
.mixleg .mv{font-weight:800;font-size:var(--t-body);color:var(--ink);font-variant-numeric:tabular-nums}
.mixleg a:hover .ml{color:var(--ink);text-decoration:underline}

/* ---- Hourly sales heatmap — day of week (rows) × hour of day (columns).
   Hours are the STRUCTURAL axis: every brand has trading hours, whereas daypart names
   and boundaries are brand-specific and don't survive multi-brand scaling. The daypart
   bands across the top are an optional per-brand annotation layer — they name the hours
   qualitatively and carry the drill — so a brand with no dayparts configured still gets
   a working grid, just without the bands.
   Sequential single-hue (grey) ramp, light = low → dark = high, scaled per scope with
   the endpoints printed on the legend. ---- */
.heat{display:flex;flex-direction:column;gap:10px}
.heat-grid{display:grid;grid-template-columns:52px repeat(18,1fr) 64px;gap:3px;align-items:stretch}
/* corner spacers — hold the grid's first/last column on the band + axis rows */
.heat-grid .hsp1{min-height:0}
/* daypart band — spans the hours it covers; the panel's only drill target */
.heat-grid .hb{display:flex;align-items:center;justify-content:center;gap:6px;height:26px;padding:0 4px;
  border-radius:var(--r2);background:var(--line2);color:var(--ink2);font-size:var(--t-cap);font-weight:800;
  letter-spacing:.03em;text-transform:uppercase;text-decoration:none;white-space:nowrap;overflow:hidden;
  transition:background .12s,color .12s}
.heat-grid .hb .hbr{font-weight:600;font-size:var(--t-cap);color:var(--ink3);letter-spacing:0;
  text-transform:none;font-variant-numeric:tabular-nums}
.heat-grid a.hb:hover{background:var(--accent);color:#fff}
.heat-grid a.hb:hover .hbr{color:#fff;opacity:.85}
/* hour axis — recessive, it labels the grid rather than competing with it */
.heat-grid .hh{font-size:var(--t-cap);font-weight:700;color:var(--ink3);text-align:center;padding:4px 0 4px;
  font-variant-numeric:tabular-nums}
/* The row of hour labels is the grid's AXIS, so it keeps the caption size above. These two are not:
   the left column of day names and the right column of day totals are table columns beside the grid
   (user, 2026-08-03) — body size, and bold where they are a header. */
.heat-grid .hh.tot{color:var(--ink2);font-size:var(--t-body);font-weight:700;letter-spacing:0;text-align:right}
/* day label — a row header: body size, bold */
.heat-grid .hd{display:flex;align-items:center;font-size:var(--t-body);font-weight:700;color:var(--ink2);padding-right:8px}
/* cells — pure shade + hover tooltip. 126 printed numbers would be noise, so the value
   lives in the hover and the day totals carry the quantities. */
.heat-grid .hc{height:30px;border-radius:4px;cursor:help;transition:box-shadow .1s}
/* No direct label on the peak cell: white glyphs lighten a small cell optically, which made
   the busiest hour read as PALER than its darker-shaded neighbour and inverted the very
   encoding the grid runs on. The peak is called out as text beside the legend instead, so
   the grid stays pure shade. */
/* sequential ramp — 5 steps */
/* VIOLET, not indigo (user, 2026-08-03): blue is reserved for links, green/red/amber for judgements, so
   an intensity ramp — which is neither — uses the non-semantic violet family. Five steps of one hue.
   This is the MONEY AND VOLUME ramp; counts of people use the cyan one (see .cohort). This grid is
   sales by day × hour, so it stays here.
   s2 was #e9d5ff, one sixth of a step from s1 (0.72 vs 0.84 luminance) while s2→s3 jumped half the
   scale — two of the five steps were doing almost nothing. The steps now fall at 0.84/0.54/0.35/
   0.15/0.07, near-even, so a quiet hour and a moderate one are actually told apart. */
/* LIGHTER AND MORE NEUTRAL (user, 2026-08-05: "for all heatmap components … use more neutral and
   lighter colouring. There's already so much going on"). The ramp ran to #6b21a8 — a fully saturated
   deep violet — for the top band, which on a 7x18 grid meant ~20 loud blocks competing with the daypart
   header, the totals column and the two winner grids under it.
   It is the same violet HUE, desaturated toward slate and lifted: the darkest step is now lighter than
   the OLD third step, and the whole ramp sits in the pale half of the scale. Still five monotonic steps
   — a sequential ramp has to stay ordered, and lightening it must not flatten it — measured at
   L* 97 / 93 / 87 / 78 / 68, a 29-point spread across the set with 6–10 points between neighbours.
   --ink2 clears 4.5:1 on every band, which is what lets the totals and the hour labels stay readable
   over the top of it. */
.heat-grid .hc.s1{background:#f6f4fb}
.heat-grid .hc.s2{background:#eae6f4}
.heat-grid .hc.s3{background:#dcd5ec}
.heat-grid .hc.s4{background:#c3b8dd}
.heat-grid .hc.s5{background:#a698c9}
.heat-grid .hc:hover{box-shadow:inset 0 0 0 2px var(--ink)}
/* day total — outside the ramp, so it reads as a summary not a cell */
.heat-grid .hct{display:flex;align-items:center;justify-content:flex-end;height:30px;padding-left:8px;
  font-size:var(--t-body);font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums;
  border-left:1px solid var(--line2)}
/* legend — light→dark with the endpoints printed, so the ramp is never guessed */
/* the heatmap's sequential ramp, now shown in the panel header with every other legend. Scoped
   to .plegend (not the old bottom-of-panel .heat-legend) and more specific than .plegend i, so
   the ramp swatches stay rectangles instead of inheriting the legend's round dots. */
.plegend .hs{display:flex;gap:2px}
.plegend .hs i{width:22px;height:10px;border-radius:4px}
/* The five swatches ARE the five .hc bands — a legend that shows a different ramp from the grid it
   labels is worse than no legend, so these move together (2026-08-05). */
.plegend .hs i:nth-child(1){background:#f6f4fb}
.plegend .hs i:nth-child(2){background:#eae6f4}
.plegend .hs i:nth-child(3){background:#dcd5ec}
.plegend .hs i:nth-child(4){background:#c3b8dd}
.plegend .hs i:nth-child(5){background:#a698c9}
.plegend .hsp{font-weight:500;padding-left:8px}
.plegend .hsp b{font-weight:800;color:var(--ink2)}

/* ── Week grid: weekdays DOWN the side, hours ACROSS the top (see weekgrid.js) ──
   Shared by Hourly sales and Top outlet by hour on Performance and by Top seller by hour on Menu, so
   the three read on one axis pair, column for column, instead of making a reader mentally rotate one
   to compare it with another. Page-local to performance.html until 2026-08-04, when the seller grid
   moved to Menu — two pages draw this now, so the rules live with the kit.
   This is the conventional heat-grid orientation and .heat-grid's own tracks above. It became viable
   for a LABELLED grid only once consecutive hours were merged into runs: with a label in every hour,
   18 columns give ~38px, which holds a six-character stub ("Combo", "Lemon", "Feast") and was
   rejected as such. A run spans its columns, so Monday's 16:00–23:00 block is one ~288px bar — the
   width comes from the hour count, not the column count, and the item's real name fits.
   minmax(0,1fr), not a bare 1fr, which is minmax(AUTO,1fr): a column whose min-content exceeds its
   share widens itself and steals from its neighbours, which desynchronised the grids at exactly the
   widths where lining up matters. Zero-floor tracks keep all 18 hours equal in every copy.
   Rows: daypart band, hour axis, then the seven days. Everything is placed explicitly, so cell source
   order is irrelevant, and a day row grows if its blocks need a second line.
   Day rows share a 30px floor so the grids keep one row rhythm no matter what sits in each. With a
   28px floor, Hourly sales' day totals held its rows at 30px while the winner grids — which leave
   that column empty — settled at 28px, and a pair drifted 2px a row, 14px by Sunday. */
.heat-grid.hg-wk{grid-template-columns:52px repeat(18,minmax(0,1fr)) 64px;
  grid-template-rows:auto auto repeat(7,minmax(30px,auto))}
/* ── The hour axis is BOUNDARY-ALIGNED in these three grids (user, 2026-08-05) ──
   "7am should align to the start of the box, end of box at 8am. And end of 'breakfast' is aligned with
   11am. Rather than being centred currently."
   A grid item fills its own track, so half of .hh's width IS half a column: with the text still
   centred in the box, translateX(-50%) moves that centre onto the track's leading edge — the hour
   label now marks the instant its cell begins. It also puts the axis and the daypart bands in
   agreement, which centring never could: Breakfast spans the 07–10 cells, and its right edge lands on
   the 11 label rather than half a cell short of it.
   overflow:visible because the first label (07) now hangs half its width into the 52px day-name
   column, and the shift must not clip it.
   NOT .tot — the "Total" header shares the .hh class and lives in the 64px column at the far right; it
   labels a column of figures, not a gridline, and translating it would take it off its own column.
   Scoped to .hg-wk: rm-sales.html draws the generic .heat-grid with hand-written daypart chips and no
   closing label, so its axis stays centred until that page is brought across. */
.hg-wk .hh:not(.tot){transform:translateX(-50%);overflow:visible}
/* The closing boundary, 01:00 — the midnight cell's right edge, which no cell starts. It is emitted
   into the totals column and shifted back onto that edge, so it sits to the LEFT of the right-aligned
   "Total" in the same cell rather than colliding with it. */
.hg-wk .hh.hh-end{transform:translateX(-50%);overflow:visible;text-align:center}
/* The grid track sets a cell's height here, not the shared 30px .hc rule. */
.hg-wk .hc{height:auto}
/* Winner blocks carry a name, so they need centred, wrappable type where a shaded cell needs none.
   The fill is set from JS (one hue per winner, tinted), hence no background here. */
.hg-wk .hci{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
  text-align:center;height:auto;padding:4px 4px;overflow-wrap:break-word;min-width:0;
  font-size:var(--t-cap);font-weight:700;line-height:1.15;letter-spacing:-.01em;
  cursor:pointer;transition:background .12s,color .12s,box-shadow .12s}
/* A block's hover is a RANKED read — the block's hours, then its top three by net sales — so it needs
   real lines. pre-line honours the newlines in data-tip while still collapsing ordinary whitespace, so
   it wraps exactly like the deck's other tooltips otherwise. Scoped to these blocks rather than set on
   [data-tip] globally: every other tooltip in the deck is prose. text-align too — the ::after inherits
   the block's centring, and a ranked list wants a common left edge so the 1. / 2. / 3. line up. */
.hg-wk .hci:hover::after{white-space:pre-line;text-align:left}
/* Right-hand summary column — Hourly sales' day totals. The winner grids leave this track empty: they
   spent it on an "owns most" column that only restated the row of blocks beside it, but the track
   itself has to stay in all of them or the 18 hour columns stop lining up.
   Body size: a summary column beside the grid is a table column (user, 2026-08-03). */
.hg-wk .hct{font-size:var(--t-body);letter-spacing:0;white-space:nowrap;overflow:hidden}
/* The item chips that used to sit above the grid are gone: every block names its own winner, so the
   row of chips was a legend for labels that were already legible, and it cost two lines of the panel.
   Tracing is now purely direct — click a block. This is the only residue: the way back out, shown in
   the header while a trace is active. */
.ihclr{margin-left:8px;font-weight:700;color:var(--accent);cursor:pointer;text-decoration:underline}
.ihclr:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* alerts feed + actions inbox */
.feed{display:flex;flex-direction:column;gap:12px}
.feed .ins{display:flex;gap:16px;align-items:flex-start;padding:16px 20px}
.feed .ins .body{flex:1}
.feed .ins .act{display:flex;gap:8px;flex-shrink:0;align-items:center}
.btn{padding:12px 16px;border:1px solid var(--ink2);border-radius:8px;font-weight:700;font-size:14px;color:var(--ink);background:#fff;cursor:pointer;text-decoration:none;white-space:nowrap}
.btn.pri{background:var(--accent);color:#fff;border-color:var(--accent)}
.status{font-size:var(--t-body);font-weight:600;letter-spacing:0;padding:4px 12px;border-radius:12px;border:1.5px solid var(--ink3);color:var(--ink2);white-space:nowrap}
.status.new{border-color:var(--ink);color:var(--ink)}
.status.done{border-style:dashed;color:var(--ink3)}
.grouph{font-size:var(--t-body);font-weight:800;color:var(--ink2);margin:22px 0 10px;display:flex;align-items:center;gap:9px}
.grouph:first-child{margin-top:2px}
.grouph .dt{width:10px;height:10px;border-radius:50%;background:var(--ink)}
.grouph.watch .dt{background:transparent;border:2px solid var(--ink)}
.grouph.info .dt{background:var(--ink3)}
.actmeta{font-size:13px;color:var(--ink3);margin-top:4px}

/* stacked distribution bars (ops health by region) */
.rstack{display:flex;flex-direction:column;gap:13px;margin-top:8px}
.rstack .row{display:flex;align-items:center;gap:12px}
.rstack .rl{width:118px;font-weight:700;font-size:14.5px}
.rstack .bar{flex:1;height:28px;display:flex;border-radius:8px;overflow:hidden;background:var(--line2)}
/* The three Figma fills sit at different lightnesses, so the count can't take one text colour
   across all of them: Error is dark (ink on it is 3.5:1, fails) and Warning/Success are light
   (white on them is 1.7:1 and 2.2:1, fails much worse). Each segment therefore takes whichever
   of the two clears 4.5:1 on its own fill — white on crit, ink on watch and healthy. */
.rstack .seg{height:100%;display:grid;place-items:center;font-size:12.5px;font-weight:800}
.rstack .seg.crit{background:var(--danger-fill);color:#fff}
.rstack .seg.watch{background:var(--warn-fill);color:var(--ink)}
.rstack .seg.healthy{background:var(--good-fill);color:var(--ink)}
.rstack .rt{width:96px;text-align:right;font-size:13px;color:var(--ink3)}
.leg{display:flex;gap:20px;margin-top:14px;font-size:var(--t-body);color:var(--ink2)}
.leg span{display:flex;align-items:center;gap:7px}
/* Same hairline ring as the outlet status dots: these swatches key the vibrant Figma hues, and the
   bright amber is 2.14:1 on white on its own — a 13px square with no edge. The ring costs nothing on
   the dark fills (violet, cyan) where it is imperceptible. */
.leg i{width:13px;height:13px;border-radius:4px;display:inline-block;
  box-shadow:inset 0 0 0 1px rgba(17,24,39,.28)}
/* the legend keys the BAR, so its swatches take the area fills — a key that doesn't match the
   thing it keys is worse than no key */
.leg .crit{background:var(--danger-fill)}.leg .watch{background:var(--warn-fill)}.leg .healthy{background:var(--good-fill)}

/* global notification center */
.notifwrap{position:fixed;inset:0;z-index:60;display:none}
.notifwrap.open{display:block}
.notif-backdrop{position:absolute;inset:0;background:rgba(20,24,30,.30)}
.notif-drawer{position:absolute;top:0;right:0;width:452px;max-width:94vw;height:100%;background:#fff;border-left:1px solid var(--line);box-shadow:-10px 0 34px rgba(0,0,0,.10);display:flex;flex-direction:column}
.notif-drawer .nh{display:flex;align-items:center;justify-content:space-between;padding:20px 20px;border-bottom:1px solid var(--line)}
.notif-drawer .nh h3{font-size:19px;margin:0}
.notif-drawer .nclose{border:none;background:none;font-size:24px;cursor:pointer;color:var(--ink2);line-height:1}
.notif-body{overflow:auto;padding:16px 20px;display:flex;flex-direction:column;gap:11px;flex:1}
.notif-foot{border-top:1px solid var(--line);padding:16px 20px;text-align:center}
.ntab{display:none}.ntab.on{display:flex;flex-direction:column;gap:11px}

/* ---- interaction states: hover · focus · tooltip · dropdown ---- */
.fpill,.iconbtn,.btn,.viewall,.tabs a,.rup,.lnk,.linkcta,.rail a,.nav-sec a{transition:background .12s,border-color .12s,color .12s}
/* KPI cards are display-only — no hover state (they aren't clickable). */
.fpill:hover{border-color:var(--ink2);background:#f5f7fd}
.fpill.lock:hover{border-color:var(--line);background:#eef0f7;cursor:default}
.btn:hover,.viewall:hover{background:#f1f4fb}
.btn.pri:hover,.linkcta:hover{background:#2e2885}
.tabs a:hover:not(.on){color:var(--ink);background:rgba(255,255,255,.55)}
a.prow{text-decoration:none;color:inherit}
.prow:hover{cursor:pointer}.prow:hover .nm{text-decoration:underline}
.rup:hover{border-color:var(--ink3)}
/* ---- .lnk — an inline text link, in a table cell or in body copy ----
   Body style at rest, link style on hover/focus (user, 2026-08-05: "for all the blue coloured links,
   by default they shouldn't be link styled … they should only show link style on hover and tap").
   This class had NO colour rule at all, so 35 item names across Menu, Operations, My Dashboard and
   Sales were being painted by the browser's own stylesheet — #0000EE and permanently underlined,
   the one thing on screen that wasn't the deck's blue or the deck's type.
   It also brings .lnk into line with every other clickable name in the deck: .prow .nm, a.rbar .rl,
   a.cbar .cl and .mv-nm all sit in body ink and underline only on hover. The row or cell is the
   affordance; a table of forty blue underlined names is a table you cannot read down.
   :focus-visible carries the same treatment, or the state would be reachable by mouse only. */
a.lnk{color:inherit;text-decoration:none;cursor:pointer}
a.lnk:hover,a.lnk:focus-visible{color:var(--accent);text-decoration:underline;text-underline-offset:2px}
.lnk:hover{border-color:var(--ink2)}
/* The Ask Genie FAB is an <a> with no text-decoration rule, so it too was inheriting the UA link
   colour — it carries an icon rather than a word, which is why nobody caught it. */
a.askfab{color:inherit;text-decoration:none}
.inp:focus{outline:2px solid var(--ink);outline-offset:1px;border-color:var(--ink)}
.askbtn:focus-visible,.btn:focus-visible,.iconbtn:focus-visible,.profile:focus-visible,.fpill:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* tooltip on info icons / any [data-tip] */
.i{cursor:help}
[data-tip]{position:relative}
[data-tip]:hover::after{content:attr(data-tip);position:absolute;top:155%;left:-8px;background:#1b1f26;color:#fff;
  padding:12px 12px;border-radius:8px;font-size:12.5px;font-weight:500;width:232px;line-height:1.45;
  z-index:40;box-shadow:0 6px 20px rgba(0,0,0,.20);text-transform:none;letter-spacing:normal;white-space:normal}
[data-tip]:hover::before{content:"";position:absolute;top:calc(155% - 6px);left:1px;border:6px solid transparent;border-bottom-color:#1b1f26;z-index:40}
/* near the right edge (against the insights rail) the tooltip flips to open leftward so it
   isn't clipped by the content edge — the flip class is set in shell.js (positionTips). */
.tip-flip:hover::after{left:auto;right:-8px}
.tip-flip:hover::before{left:auto;right:1px}

/* filter dropdowns (open state + menu) */
.fdrop{position:relative}
.fpill.open{border-color:var(--ink2);background:#f1f4fb}
.fmenu{position:absolute;top:calc(100% + 7px);left:0;min-width:232px;background:#fff;border:1px solid var(--line);
  border-radius:12px;box-shadow:0 10px 28px rgba(0,0,0,.14);padding:8px;z-index:45;display:none}
.fdrop.open .fmenu{display:block}
.fmenu a{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 12px;border-radius:8px;
  text-decoration:none;color:var(--ink);font-size:14.5px;font-weight:600}
.fmenu a:hover{background:#f1f4fb}
.fmenu a.sel{background:#eef1fa}
.fmenu a .ck{color:var(--ink);font-weight:800}

/* ---- collapsed (icon-only) rail — desktop only; the mobile rail is already a compact bar ---- */
@media (min-width:861px){
  .rail.collapsed{width:64px;flex:0 0 64px;padding-left:12px;padding-right:12px}
  .rail.collapsed .brand{flex-direction:column;gap:10px;margin-bottom:16px}
  /* The lockup gives way to the stroked mark: 40px of usable width is an icon slot, not a logo slot. */
  .rail.collapsed .brand .blogo{display:none}
  .rail.collapsed .brand .logo{display:grid;place-items:center;color:var(--rail-ink);flex:0 0 auto;
    width:26px;height:26px;border-radius:8px;background:rgba(255,255,255,.1)}
  .rail.collapsed .navtoggle{margin-left:0;transform:rotate(180deg)}
  .rail.collapsed .nav-label{display:none}
  .rail.collapsed .nav-sec{margin-top:6px}
  .rail.collapsed nav a{justify-content:center;padding-left:0;padding-right:0}
  .rail.collapsed nav a .lbl,.rail.collapsed nav a .navtag{display:none}
  /* the sign-off keeps its sparkle when collapsed — the words are what doesn't fit, not the mark */
  .rail.collapsed .foot,.rail.collapsed .genieby .gb-txt{display:none}
  .rail.collapsed .genieby{justify-content:center;gap:0}
  .rail.collapsed .railprofile{justify-content:center;padding-left:0;padding-right:0}
  .rail.collapsed .railprofile .who-txt,.rail.collapsed .railprofile > svg{display:none}
}

/* ---- below 860px the one-screen constraint is dropped — stack & scroll ---- */
@media (max-width:860px){
  .app{height:auto;min-height:100vh;overflow:visible;flex-direction:column}
  .content.ask{height:auto;overflow:visible}
  .ask-conv{overflow:visible}
  .main{height:auto;overflow:visible}
  .content{overflow:visible}
  .content.ov,.ov .grid-main,.ov .leftcol{display:block;height:auto;min-height:0}
  .grid-main,.ov .grid-main,.two,.ov .two{grid-template-columns:1fr}
  .ov .rail-r{overflow:visible;margin-top:16px}
  .ov .two{margin-top:16px}
  .kpis{grid-template-columns:repeat(2,minmax(0,1fr))!important}
  .kcard{min-width:0}
  .kcard .val{font-size:24px}
  .kcard svg.spark{max-width:100%}
  .top-row{flex-wrap:wrap;gap:8px}
  .filters{flex-wrap:wrap}
}
/* tablet (641–860): the rail compacts into a horizontal bar */
@media (min-width:641px) and (max-width:860px){
  .rail{width:auto;flex:0 0 auto;height:auto;overflow:visible;flex-direction:row;flex-wrap:wrap;
    align-items:center;gap:6px 14px;padding:12px 16px}
  .rail .brand{margin-bottom:0;margin-right:auto;padding-bottom:0;border-bottom:none}
  .rail .navtoggle{display:none}
  .rail .nav-label{display:none}
  .rail nav{flex-direction:row;flex-wrap:wrap;margin-top:0;gap:4px}
  .rail nav a .ico{display:none}
  .rail a{padding:8px 12px}
  .rail .railbottom{margin-top:0;flex-direction:row;align-items:center;gap:14px;flex-basis:100%}
  .rail .nav-sec{flex-direction:row;border-top:none;padding-top:0;margin-top:0;gap:14px}
  .rail .genieby{border-top:none;padding-top:0;margin-left:auto}   /* sign-off trails the profile row */
  .rail .railprofile-wrap{margin-top:0}
  .rail .foot{display:none}
}
@media (max-width:560px){
  .perf{grid-template-columns:1fr;gap:8px}
  .rollup{flex-wrap:wrap}.rup{flex:1 1 40%}
}

/* ---- mobile chrome (hidden on desktop): hamburger, drawer backdrop, Ask Genie FAB ---- */
.mburger,.mnavbk,.askfab{display:none}

/* ---- phone (≤640): nav rail becomes an off-canvas drawer; Ask Genie floats as a FAB ---- */
@media (max-width:640px){
  /* drawer: keeps the desktop rail's vertical anatomy (brand, nav, profile, Genie sign-off) */
  .rail{position:fixed;top:0;left:0;bottom:0;z-index:75;width:min(80vw,300px);height:100vh;height:100dvh;
    flex-direction:column;overflow-y:auto;transform:translateX(-105%);transition:transform .25s ease;
    box-shadow:14px 0 40px rgba(0,0,0,.18)}
  .rail.mopen{transform:none}
  .rail .navtoggle{display:none}
  .mnavbk{position:fixed;inset:0;z-index:74;background:rgba(20,24,31,.42)}
  .mnavbk.open{display:block}
  .mburger{display:grid;place-items:center;width:38px;height:38px;flex:0 0 auto;border:1px solid var(--line);
    border-radius:12px;background:#fff;color:var(--ink);cursor:pointer;padding:0}
  .mburger svg{width:20px;height:20px}

  /* top bar: burger + title + sync icon on row 1; filter pills scroll on row 2 */
  .top{padding:12px 16px;gap:10px}
  .top .crumb{font-size:17px}
  .fresh .fresh-txt{display:none}
  .filters{order:5;flex-basis:100%;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;
    scrollbar-width:none;padding-bottom:4px}
  .filters::-webkit-scrollbar{display:none}
  .fpill{height:34px;font-size:13px;padding:0 12px;flex:0 0 auto}

  /* Ask Genie FAB — bottom-right, thumb reach */
  .askfab{display:grid;place-items:center;position:fixed;right:16px;bottom:18px;z-index:60;
    width:52px;height:52px;border-radius:50%;background:var(--ink);color:#fff;
    box-shadow:0 10px 26px rgba(0,0,0,.3)}
  .askfab svg{width:24px;height:24px}

  /* content & components */
  .content{padding:16px}
  .det-grid{grid-template-columns:1fr}
  .dt-search{flex:1 1 auto}
  .omodal-regions{width:40%;padding:8px}
  .oregion{padding:12px 8px;gap:7px}

  /* Select period modal: stack — fields, then presets as a scrolling chip row, then calendar */
  .pmodal-top{flex-direction:column;gap:8px}
  .pcmp-arrow{align-self:center;transform:rotate(90deg)}
  .pmodal-main{flex-direction:column;height:auto}
  .presets{width:100%;flex-direction:row;overflow-x:auto;border-right:none;border-bottom:1px solid var(--line);
    padding:8px 12px;gap:6px;scrollbar-width:none}
  .presets::-webkit-scrollbar{display:none}
  .preset{white-space:nowrap;flex:0 0 auto;padding:8px 12px}
}

/* ══════════════ shared widgets for the reference-derived pages (widgets.js) ══════════════
   Categorical ramp for share bars / legends. Six steps, differing by HUE first and lightness
   second so they stay separable, and each carries either --ink or white at 4.5:1 (see .sh*
   text rules below). This is the only categorical scale in the file — every breakdown uses it
   so a colour means the same thing across pages. */
/* The categorical palette itself lives in widgets.js (CAT) because the donut needs the values
   in JS to stroke each arc — duplicating them here as .sh1….sh8 classes left two lists to keep
   in sync. All eight clear 3:1 on white, the threshold for a mark that carries meaning on its
   own; the obvious amber and grey measured 2.96 and 2.58 and are one step darker than they
   look like they want to be. */
/* Donut + keyed legend (widgets.js). The ring encodes share; the legend carries the label,
   percentage, value and movement — a donut alone cannot show four things per slice, so the
   legend is not decoration here, it is where most of the information lives. */
.donutw{display:flex;align-items:center;gap:20px;flex-wrap:wrap;margin-top:8px}
.donut{position:relative;flex:0 0 auto;width:150px;height:150px}
.donut svg{width:100%;height:100%;transform:rotate(-90deg);display:block}
.donut .dseg{transition:opacity .12s}
.donutw:hover .dseg{opacity:.55}
.donutw .dseg:hover{opacity:1}
.donut-c{position:absolute;inset:0;display:grid;place-items:center;text-align:center;pointer-events:none;padding:0 24px}
.donut-c .dv{font-size:19px;font-weight:800;line-height:1.15;font-variant-numeric:tabular-nums}
.donut-c .dl{font-size:var(--t-cap);color:var(--ink3);font-weight:500;margin-top:3px;line-height:1.25}
.shleg{flex:1 1 250px;min-width:0;display:flex;flex-direction:column;gap:1px}
.shleg-row{display:flex;align-items:center;gap:10px;padding:4px 0;border-top:1px solid var(--line2);font-size:var(--t-body)}
.shleg-row:first-child{border-top:none}
/* ringed, like .leg i and the outlet dots: the palest step of either sequential ramp (Basic tier,
   the quietest hour) is a 1.2:1 disc on white and vanished without an edge */
.shkey{width:11px;height:11px;border-radius:50%;flex:0 0 auto;box-shadow:inset 0 0 0 1px rgba(17,24,39,.28)}
.shk{flex:1;min-width:0;font-weight:600;color:var(--ink2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shp{width:52px;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
.shv{width:96px;text-align:right;color:var(--ink3);font-weight:600;font-variant-numeric:tabular-nums}   /* fits a 7-figure full-ringgit value (RM 2,656,830) on one line */
.shc{width:64px;text-align:right;font-size:var(--t-body);font-variant-numeric:tabular-nums}

/* ---- 100% stacked horizontal bar (widgets.js stackBar) ----
   Promoted out of customers.html's local block, where it was built by hand for member penetration: it is
   now a widget, and a rule for a shared component cannot live on one page. Penetration's own markup
   still emits these same classes, so the two render identically — it should adopt stackBar() next.
   The 2px card-coloured divider between segments is what keeps Basic at 3.7% legible against Bronze
   rather than reading as one continuous band, and it is drawn as a border so it costs no width. */
.stackbar{margin-top:8px}
/* 26px, not 24: the segments carry their own labels now (see stackBar) and 24px left a 12px cap-height
   sitting on the track's own edges. overflow stays hidden — it is what rounds the end segments. */
.sb-track{display:flex;height:26px;border-radius:8px;overflow:hidden;background:var(--line2)}
.sb-seg{height:100%;display:flex;align-items:center;justify-content:center;min-width:0;
  transition:filter .12s}
.sb-seg + .sb-seg{border-left:2px solid var(--card)}
/* The in-bar name. Ink is chosen per segment by W.inkOn against that segment's own fill, so a label on
   slate and a label on violet are each the readable one rather than both being white and hoping. */
/* No ellipsis and no overflow clipping: fitStackLabels() in shell.js hides a label that does not fit, so
   one that IS shown is always whole. overflow:hidden would let a too-long label be silently cut instead. */
.sb-lab{font-size:var(--t-cap);font-weight:700;line-height:1;white-space:nowrap;padding:0 4px;
  letter-spacing:.01em}
/* Hover: the segment brightens and lifts its own label, which is what makes the bar read as pointable.
   No colour CHANGE — the fill is the tier's identity and swapping it on hover would break the mapping to
   the legend at the moment the reader is using it. */
.sb-seg[data-tip]{cursor:help}
.sb-seg[data-tip]:hover{filter:brightness(1.08) saturate(1.06)}
/* A legend UNDER the bar, not beside it: .shleg is flex:1 1 250px for the donut's side-by-side layout,
   which here would sit it next to a full-width bar and squeeze both. */
.shleg.sb-leg{flex:1 1 100%;width:100%;margin-top:14px}

/* ranked horizontal bars */
.rankbars{display:flex;flex-direction:column;gap:2px;margin-top:6px}
/* Cap the block, not the track: .rb-track is flex:1, so in a full-width panel a six-row share
   list stretched its bars to ~750px, where the eye can no longer compare lengths and the panel
   reads as far too wide for what's in it. 780px keeps the label, a ~450px bar and both number
   columns, which is as wide as this component is ever useful. */
.rankbars{max-width:780px}
.rankbars{container-type:inline-size}
.rb-row{display:flex;align-items:center;gap:11px;padding:8px 0;border-top:1px solid var(--line2);font-size:13.5px}
.rb-row:first-child{border-top:none}
.rb-k{width:150px;flex:0 0 auto;font-weight:600;color:var(--ink2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rb-track{flex:1;min-width:40px;height:14px;background:var(--line2);border-radius:4px;overflow:hidden}
.rb-track i{display:block;height:100%;background:var(--bar);border-radius:4px}
.rb-v{width:88px;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
.rb-p{width:52px;text-align:right;color:var(--ink3);font-weight:500;font-size:var(--t-body);font-variant-numeric:tabular-nums}
.rb-c{width:64px;text-align:right;font-size:var(--t-body);font-weight:700;font-variant-numeric:tabular-nums}
/* Narrow panels stack the row instead of squeezing the bar (user, 2026-07-30: bars were chosen for
   channel mix so the categories can be compared precisely, and a squeezed bar is exactly what that
   choice was meant to avoid). One line needs 150px of label + a 40px minimum track + 88 + 52 + 64 =
   ~440px; below that the label and the numbers take the first line and the TRACK gets a full-width
   second one — where it is LONGER than it could ever be on one line, not shorter.

   A container query, not a viewport one: the same component sits in a third of a column on one page
   and most of the width on another, so what matters is the box it is in, never the window. It has to
   come AFTER the fixed widths above — a container query adds no specificity, so declared earlier it
   lost every width to the plain .rb-k / .rb-track rules and the row half-collapsed instead. */
@container (max-width:440px){
  .rb-row{flex-wrap:wrap;gap:0 8px;padding:8px 0}
  .rb-k{width:auto;flex:1 1 auto;min-width:0}
  .rb-v{width:auto;flex:0 0 auto}
  .rb-p{width:44px}
  .rb-c{width:56px}
  /* order, not DOM position: the wide layout keeps label · bar · numbers, and only here does the bar
     move to the end so it can own the second line. */
  .rb-track{flex:0 0 100%;order:3;margin-top:5px;height:10px}
}
/* .rb-compact (padding trimmed to 5px) was added and REVERTED on 2026-08-05 — the user asked for it and
   then took it back: "increase the padding so it looks more breatheable again". Left as a note rather
   than a dead class, so the next person to think of it knows it was tried. The height of a narrow ranked
   list comes from the container query above giving each row a second line for its bar, not from padding. */
/* ---- .rb-alt — the second bar colour, for a ranked list sitting BESIDE another one ----
   Cyan against the default violet: the data palette's primary/secondary pair, both outside the four
   reserved hues. Two identical bar charts side by side otherwise read as one ranking split in half.
   Hover still goes to --brand, which is the deck's "you can click this" and belongs to neither series. */
.rb-alt .rb-track i{background:var(--d2)}
/* ---- .rb-head — the column headers of a ranked bar list (user, 2026-08-05) ----
   Built as a .rb-row so it inherits every column width AND the wrapped layout under 440px; only what
   makes it a header is restated. This IS a table header row, so it takes the table-header treatment from
   the type review (2026-08-03): body size, semibold, ink3, no tracking, Title Case.
   Its track cell is a SPACER, not a bar — the grey fill would read as a zero-length bar for a row with
   no name. And it keeps no bottom border of its own: the first data row's own top border is the rule
   under the header, which is why .rb-row:first-child (the header, now) drops its. */
.rb-head{font-size:var(--t-body);font-weight:600;color:var(--ink3);padding-bottom:6px}
/* The same header row for a donut / stacked-bar legend (shareLegend's `head`). Same reasoning as
   .rb-head: it is a table header, so it takes the table-header treatment and none of the row's
   interactive behaviour. */
.shleg-head{font-size:var(--t-body);font-weight:600;color:var(--ink3)}
.shleg-head .shp,.shleg-head .shv,.shleg-head .shc{font-weight:600;color:var(--ink3)}
.shleg-head:hover{background:none}
.rb-head .rb-track{background:none}
.rb-head .rb-v,.rb-head .rb-p,.rb-head .rb-c{font-weight:600;color:var(--ink3)}
.rb-head:hover{background:none}                  /* not a row: the row hover tint would read as clickable */
@container (max-width:440px){
  /* In the wrapped layout the track owns a second line, and an empty one under the header is 10px of
     nothing plus its own margin. */
  .rb-head .rb-track{display:none}
}
/* The base the shares are of, where a donut used to print it in the middle of the ring. */
.rb-total{display:flex;align-items:baseline;gap:7px;margin:2px 0 2px}
.rb-total b{font-size:22px;font-weight:800;line-height:1.1;font-variant-numeric:tabular-nums}
.rb-total span{font-size:var(--t-cap);font-weight:500;color:var(--ink3)}
/* A drillable row: same geometry, and it must not pick up the UA link colour or underline. Hover
   matches the donut legend row it replaces (a.shleg-link) so drilling feels the same either way. */
a.rb-link{text-decoration:none;color:inherit;cursor:pointer}
a.rb-link:hover .rb-k{text-decoration:underline;color:var(--brand)}

/* ---- stage funnel (widgets.js): a CENTRED TAPER ----
   Reference: files/Reference Images/Marketing Funnel.webp. See funnel() for why the shape changed and
   why no text sits inside a band.

   ONE grid, two columns — taper · drop-off — rather than a flex row per stage. The grid is what makes
   the drop note land at exactly the height of the band it describes. The stage column is gone: names
   and figures moved onto the bands themselves (user, 2026-07-31), which is both what the reference does
   and one less thing for the eye to pair up.

   Bands are clip-path polygons, so the taper is real geometry rather than a rotated pseudo-element,
   and the trapezoid can be given each stage's own top and bottom width. It costs the deck's rounded
   corners — a clipped polygon cannot carry a border-radius — which is the one place this component
   departs from the deck's shape language, and the departure is the point: a funnel is not a bar. */
/* The wrapper is the query container and nothing else. It has to be a level above the grid: a container
   query cannot style the element that declares the container, and the narrow layout's whole job is to
   change the grid's own columns. */
.funnelc{container-type:inline-size}
.funnelw{display:grid;grid-template-columns:minmax(0,1fr) 126px;column-gap:12px;row-gap:4px;
  align-items:center;margin-top:8px}
/* The outside-label column exists in the markup at every width and is SHOWN only by the container query
   at the end of this block. Hidden with display:none, not visibility, so it leaves the grid entirely and
   the wide layout stays the two columns it was validated as. */
.funnelw .fn-lab{display:none}
/* The band fills its cell; the polygon inside it is what narrows. Height is generous because the SLOPE
   is the message — and because two lines of text now sit on each band. */
.funnelw .fn-band{position:relative;min-width:0;height:56px}
.funnelw .fn-band i{display:block;width:100%;height:100%}
/* The label sits OVER the shape, not in it: clip-path clips its element's children, so text placed
   inside the polygon would be sliced by the taper's own edges. Absolute and centred on the funnel's
   axis, so a stage's name is at its band and there is nothing to pair up. */
.funnelw .fn-in{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:1px;text-align:center;line-height:1.2;pointer-events:none}
.funnelw .fn-in b{font-size:var(--t-body);font-weight:600}
.funnelw .fn-in span{font-size:var(--t-cap);font-weight:500;font-variant-numeric:tabular-nums;opacity:.85}
.funnelw .fn-note{line-height:1.25;min-width:0}
.funnelw .fn-d{font-size:var(--t-body);font-weight:800;color:var(--ink2);font-variant-numeric:tabular-nums}
.funnelw .fn-d.hi{color:#8a4a00}   /* the flagged band — same amber as its tag, so the two read as one */
.funnelw .fn-l{display:block;font-size:var(--t-cap);font-weight:500;color:var(--ink3);
  font-variant-numeric:tabular-nums}
/* The one band that loses the most members earns a tag, so the bottleneck is named and not only
   drawn. Amber, not red: losing members between stages is what a funnel DOES — this is the step worth
   working on, not a failure. */
.funnelw .fn-tag{display:inline-block;margin-top:3px;font-size:var(--t-cap);font-weight:600;
  letter-spacing:0;color:#8a4a00;background:#fef0ce;
  border-radius:999px;padding:4px 8px}
/* ── NARROW CONTAINER: the label comes OUT of the band ──────────────────────────────────────────
   Labelling inside the taper has a hard width floor, and it is not a preference — it is arithmetic.
   The longest stage name ("Active Repeat Member") needs ~126px and its figures line ~93px, so the
   inside layout only holds while every band is at least that wide. Band width = container − 138px of
   note column and gap; the narrowest band is drawn at FN_MIN (20%). Both conditions clear at about
   600px of container, which the full content column and a HALF column at 1920 both give.

   Below that the bands are physically thinner than their own text and it spilled over the trapezoid
   edges — "Regular Member" rendered as "gular Membe" across the taper's sides (the funnel went to half
   a column on 2026-08-03, to share a row with Churn & reactivation). So under 600px the label moves to
   a right-aligned column immediately left of the taper: still its band's own row, still adjacent, and
   nothing is clipped. The wide layout is untouched — this is the narrow fallback, not a replacement.
   A container query, not a viewport one: what decides this is the box, and this same funnel is inside
   labels at 1920 and outside labels at 1440.

   FN_MIN stays in force here, so the last band is still drawn at 20% rather than its true 14.7% even
   though nothing sits on it now. That is 5.3pp of distortion doing no work — it is left alone because
   the floor is baked into the clip-path percentages in JS, which cannot see the container width, and
   because both figures are printed beside the band regardless. */
@container (max-width:600px){
  .funnelw{grid-template-columns:106px minmax(0,1fr) 92px;column-gap:10px}
  .funnelw .fn-lab{display:flex;flex-direction:column;align-items:flex-end;gap:1px;
    text-align:right;line-height:1.2;min-width:0}
  .funnelw .fn-lab b{font-size:var(--t-body);font-weight:600;color:var(--ink)}
  .funnelw .fn-lab span{font-size:var(--t-cap);font-weight:500;color:var(--ink3);font-variant-numeric:tabular-nums}
  .funnelw .fn-in{display:none}
  /* the note column loses 34px, so its two lines drop a step with it rather than wrapping mid-number */
  .funnelw .fn-tag{padding:4px 8px}
}

/* cohort retention grid — a sequential ramp where darker = higher retention */
.cohort{width:100%;border-collapse:separate;border-spacing:3px;font-size:var(--t-body)}
.cohort th{font-size:var(--t-body);font-weight:600;color:var(--ink3);padding:4px 4px;text-align:center;white-space:nowrap}
.cohort th.rl{text-align:left}
.cohort td{text-align:center;padding:8px 4px;border-radius:4px;font-weight:700;font-variant-numeric:tabular-nums;background:var(--line2);color:var(--ink3)}
.cohort td.rl{text-align:left;background:none;font-weight:700;color:var(--ink2);white-space:nowrap}
/* CYAN, not violet. There are two sequential ramps in the deck, split by what they measure rather
   than by which page they land on: violet for MONEY AND VOLUME (the sales day×hour grids, the
   choropleth of takings) and cyan for COUNTS OF PEOPLE — this grid, member visits by time of day,
   the lifecycle funnel, the loyalty tiers and the member choropleth. One hue per ramp, because
   lightness is what encodes the magnitude; two ramps, because a deck where every shaded thing is
   violet reads as one enormous chart (user, 2026-08-04).
   The six steps are matched to the violet ramp's LUMINANCES, not picked by eye (0.79/0.67/0.53/
   0.38/0.24/0.15 against violet's 0.84/0.72/0.54/0.35/0.22/0.13), so retention at step 4 looks
   exactly as strong here as sales at step 4 does on Performance. Ink flips where the ramp crosses
   4.5:1: white on the two darkest, --ink above (c2 measures 4.8:1 with dark ink, 3.7:1 with white). */
.cohort td.c0{background:#164e63;color:#fff}.cohort td.c1{background:#0e7490;color:#fff}
.cohort td.c2{background:#0891b2;color:var(--ink)}   /* dark ink: white measures 3.68:1 on this step */.cohort td.c3{background:#06b6d4;color:var(--ink)}
.cohort td.c4{background:#67e8f9;color:var(--ink)}.cohort td.c5{background:#a5f3fc;color:var(--ink)}
.cohort td.cx{background:transparent;color:var(--ink3);font-weight:500}

/* status pill — scorecards, stock states, item lifecycle badges */
.pill{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;
  font-size:var(--t-body);font-weight:600;white-space:nowrap}
.pill::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;flex:0 0 auto}
/* Pills carry their own text colours, one step darker than the --good/--warn/--danger text
   tokens. Those tokens are tuned to clear 4.5:1 on WHITE; on the pill's own tint they measured
   4.19 and 4.28. Same failure mode as red values on a hovered table row — a tinted backing eats
   the headroom, so anything sitting on a tint needs its own value. */
.pill.good{background:#d7f7ec;color:#066b49}
.pill.watch{background:#fef0ce;color:#8a4a00}
.pill.bad{background:#fde3e9;color:#b01238}
.pill.neutral{background:var(--line2);color:var(--ink2)}
.pill.flat::before{display:none}

/* ═══ metric STRIP — the inline summary figures inside a panel ═══
   Customer metrics, cohort averages, churn & reactivation, voucher and PO summaries, the RFM readout.

   NOT cards any more (user, 2026-08-03: "there are already too many boxes with outlines… within a
   component we shouldn't use cards"). Every one of these strips sits INSIDE a bordered panel, so each
   tile drew a second outline 15px inside the first — four tiles in a panel is five nested rectangles for
   what is really one row of figures. The border was also doing no work the position wasn't: a figure
   under a panel title, in a row of its peers, is already unambiguously a summary figure.

   What replaces the box is TYPE, SPACE and one hairline. The first pass at this used space alone and it
   did not hold (user, 2026-08-03: "the contents should still look grouped together well — there's not
   enough whitespace to make it convincing"), for two measurable reasons:

     1. THE SPACING WAS INVERTED. The gap between two figures was 16px while the gap from the strip to the
        chart under it was 15px and to the title above it 12px. Proximity is the only grouping cue left
        once the boxes go, and it was saying the four figures belonged together no more strongly than they
        belonged to the table below. Internal now runs 22px, external 34px, so the group is bound tighter
        than it is bound to its neighbours — the ratio, not the absolute numbers, is what does the work.
     2. EACH FIGURE WAS THREE LOOSE LINES. Label, value and movement each sat on their own line, so a
        single figure occupied as much vertical space as the moat around it. Value and movement now share
        a baseline (see .tile below), which compresses a figure to two lines and lets the whitespace read
        as separation rather than as more of the same.

   The hairline above the strip is the one piece of ink: a single 1px rule spanning the panel, which binds
   the figures under it into a section the way a table's header rule does. One line for the whole group,
   against the sixteen edges four bordered tiles drew. */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:22px 40px;
  margin-top:14px;padding-top:16px;border-top:1px solid var(--line2)}
/* A strip followed by the chart or table it summarises needs to be further from it than its own figures
   are from each other — 34px against a 22px row gap. A strip that ENDS the panel needs none of that: the
   panel's own padding closes it, and a bottom margin there would push the panel 34px taller than its
   content. */
.tiles:not(:last-child){margin-bottom:34px}
/* A figure is a two-line unit: label on its own line, then value and movement sharing a baseline. The row
   is allowed to WRAP, so a narrow column (a half-panel at 1280 puts a figure in 146px) drops the movement
   under the value instead of overflowing — the old stacked layout, reached only where it is needed. */
.tile{border:none;border-radius:0;padding:0;background:none;
  display:flex;flex-wrap:wrap;align-items:baseline;column-gap:9px}
.tile .tl{flex:0 0 100%;font-size:var(--t-body);color:var(--ink3);font-weight:600;line-height:1.3;margin-bottom:3px}
.tile .tv{font-size:22px;font-weight:800;margin:0;line-height:1;font-variant-numeric:tabular-nums}
.tile .td{font-size:var(--t-body);margin:0;font-weight:600}
/* A caption is a SENTENCE about the figure ("Average of 11 cohorts", "Per visit"), not a second number,
   so it takes its own line under the pair rather than joining the baseline. */
/* CAPTION, the third step of the type scale — 12px, 500, dimmed (see --t-cap). Page-local to
   customers.html until 2026-08-04, when the loyalty and promotion tables started printing a sub-line
   under a name (a tier's qualification, a reward's points price, a promotion's code) and the class had to
   mean the same thing on every page. Anything nested keeps its own layout rules, like .tile below. */
.cap{font-size:var(--t-cap);font-weight:500;color:var(--ink3);margin-top:3px;line-height:1.3}
.tile .cap{flex:0 0 100%;margin-top:4px}

/* two-value comparison row (Actual vs Target vs Budget scorecards) */
.tbl td.r,.tbl th.r{text-align:right;font-variant-numeric:tabular-nums}
.tbl td.num{font-variant-numeric:tabular-nums;text-align:right}

/* bubble / quadrant matrix reuses the existing .sc-* scatter idiom from sales.html */
.qgrid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:2px;
  border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;background:var(--line2)}
.qcell{background:var(--card);padding:12px;min-height:132px;position:relative}
.qcell .qt{font-size:var(--t-body);font-weight:600;color:var(--ink3);letter-spacing:0;line-height:1.3}
.qcell .qn{font-size:var(--t-cap);color:var(--ink3);font-weight:500;margin-top:2px}
.qcell .qitems{margin-top:9px;display:flex;flex-wrap:wrap;gap:5px}
.qcell .qi{font-size:var(--t-body);font-weight:600;background:var(--line2);border-radius:999px;padding:4px 12px;
  color:var(--ink2);text-decoration:none}
.qcell .qi:hover{background:var(--accent-soft);color:var(--brand)}

/* hour-of-day column chart (Performance › Time Performance) */
.hourbars{display:flex;align-items:flex-end;gap:3px;height:190px;margin-top:10px}
.hb-col{flex:1;display:flex;flex-direction:column;justify-content:flex-end;height:100%;min-width:0}
.hb-fill{background:var(--bar2);border-radius:4px 4px 0 0;min-height:2px}
.hb-fill.peak{background:var(--bar)}
.hb-lb{font-size:var(--t-cap);color:var(--ink3);font-weight:600;text-align:center;margin-top:5px;height:13px;white-space:nowrap}

/* year-on-year grouped columns (Executive Dashboard) */
.yoy{display:flex;align-items:flex-end;gap:10px;height:132px;margin-top:8px}   /* height mirrors YOY_H in overview.html */
.yoy-col{flex:1;display:flex;flex-direction:column;justify-content:flex-end;height:100%;min-width:0}
.yoy-bars{display:flex;align-items:flex-end;gap:3px;height:100%}
.yoy-bars i{flex:1;border-radius:4px 4px 0 0;min-height:2px}
.yoy-bars i.ty{background:var(--bar)}
.yoy-bars i.ly{background:var(--bar2)}
.yoy-lb{font-size:var(--t-cap);color:var(--ink3);font-weight:700;text-align:center;margin-top:6px}

/* <i> is used as a generic bar-segment element throughout these widgets (it is the shortest
   tag that takes a background). It defaults to italic, which turned every count printed inside
   a segment into italics — reset it wherever a segment can carry text. */
.sharebar i,.lc-bar i,.age-bar i,.rb-track i,.mig-bars .mb,.yoy-bars i,.hb-fill{font-style:normal}

/* year-on-year: value axis so the bars can be read as amounts, not just shapes */
.yoy-wrap{display:flex;align-items:stretch;gap:10px;margin-top:10px}
.yoy-ax{width:56px;flex:0 0 auto;display:flex;flex-direction:column;justify-content:space-between;
  height:132px;position:relative}   /* == .yoy height / YOY_H — the axis, bars and gridline size below must all match */
.yoy-tick{position:relative;height:0}
.yoy-tick span{position:absolute;right:0;top:-7px;font-size:var(--t-cap);color:var(--ink3);
  font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.yoy-wrap .yoy{flex:1;margin-top:0;background-image:repeating-linear-gradient(to bottom,
  var(--line2) 0 1px, transparent 1px var(--yoy-gap,47.5px));background-position:0 0;
  background-repeat:no-repeat;background-size:100% 132px}

/* ══════════════ new chart widgets (widgets.js batch 2) ══════════════ */
/* gauge — semicircle, Operations kitchen efficiency */
.gaugew{text-align:center;padding:4px 8px}
.gauge{width:100%;max-width:150px;display:block;margin:0 auto}
.gauge .gtrack{stroke:var(--line2)}
.gauge .gval{font-size:22px;font-weight:800;fill:var(--ink);font-variant-numeric:tabular-nums}
.glabel{font-size:var(--t-body);color:var(--ink3);font-weight:600;margin-top:2px;line-height:1.3}
.gband{font-size:var(--t-body);font-weight:800;margin-top:5px}
.gband.good{color:var(--good)}.gband.watch{color:var(--warn)}.gband.bad{color:var(--danger)}
.gband .sub{color:var(--ink3);font-weight:500}

/* bubble scatter — Menu Engineering */
.bubblew{margin-top:8px}
/* No max-height. It was 380px against viewBoxes declared 866x380 (this deck's two matrices), so at 1920
   the box stayed 380 tall while the width grew to 1346 — the aspect no longer matched the viewBox and
   preserveAspectRatio="meet" letterboxed the whole chart into an 866px column with ~240px of white each
   side. The scale is held by chartAbs() instead, which keeps the axis and quadrant type at its authored
   pixel size however far the box has grown. Same fix, same reason, as the Sankey above. */
.bubblew svg{width:100%;display:block}
.bubblew .grid{stroke:var(--line);stroke-width:1;stroke-dasharray:4 4}
/* Quadrant captions sit UNDER the bubbles they describe, so a dense corner swallowed them. A
   halo in the panel colour keeps them readable without adding a box that would fight the plot. */
.bubblew .q-lab{font-size:var(--t-cap);fill:var(--ink3);font-weight:700;
  stroke:var(--card);stroke-width:3.5px;paint-order:stroke fill;stroke-linejoin:round}
.bubblew .sc-axt{font-size:12px;fill:var(--ink2);font-weight:700}
/* Bubble name, for the charts that opt into labelling (bubble's label:true). Ink rather than --ink3: it
   names the mark, it is not a caption about it. */
.bubblew .bub-lb{font-size:var(--t-cap);fill:var(--ink);font-weight:700}
.bubblew .bub:hover circle{fill-opacity:0.8}

/* sankey — member migration */
.sankeyw{margin-top:8px}
/* No max-height. It was 320px against a viewBox declared 866x340, so at 1920 the diagram wanted 528px,
   got clamped, and preserveAspectRatio="meet" letterboxed it — the whole Sankey shrank into the middle of
   the panel with ~200px of white either side. The scale is held instead by chartAbs(), which keeps the
   node labels at their authored pixel size however far the box has grown. */
.sankeyw svg{width:100%;display:block}
.sk-head{display:flex;justify-content:space-between;font-size:12px;font-weight:700;color:var(--ink3);margin-bottom:6px;padding:0 4px}
.sk-lb{font-size:var(--t-cap);font-weight:800}
.sk-v{font-size:var(--t-cap);font-weight:700;font-variant-numeric:tabular-nums}

/* trend — dated bars + optional line */
.trendw{margin-top:8px}
.trendw svg{width:100%;max-height:280px;display:block}
/* .grid and .tr-ax were restated here — a third copy of the same furniture, at a stroke-width and
   font-size that scaled with each chart's box. Both come from the shared chart block above now. */

/* heatmap grid — day × hour */
.heatgridw{margin-top:8px;font-size:var(--t-cap)}
.hg-row{display:grid;grid-template-columns:42px repeat(var(--hg-cols,24),1fr);gap:3px;align-items:center;margin-bottom:3px}
.hg-head .hg-h{font-size:var(--t-cap);color:var(--ink3);font-weight:700;text-align:center}
.hg-cnr{width:42px}
.hg-rl{font-size:var(--t-body);color:var(--ink2);font-weight:700}   /* row header beside the grid */
.hg-c{height:20px;border-radius:4px;background:var(--line2)}
/* the PEOPLE ramp — this grid is "members by time of day" and its only caller is Customers, so it
   takes the cyan steps (see .cohort above for the split). Cells carry no text; the legend prints
   Low/High at either end. */
.hgl0{background:#a5f3fc}.hgl1{background:#67e8f9}.hgl2{background:#22d3ee}
.hgl3{background:#06b6d4}.hgl4{background:#0891b2}.hgl5{background:#0e7490}
.hg-legend{display:flex;align-items:center;gap:4px;margin-top:8px;font-size:var(--t-cap);color:var(--ink3);font-weight:600}
.hg-legend i{width:16px;height:12px;border-radius:4px;display:inline-block}

/* donut legend rows that are drill links */
a.shleg-link{text-decoration:none;color:inherit;cursor:pointer}
a.shleg-link:hover .shk{text-decoration:underline;color:var(--brand)}

/* gauge row (Operations kitchen) */
.gaugerow{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:6px;align-items:stretch}
.gaugerow .gaugew{border:1px solid var(--line);border-radius:var(--r);padding:12px 8px}

/* stylised Malaysia map (widgets.js) */
.mapw{margin-top:8px;position:relative}
/* 400px gave the heat map a 430px panel that is mostly SEA — Malaysia's two landmasses sit at
   opposite ends of a wide bbox, so height buys empty water, not readable dots. */
.mapw svg{width:100%;max-height:300px;display:block}
.map-rl{font-size:var(--t-cap);fill:var(--ink3);font-weight:700}
.map-leg{margin-top:10px;justify-content:center}

/* ---- choropleth (.mapw-cho, user 2026-08-03) ----
   Sized by WIDTH, not by a height cap (2026-08-03, second pass: enlarge the peninsula further). A
   height cap means the box aspect stops matching the viewBox, and "meet" then letterboxes: at 1440 the
   470px cap drew a 706px-wide map in an 866px-wide panel and left 160px of white for nothing. Filling
   the column instead is a straight 1.4x on the peninsula, at no cost but page height.
   max-width caps the growth so 1920 does not get a 1,346px map: past ~900px the extra pixels buy
   nothing — six fills are already comparable — and the panel would run past a laptop screen. The map
   is centred inside the column when the cap binds. */
.mapw-cho svg{max-height:none;max-width:900px;margin-left:auto;margin-right:auto}
/* Region names, haloed whatever they land on: most sit ON their region, where --ink3 over the dark
   end of the indigo ramp is 1.6:1, and they cross the outlet dots besides. paint-order puts the white
   stroke UNDER the fill, so the letterforms keep their real weight instead of thinning. */
.mapw-cho .map-rl{fill:var(--ink);stroke:#fff;stroke-width:3;paint-order:stroke}
/* pointer-events:none because a leader line crosses the map: Kuala Lumpur's ran over Berjaya Times
   Square and The Exchange TRX and swallowed their hover. Decoration must never be a hit target. */
.map-lead{stroke:var(--ink3);stroke-width:.5;opacity:.65;pointer-events:none}
/* The fill IS the reading, so hover must not recolour it: a white inner edge lifts the region off
   its neighbours and leaves the value alone. */
.mapw-cho path.st-reg{cursor:pointer}
.mapw-cho path.st-reg:hover{stroke:#fff;stroke-width:2}
/* Outlet-dot hover tooltip — floats just above the dot, name in ink + status muted. */
.map-tip{position:absolute;transform:translate(-50%,-125%);pointer-events:none;z-index:5;
  background:var(--ink);color:#fff;border-radius:8px;padding:8px 12px;white-space:nowrap;
  font-size:var(--t-cap);line-height:1.35;box-shadow:0 4px 14px rgba(15,23,42,.22)}
.map-tip[hidden]{display:none}
.map-tip b{font-weight:700;display:block}
.map-tip .mt-st{display:block;margin-top:2px;opacity:.78;font-weight:600}

.askbtn svg{width:15px;height:15px}

/* diverging bar — signed values around a centre zero (mix shift) */
.divbars{display:flex;flex-direction:column;gap:2px;margin-top:6px}
.dv-row{display:flex;align-items:center;gap:11px;padding:8px 0;border-top:1px solid var(--line2);font-size:13.5px}
.dv-row:first-child{border-top:none}
.dv-k{width:120px;flex:0 0 auto;font-weight:600;color:var(--ink2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dv-track{flex:1;position:relative;height:16px;background:var(--line2);border-radius:4px;min-width:60px}
.dv-mid{position:absolute;left:50%;top:-2px;bottom:-2px;width:1px;background:var(--ink3);opacity:.5}
.dv-fill{position:absolute;top:0;bottom:0;border-radius:4px}
.dv-fill.g{background:#0fb07a}.dv-fill.b{background:#e11d48}
.dv-v{width:56px;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}

/* ══════════════ hover feedback for chart marks (batch A–D widgets) ══════════════
   Every mark that carries a <title> tooltip now gives a visible cue on hover so users discover
   it's interactive. SVG :hover works on the shapes directly. */
.trendw rect{transition:opacity .1s}
.trendw:hover rect{opacity:.5}
.trendw rect:hover{opacity:1}
.trendw polyline{pointer-events:none}
.hg-c{transition:box-shadow .1s,transform .1s}
.hg-c:hover{box-shadow:inset 0 0 0 2px var(--ink);position:relative;z-index:1}
/* Hover grows the dot proportionally (scale, not a fixed r) so variable-size dots on the sales
   heat map enlarge relative to their own size instead of all snapping to one radius. */
.mapw circle[fill-opacity]{transition:transform .1s,stroke-width .1s;cursor:pointer;transform-box:fill-box;transform-origin:center}
.mapw circle[fill-opacity]:hover{stroke:var(--ink);stroke-width:1.4;transform:scale(1.35)}
/* ---- Sankey selection ----
   The old rules were `.sankeyw:hover path{fill-opacity:.22}` plus `path:hover{fill-opacity:.6}`: entering
   the PANEL faded all eighteen ribbons, and recovering one meant hitting it — 1.2px wide at the thin end.
   Dimming is now driven by .sk-on, which only appears once a node or a ribbon is actually selected, so
   nothing fades until there is something to compare it against. */
.sankeyw path{transition:fill-opacity .12s}
.sankeyw.sk-on path{fill-opacity:.1}
.sankeyw.sk-on path.on{fill-opacity:.78}
.sankeyw path:hover{fill-opacity:.72}
.sk-node{cursor:pointer}
.sk-node rect{transition:opacity .12s}
.sankeyw.sk-on .sk-node:not(.on) rect{opacity:.45}
.sk-node:focus{outline:none}
.sk-node:focus-visible rect{stroke:var(--ink);stroke-width:2.5}
/* The flow labels written onto the diagram on selection (reference: Sankey Diagram Percentages.jpg).
   They sit over ribbons of any fill and over white, so the text is stroked white UNDER its own fill —
   paint-order:stroke is what makes that a halo rather than an outline drawn on top of the glyphs. The
   floating readout that used to do this job is gone: everything it said is now either on the ribbon it
   describes or on the node block beside it. */
.sankeyw{position:relative}
.sk-flab{font-size:var(--t-cap);font-weight:800;fill:var(--ink);stroke:#fff;stroke-width:3;
  paint-order:stroke;pointer-events:none}
.stArea polygon,.trendw polygon{transition:fill-opacity .12s}
.trendw polygon:hover{fill-opacity:1}
/* ranked & diverging bars: subtle row highlight for scanning */
.rb-row,.dv-row{transition:background .1s}
.rb-row:hover,.dv-row:hover{background:var(--accent-soft)}
.rb-row:hover .rb-track i{background:var(--brand)}
/* gauge card: lift on hover (they group per-metric). .tile dropped out of both rules on 2026-08-03 — it
   has no border or shadow left to animate, and a hover lift on something that isn't clickable was always
   claiming an affordance the tile does not have. */
.gaugerow .gaugew{transition:border-color .12s,box-shadow .12s}
.gaugerow .gaugew:hover{border-color:var(--line);box-shadow:0 2px 10px rgba(20,24,31,.06)}
