/* ==========================================================================
   Scale Info · Whop Ads: design system, techy edition. DESIGN-TECH.md is the spec; this file implements it.
   DESIGN.md stays as the record of why the older rules were made (layout-shift guards, the sticky table, touch
   targets); where the two disagree, DESIGN-TECH.md wins.

   A data terminal for one ad account: Scale Info green on near-black, every number in a monospace with fixed-width
   figures, panels drawn as 1px outlines with corner brackets, a faint engineering grid behind everything and one slow
   scan band on mouse screens. It still has to read on a phone between sales calls, so body copy stays in DM Sans,
   contrast is measured (DESIGN-TECH section 6) and nothing blinks.

   1. Tokens                            5. Primitives: page header, panels, buttons, forms, segmented controls,
   2. Base                                 badges and LEDs, tables, stats, console, empty state, loading, focus
   3. Motion                            6. Login
   4. App shell                         7. Responsive + user preferences
   then: backdrop (grid + scan) · this page (range chip, picker, Overview hero tiles, sparklines, trend, board, table,
   thumbnails, Clients) · sections (views, grids, strips, panels, top lists, toolbars, Creatives, Leads, Cash, Data,
   tags) · phones (tab bar, More sheet) · the Cortana build (funnel, sources, latest clients, ranked campaigns and
   creatives, data health, People links, source pills, People modal, journey, dropdown) · the edit build (lockbox,
   switches, row menu, budget pencil, parent bar, PIN and edit dialogs, toasts, Data → Editing) · forced colours · perf
   (fallback fonts). The notes disclosure sits with the console it wraps, in the primitives.

   Load order: one sheet, one request, sections in the order above so a later rule wins a specificity tie. The page
   rules come after the primitives on purpose.
   Perf: every surface is opaque and nothing uses backdrop-filter, filter or mix-blend-mode: the scan band moves
   behind the page on the compositor, and anything that sampled it would repaint every frame. Animations and
   transitions touch transform and opacity only. The one infinite animation in this file is the edit build's spinner,
   which runs only while a change is being applied in Whop (a few seconds, on one control) and never under reduced
   motion; the other loop in the product is the scan band, which atmosphere.js starts with the Web Animations API on
   mouse screens only and cancels (not pauses) when the tab is hidden or the page goes unused.
   ========================================================================== */

/* ---------- 1. tokens ---------- */
:root {
  color-scheme: dark;

  /* Surfaces: all opaque. With no glass there is nothing for reduced transparency to solidify, contrast is measured
     once, and the scan band behind the panels can never show through a figure. */
  --bg: #070A08;
  --surface: #0C110D;         /* every panel: KPI tiles, the trend panel, the board card, the login card, the picker */
  /* The same value as --surface. The sticky table cells once needed an opaque twin of a translucent card; the card is
     opaque now, so the twin is simply the panel colour. The name stays so the sticky rules read as before. */
  --surface-flat: #0C110D;
  --surface-2: #101711;       /* insets: sticky header row, totals row, segmented tracks, preset list, tooltip, inputs */
  --surface-hover: #151E16;   /* row hover, hovered preset or day, the selected segment under its accent bar */
  --surface-solid: #151E16;   /* <option> */
  --surface-sheen: none;      /* retired with the glass; kept as none so any old reference is harmless */
  --surface-sidebar: #090D0A;
  --surface-bar: #090D0A;     /* phone top bar and tab bar: page text scrolls under both, so they are opaque */
  --border: rgba(161, 249, 99, 0.14);        /* panel outlines: decorative, panels are also told apart by spacing */
  --border-strong: rgba(161, 249, 99, 0.30); /* dividers that must read: under the header row, above the totals */
  /* The boundary of every interactive control: 3.4:1 on --bg and --surface, over WCAG 1.4.11's 3:1 for components. */
  --line-ui: rgba(161, 249, 99, 0.45);
  /* Neutral, so a 200-row table and the chart's gridlines are not a green cage. */
  --line-neutral: rgba(255, 255, 255, 0.06);
  --bracket: rgba(161, 249, 99, 0.60);
  --grid-minor: rgba(161, 249, 99, 0.035);
  --grid-major: rgba(161, 249, 99, 0.07);
  --scrim: rgba(3, 5, 4, 0.72); /* the picker sheet's backdrop on phones: a flat colour, never a blur */

  /* Text. Measured (on --bg / --surface / --surface-hover): --text 17.0 / 16.3 / 14.6, --text-2 9.0 / 8.7 / 7.8,
     --text-3 5.6 / 5.3 / 4.8 (and 5.1 on --surface-2). --text-3 never sits on an accent tint (3.9:1 there). */
  --text: #E6F0E7;
  --text-2: #A3B3A5;
  --text-3: #7A8C7D;

  /* One accent (Scale Info's yellow-green) and the semantic colours. */
  --accent: #A1F963;
  --accent-hover: #B4FB82;     /* accent *text*: 12:1 on its own tint over a panel */
  /* Filled controls carry dark text: white on this green is 1.5:1, the page colour is 13.7:1. */
  --accent-fill: #A1F963;
  --accent-fill-hover: #B4FB82; /* lighter, not darker: on a dark page a filled control brightens when touched */
  --accent-fill-text: #070A08;
  --accent-soft: rgba(161, 249, 99, 0.12);
  --accent-glow: rgba(161, 249, 99, 0.45); /* the only glow colour: lit LEDs, primary hover, range ends */
  --success: #A1F963;
  --success-soft: rgba(161, 249, 99, 0.12);
  --warning: #FFC23D;
  --warning-soft: rgba(255, 194, 61, 0.10);
  --warning-text: #FFD98A;     /* 13.5:1 on --surface-2, 11.8:1 on its tint */
  --danger: #FF5A4E;           /* 6.2:1 on --surface */
  --danger-soft: rgba(255, 90, 78, 0.12);
  --danger-text: #FF8F86;      /* 6.7:1 on its tint over a hovered row */

  /* Status LEDs. */
  --led-on: #A1F963;
  --led-warn: #FFC23D;
  --led-bad: #FF5A4E;
  --led-off: #5C6B5E;

  /* Chart series, validated as a set (DESIGN-TECH 2.3): each at least 4.6:1 on --surface-2, and every strip carries
     its own label, so identity never rests on colour. The brand green is deliberately not a series: it means
     "interactive / selected" everywhere else on the page. Text never wears a series colour. */
  --series-spend: #D55181;
  --series-leads: #199EB0;
  --series-calls: #8F7FF0;
  --series-cash: #3C9E2C;

  /* Where a client came from (DESIGN-TECH 13.4): one text colour per source, each at least 4.5:1 on --surface and on
     its own tint over it (measured on --surface / on the tint: whop 15.5 / 12.1, meta 8.8 / 7.3, ig 8.5 / 7.1, fb
     10.2 / 9.0, google 11.5 / 9.1, ref 10.1 / 8.2; on --surface-2 each is at least 6.7 on its tint). The
     word inside the pill always names the source, so the hue only repeats it. Whop ads wear the brand's accent text,
     because Whop ads are what this dashboard is about; the organic and unpaid sources are quieter. */
  --src-whop: #A3FF66;
  --src-meta: #7FB2FF;
  --src-ig: #F58BC0;
  --src-fb: #9DBEFF;
  --src-google: #7FD9C4;
  --src-ref: #C2B2FF;
  /* The journey's "Page View" tag (Cortana's orange): 10.3:1 on --surface, 8.4:1 on its own tint. */
  --tag-page: #FFAD5C;

  /* Radius: chips, badges, skeletons, day cells · buttons, inputs, segments, the range chip · tooltip, preset list ·
     panels, the picker. Sharp, not razor: 2–4px keeps antialiased corners crisp at 1px outlines. */
  --r-sm: 2px;
  --r-md: 3px;
  --r-lg: 4px;
  --r-xl: 4px;
  --r-full: 999px; /* LED dots and the sheet's grab bar only */

  /* spacing: the only spacings allowed */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-7: 32px;
  --s-8: 40px;

  /* sizes */
  --head-h: 32px;      /* the table's header row */
  --row-h: 40px;       /* body and totals rows; 48px on a coarse pointer (section 7) */
  --row-h-thumb: 52px; /* Ads and Clients rows: the creative thumbnail needs the height */
  --thumb: 40px;       /* thumbnail square; 32px on phones */
  /* The chart's reserved height (a layout-shift guard): four strips of a 16px label row, a 4px gap and a plot band,
     the gaps between strips, and a 20px axis row. chart.js draws to the same numbers; they are named here so the
     skeleton and the empty state hold exactly the box the chart will take. */
  --trend-band: 48px;
  --trend-strip-gap: var(--s-3);
  --trend-h: calc(4 * (16px + 4px + var(--trend-band)) + 3 * var(--trend-strip-gap) + 20px); /* 328px */

  /* effects */
  --focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
  --shadow-pop: 0 12px 32px -12px rgba(0, 0, 0, 0.8); /* the popover and the tooltip; nothing blurs */

  /* motion: entrances only */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-1: 120ms;
  --dur-2: 160ms;
  --dur-3: 220ms;

  /* layout */
  --content-max: 1200px;
  --sidebar-w: 240px;
  --gutter: var(--s-8); /* .main side padding: 40 → 32 (≤1100px) → 16 (≤860px) */
  --card-pad: 0px;      /* set by .card; lets the table run edge to edge inside its card */
  --topbar-h: 0px;      /* phone top bar, 56px at ≤860px */
  --tabbar-h: 0px;      /* phone tab bar, 56px at ≤860px */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);

  /* Two families from one Google Fonts request (index.html, login.html). DM Sans for words people typed and read;
     JetBrains Mono for every number and every label, so columns of money line up digit for digit. The "Early" faces
     (the same files, preloaded by index.html) and the stand-ins are in the perf block at the end. */
  --sans: "DM Sans", "DM Sans Early", "DM Sans Fallback", "DM Sans Fallback Android", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", "JetBrains Mono Early", "JetBrains Mono Fallback Consolas", "JetBrains Mono Fallback", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* type: the title and the KPI figures scale with the screen up to their desktop size and never past it. A 28px mono
     figure is about as wide as the old 36px DM Sans one, so the KPI grid's steps still hold; fitKpiValues() in app.js
     stays the safety net for the odd long figure. */
  --fs-title: clamp(22px, 4vw + 8px, 28px);
  --fs-kpi: clamp(22px, 2.4vw + 13px, 28px);
  /* The Overview's five headline figures are the page's loudest type: 24px on a phone's half-width tile (so
     "A$43,281" still fits 131px), 32px from a laptop up. fitKpiValues() in app.js shrinks the odd long one. */
  --fs-hero: clamp(24px, 1.2vw + 14px, 32px);
  --fs-body: 14px;
  --fs-input: 14px;

  /* icons for CSS masks (painted with background-color) */
  --icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7.5v5.5M12 16.5h.01'/%3E%3C/svg%3E");
  --icon-broken: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='1'/%3E%3Cpath d='M3 16l5-5 4 4M14 13l2-2 5 5M3 3l18 18'/%3E%3C/svg%3E");
  /* The edit lockbox's padlock, shut ("Unlock editing") and open ("Editing on"). */
  --icon-lock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='11' width='14' height='10' rx='1'/%3E%3Cpath d='M8 11V7a4 4 0 0 1 8 0v4'/%3E%3C/svg%3E");
  --icon-unlock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='11' width='14' height='10' rx='1'/%3E%3Cpath d='M8 11V7a4 4 0 0 1 7.6-1.7'/%3E%3C/svg%3E");
}

@media (max-width: 1100px) {
  :root { --gutter: var(--s-7); }
}
@media (max-width: 860px) {
  /* Phones: reading text steps up to 15px, and the chart's strips get shorter so the panel stays a thumb's scroll. */
  :root {
    --gutter: var(--s-4); --topbar-h: 56px; --tabbar-h: 56px; --fs-body: 15px;
    --trend-band: 36px; --trend-strip-gap: var(--s-2); /* --trend-h: 268px */
  }
}
@media (max-width: 639px) {
  :root { --thumb: 32px; }
}
/* A finger needs a taller row than a mouse does, and a header row it can sort by: every header cell is a sort button,
   44px tall on touch (section 7), over the cell's 1px rule. */
@media (pointer: coarse) {
  :root { --row-h: 48px; --head-h: 45px; }
}

/* ---------- 2. base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  background: var(--bg);
  /* Nothing on the page is meant to be wider than the viewport, but a long campaign name in a flex row can still
     push one. `clip` stops the page scrolling sideways without turning the root into a scroll container, which
     `hidden` would do, and which would break every sticky cell in the table. */
  overflow-x: clip;
  -webkit-text-size-adjust: 100%; text-size-adjust: 100%;
  /* Focus and in-page jumps never land under the fixed phone bars. */
  scroll-padding-top: calc(var(--topbar-h) + var(--safe-top) + var(--s-4));
  scroll-padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom) + var(--s-4));
  scrollbar-color: rgba(161, 249, 99, 0.25) transparent;
}
body {
  margin: 0; min-height: 100dvh;
  background: var(--bg); color: var(--text);
  font-family: var(--sans); font-size: var(--fs-body); font-weight: 400; line-height: 1.55;
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
::selection { color: var(--text); background: rgba(161, 249, 99, 0.35); }
a { color: inherit; text-decoration: none; }
/* Plain inline links (no class) read as links: accent text, underline on hover. */
a:not([class]) { color: var(--accent-hover); text-underline-offset: 3px; }
/* Touch: no grey flash on tap (every control has its own pressed and focus look), no double-tap-to-zoom wait. */
a, button, summary, label, select, [role="button"] { -webkit-tap-highlight-color: transparent; }
a, button, summary, label, select, input, textarea, [role="button"] { touch-action: manipulation; }

h1, h2, h3, h4 { margin: 0; font-weight: 600; color: var(--text); }
/* The page title is plain text now: the silver gradient went with the glass. */
h1 { font-size: var(--fs-title); line-height: 1.15; letter-spacing: -0.02em; overflow-wrap: anywhere; }
h2 { font-size: 15px; line-height: 1.4; letter-spacing: -0.01em; }
h3 { font-size: 14px; line-height: 1.4; letter-spacing: -0.005em; }
h4 { font-size: 13px; line-height: 1.4; }
p { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; letter-spacing: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; }
option, optgroup { background: var(--surface-solid); color: var(--text); }
[hidden] { display: none !important; }
/* left: 0, so it never sits where it would in the line: after a long name that's cut off with an ellipsis, that spot can
   be past the screen's edge, and a phone then widens the whole page to reach it. */
.sr-only {
  position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* A table ignores width: 1px: as display: table it sizes to its content, so the trend's visually hidden table was
   674px wide on a 375px phone and widened the whole page (verify-1: the fixed bars grew past the screen and the
   picker sheet opened below it). As a block the box obeys the 1px; the rows inside still form a table, so a screen
   reader reads it exactly as before. Every visually hidden table gets this, not only the trend's. */
table.sr-only { display: block; }
@media (hover: hover) {
  a:not([class]):hover { text-decoration: underline; }
}

/* ---------- the Label style ---------- */
/* Mono 11 / 500 / 0.10em uppercase: every overline, column header, tab, button, panel title, LED word, weekday and
   tag. One rule group, so the voice of the labels is set in one place; a component that needs another size or colour
   says only that, after this group. Figures get tabular numerals everywhere: a no-op in a monospace, but the
   fallback stack may land on a proportional face before the web font arrives. */
.stat .label, .kpi-label, .table thead th, .tab, .seg-btn, .btn, .panel-title, .badge, .led-word,
.range-tag, .brand-sub, .range-pop-title, .cal-month-label, .cal-month th, .trend-tip-date, .tag, .field, .login-h,
.sys, .notice::before, .banner::before, .grid-wrap .table tfoot th:first-child,
.nav-group-label, .page-kicker, .more-title, .panel-link, .stat-k, .creative-k, .creative-stat dt,
.top-fig dt, .conn-status, .method-k, .back-link, .step::before,
.notes-label, .funnel-k, .ov-creative-figs dt, .ov-health-item dt, .jd-summary dt, .journey-attr dt, .journey-more > summary,
.edit-unlocked, .edit-lock-btn, .edit-sample, .edit-parent-level, .ed-label, .ed-diff dt, .edit-toast-action, .edp-result {
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1.3;
  letter-spacing: 0.10em; text-transform: uppercase; font-variant-numeric: tabular-nums;
  color: var(--text-2);
}
.num, .kpi-value, .kpi-meta, .range-dates, .range-pop-status, .panel-meta, .crumbs, .trend-tip, .count-link,
.cal-day, .preset-hint, .range-zone, .kpi-fx, .col-src,
.nav-badge, .tabbar-badge, .tabbar-label, .stat-v, .creative-v, .creative-stat dd, .top-fig dd, .view-count, .method-n,
.conn-meta, .step-live, .creative-path, .leads-tick, .band-n,
.notes-count, .kpi-from, .funnel-v, .funnel-conv, .funnel-cost, .src-total, .src-n, .src-amt, .src-pct, .latest-meta,
.latest-amt, .share-pct, .ov-creative-rank, .ov-creative-figs dd, .ov-health-item dd, .pm-tz, .pm-count, .pm-value,
.pm-date, .journey-time, .dd-button-label,
.edit-parent-budget, .ed-money, .ed-chip, .ed-count, .ed-step, #edit-count, .ed-diff dd, .ed-pct, .ed-usd, .edp-when, .edp-who {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}

/* ---------- 3. motion ---------- */
/* 120–220ms entrances, transform and opacity only. The page header and the login card fade up once, the picker pops
   in, the skeleton pulses once, the selected level tab's bar grows. State colour changes are instant: no transition
   on a colour, border or background anywhere. */
@keyframes fade-up { from { opacity: 0; transform: translateY(6px); } }
@keyframes skeleton-pulse { 50% { opacity: 0.5; } }
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px); } }
@keyframes sheet-up { from { opacity: 0; transform: translateY(16px); } } /* the picker sheet and the More sheet */

.page-head { animation: fade-up var(--dur-3) var(--ease) backwards; }

/* ---------- 4. app shell ---------- */
/* App root (both pages) hosts the backdrop; isolation keeps its negative-z layers above body. */
.app { position: relative; isolation: isolate; min-height: 100dvh; }

.skip-link {
  position: fixed; top: var(--s-3); left: var(--s-3); z-index: 200;
  display: inline-flex; align-items: center; height: 40px; padding: 0 var(--s-4);
  border-radius: var(--r-md); background: var(--accent-fill); color: var(--accent-fill-text);
  font-size: 14px; font-weight: 600;
  transform: translateY(calc(-100% - var(--s-6)));
  transition: transform var(--dur-2) var(--ease);
}
.skip-link:focus { transform: none; }

.shell { position: relative; isolation: isolate; display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100dvh; }

/* Sidebar (desktop): brand · nav · Open in Whop · system status · (space) · Sign out. */
.side {
  position: sticky; top: 0; z-index: 20; height: 100dvh;
  display: flex; flex-direction: column; gap: var(--s-1);
  padding: var(--s-6) var(--s-4) var(--s-4);
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface-sidebar);
  border-right: 1px solid var(--border);
}
/* Brand block: the bolt in an outlined square beside the wordmark and its "WHOP ADS" line. */
.brand { display: flex; align-items: center; gap: var(--s-3); min-width: 0; padding: 0 var(--s-2) var(--s-5); border-radius: var(--r-md); }
.brand-mark {
  display: inline-grid; place-items: center; flex: none; width: 28px; height: 28px;
  border: 1px solid var(--line-ui); border-radius: var(--r-sm); background: transparent; color: var(--accent);
}
.brand-mark svg { width: 16px; height: 16px; }
.brand-text { display: flex; flex-direction: column; min-width: 0; }
.wordmark { font-size: 17px; font-weight: 600; line-height: 1.2; letter-spacing: -0.02em; white-space: nowrap; color: var(--text); }
.brand-sub { color: var(--text-3); }

/* The sidebar CTA: the plain outlined button (the halo and its two loops are retired), full width. */
.side .btn-new { width: 100%; height: 40px; margin: var(--s-5) 0 var(--s-3); }

/* The sections, in four labelled groups (PERFORMANCE · REVENUE · FUNNEL · SYSTEM). The group label is the Label
   style in --text-3 (5.6:1 on the sidebar), so it reads as a heading for the rows under it without competing with
   them; the space above each group does the separating, so no rule is drawn between them. */
.side-nav { display: flex; flex-direction: column; }
.nav-group-label { margin: var(--s-5) 0 var(--s-2); padding: 0 var(--s-3); color: var(--text-3); }
.nav-group:first-child .nav-group-label { margin-top: var(--s-4); }
.nav-list { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
.nav-link {
  position: relative; display: flex; align-items: center; gap: var(--s-3);
  width: 100%; height: 40px; padding: 0 var(--s-3);
  border: 0; border-radius: var(--r-md); background: none;
  color: var(--text-2); font-size: 14px; font-weight: 500; line-height: 1; text-align: left;
  cursor: pointer;
}
.nav-icon { display: inline-grid; place-items: center; flex: none; width: 18px; height: 18px; color: var(--text-3); }
.nav-icon svg, .nav-link > svg { width: 18px; height: 18px; }
/* Its own line-height: the row's is 1, and the clip would shave the bottom off descenders (g, p, y). */
.nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; }
.nav-link[aria-current="page"], .nav-link.active { background: var(--accent-soft); color: var(--accent-hover); }
.nav-link[aria-current="page"] .nav-icon, .nav-link.active .nav-icon { color: var(--accent-hover); }
/* Active indicator: a 2px square-ended accent bar on the sidebar's left edge. */
.nav-link[aria-current="page"]::before, .nav-link.active::before {
  content: ""; position: absolute; top: var(--s-2); bottom: var(--s-2); left: calc(var(--s-4) * -1);
  width: 2px; background: var(--accent);
}
/* A count beside a section's name ("12" campaigns with spend, "8" new clients), pushed to the row's right edge. The
   number is decoration (aria-hidden, in .badge-n); the visually hidden words beside it are what a screen reader
   hears. The same chip serves the More sheet's rows. */
.nav-badge {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-1); flex: none;
  height: 18px; min-width: 20px; margin-left: auto; padding: 0 var(--s-1);
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-2); color: var(--text-2);
  font-size: 11px; font-weight: 500; line-height: 1; letter-spacing: 0;
}
:is(.nav-link, .more-link)[aria-current="page"] .nav-badge { color: var(--accent-hover); }
/* Data's alert: an amber dot and "!" in warning text, so the state is said by the mark and the word, not by colour. */
.nav-badge[data-tone="warn"] { border-color: rgba(255, 194, 61, 0.40); color: var(--warning-text); }
.nav-badge[data-tone="warn"]::before {
  content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--led-warn);
}
@media (hover: hover) {
  .nav-link:hover { background: var(--surface-hover); color: var(--text); }
  .nav-link:hover .nav-icon { color: var(--text-2); }
  .nav-link[aria-current="page"]:hover, .nav-link.active:hover { background: var(--accent-soft); color: var(--accent-hover); }
  .nav-link[aria-current="page"]:hover .nav-icon, .nav-link.active:hover .nav-icon { color: var(--accent-hover); }
}
/* The footer row (Sign out): pinned to the bottom, under a hairline. */
.side-foot { display: flex; flex-direction: column; gap: var(--s-1); margin-top: auto; padding-top: var(--s-3); border-top: 1px solid var(--border); }

/* System status: an LED and two short mono lines, "LIVE" and "UPD 14:02". app.js sets data-state on the block; the
   LED's colour follows it, and the word is always there beside it, so the state never rests on colour. */
.sys { display: flex; align-items: flex-start; gap: var(--s-2); padding: 0 var(--s-3); }
.sys .led { margin-top: 4px; }
.sys-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
#sys-label { color: var(--text-2); }
/* The time line keeps its height while it is empty, so the sidebar does not shift when the first answer lands. */
#sys-updated { min-height: 1.3em; color: var(--text-3); }
.led {
  display: inline-block; flex: none; width: 6px; height: 6px; border-radius: var(--r-full);
  background: var(--led-off);
}
/* Live: lit, with the one static glow an LED is allowed (painted with the element, never animated). Busy: a hollow
   green ring, "working", not "fine". Stale and sample: amber. Error: red. */
.sys[data-state="live"] .led { background: var(--led-on); box-shadow: 0 0 6px var(--accent-glow); }
.sys[data-state="busy"] .led { background: transparent; box-shadow: inset 0 0 0 1px var(--led-on); }
.sys[data-state="stale"] .led, .sys[data-state="sample"] .led { background: var(--led-warn); }
.sys[data-state="error"] .led { background: var(--led-bad); }
.sys[data-state="live"] #sys-label { color: var(--accent-hover); }
.sys[data-state="stale"] #sys-label, .sys[data-state="sample"] #sys-label { color: var(--warning-text); }
.sys[data-state="error"] #sys-label { color: var(--danger-text); }

/* Main column: 1200px of content, centred. It is also the container the KPI grid's steps measure: how many tiles
   share a row depends on this column's width, not the window's. */
.main {
  width: 100%; min-width: 0; max-width: calc(var(--content-max) + var(--gutter) * 2); margin-inline: auto;
  padding: var(--s-8) var(--gutter) calc(var(--s-8) * 2);
  container: main / inline-size;
}
.main:focus { outline: none; }

/* ---------- 5. primitives ---------- */

/* page header: title + one-line description left, the page's actions right */
.page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--s-4) var(--s-6); margin-bottom: var(--s-7); }
.page-head > :first-child { flex: 1 1 320px; min-width: 0; }
/* "PERFORMANCE · 01": which group the open section belongs to and its place in the list, over the title. --text-3 on
   the page (5.6:1). Ships with Overview's text, so its line is there from the first paint. */
.page-kicker { margin-bottom: var(--s-2); color: var(--text-3); }
.page-head-text { min-width: 0; }
.lede { max-width: 68ch; margin-top: var(--s-2); font-size: 14px; font-weight: 400; line-height: 1.5; color: var(--text-2); }
.page-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--s-2); min-width: 0; max-width: 100%; }

/* text helpers */
.muted { color: var(--text-2); } /* secondary copy stays readable (4.5:1+); --text-3 is for meta only */
.small { font-size: 13px; line-height: 1.5; }
.tiny { font-size: 12px; line-height: 1.5; }

/* ---------- panels ---------- */
/* An opaque panel drawn as a 1px green hairline. isolation makes each panel its own stacking context, so the
   brackets below can sit over the board's sticky cells (z 1–3) without reaching anything outside the card. */
.card {
  --card-pad: var(--s-6);
  position: relative; isolation: isolate; padding: var(--card-pad);
  border: 1px solid var(--border); border-radius: var(--r-xl);
  background: var(--surface); box-shadow: none;
}
/* Corner brackets: eight 1px strokes painted once by one pseudo-element, no extra DOM and nothing that moves. The
   length is a custom property on the host (KPI tiles use 8px, the empty-state icon 6px) so one rule serves all. */
.card::before, .empty-state-icon::before, .range-pop::before, .stat-strip > .stat::before {
  content: ""; position: absolute; inset: -1px; z-index: 4; pointer-events: none;
  --bl: var(--bracket-l, 10px);
  background:
    linear-gradient(var(--bracket) 0 0) top left / var(--bl) 1px no-repeat,
    linear-gradient(var(--bracket) 0 0) top left / 1px var(--bl) no-repeat,
    linear-gradient(var(--bracket) 0 0) top right / var(--bl) 1px no-repeat,
    linear-gradient(var(--bracket) 0 0) top right / 1px var(--bl) no-repeat,
    linear-gradient(var(--bracket) 0 0) bottom left / var(--bl) 1px no-repeat,
    linear-gradient(var(--bracket) 0 0) bottom left / 1px var(--bl) no-repeat,
    linear-gradient(var(--bracket) 0 0) bottom right / var(--bl) 1px no-repeat,
    linear-gradient(var(--bracket) 0 0) bottom right / 1px var(--bl) no-repeat;
}
/* The board card and the picker clip their overflow, which would cut brackets drawn on the border; inside the border
   they still read as corners. */
.board-card::before, .range-pop::before { inset: 0; }

/* Panel head: a channel title on the left (an accent square, then the Label), a mono meta line on the right. */
.panel-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--s-1) var(--s-3); margin-bottom: var(--s-4); }
.panel-title { color: var(--text); }
.panel-title::before {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-right: var(--s-2); vertical-align: 1px;
  background: var(--accent);
}
/* Ships empty and is written with the first answer: a line's height is kept so the plot under it never moves. */
.panel-meta { min-height: 1.3em; font-size: 11px; font-weight: 500; line-height: 1.3; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-2); }

/* ---------- buttons ---------- */
/* Outlined by default (the --line-ui boundary), Label-style text; primary = solid accent with dark text. */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  height: 36px; padding: 0 var(--s-4);
  border: 1px solid var(--line-ui); border-radius: var(--r-md);
  background: transparent; color: var(--text);
  font-size: 12px; line-height: 1; letter-spacing: 0.08em; white-space: nowrap;
  text-decoration: none; cursor: pointer; user-select: none;
  transition: transform var(--dur-1) var(--ease);
}
.btn svg { flex: none; width: 16px; height: 16px; }
.btn:active { transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; transform: none; }

.btn-primary { border-color: var(--accent-fill); background: var(--accent-fill); color: var(--accent-fill-text); }
.btn-ghost { border-color: transparent; background: transparent; color: var(--text-2); }
/* A square icon button: the picker's close and month arrows, Refresh on phones. */
.btn-icon { width: 36px; padding: 0; }

.btn-lg { height: 40px; padding: 0 var(--s-5); }
.btn-block { display: flex; width: 100%; }

@media (hover: hover) {
  .btn:where(:not(:disabled, [aria-disabled="true"])):hover { background: var(--accent-soft); border-color: var(--accent); }
  /* The one button glow: a static ring and halo on the primary's hover, painted, never animated. */
  .btn-primary:where(:not(:disabled, [aria-disabled="true"])):hover {
    background: var(--accent-fill-hover); border-color: var(--accent-fill-hover);
    box-shadow: 0 0 0 1px var(--accent), 0 0 16px -4px var(--accent-glow);
  }
  .btn-ghost:where(:not(:disabled, [aria-disabled="true"])):hover { background: var(--surface-hover); border-color: transparent; color: var(--text); }
}

/* A <button> reset to read as a link: names that drill down, breadcrumb segments, path segments. It stays a button so
   the keyboard and screen readers get a real control. */
.link {
  border: 0; padding: 0; background: none; text-align: left;
  color: var(--text-2); font-weight: 500; text-decoration: none; text-underline-offset: 3px; cursor: pointer;
}
@media (hover: hover) {
  .link:hover { color: var(--accent-hover); text-decoration: underline; }
}

/* ---------- forms ---------- */
/* The label above in the Label style, the error below in --danger. Works on <label> or <div>. */
.field { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.hint { font-size: 12px; font-weight: 400; line-height: 1.5; color: var(--text-3); }
.field-error { font-size: 12px; font-weight: 500; line-height: 1.5; color: var(--danger); overflow-wrap: anywhere; }
.field-error::before {
  content: ""; display: inline-block; width: 14px; height: 14px; margin-right: 6px; vertical-align: -2px;
  background: currentColor;
  -webkit-mask: var(--icon-alert) center / contain no-repeat; mask: var(--icon-alert) center / contain no-repeat;
}
/* Gone, and taking no room, when there is nothing to say. */
.field-error:empty { display: none; }

/* Sans, not mono, so password dots are the familiar ones. Every font property is set outright: the input sits inside a
   .field label, and `font: inherit` (base) would otherwise hand it the label's mono capitals. */
.input {
  display: block; width: 100%; min-width: 0; height: 40px; padding: 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md);
  background: var(--surface-2); color: var(--text);
  font-family: var(--sans); font-size: var(--fs-input); font-weight: 400; line-height: 1.4;
  letter-spacing: normal; text-transform: none;
  -webkit-appearance: none; appearance: none;
}
.input::placeholder { color: var(--text-3); opacity: 1; }
.input:disabled { opacity: 0.5; cursor: not-allowed; }
.input[aria-invalid="true"], .input.bad { border-color: var(--danger); }
/* Browser autofill paints its own background; cover it with the input's look. */
.input:-webkit-autofill { -webkit-text-fill-color: var(--text); caret-color: var(--text); box-shadow: inset 0 0 0 100px var(--surface-2); }
@media (hover: hover) {
  .input:where(:not(:disabled, [aria-invalid="true"], .bad)):hover { border-color: var(--accent); }
}

/* ---------- segmented controls: level tabs and the attribution toggle ---------- */
/* A square track with an interactive boundary; segments butt together, split by a hairline. The selected segment is
   lifted and gets a 2px accent bar along its bottom edge, which grows in with scaleX: the one state transition the
   motion rules allow, because it runs on the compositor. */
.tabs, .seg {
  display: inline-flex; max-width: 100%; padding: 0;
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2);
}
.tabs {
  --tab-h: 32px; /* 44px on a coarse pointer (section 7); named so the empty track below can use it */
  overflow-x: auto; scrollbar-width: none; /* scrolls instead of wrapping on a narrow screen */
  scroll-snap-type: x proximity; overscroll-behavior-x: contain;
}
.tabs::-webkit-scrollbar { display: none; }
/* The level tabs ship empty in index.html and app.js builds them; a frame painted before the scripts ran would show a
   2px sliver that then grew and pushed the board down. Empty, the track is already one segment tall. */
.tabs:empty { min-height: calc(var(--tab-h) + 2px); }
.seg { --seg-h: 34px; overflow: hidden; } /* 36px with its border; 46px on phones and coarse pointers */
.tab, .seg-btn {
  position: relative; flex: none;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  padding: 0 var(--s-4);
  border: 0; border-radius: 0; background: transparent;
  box-shadow: inset -1px 0 0 var(--border);
  white-space: nowrap; cursor: pointer;
}
.tab { height: var(--tab-h, 32px); scroll-snap-align: start; }
.seg-btn { height: var(--seg-h); }
.tab:last-child, .seg-btn:last-child { box-shadow: none; }
.tab::before, .seg-btn::before {
  content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transition: transform var(--dur-2) var(--ease);
}
.tab.active, .tab[aria-pressed="true"], .tab[aria-selected="true"], .seg-btn[aria-pressed="true"] {
  background: var(--surface-hover); color: var(--accent-hover);
}
.tab.active::before, .tab[aria-pressed="true"]::before, .tab[aria-selected="true"]::before, .seg-btn[aria-pressed="true"]::before {
  transform: scaleX(1);
}
@media (hover: hover) {
  .tab:hover, .seg-btn:hover { color: var(--text); }
  .tab[aria-selected="true"]:hover, .tab.active:hover, .seg-btn[aria-pressed="true"]:hover { color: var(--accent-hover); }
}

/* ---------- badges and LEDs ---------- */
/* 20px, square-cornered, a 1px border over a soft tint; Label-style text. */
.badge {
  display: inline-flex; align-items: center; gap: var(--s-1);
  height: 20px; padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-hover); color: var(--text-2);
  line-height: 1; white-space: nowrap;
}
.badge.success { border-color: rgba(161, 249, 99, 0.40); background: var(--success-soft); color: var(--accent-hover); }
.badge.warning { border-color: rgba(255, 194, 61, 0.40); background: var(--warning-soft); color: var(--warning-text); }
.badge.danger { border-color: rgba(255, 90, 78, 0.40); background: var(--danger-soft); color: var(--danger-text); }
/* The KPI delta badge: neutral by default (the base badge), green or red by direction; the arrow and the number say
   which way it went, so the colour is never the only signal. */
.badge.delta { padding: 0 var(--s-2); }
.badge.accent { border-color: rgba(161, 249, 99, 0.40); background: var(--accent-soft); color: var(--accent-hover); }

/* Status LEDs: the status badge app.js already emits (span.badge.st-<status>, the word inside) becomes a dot and the
   word. No fill, no frame; the word is always present, so the state never rests on colour, and nothing blinks (a
   pulse would be an infinite animation per row in a 200-row table). */
.badge[class*="st-"] { height: auto; padding: 0; border: 0; background: none; gap: var(--s-1); color: var(--text-2); }
.badge[class*="st-"]::before {
  content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--led-off);
}
.badge.st-active { color: var(--accent-hover); }
.badge.st-active::before { background: var(--led-on); box-shadow: 0 0 6px var(--accent-glow); }
/* In the long lists (the grid's rows, the creative cards) the LED is lit without its glow: a blurred shadow per active
   row is a blur per row on every sort and filter, and the word beside the dot already says "active". */
:is(.grid-wrap, .creative-grid) .badge.st-active::before { box-shadow: none; }
.badge.st-in_review { color: var(--warning-text); }
.badge.st-in_review::before { background: var(--led-warn); }
.badge.st-rejected, .badge.st-payment_failed { color: var(--danger-text); }
.badge.st-rejected::before, .badge.st-payment_failed::before { background: var(--led-bad); }
/* Hollow LEDs are drawn with a border, not an inset shadow: most of the 216 Ads rows are paused, and a shadow per row
   was part of what a sort had to repaint (verify-1). */
.badge.st-paused::before { background: transparent; border: 1px solid var(--led-off); }
.badge.st-draft { color: var(--text-3); }
.badge.st-draft::before { background: transparent; border: 1px solid var(--text-3); }

/* A small Label-style tag, e.g. "Not matched" in the Clients tab. */
.tag {
  display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s-2);
  border: 1px solid var(--border); border-radius: var(--r-sm); white-space: nowrap; vertical-align: middle;
}
.tag-muted { background: var(--surface-2); color: var(--text-2); }

/* ---------- tables ---------- */
/* Dense rows, Label-style headers, neutral hairline dividers. Names stay in DM Sans (people typed them); figures are
   mono. A row header (th[scope="row"], the name cell app.js builds for every body row and the totals row) is a body
   cell that names its row, and looks like the cells beside it. */
.table { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; }
.table thead th {
  height: var(--head-h); padding: 0 var(--s-3);
  text-align: left; vertical-align: middle; white-space: nowrap;
  border-bottom: 1px solid var(--border-strong);
}
.table td, .table :is(tbody, tfoot) th { height: var(--row-h); padding: var(--s-2) var(--s-3); vertical-align: middle; color: var(--text-2); border-bottom: 1px solid var(--line-neutral); }
/* The browser's own th is bold and centred; a row header takes neither. */
.table :is(tbody, tfoot) th { text-align: left; font-weight: 400; }
.table tbody tr:last-child :is(td, th) { border-bottom: 0; }
.table td.num { font-size: 13px; line-height: 1.3; }
/* A table wider than its card scrolls inside it, settles on a column's edge, and keeps the swipe to itself. */
.table-wrap {
  overflow-x: auto; margin-inline: calc(var(--card-pad) * -1);
  scroll-snap-type: x proximity; scroll-padding-inline: calc(var(--card-pad) - var(--s-3)); overscroll-behavior-x: contain;
}
.table-wrap > .table th { scroll-snap-align: start; }
:is(.table-wrap, .card) > .table :is(th, td):first-child { padding-left: var(--card-pad); }
:is(.table-wrap, .card) > .table :is(th, td):last-child { padding-right: var(--card-pad); }
@media (hover: hover) {
  .table tbody tr:hover :is(td, th) { background: var(--surface-hover); }
}

/* ---------- stats ---------- */
.stat .label { display: block; }
.stat .value { margin-top: var(--s-2); font-size: var(--fs-kpi); font-weight: 500; line-height: 1.1; letter-spacing: -0.02em; color: var(--text); }
.stat .sub { margin-top: var(--s-1); font-size: 13px; line-height: 1.5; color: var(--text-2); }

/* ---------- console: notices and the sample banner ---------- */
/* The warnings strip reads as a system log: an inset panel with an amber rule down its left edge, a "NOTICES" header
   line, and each sentence numbered [01], [02]. The header word means the state never rests on colour. Decorative
   generated text uses the `content: "…" / ""` alt syntax so it stays out of what a screen reader announces; the plain
   declaration before it is the fallback for a browser without the syntax. */
.notice, .banner {
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border); border-left: 2px solid var(--warning); border-radius: var(--r-md);
  background: var(--surface-2); color: var(--warning-text);
  font-family: var(--mono); font-size: 12.5px; font-weight: 400; line-height: 1.6; overflow-wrap: anywhere;
}
.notice::before, .banner::before { display: block; margin-bottom: var(--s-1); color: var(--warning); }
.notice::before { content: "NOTICES"; content: "NOTICES" / ""; }
.banner::before { content: "SAMPLE"; content: "SAMPLE" / ""; }
.notice p { display: block; }
.notice p + p { margin-top: var(--s-1); }
#warnings { counter-reset: note; }
#warnings p::before {
  counter-increment: note; color: var(--text-3);
  content: "[" counter(note, decimal-leading-zero) "] ";
  content: "[" counter(note, decimal-leading-zero) "] " / "";
}
/* Sample data: the same console, centred, first in the content so a screenshot of made-up rows carries the label.
   Its text is the API's own sentence, which already starts "Sample data", so nothing is prefixed to it. The sentence
   is one line, cut with an ellipsis where it is longer (the notes carry it in full): the banner is then one height
   whatever the reason for the sample, so the room held for it before the first answer is exactly the room it takes,
   and on a phone it is two short lines rather than five. */
.banner { margin-bottom: var(--s-5); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ?mock=1 on the local server (the head script marks <html> before the first paint): the banner is shown from the
   first frame, with its placeholder sentence, rather than unhidden by app.js once the first answer says mock, which
   pushed the whole page down after it had painted. A deployed copy ignores ?mock=1 and never sets the mark. */
html[data-mock] #sample-banner[hidden] { display: block !important; }

/* ---------- notes: the console behind a disclosure ---------- */
/* The shared warnings console (#warnings) lives inside details#notes, closed at every page load: its sentences are for
   whoever runs the dashboard, and five of them in amber were the first thing the client read. What stays on screen is
   one 36px row (44px on touch): an LED whose colour follows data-tone (ok green, info neutral, warn amber), "NOTES",
   the count "· 4", the first note cut to one line (gone on phones, where the row has no room for it) and a chevron
   that turns when the row opens. The row is always there, so the page under it never moves however many notes arrive.
   The browser's own marker goes; the chevron says the same thing in the system's line style. */
.notes { min-width: 0; }
.notes-summary {
  position: relative; display: flex; align-items: center; gap: var(--s-2);
  min-width: 0; height: 36px; padding: 0 var(--s-3);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
  list-style: none; cursor: pointer; user-select: none;
}
.notes-summary::-webkit-details-marker { display: none; }
.notes-summary::marker { content: ""; }
.notes .led { background: var(--text-2); }
.notes[data-tone="ok"] .led { background: var(--led-on); }
.notes[data-tone="warn"] .led { background: var(--led-warn); }
.notes[data-tone="warn"] .notes-summary { border-left: 2px solid var(--warning); }
.notes-label { flex: none; color: var(--text); }
.notes-count { flex: none; font-size: 12px; font-weight: 500; line-height: 1.3; color: var(--text-2); }
/* "· 0": nothing to read, so the count steps back. */
.notes[data-tone="ok"] .notes-count { color: var(--text-3); }
.notes-count .skeleton { display: inline-block; vertical-align: middle; width: 3ch; height: 1em; }
.notes-peek {
  flex: 1 1 auto; min-width: 0; margin-left: var(--s-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px; line-height: 1.3; color: var(--text-2);
}
/* Closed it points right (more behind it), open it points down; turned on the compositor, never redrawn. */
.notes-chev {
  flex: none; width: 14px; height: 14px; margin-left: auto; color: var(--text-2);
  transform: rotate(-90deg); transition: transform var(--dur-2) var(--ease);
}
.notes[open] .notes-chev { transform: none; }
.notes[open] .notes-peek { visibility: hidden; }
.notes .notes-summary:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
@media (hover: hover) {
  .notes-summary:hover { border-color: var(--line-ui); }
  .notes-summary:hover .notes-chev { color: var(--text); }
}
@media (pointer: coarse) {
  .notes-summary { height: 44px; }
}
@media (max-width: 860px) {
  .notes-peek { display: none; }
}
/* The opened console: the same log as before, without its "NOTICES" header (the row above already says "Notes") and
   in the tone of the row: amber only when something is wrong, the plain text colour when the notes are only
   information, so a fine week does not read as an alarm. */
.notes > .notes-body { margin-top: var(--s-2); }
.notes > .notes-body::before { display: none; }
.notes:not([data-tone="warn"]) .notes-body { border-left-color: var(--line-ui); color: var(--text-2); }
/* "Nothing to note." is not a numbered entry. */
#warnings p.notes-none::before { content: none; }

/* Empty state: icon + title + one sentence + (for an error) one action. */
.empty-state {
  display: flex; flex-direction: column; align-items: center;
  padding: var(--s-8) var(--s-6); text-align: center;
  font-size: var(--fs-body); line-height: 1.55; color: var(--text-2); white-space: normal;
}
.empty-state-icon {
  --bracket-l: 6px;
  position: relative; display: grid; place-items: center; width: 44px; height: 44px; margin-bottom: var(--s-4);
  border: 1px solid var(--line-ui); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text-2);
}
.empty-state-icon svg { width: 22px; height: 22px; }
.empty-state h3 { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.empty-state[role="alert"] h3 { color: var(--danger); }
.empty-state-text { max-width: 46ch; margin-top: var(--s-1); font-size: 14px; overflow-wrap: anywhere; }
.empty-state-action { margin-top: var(--s-5); }

/* ---------- loading ---------- */
/* Static fill; one soft pulse when it appears, then still. The pulse is not on the block itself: this page draws a
   hundred-odd skeletons at once, and an opacity animation on each would give every one its own compositor layer. The
   group that holds them pulses instead (the KPI value slot, the table's skeleton body, the trend skeleton). */
.skeleton { display: block; max-width: 100%; border-radius: var(--r-sm); background: rgba(161, 249, 99, 0.06); }

/* ---------- focus ---------- */
/* One visible ring on everything interactive. The transparent outline shows up in forced-colours mode, where
   box-shadow is dropped. Placed after the components so it wins over their shadows. */
:is(a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus-visible {
  outline: 2px solid transparent; outline-offset: 2px;
  box-shadow: var(--focus);
}
[tabindex="-1"]:focus { outline: none; }
.input[aria-invalid="true"]:focus-visible, .input.bad:focus-visible { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--danger); }
/* Inside a clipping track the ring goes inside the control. */
.tabs .tab:focus-visible, .seg .seg-btn:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }

/* ---------- 6. login ---------- */
/* One flexible column: an auto column would size itself to the 400px card, so the card's max-width: 100% could never
   shrink it on phones narrower than 432px. */
.login { min-height: 100dvh; display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; padding: var(--s-8) var(--s-4) calc(var(--s-8) + var(--safe-bottom)); }
/* The card fades up once as it arrives. */
.login > * { animation: fade-up var(--dur-3) var(--ease) backwards; }
.login-card { width: 400px; max-width: 100%; padding: var(--s-7) var(--s-6) var(--s-6); display: flex; flex-direction: column; gap: var(--s-4); }
.login-brand { flex-direction: column; justify-content: center; text-align: center; padding: 0; }
.login-brand .brand-mark { width: 36px; height: 36px; }
.login-brand .brand-mark svg { width: 20px; height: 20px; }
.login-brand .brand-text { align-items: center; }
.login-brand .wordmark { font-size: 22px; }
.login-h { font-size: 13px; letter-spacing: 0.18em; text-align: center; color: var(--text); }

/* ---------- 7. responsive + preferences ---------- */
@media (max-width: 860px) {
  .shell { grid-template-columns: minmax(0, 1fr); }

  /* Top bar: brand · status LED · Sign out. Fixed (not in flow), above anything on the page, which scrolls under it. */
  .side {
    position: fixed; top: 0; right: 0; left: 0; z-index: 50;
    height: calc(var(--topbar-h) + var(--safe-top));
    flex-direction: row; align-items: center; gap: var(--s-2);
    padding: var(--safe-top) max(var(--gutter), env(safe-area-inset-right, 0px)) 0 max(var(--gutter), env(safe-area-inset-left, 0px));
    overflow: visible;
    background: none;
    border-right: 0; border-bottom: 1px solid var(--border);
  }
  /* The bar's fill lives on a pseudo-element, so .side itself carries no effect that could trap the fixed tab bar
     inside it. */
  .side::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: var(--surface-bar);
  }
  /* Scoped to the bar: the login card's brand block keeps its full lockup on phones. */
  .side .brand { height: 44px; margin-right: auto; padding: 0; }
  .side .brand-sub { display: none; }
  .side .brand-mark { width: 24px; height: 24px; }
  .side .brand-mark svg { width: 14px; height: 14px; }
  /* The More sheet carries the Whop link on phones. */
  .side .btn-new { display: none; }
  /* Only the LED shows in the bar; its words stay in the DOM (visually hidden), so they are still its accessible text. */
  .sys { align-items: center; padding: 0 var(--s-2); }
  .sys .led { margin-top: 0; }
  .sys-text {
    position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  /* Sign out as a 44px icon button; aria-label on the button keeps its name. */
  .side-foot { flex-direction: row; align-items: center; margin: 0; padding: 0; border: 0; }
  .side-foot .nav-link { width: 44px; height: 44px; padding: 0; justify-content: center; }
  .side-foot .nav-label { display: none; }
  .side-foot .nav-icon, .side-foot .nav-icon svg { width: 20px; height: 20px; }

  /* The grouped list leaves with the sidebar: on a phone the bottom tab bar (.tabbar, after the sections block) and the
     More sheet carry the seven sections. */
  .side-nav { display: none; }

  /* Content clears both bars and the briefing orb: the tab bar's 56px and the home-indicator inset, then the orb's
     44px slot 12px above the bar (jarvis.css) and 24px of breath, so the last row of a table or the last line of a
     note scrolls clear of both (with 40px it ended under the orb's right-hand corner). */
  .main {
    padding:
      calc(var(--topbar-h) + var(--safe-top) + var(--s-6))
      max(var(--gutter), env(safe-area-inset-right, 0px))
      calc(var(--tabbar-h) + var(--safe-bottom) + var(--s-3) + 44px + var(--s-6))
      max(var(--gutter), env(safe-area-inset-left, 0px));
  }
  .page-head { margin-bottom: var(--s-6); }
  .card { --card-pad: var(--s-5); }
  .notice, .banner { font-size: 13px; }
}
/* Form text is never under 16px where the page could zoom into a field: iOS zooms the whole page into any field with
   smaller text the moment it's focused, and the page then no longer fits the screen. */
@media (hover: none) and (pointer: coarse), (max-width: 860px) {
  :root { --fs-input: 16px; }
  .input { height: 44px; }
  input, select, textarea { font-size: max(16px, 1em) !important; }
}
@supports (-webkit-touch-callout: none) {
  input, select, textarea { font-size: max(16px, 1em) !important; }
}

/* Touch: invisible hit areas so every control reaches 44px without changing the layout. A hit area hangs off the
   padding box, inside the 1px border: 36px buttons need -5px. */
@media (pointer: coarse) {
  .btn::after { content: ""; position: absolute; inset: -5px; }
  /* The tab and the segment are themselves the 44px target (46px with the track's border): the strip scrolls
     sideways, which clips any hit area hung off it, and the 1px borders are the track's, so a 42px button left a
     42px target. */
  .tabs { --tab-h: 44px; }
  .seg { --seg-h: 44px; }
  /* The sort buttons need no hit area: the header row is 44px here (--head-h, section 1). A band hanging below a
     32px row reached 13px into the first body row, and the sticky header painted over it, so it took the top of that
     row's ⋯, pencil, name and People links (verify: 34px targets on the Ad sets level at 375px). */
  /* A name is an 18px line mid-row: a 44px hit area centred on it stays inside its 48px row, so a thumb aimed a little
     above or below the word still lands, and never on the row next door. */
  .link.name, .count-link { position: relative; }
  .link.name::after, .count-link::after {
    content: ""; position: absolute; top: 50%; right: 0; left: 0; height: max(100%, 44px); transform: translateY(-50%);
  }
  .btn-icon { width: 44px; height: 44px; }
}
/* A touch screen wide enough for the sidebar (a tablet on its side): 44px rows. */
@media (pointer: coarse) and (min-width: 861px) {
  .nav-link, .side .btn-new { height: 44px; }
}

/* Reduced motion: every entrance resolves at once, the tab bar does not slide, the picker does not move, and the scan
   band is gone (atmosphere.js does not start it either; this is the belt to its braces). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important; scroll-behavior: auto !important;
  }
  .scan { display: none; }
}
/* Reduced transparency: nothing to do. Every surface is opaque already, and the only translucent fills left are
   tints on opaque panels, which are colour, not see-through glass. */

/* ==========================================================================
   Backdrop (assets/atmosphere.js mounts these two layers as .app's first children)
   Stacking inside .app (isolation: isolate): .ambient (-2) static grid < .scan (-1) band < content (auto).

   The grid paints once. The scan band is the product's one loop: a Web Animation on transform, mouse screens only,
   cancelled (not paused) when the tab is hidden or the page has gone unused, so its base style below parks it off
   screen above the top and a resting page never shows a frozen stripe. Panels are opaque, so the band only shows in
   the gutters and gaps between them: depth, not distraction.
   ========================================================================== */
.ambient {
  position: fixed; inset: 0; z-index: -2; pointer-events: none; contain: strict;
  background:
    radial-gradient(70% 45% at 65% -10%, rgba(161, 249, 99, 0.08), transparent 70%),
    linear-gradient(var(--grid-major) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(90deg, var(--grid-major) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(var(--grid-minor) 1px, transparent 1px) 0 0 / 24px 24px,
    linear-gradient(90deg, var(--grid-minor) 1px, transparent 1px) 0 0 / 24px 24px;
  /* A static mask fades the grid out towards the bottom, so a long page ends in plain black. Static masks are fine;
     an animated one would repaint every frame. */
  -webkit-mask-image: radial-gradient(120% 85% at 50% 0%, #000 35%, transparent 100%);
  mask-image: radial-gradient(120% 85% at 50% 0%, #000 35%, transparent 100%);
}
.scan {
  position: fixed; top: 0; right: 0; left: 0; z-index: -1; height: 28vh;
  pointer-events: none; contain: strict; will-change: transform;
  transform: translateY(-100%);
  /* A soft band with a hairline leading edge. */
  background: linear-gradient(180deg, transparent, rgba(161, 249, 99, 0.025) 75%, rgba(161, 249, 99, 0.06) 99%, rgba(161, 249, 99, 0.12) 99.5%, transparent);
}
/* Touch screens get the static grid only: the least battery and GPU to spare, and the client's phone is the main
   screen. atmosphere.js already skips the band there; hiding the layer makes sure a stray start costs nothing. */
@media (pointer: coarse) {
  .scan { display: none; }
}

/* ==========================================================================
   This page: the header actions and range picker, the KPI grid, the trend panel, the board. After the primitives
   so these win ties.
   ========================================================================== */

/* ---------- header actions: range chip, Refresh, attribution ---------- */
.range { display: flex; align-items: center; gap: var(--s-2); min-width: 0; max-width: 100%; }

/* The range chip: calendar icon · preset tag · the dates · chevron. A fixed 36px tall (44px on touch), and on a wide
   screen the dates hold a 20-character mono width, so moving between presets (or the first write by picker.js after a
   frame painted empty) does not push Refresh and the toggle sideways. */
.range-trigger {
  position: relative; display: inline-flex; align-items: center; gap: var(--s-2);
  height: 36px; min-width: 0; padding: 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md);
  background: transparent; color: var(--text); cursor: pointer;
}
.range-icon { flex: none; width: 16px; height: 16px; color: var(--accent); }
.range-chev { flex: none; width: 14px; height: 14px; color: var(--text-2); }
.range-tag {
  display: inline-flex; align-items: center; flex: none; height: 20px; padding: 0 var(--s-1);
  border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent-hover);
}
/* The chip ships with an empty tag that picker.js fills at boot. Every preset has a tag, so empty means "not written
   yet": the slot keeps the default preset's room ("7D"), unseen, instead of going, which widened the chip by 31px
   when the tag landed, re-wrapped the lede beside it and moved the page down 21px at 1440 (a shift on every load). */
.range-tag:empty { visibility: hidden; width: calc(2ch + 0.2em + 2 * var(--s-1)); }
.range-dates {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;
  font-size: 13px; font-weight: 500; line-height: 1.3; color: var(--text);
}
@media (min-width: 861px) {
  .range-dates { min-width: 20ch; }
}
.range-trigger[aria-expanded="true"] { border-color: var(--accent); }
@media (hover: hover) {
  .range-trigger:hover { background: var(--accent-soft); }
}
@media (pointer: coarse) {
  .range-trigger { height: 44px; }
}

/* ---------- the range picker: popover on desktop, bottom sheet on phones ---------- */
/* A modal <dialog> on every screen (top layer, focus containment, inert page, Escape as cancel). Opaque surface, no
   blur on it or its backdrop: the scan band moves behind everything. The dialog's own display is never set here:
   the browser hides a closed dialog with display: none, and any display value would show it. The layout lives on
   .range-pop-inner instead. */
.range-pop {
  margin: 0; padding: 0;
  border: 1px solid var(--line-ui); border-radius: var(--r-xl);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-pop);
  max-width: calc(100vw - 2 * var(--s-4));
  max-height: calc(100vh - 2 * var(--s-4)); max-height: calc(100dvh - 2 * var(--s-4));
  overflow: auto; overscroll-behavior: contain;
}
.range-pop[open] { animation: pop-in var(--dur-2) var(--ease); }
/* Desktop: transparent, so the page stays in view behind the popover. A flat colour on phones (below). Never a blur. */
.range-pop::backdrop { background: transparent; }

/* Desktop (≥861px): presets on the left, two months on the right, the footer across both; app.js anchors the dialog
   under the chip with inline top and left. */
.range-pop-inner {
  position: relative;
  display: grid; grid-template-columns: 176px auto;
  grid-template-areas: "head head" "presets cal" "foot foot";
}
.range-pop-head {
  grid-area: head; display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-4); border-bottom: 1px solid var(--border);
}
.range-pop-title { color: var(--text); }
/* The account zone the dates are in. --text-3 at 11px on the panel: 5.3:1. */
.range-zone { margin-right: auto; font-size: 11px; font-weight: 500; line-height: 1.3; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }

/* Presets: a list of rows on desktop. A preset applies at once (picker.js), so pressed means "this is the range". */
.range-presets {
  grid-area: presets; display: flex; flex-direction: column; gap: var(--s-1);
  padding: var(--s-2); background: var(--surface-2); border-right: 1px solid var(--border);
}
.preset {
  position: relative; display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
  width: 100%; height: 32px; padding: 0 var(--s-3);
  border: 0; border-radius: var(--r-sm); background: transparent; color: var(--text); text-align: left; cursor: pointer;
}
.preset-label { font-family: var(--sans); font-size: 13px; font-weight: 500; line-height: 1.3; white-space: nowrap; }
/* The hint ("3 OCT", "7D", "1–31 AUG"): --text-3 on the inset (5.1:1); on the pressed row's accent tint --text-3
   would fall to 3.9:1, so there it turns accent text like the label. */
.preset-hint { font-size: 11px; font-weight: 500; line-height: 1.3; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; color: var(--text-3); }
.preset[aria-pressed="true"] { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
.preset[aria-pressed="true"] .preset-label, .preset[aria-pressed="true"] .preset-hint { color: var(--accent-hover); }
@media (hover: hover) {
  .preset:not([aria-pressed="true"]):hover { background: var(--surface-hover); }
}
.range-presets .preset:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
@media (pointer: coarse) {
  .preset { height: 44px; }
}

/* The calendar: the month arrows sit over the label row's ends, and the labels share the months' own columns, so each
   label is centred over its month whatever the arrows' width. */
.cal { grid-area: cal; min-width: 0; padding: var(--s-4); }
.cal-nav { position: relative; margin-bottom: var(--s-2); }
.cal-nav #cal-prev, .cal-nav #cal-next { position: absolute; top: 0; }
.cal-nav #cal-prev { left: 0; }
.cal-nav #cal-next { right: 0; }
.cal-labels, .cal-months { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); column-gap: var(--s-6); }
.cal-labels { min-height: 36px; align-items: center; text-align: center; }
.cal-month-label { color: var(--text); }
.cal-months { justify-content: start; }
.cal-month { border-collapse: separate; border-spacing: 0; table-layout: fixed; }
/* Weekday headers: --text-3 at 11px on the panel, 5.3:1. */
.cal-month th { width: 36px; height: 28px; padding: 0; text-align: center; vertical-align: middle; color: var(--text-3); }
/* Square cells, no spacing, so the in-range band is one continuous strip across a week. */
.cal-month td { width: 36px; height: 36px; padding: 0; text-align: center; vertical-align: middle; }
.cal-day {
  position: relative; cursor: pointer; user-select: none;
  font-size: 12.5px; font-weight: 500; line-height: 1; color: var(--text);
}
/* Days of the neighbouring months: blank, the same size, not a control. */
.cal-blank { cursor: default; }
.cal-day.is-in-range { background: var(--accent-soft); color: var(--text); }
/* The preview between the anchor and the hovered or focused day, before the end is chosen: half the in-range tint
   with dashed hairlines along the top and bottom, painted as backgrounds so the cell's size never changes. */
.cal-day.is-preview {
  background:
    linear-gradient(90deg, var(--line-ui) 50%, transparent 0) left top / 6px 1px repeat-x,
    linear-gradient(90deg, var(--line-ui) 50%, transparent 0) left bottom / 6px 1px repeat-x,
    rgba(161, 249, 99, 0.06);
}
.cal-day.is-today { box-shadow: inset 0 0 0 1px var(--line-ui); }
/* Today also carries a short accent tick under its number, and the word "today" in its label (picker.js). */
.cal-day.is-today::after {
  content: ""; position: absolute; bottom: 6px; left: calc(50% - 2px); width: 4px; height: 1px; background: var(--accent);
}
.cal-day.is-start, .cal-day.is-end {
  background: var(--accent-fill); color: var(--accent-fill-text); font-weight: 600;
  box-shadow: 0 0 10px -2px var(--accent-glow);
}
.cal-day.is-start.is-today::after, .cal-day.is-end.is-today::after { background: var(--accent-fill-text); }
/* Unavailable days dim and say "unavailable" in their label; they stay focusable (APG), never selectable. */
.cal-day.is-disabled, .cal-day[aria-disabled="true"] { color: var(--text-3); opacity: 0.45; cursor: not-allowed; }
@media (hover: hover) {
  .cal-day:not(.is-disabled, [aria-disabled="true"], .is-start, .is-end):hover { background: var(--surface-hover); }
}
/* Focus goes inside the cell: the grid clips, and the next cell would paint over an outer ring. On a filled range end
   the ring is dark, since an accent ring would vanish into the fill. Selectors match the global ring's weight and
   come after it; tabindex="-1" cells (the roving grid's others) are covered as well. */
.cal-month td.cal-day:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }
.cal-month td.cal-day.is-start:focus-visible, .cal-month td.cal-day.is-end:focus-visible {
  box-shadow: inset 0 0 0 2px var(--accent-fill-text), 0 0 10px -2px var(--accent-glow);
}

.range-pop-foot {
  grid-area: foot; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-4);
  padding: var(--s-3) var(--s-4); border-top: 1px solid var(--border); background: var(--surface);
}
/* The readout ("27 SEP 2026 → 3 OCT 2026 · 7 DAYS") keeps a line while empty, so the footer never changes height. */
.range-pop-status { flex: 1 1 auto; min-width: 0; min-height: 1.5em; font-size: 12px; line-height: 1.5; letter-spacing: 0.04em; color: var(--text-2); }
.range-pop-foot .field-error { order: 3; flex: 1 1 100%; }
.range-pop-actions { display: flex; gap: var(--s-2); margin-left: auto; }

/* ---------- Overview: the five headline tiles ---------- */
/* Ad spend · New-client cash · ROAS · Cost per qualified lead · New clients (DESIGN-TECH 13.6). Five across from a
   1012px main column; from 640px a 6-column grid where the first three tiles take two columns and the last two take
   three, so both rows end flush; under 640px two across with ROAS first and full width (the one figure a reader looks
   for first), then 2 × 2. The steps are container queries on .main, like every grid on the page: how many tiles share
   a row depends on the main column's width, not the window's.
   Every tile has every row (overline, figure, sparkline, meta, line, fx, caption) and each row has a fixed or reserved
   height, so the rows line up across a row of tiles in every state; the caption sits at the bottom (margin-top: auto),
   so a tile whose fx line wraps to two lines moves nothing but its own caption's gap. A reservation is made only where
   its content can appear: a row of tiles is otherwise as tall as the tallest tile in it, and an empty band reserved in
   every tile for a line only one of them can have was the spare space under the figures. */
.ov-hero-row {
  /* The tiles' padding and the grid's gap, named here because the empty row's reserved height below is worked out
     from them: a parent cannot read a padding its children set. */
  --kpi-pad: var(--s-5);
  --kpi-gap: var(--s-4);
  /* How many lines an overline is given. "COST PER QUALIFIED LEAD" is about 180px of 11px mono with its tracking, so
     it wraps on a five-across tile and on a phone's half-width tile; every overline gets the same room, so all five
     figures start at one height. Between 640 and 1011px it fits on one line. */
  --kpi-label-lines: 2;
  --kpi-fx-lines: 1;
  /* The fx line and the 8px under it: reserved in the New-client cash tile only (below). */
  --hero-fx: calc(var(--kpi-fx-lines) * 11px * 1.4 + var(--s-2));
  /* One tile, top to bottom: border, padding, overline, figure (1.1 × --fs-hero) under its 8px margin, the 24px
     sparkline and the 22px meta row under theirs, two 12px lines for .kpi-line under 4px, the fx line and its 8px
     margin, then the caption's hairline, 8px and two 12px lines, padding and border. */
  --hero-tile: calc(2px + 2 * var(--kpi-pad) + var(--kpi-label-lines) * 11px * 1.3 + var(--s-2) + 1.1 * var(--fs-hero)
    + var(--s-2) + 24px + var(--s-2) + 22px + var(--s-1) + 2 * 12px * 1.5 + var(--hero-fx)
    + 1px + var(--s-2) + 2 * 12px * 1.5);
  /* The empty row's height on a phone, four rows of tiles: ROAS alone, then LTV ROAS alone (each one overline line, one
     second line, one caption line, no fx), Ad spend beside New-client cash (a whole tile), then Cost per qualified lead
     beside New clients (no fx, and a three-line caption at a half tile's width). */
  --hero-empty: calc(4 * var(--hero-tile) - 3 * var(--hero-fx) - 2 * (var(--kpi-label-lines) - 1) * 11px * 1.3
    - 3 * 12px * 1.5 + 3 * var(--kpi-gap));
  /* No row gap: the tiles share their rows through subgrid (below), and a row gap falls between every one of those
     rows, so a row empty in all the tiles of a line (the fx line beside no New-client cash) would still have cost one.
     A line of tiles below the first is held off by its tiles' own top margin instead. */
  display: grid; column-gap: var(--kpi-gap); row-gap: 0; grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) { order: -1; grid-column: 1 / -1; }
/* On a phone ROAS and then LTV ROAS are the first two lines, each alone; every other tile is on a line under them.
   (:where keeps this at the weight of the wider steps' resets below.) */
.ov-hero-row > .kpi:where(:not([data-kpi="roas"])) { margin-top: var(--kpi-gap); }
/* Alone on their rows on a phone, the two ROAS tiles need no second overline line, and their second lines
   ("Whop-attributed: 0.45×", "Whop ads only: 3.20×") fit the whole width on one. */
.ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) .kpi-label { min-height: calc(11px * 1.3); }
.ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) .kpi-line { min-height: calc(12px * 1.5); }
@container main (min-width: 640px) {
  /* Two rows of three: the first (New-client cash among them, so the fx line is reserved there), then LTV ROAS, Cost
     per qualified lead and New clients, which have none. */
  .ov-hero-row {
    --kpi-label-lines: 1; grid-template-columns: repeat(6, minmax(0, 1fr));
    --hero-empty: calc(2 * var(--hero-tile) - var(--hero-fx) + var(--kpi-gap));
  }
  .ov-hero-row > .kpi, .ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) { order: 0; grid-column: span 2; margin-top: 0; }
  .ov-hero-row > .kpi:nth-child(n+4) { margin-top: var(--kpi-gap); }
  .ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) .kpi-line { min-height: calc(2 * 12px * 1.5); }
}
/* From a 898px column the first row's third-width tiles fit "First payment of each new client, all sources" (247px) on
   one line too, so both caption rows are one line (at 1280 × 800 the empty row held 18px more than its tiles took). */
@container main (min-width: 898px) and (max-width: 1011.98px) {
  .ov-hero-row { --hero-empty: calc(2 * var(--hero-tile) - var(--hero-fx) - 2 * 12px * 1.5 + var(--kpi-gap)); }
}
@container main (min-width: 1012px) {
  .ov-hero-row { --kpi-label-lines: 2; --hero-empty: var(--hero-tile); grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .ov-hero-row > .kpi, .ov-hero-row > .kpi:nth-child(n+4), .ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) { grid-column: auto; margin-top: 0; }
  .ov-hero-row > :is([data-kpi="roas"], [data-kpi="ltv_roas"]) .kpi-label { min-height: calc(var(--kpi-label-lines) * 11px * 1.3); }
}
/* The smallest phones: the overline of a half-width tile can take a third line. */
@container main (max-width: 299.98px) {
  .ov-hero-row { --kpi-label-lines: 3; }
}
/* The row ships empty and app.js fills it; a frame can paint before the scripts at the end of body run, so the empty
   row is held at the height its tiles will take, and the skeletons and the figures after them land in room already
   made. Only ever a reservation: the row's own content takes over the moment there is any. */
.ov-hero-row:empty { min-height: var(--hero-empty); }
.kpi { --card-pad: var(--kpi-pad); --bracket-l: 8px; min-width: 0; }
.kpi-hero { display: flex; flex-direction: column; }
.kpi-label { min-height: calc(var(--kpi-label-lines) * 11px * 1.3); }
/* A figure is never allowed to break: "A$1,026,285.0" over a lone "0" reads as a different amount. nowrap keeps it on
   one line and fitKpiValues() in app.js shrinks the ones that would not fit, so --fs-hero is the ceiling, not the
   rule. A figure that is a list of people (New clients) is a People link set in the figure's own type. */
.kpi-value { white-space: nowrap; overflow: hidden; }
.kpi-hero .kpi-value { font-size: var(--fs-hero); }
/* The overflow clip would cut a People link's focus ring and its underline, so every hero figure keeps 4px of room
   round it and gives the same 4px back with negative margins: the box grows, the layout does not. */
.kpi-hero .kpi-value { padding: 0 4px 4px; margin-inline: -4px; margin-bottom: -4px; }
/* As tall as the line of figures it stands in for, so the tile does not change height when the number lands. */
.kpi-value .skeleton { height: 1.1em; width: 70%; }
/* A figure that cannot be given yet ("Needs Typeform token", "Needs Whop permission"): a sentence in DM Sans, set in
   exactly the box the figure would take, so the tile is one height whichever it shows. One line, cut with an ellipsis
   on the narrowest tiles; app.js puts the full sentence in the title. */
.kpi-value.is-na, .kpi-hero .kpi-value.is-na {
  height: calc(1.1 * var(--fs-hero)); line-height: calc(1.1 * var(--fs-hero));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--sans); font-size: 14px; font-weight: 500; letter-spacing: normal;
  color: var(--text-2);
}
/* ROAS is the headline of the headline row: its figure in accent text and its outline at the control line's
   strength, nothing louder (no glow, no fill). */
.kpi-hero:is([data-kpi="roas"], [data-kpi="ltv_roas"]) { border-color: var(--line-ui); }
.kpi-hero:is([data-kpi="roas"], [data-kpi="ltv_roas"]) .kpi-value:not(.is-na) { color: var(--accent-hover); }
/* The sparkline row: always rendered, empty while pending or when a tile has no daily series, so every tile is one
   height in every state. */
.kpi-spark { height: 24px; margin-top: var(--s-2); }
/* The meta row is always there and always a badge tall: the delta badge, "from 5.28×", and the People links
   ("8 new clients", "56 qualified"). */
.kpi-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); min-height: 22px; margin-top: var(--s-2); font-size: 12px; line-height: 1.5; color: var(--text-2); }
.kpi-meta .skeleton { height: 20px; width: 5em; }
/* "from 5.28×": the previous window's figure beside its delta, quiet, so the badge stays the thing read. */
.kpi-from { font-size: 11px; line-height: 1.5; letter-spacing: 0.02em; white-space: nowrap; color: var(--text-3); }
/* The tile's second line ("Whop-attributed: 0.45×", "Total cash A$63,106 · 15 payments", "1 from Whop ads"): two
   lines reserved at every width, since the longest wraps on a five-across tile, and never clipped, because it can
   hold a People link whose focus ring must show. */
.kpi-line {
  min-height: calc(2 * 12px * 1.5); margin-top: var(--s-1);
  font-size: 12px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere;
}
/* "incl. US$19,000 @ A$1.4411": one quiet mono line wherever a figure includes converted US$; the full sentence with
   the ECB date is the tile's title. One line is reserved; a narrow tile wraps it to a second (clamped there) rather
   than cut it mid-rate, and only that tile's caption gap gives way. */
.kpi-fx {
  display: block; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  min-width: 0; max-width: 100%; min-height: calc(var(--kpi-fx-lines, 0) * 11px * 1.4);
  overflow: hidden; white-space: normal; overflow-wrap: anywhere;
  font-size: 11px; font-weight: 400; line-height: 1.4; letter-spacing: 0; text-transform: none; color: var(--text-3);
}
/* In the headline row only New-client cash ever has an fx line (it is the one figure with converted US$ in it; ROAS
   carries the rate in its title), so only that tile holds the line and the 8px under it, filled or not. Tiles beside
   it share its row through subgrid and line up with it; a row without it (ROAS alone on a phone, the second row from
   640px) has no empty fx band at all. Where the overline has a spare second line the fx line moves into it (below,
   with the subgrid). */
/* The other tiles' fx slot stays in the tile, so the caption keeps its row in the subgrid, but takes no height: app.js
   fills an empty slot with a no-break space, which would otherwise make a line of its own. */
.kpi-hero:not([data-kpi="new_cash"]) .kpi-fx { height: 0; min-height: 0; }
.kpi-hero[data-kpi="new_cash"] .kpi-fx { min-height: calc(var(--kpi-fx-lines) * 11px * 1.4); margin-bottom: var(--s-2); }
/* The caption: what the figure is made of ("New-client cash ÷ ad spend (blended)"), at the foot of the tile under a
   hairline, so the five footnotes line up whatever the rows above them hold. */
.kpi-cap { overflow-wrap: anywhere; }
.kpi-hero .kpi-cap {
  margin-top: auto; padding-top: var(--s-2); border-top: 1px solid var(--line-neutral);
  font-size: 12px; line-height: 1.5; color: var(--text-3);
}
/* Tiles side by side share their seven rows (overline, figure, sparkline, meta, line, fx, caption) with the hero row
   through subgrid. Each row is then as tall as the tallest tile's, so one tile whose fx line or caption wraps no longer
   leaves a block of empty space above the four other captions: every row, and every caption's hairline, reads
   straight across the five, like the columns of one table. The caption sits at the top of its row rather than being
   pushed to the foot (the flex fallback above), so the hairlines align. A tile spans seven of the row's tracks and
   auto-placement puts each line of tiles on the next seven; the row's own gap then falls only between lines. */
@supports (grid-template-rows: subgrid) {
  .ov-hero-row > .kpi-hero { display: grid; grid-row: span 7; grid-template-rows: subgrid; row-gap: 0; align-content: start; }
  .ov-hero-row > .kpi-hero > * { min-width: 0; }
  .ov-hero-row > .kpi-hero > .kpi-cap { margin-top: 0; }
  /* Where a line of tiles keeps two overline lines that New-client cash does not use, its fx line takes the second
     one, under "NEW-CLIENT CASH" and over the figure it is part of, instead of a row of its own: five across (from a
     1012px column) every tile keeps two because "COST PER QUALIFIED LEAD" wraps, and on a phone (from a 328px column)
     Ad spend and New-client cash share a line that keeps two for the same reason. Shared through subgrid, the fx row
     (15px and the 8px under it) had been an empty band above the captions of every tile beside New-client cash: 41px
     between Ad spend's, ROAS's and New clients' second line and their hairline at 1440. Every part is put in its row
     by number, since one part placed by hand would send auto-placement round it; overline and fx share the cell. */
  @container main ((min-width: 328px) and (max-width: 639.98px)) or (min-width: 1012px) {
    .ov-hero-row { --hero-fx: 0px; }
    .ov-hero-row > .kpi-hero > .kpi-label { grid-area: 1 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-value { grid-area: 2 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-spark { grid-area: 3 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-meta { grid-area: 4 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-line { grid-area: 5 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-fx { grid-area: 6 / 1; }
    .ov-hero-row > .kpi-hero > .kpi-cap { grid-area: 7 / 1; }
    .ov-hero-row > .kpi-hero[data-kpi="new_cash"] > .kpi-fx {
      grid-area: 1 / 1; align-self: end; min-height: 0; margin-bottom: 0;
      -webkit-line-clamp: 1; line-clamp: 1; line-height: 1.3;
    }
  }
}
/* A refresh of the same window keeps its figures on screen, dimmed, until the new ones land. A value slot or meta row
   holding a skeleton is not dimmed: the skeleton is already the faint fill, and the pulse owns that slot's opacity. */
.ov-hero-row[aria-busy="true"] .value:not(:has(> .skeleton)), .ov-hero-row[aria-busy="true"] .kpi-meta:not(:has(.skeleton)),
.ov-hero-row[aria-busy="true"] .kpi-spark, .ov-hero-row[aria-busy="true"] .kpi-line { opacity: 0.55; }
/* The skeletons' one pulse, on the groups that hold them. */
.kpi-value:has(> .skeleton), .kpi-meta:has(.skeleton), #warnings p:has(.skeleton), .grid-wrap tbody:has(.skeleton), .trend-skeleton,
.creative-grid:has(> .is-skeleton), .stat-strip:has(.skeleton), .top-list:has(.skeleton), .mini-table tbody:has(.skeleton),
.funnel:has(.skeleton), .src-list:has(.skeleton), .latest-list:has(.skeleton), .ov-creative-row:has(.skeleton),
.ov-health-list:has(.skeleton), .notes-count:has(.skeleton), .pm-table tbody:has(.pm-skel), .journey-list:has(.skeleton),
.edit-parent-inner.is-skeleton {
  animation: skeleton-pulse 1.2s ease-in-out 1;
}
#grid[aria-busy="true"] tbody:not(:has(.skeleton)), #grid[aria-busy="true"] tfoot,
#clients-grid[aria-busy="true"] tbody:not(:has(.skeleton)), #clients-grid[aria-busy="true"] tfoot { opacity: 0.55; }
#refresh[aria-busy="true"] { opacity: 0.7; }
/* The scope line keeps a line's height while empty so the funnel under it does not move when a sentence arrives; the
   console's first p does the same inside the notes. */
#warnings, #kpi-scope { min-height: 1.55em; min-height: 1lh; }
/* While the first answer for a view is out, the console holds skeleton bars rather than the last view's sentences;
   app.js may hold it at the last answer's height with an inline min-height. */
#warnings .skeleton { display: inline-block; vertical-align: middle; height: 1em; width: 60%; }
.kpi-scope { max-width: 68ch; margin-top: var(--s-2); }
.notice { margin-top: var(--s-4); }

/* ---------- sparklines (chart.js) ---------- */
/* A fixed viewBox scaled to the tile, so building one reads no layout. The line keeps 1.5px under the non-uniform
   scale (non-scaling-stroke) and the last point is a zero-length square-capped segment, a square dot at any width.
   Decoration only (aria-hidden): the chart and its table carry the accessible numbers. */
.spark { display: block; width: 100%; height: 24px; overflow: visible; }
.spark-area { fill: var(--series, var(--text-3)); fill-opacity: 0.14; stroke: none; }
.spark-line {
  fill: none; stroke: var(--series, var(--text-3)); stroke-width: 1.5px;
  stroke-linejoin: round; stroke-linecap: butt; vector-effect: non-scaling-stroke;
}
.spark-last {
  fill: none; stroke: var(--series, var(--text-3)); stroke-width: 4px;
  stroke-linecap: square; vector-effect: non-scaling-stroke;
}

/* Series identity: one custom property per series, set wherever a data-series attribute names one (a sparkline, a
   chart strip, a tooltip swatch), and read by the marks inside. Aliases map the tooltip's derived rows onto the
   series they come from. */
[data-series="spend"] { --series: var(--series-spend); }
[data-series="leads"], [data-series="cpl"] { --series: var(--series-leads); }
[data-series="calls"], [data-series="schedules"], [data-series="cpc"], [data-series="cpc_call"] { --series: var(--series-calls); }
[data-series="cash"], [data-series="new_cash"], [data-series="cash_new"], [data-series="roas"], [data-series="roas_new"],
[data-series="repeat"], [data-series="cash_repeat"], [data-series="total_cash"], [data-series="cash_total"] { --series: var(--series-cash); }

/* ---------- trend panel ---------- */
/* Four small multiples (Spend, Leads, Booked calls, Cash) sharing one time axis and one crosshair. The plot is always
   --trend-h tall, in every state, so the board under it never moves. */
/* Spacing above the panel comes from its view's stack (.view > * + *), like every block in a section. */
.trend { margin-top: 0; }
.trend-plot { position: relative; min-height: var(--trend-h); }
/* The height comes from the SVG's own width/height (or viewBox), which chart.js draws in CSS pixels; forcing one here
   could only letterbox the drawing. The plot box above already holds --trend-h. */
.trend-svg { display: block; width: 100%; height: auto; overflow: visible; user-select: none; -webkit-user-select: none; }
#trend[aria-busy="true"] .trend-svg { opacity: 0.55; }
.trend-svg text { font-family: var(--mono); font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums; fill: var(--text-2); }
.trend-label { letter-spacing: 0.08em; text-transform: uppercase; fill: var(--text-2); }
.trend-peak { fill: var(--text-2); }
.trend-total { fill: var(--text); }
.trend-tick { fill: var(--text-2); }
/* Strip text and the axis are read by the hit layer's labels, not by the pointer. */
.trend-strip text, .trend-axis { pointer-events: none; }
.trend-swatch { fill: var(--series, var(--text-3)); }
.trend-base { stroke: var(--border-strong); stroke-width: 1px; shape-rendering: crispEdges; }
.trend-gridline { stroke: var(--line-neutral); stroke-width: 1px; stroke-dasharray: 2 3; shape-rendering: crispEdges; }
/* Square-ended bars in the strip's series colour (the system is sharp; rounded ends are deliberately not used). */
.trend-bar { fill: var(--series, var(--text-3)); stroke: none; shape-rendering: crispEdges; }
/* Cash: the new-client part is the solid bar from the baseline; the repeat part above it is the same colour as a 1px
   outline over a quarter-strength tint, so "new" and "repeat" differ by shape as well as by fill. */
.trend-bar.trend-bar-repeat { fill: var(--series, var(--text-3)); fill-opacity: 0.25; stroke: var(--series, var(--text-3)); stroke-width: 1px; }
/* A null bucket: a dotted stub on the baseline, so "no data" never looks like "zero". */
.trend-null { stroke: var(--text-3); stroke-width: 1px; stroke-dasharray: 1 2; shape-rendering: crispEdges; }
/* The crosshair: moved with transform only, one write per frame; never takes the pointer. */
.trend-hl { fill: var(--accent-soft); pointer-events: none; }
.trend-cross { stroke: var(--accent); stroke-width: 1px; pointer-events: none; shape-rendering: crispEdges; }
.trend-dot { fill: var(--accent); stroke: var(--bg); stroke-width: 1px; pointer-events: none; }
/* The keyboard layer: one transparent full-height rect per bucket with a roving tabindex. SVG elements do not take
   the --focus box-shadow, so focus is an accent stroke; the selector outweighs the global ring and comes after it. */
.trend-hits .trend-hit { fill: transparent; stroke: none; outline: none; cursor: crosshair; }
.trend-hits .trend-hit:focus-visible { outline: none; box-shadow: none; stroke: var(--accent); stroke-width: 2px; }

/* The tooltip: HTML over the plot, placed with transform by chart.js, never takes the pointer. */
.trend-tip {
  position: absolute; top: 0; left: 0; z-index: 5; min-width: 180px; max-width: min(280px, 100%);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-lg);
  background: var(--surface-2); box-shadow: var(--shadow-pop); color: var(--text);
  font-size: 12px; line-height: 1.5; pointer-events: none;
}
.trend-tip-date { margin-bottom: var(--s-1); color: var(--text); }
.trend-tip-row { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: center; gap: var(--s-2); }
.trend-tip-sw { width: 8px; height: 8px; background: var(--series, var(--text-3)); }
/* The repeat-cash row's swatch mirrors its bar: an outline over a tint. */
.trend-tip-sw:is([data-series="repeat"], [data-series="cash_repeat"], [data-series="total_cash"], [data-series="cash_total"]) {
  background: rgba(60, 158, 44, 0.25); box-shadow: inset 0 0 0 1px var(--series);
}
.trend-tip-k { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.trend-tip-v { text-align: right; white-space: nowrap; color: var(--text); }

/* Pending: four bars at the strips' plot heights, one pulse on the group (above). */
.trend-skeleton {
  display: grid; grid-template-rows: repeat(4, calc(16px + 4px + var(--trend-band))); row-gap: var(--trend-strip-gap);
  height: var(--trend-h); padding-bottom: 20px;
}
.trend-skeleton .skeleton { align-self: end; width: 100%; height: var(--trend-band); }
/* Empty or error: one centred sentence in the plot's full reserved height. */
.trend-empty {
  display: grid; place-items: center; min-height: var(--trend-h); padding: 0 var(--s-4);
  text-align: center; font-size: 14px; line-height: 1.5; color: var(--text-2);
}

/* ---------- board: tabs, crumbs, table ---------- */
/* The tablist sits outside the panel it controls (the card, role tabpanel, holding the crumbs and the grid). The
   board is the Campaigns section's whole body now, so it starts right under the page header like every view. */
.board { margin-top: 0; }
.board .tabs { margin-bottom: var(--s-4); }
/* The card pads nothing itself: the grid fills it, and the cells carry --card-pad. */
.board-card { padding: 0; overflow: hidden; background: var(--surface-flat); }

/* The breadcrumb as a path: a green "~/" prompt, then segments separated by "/". app.js writes the prompt itself
   (span.crumb-home, first in both #crumbs and #clients-crumbs), so it is the only "~/": the stylesheet used to add a
   second one with ::before, and hiding that duplicate needed a :has() test on every crumbs write. */
.crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2);
  padding: var(--s-3) var(--card-pad) 0; min-width: 0;
  font-size: 12.5px; font-weight: 500; line-height: 1.5; color: var(--text-2);
}
/* The crumbs row is always there (app.js writes a root crumb at every level), and it holds one line's height even
   before its first write, so going back from a drill never moves the grid under it (verify-1: 0.149 shift). */
#crumbs, #clients-crumbs { min-height: calc(var(--s-3) + 12.5px * 1.5); }
.crumbs .link { font-family: inherit; font-size: inherit; overflow-wrap: anywhere; }
.crumbs .sep { color: var(--text-3); }
.crumbs .here { font-weight: 500; color: var(--text); overflow-wrap: anywhere; }
/* app.js gives the prompt .sep as well (it is path punctuation, aria-hidden like the slashes), so this rule comes
   after .crumbs .sep and wins on source order to keep the prompt green rather than dim. */
.crumbs > .crumb-home { color: var(--accent); }
/* Touch: each segment is a 44px-tall button, so a thumb aimed a little above or below the word still lands. */
@media (pointer: coarse) {
  .crumbs { padding-top: var(--s-1); }
  .crumbs .link { min-height: 44px; }
  #crumbs, #clients-crumbs { min-height: calc(var(--s-1) + 44px); }
}

/* The table scrolls inside its own box, both ways: horizontally because the columns do not fit a phone or most
   laptops; vertically, with a ceiling, because that is the only way the header and the totals row can stay pinned (an
   element sticks to its nearest scroll container, and once the box scrolls sideways it is that container). */
.grid-wrap {
  --name-max: 320px; /* the name cell's ceiling; the pinned column is at most this plus its two paddings */
  position: relative; overflow: auto; max-height: min(72vh, 760px); margin-inline: 0; border-radius: inherit;
  -webkit-overflow-scrolling: touch;
  /* A control scrolled into view (a sort button tabbed to, a name under the totals row) is aligned to the box's
     nearest edge, which knows nothing of the pinned header, totals and name column painted over those edges; the
     paddings are those parts' sizes, so the focus ring is never under an opaque cell. */
  scroll-padding-block: var(--head-h) var(--row-h);
  scroll-padding-inline-start: calc(var(--name-max) + var(--card-pad) + var(--s-3));
}
.board-card .grid-wrap:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.grid-wrap .table { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }
.grid-wrap .table th, .grid-wrap .table td { white-space: nowrap; }
/* The whole header cell is the sort control: the cell pads nothing, the button pads. Opaque inset fill, so the rows
   scrolling under the pinned header never show through. */
.grid-wrap .table thead th { position: sticky; top: 0; z-index: 2; padding: 0; background: var(--surface-2); }
/* One pixel short of the row: in border-collapse: separate a cell is its content plus its own border, so the cell's
   bottom hairline supplies the last pixel. */
.sort {
  position: relative; display: flex; align-items: center; gap: var(--s-1); width: 100%; height: calc(var(--head-h) - 1px); padding: 0 var(--s-3);
  border: 0; background: none; color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit; text-align: inherit;
  cursor: pointer;
}
.grid-wrap .table th:first-child .sort { padding-left: var(--card-pad); }
.grid-wrap .table th:last-child .sort { padding-right: var(--card-pad); }
.grid-wrap .table thead th:first-child, .grid-wrap .table thead th:last-child { padding: 0; }
.grid-wrap .table .sort:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
@media (hover: hover) {
  .sort:hover { color: var(--text); }
}
.grid-wrap .table th[aria-sort="ascending"], .grid-wrap .table th[aria-sort="descending"] { color: var(--accent-hover); }
/* A fixed 1em wide so the headers don't jump when the sort moves. */
.arrow { display: inline-block; width: 1em; font-size: 10px; color: var(--accent); }
.num { text-align: right; }
.num .sort { justify-content: flex-end; }
.num .skeleton { margin-left: auto; }
/* A figures column reads from its right edge: the header label ends where the figures end, so the arrow's 1em slot
   goes before the label rather than after it, and a header cell of figures is right-aligned like its cells (the
   `.table thead th` left alignment above would otherwise win over `.num`). */
.num .sort .arrow { order: -1; }
.table thead th.num { text-align: right; }
/* The source of a figure, said once in its column header ("TYPEFORM", "MATCHED", "PIXEL"); the sort button also holds
   an sr-only phrase, so its name reads as words. --text-3 on the header inset: 5.1:1. */
.col-src { margin-left: var(--s-1); font-size: 11px; font-weight: 500; letter-spacing: 0.10em; color: var(--text-3); }

.grid-wrap .table :is(td, th):first-child { padding-left: var(--card-pad); }
.grid-wrap .table td:last-child { padding-right: var(--card-pad); }
.grid-wrap .table tbody td { background: transparent; }
/* Name pinned to the left so the row you are reading keeps its label while the numbers scroll under it. Its divider
   is a real border, not a box-shadow: 216 rows of static shadows were what made a sort on the Ads table cost 52–76ms
   of paint (verify-1); a border is painted with the cell's own background at no extra cost. */
.grid-wrap .table th:first-child, .grid-wrap .table td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface-flat);
  border-right: 1px solid var(--border);
}
.grid-wrap .table thead th:first-child { z-index: 3; background: var(--surface-2); }
/* Column groups: a stronger hairline before the first column of each group (Spend… | Sales… | Purchases… | Budget).
   A border-left for the same reason as the name column's divider: no shadow on any static body cell. */
.grid-wrap .table .col-group-start { border-left: 1px solid var(--border-strong); }
/* The totals row: an inset band pinned to the bottom, mono figures at 600, a stronger line above. */
.grid-wrap .table tfoot :is(td, th) {
  position: sticky; bottom: 0; z-index: 2; height: var(--row-h); background: var(--surface-2);
  font-family: var(--mono); font-weight: 600; color: var(--text); border-top: 1px solid var(--border-strong); border-bottom: 0;
}
.grid-wrap .table tfoot td { font-size: 13px; }
.grid-wrap .table tfoot :is(td, th):first-child { z-index: 3; }
.grid-wrap .table tfoot th:first-child { color: var(--text); }
tfoot .count { display: block; font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1.4; letter-spacing: 0; text-transform: none; color: var(--text-2); }
@media (hover: hover) {
  .grid-wrap .table tbody tr:hover :is(td, th) { background: var(--surface-hover); }
  /* The pinned name gets a cursor bar on its left edge as well. */
  .grid-wrap .table tbody tr:hover :is(td, th):first-child { box-shadow: inset 2px 0 0 var(--accent); }
}

/* Row heights by level: app.js sets data-level on #grid, and the Clients section's own grid ships with
   data-level="clients". Ads and Clients rows hold a thumbnail and are taller; their cells give up some padding so the
   40px picture fits the 52px row. The totals row is --row-h at every level. */
#grid[data-level="ad"] tbody :is(td, th), #grid[data-level="clients"] tbody :is(td, th),
#clients-grid tbody :is(td, th) {
  height: var(--row-h-thumb); padding-top: var(--s-1); padding-bottom: var(--s-1);
}

/* The name and its status LED sit side by side, so a row stays the row height and the skeleton rows come out the same
   height as the rows that replace them. A name too long for the room wraps, and the LED drops under it. */
.name-cell { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); min-width: 200px; max-width: var(--name-max); white-space: normal; }
/* On a phone the pinned name column has to leave room for the numbers to scroll past it. */
@media (max-width: 639px) { .grid-wrap { --name-max: 160px; } .name-cell { min-width: 130px; } }
.name { overflow-wrap: anywhere; font-family: var(--sans); font-size: 14px; line-height: 1.3; font-weight: 600; color: var(--text); }
.link.name { font-weight: 600; color: var(--text); }
/* The drill chevron after a name: centred on the name's capitals instead of standing on the baseline 3px high, and
   never alone on a line: a wrapping name breaks a word earlier so the chevron stays with its last word. */
.link.name > .chev { vertical-align: -0.2em; }
.link.name { text-wrap: pretty; }
.results b { font-weight: 600; color: var(--text); }
.results small { font-size: 12px; color: var(--text-2); }

/* Ads level: the creative is the row's label (the names are "6", "8", "2"), so it sits inside the pinned name cell,
   first, and never scrolls away. Picture, then the name over its LED. */
.name-cell.has-thumb { display: grid; grid-template-columns: var(--thumb) minmax(0, 1fr); align-items: center; gap: var(--s-3); }
.name-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; }

/* The thumbnail box: a fixed square with a static hatch that shows until the image paints over it, so nothing moves
   when a picture arrives and a 200-row table scrolls without layout work. Images are lazy, async-decoded and
   referrer-free (app.js); load and error are caught by one capture listener on #main, never one per image. The hatch
   is dropped once the picture has loaded (data-state="ready"): a gradient under an opaque image is never seen, but it
   was still painted for every row on every sort (verify-1: the grid's background images and shadows were the whole of
   a sort's long task). */
.thumb {
  position: relative; display: inline-grid; place-items: center; flex: none; vertical-align: middle;
  width: var(--thumb); height: var(--thumb); overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-2);
}
.thumb:not([data-state="ready"]) {
  background: repeating-linear-gradient(45deg, rgba(161, 249, 99, 0.05) 0 2px, transparent 2px 6px), var(--surface-2);
}
.thumb-img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; }
/* A video creative: a small play glyph in the bottom-left corner on a dark square (a glyph, not a "VID" label: text
   at that size would break the 11px minimum). */
.thumb-play {
  position: absolute; bottom: 0; left: 0; z-index: 1; display: grid; place-items: center;
  width: 16px; height: 16px; background: rgba(7, 10, 8, 0.7); color: var(--text);
}
.thumb-play svg { width: 12px; height: 12px; fill: currentColor; }
/* No creative: a dash. A picture that failed: a broken-image glyph. Both on the hatch, both decorative (the row header
   names the ad). */
.thumb[data-state="none"]::after {
  font-family: var(--mono); font-size: 11px; line-height: 1; color: var(--text-3);
  content: "\2014"; content: "\2014" / "";
}
.thumb[data-state="error"]::after {
  content: ""; width: 14px; height: 14px; background: var(--text-3);
  -webkit-mask: var(--icon-broken) center / contain no-repeat; mask: var(--icon-broken) center / contain no-repeat;
}
.thumb[data-state="none"] .thumb-img, .thumb[data-state="error"] .thumb-img { display: none; }

/* A count that opens the Clients tab filtered to that ad: a mono figure in accent text with a quiet underline. */
.count-link {
  border: 0; padding: 0; background: none; cursor: pointer;
  font-size: 13px; font-weight: 500; color: var(--accent-hover);
  text-decoration: underline; text-decoration-color: rgba(163, 255, 102, 0.40); text-underline-offset: 3px;
}
@media (hover: hover) {
  .count-link:hover { text-decoration-color: currentColor; }
}

/* Clients tab: "Came from" is the ad's thumbnail beside its campaign / ad set / ad path. Names stay in DM Sans (people
   typed them); the separators are mono, 8px either side like the crumbs' path (at 4px a slash crowded the names). */
.client-path {
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0 var(--s-2);
  min-width: 0; max-width: 360px; vertical-align: middle; white-space: normal;
  font-family: var(--sans); font-size: 13px; line-height: 1.4; color: var(--text-2);
}
.client-path .link { font-weight: 500; overflow-wrap: anywhere; }
.client-path .link:last-child { color: var(--text); }
.client-path .sep { font-family: var(--mono); color: var(--text-3); }
.thumb + .client-path, .thumb + .tag { margin-left: var(--s-3); }
/* Without an ad, the source's dot leads instead, and gaps keep it 10px clear of the pill so it never crowds the border. */
/* The row is held to its cell (min-width 0, max-width 100%) and the path may shrink and clips, so a long Meta ad set
   name ends in an ellipsis at the cell's edge instead of running past the panel; the pill shrinks last, with its own
   ellipsis. */
.came-from:has(> .src-dot) { display: flex; align-items: center; gap: 10px; min-width: 0; max-width: 100%; }
.src-dot + .client-path { column-gap: var(--s-2); flex-wrap: nowrap; flex: 0 1 auto; min-width: 0; overflow: hidden; }
.src-dot + .client-path > .src-pill { flex: 0 1 auto; min-width: 4ch; }
.src-dot + .client-path > .src-detail { flex: 0 100 auto; }
/* One line beside the dot: a long Meta ad set name ends in an ellipsis (the pill's title has it in full) instead of
   wrapping, which would centre the dot between two lines and off the pill. */
.src-dot + .client-path > .src-detail { min-width: 0; }
.tag + .muted { margin-left: var(--s-2); font-size: 13px; white-space: normal; }
/* Inside a grid cell wrapper the thumbnail and the path line up like the Ads name cell. */
.name-cell.has-thumb > .client-path { max-width: none; }
/* Clients: the journey button sits at the name cell's end, so the buttons make one column down the table instead of
   following each name's length; a long name wraps beside it. */
.client-name > .client-line { display: flex; flex: 1 1 100%; align-items: center; gap: var(--s-2); min-width: 0; }
.client-line > .name { min-width: 0; }
.client-line > .journey-btn { flex: none; margin-left: auto; }
/* From a 1086px main column the Clients table fits its card: the table takes the card's width and Came from (the
   section's point) gives up its width to the others, wrapping its path in place; at 1440 it was cut mid-name at the
   card's edge, 194px of the table out of sight. The method tag goes under the path there, so the path has the
   column's width. Narrower, and on touch at any width, the table scrolls sideways as before. */
@container main (min-width: 1086px) {
  #clients-grid .table { width: 100%; }
  #clients-grid .came-from:not(:has(> .src-dot)) {
    display: grid; grid-template-columns: var(--thumb) minmax(0, 1fr); align-items: center; gap: 2px var(--s-3);
  }
  #clients-grid .came-from > .thumb { grid-row: 1 / span 2; }
  #clients-grid .came-from > .client-path { max-width: none; margin-left: 0; }
  #clients-grid .came-from > .tag { justify-self: start; margin-left: 0; }
  /* Touch keeps the path on one line (below), so there its column is as wide as the path, the cell grows to hold it,
     and the table scrolls sideways; a 0-minimum column would let the path run out of its cell instead. */
  @media (pointer: coarse) {
    #clients-grid .came-from:not(:has(> .src-dot)) { grid-template-columns: var(--thumb) auto; }
    /* The path sits over its tag, about 6px under the row's top, so a band centred on it reached 7px over the row:
       in the first row that is under the sticky header (a 36px target). Here it starts 1px inside the row and hangs
       down over the tag, a word, not a control. */
    #clients-grid .came-from:has(> .tag) > .client-path .link::after { top: -5px; height: 44px; transform: none; }
  }
}
/* Touch, at every width: the path keeps to one line. Wrapped, its 18px lines stacked two links' 44px bands in one
   row, so the later link covered the earlier (an 18px target) and reached into the next row; no hit area fits two
   lines in one row. On one line each band stays inside its row and beside its neighbours. Below a 1086px column the
   table scrolls sideways anyway and is a little wider; from 1086px (a large tablet on its side) it now scrolls
   sideways the same way rather than fit its card by wrapping the path, which only a mouse can point into. */
@media (pointer: coarse) {
  .client-path:has(> .link) { flex-wrap: nowrap; max-width: none; white-space: nowrap; }
}

/* Table skeletons: rows of bars at the column widths app.js computes (inline width). */
.grid-wrap .table td .skeleton { height: 12px; }
/* The measuring row of the two-step landing: collapsed, it takes no height and paints nothing, but its cells still
   count towards the column widths. */
.grid-wrap .table tr.measure { visibility: collapse; }

/* The empty and error states are blocks in the scroll box's own flow, pinned to the left edge so the sentence stays in
   view when the header has been scrolled sideways. */
.grid-wrap > .empty-state { position: sticky; left: 0; width: 100%; }

/* A level strip that overflows on a mouse screen: a touch screen swipes it, but a mouse gets no scrollbar and no
   reason to know there is more, so the overflowing edge fades over 16px. The fades follow the strip's own scroll
   position (a scroll-driven animation on opacity only); while the strip has nothing to scroll the timeline is
   inactive and they stay invisible. They are sticky flex items of no net width, painted in the track's colour. */
@media (hover: hover) {
  @supports (animation-timeline: scroll(nearest inline)) {
    .tabs::before, .tabs::after {
      content: ""; position: sticky; z-index: 1; flex: none; align-self: stretch; width: var(--s-4);
      pointer-events: none; opacity: 0;
      animation: linear both; animation-timeline: scroll(nearest inline);
    }
    .tabs::before {
      left: 0; margin-right: calc(var(--s-4) * -1);
      background: linear-gradient(90deg, var(--surface-2), transparent); animation-name: tabs-edge-start;
    }
    .tabs::after {
      right: 0; margin-left: calc(var(--s-4) * -1);
      background: linear-gradient(270deg, var(--surface-2), transparent); animation-name: tabs-edge-end;
    }
    @keyframes tabs-edge-start { from { opacity: 0; } to { opacity: 1; } }
    @keyframes tabs-edge-end { from { opacity: 1; } to { opacity: 0; } }
  }
}

/* ==========================================================================
   Sections: seven views on one page (SECTIONS-PLAN.md). app.js draws Overview, Campaigns and Clients; views.js draws
   Creatives, Leads, Cash and Data into their body elements. Shared parts first (views, grids, strips, panels, lists,
   toolbars, small tables), then each section's own pieces, then the tags they share, then the phone tab bar and the
   More sheet. Every section is the same vertical stack with the same gap, its panels sit on one 12-column grid, and
   its figures use the KPI tiles' type, so moving between sections feels like turning pages of one instrument.
   ========================================================================== */

/* ---------- views ---------- */
/* Which view shows is decided here alone, from html[data-section] (set before first paint by index.html's head
   script, then by app.js on every route). A display:none view is out of the accessibility tree too, so there is no
   hidden attribute to keep in step, and a visited view keeps its DOM while hidden. Section switches are instant: no
   cross-fade, so a switch costs one style recalc and the paint of the view that appears. */
.view {
  --view-gap: var(--s-5); /* the one vertical rhythm of every section body; --s-4 on phones */
  display: none; min-width: 0; margin-top: var(--s-5);
}
html[data-section="overview"] #view-overview, html[data-section="campaigns"] #view-campaigns,
html[data-section="creatives"] #view-creatives, html[data-section="clients"] #view-clients,
html[data-section="leads"] #view-leads, html[data-section="cash"] #view-cash, html[data-section="data"] #view-data,
html:not([data-section]) #view-overview {
  display: block;
}
.view > * + * { margin-top: var(--view-gap); }
/* The bodies views.js owns: a column of blocks with the section gap between them, whatever views.js puts in. */
.view-body { display: flex; flex-direction: column; gap: var(--view-gap); min-width: 0; }
.view-body > *, .view-head { min-width: 0; }
.view-body > .notice { margin-top: 0; }
/* A whole-section empty or error state (no ads in range, Typeform not connected) sits in a panel's outline, so the
   section still reads as a page of panels rather than a sentence floating on the grid. */
.view-body > .empty-state { border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--surface); }

/* ---------- section grid ---------- */
/* Twelve columns with panels of 12, 8, 6 or 4. Panels in a row stretch to one height, so their bottoms line up as
   well as their tops. Under a 1012px main column (the KPI grid's own step) every panel takes the whole row: two
   half-width panels of tables and bars are harder to read side by side than one above the other. */
.section-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--s-5); }
.section-grid > *, .section-grid > .span-12 { grid-column: span 12; min-width: 0; }
@container main (min-width: 1012px) {
  .section-grid > .span-8 { grid-column: span 8; }
  .section-grid > .span-6 { grid-column: span 6; }
  .section-grid > .span-4 { grid-column: span 4; }
}

/* ---------- panels ---------- */
/* A panel is a card whose body grows to fill it, so a panel shorter than its neighbour keeps its content at the top
   and its outline level with the neighbour's. */
.panel { display: flex; flex-direction: column; min-width: 0; }
.panel > .panel-body { flex: 1 1 auto; }
.panel-body { min-width: 0; font-size: 14px; line-height: 1.55; color: var(--text-2); }
.panel-body > p + p { margin-top: var(--s-2); }
/* The panel's one-line note ("1 of 8 new clients matched to a creative… how to raise this"): directly under the title
   row, gone while empty. */
.panel-note { margin: calc(var(--s-2) * -1) 0 var(--s-3); font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.panel-note:empty { display: none; }
/* "ALL CAMPAIGNS →": the door from a summary panel to its section, in accent text at the title row's right end. */
.panel-link { position: relative; display: inline-flex; align-items: center; gap: var(--s-1); color: var(--accent-hover); text-underline-offset: 3px; }
.panel-link::after { content: "\2192"; content: "\2192" / ""; }
@media (hover: hover) {
  .panel-link:hover { text-decoration: underline; }
}
/* A table wider than its panel (the Cash list, the recent leads, a creative's clients on a phone) scrolls inside the
   panel body, never the page. The body pads 4px and pulls back by the same, so the focus ring of a link in the first
   or last column is not cut by the scroll box's edge and the table still lines up with the panel title. */
.panel-body:has(> .mini-table) {
  overflow-x: auto; overscroll-behavior-x: contain;
  margin: -4px; padding: 4px;
}

/* ---------- stat strips ---------- */
/* The KPI tile's look at a smaller size, for a section's headline figures (Clients, Leads, Cash, a creative's detail):
   an outlined, bracketed box, a Label, a mono figure, an optional sans line. Two across on a phone, three from a
   540px column, four from 754px; a strip of exactly five goes 3 + 2 below 1012px and five across above it, a strip of
   three or nine always in threes, so no strip ends on one lonely box. Boxes in a row are one height. */
/* Every row of boxes is as tall as the tallest (grid-auto-rows: 1fr), so a strip of nine or of five never has a short
   row between two tall ones: a creative's nine were 92, 68 and 92px at 1440. */
.stat-strip {
  --stat-cols: 2;
  display: grid; grid-template-columns: repeat(var(--stat-cols), minmax(0, 1fr)); grid-auto-rows: 1fr; gap: var(--s-3);
}
.stat-strip > .stat {
  --bracket-l: 6px;
  position: relative; isolation: isolate; min-width: 0;
  display: flex; flex-direction: column; gap: var(--s-1);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--surface);
}
/* The figure breaks only between words ("Whop 1 · Email 2 · Phone 0 · Name 1"), never inside an amount, unless an
   amount alone is wider than its box. It steps down from 20px on narrow screens so a two-across "A$12,969.90" fits a
   320px phone. */
.stat-v {
  font-size: clamp(16px, 1.2vw + 12px, 20px); font-weight: 500; line-height: 1.2; letter-spacing: -0.01em;
  color: var(--text); overflow-wrap: break-word;
}
.stat-sub { font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.stat-v .skeleton { height: 1.2em; width: 60%; }
/* The Clients strip draws its five boxes with skeleton figures and no line under them until the answer lands, then
   each gains its sentence: one line or two ("first payment in this range" wraps on a phone), and By method always two.
   Every row of boxes is as tall as the tallest, so a loading box holds two lines, as the strip ends up, and the table
   under the strip stays put (verify: it moved 43px at 1440 and 129px at 375). A loading box that already carries its
   line holds nothing more. */
#clients-head .stat:has(> .stat-v > .skeleton):not(:has(> .stat-sub))::after { content: ""; height: calc(2 * 13px * 1.5); }
/* The strip itself ships empty and app.js draws it, loading boxes first. A frame can paint before the script runs, and
   the table then dropped by the strip's whole height (a 0.045 shift at 1024 × 768, 0.025 at 375), so the empty head
   holds its loading boxes' room: a box is its border and padding, the label, the figure's line (the figure's own
   clamp) and the two lines held above, 4px apart; one row of five from a 1012px column, two rows from 540px, three
   under that, where under a 320px column "MATCHED TO AN AD" takes two lines (and every row with it). */
#clients-head:empty {
  --stat-k-lines: 1;
  --stat-box: calc(2px + 2 * var(--s-3) + var(--stat-k-lines) * 11px * 1.3 + 2 * var(--s-1)
    + 1.2 * clamp(16px, 1.2vw + 12px, 20px) + 2 * 13px * 1.5);
  min-height: calc(3 * var(--stat-box) + 2 * var(--s-3));
}
@container main (max-width: 319.98px) {
  #clients-head:empty { --stat-k-lines: 2; }
}
@container main (min-width: 540px) {
  #clients-head:empty { min-height: calc(2 * var(--stat-box) + var(--s-3)); }
}
@container main (min-width: 1012px) {
  #clients-head:empty { min-height: var(--stat-box); }
}
@container main (max-width: 539.98px) {
  /* Two across: an odd last box takes the whole row. */
  .stat-strip > .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@container main (min-width: 540px) {
  .stat-strip { --stat-cols: 3; }
}
@container main (min-width: 754px) {
  .stat-strip { --stat-cols: 4; }
  .stat-strip:has(> .stat:nth-child(5):last-child) { --stat-cols: 3; }
}
@container main (min-width: 540px) {
  .stat-strip:has(> .stat:nth-child(3):last-child), .stat-strip:has(> .stat:nth-child(9):last-child) { --stat-cols: 3; }
}
@container main (min-width: 1012px) {
  .stat-strip:has(> .stat:nth-child(5):last-child) { --stat-cols: 5; }
}

/* ---------- top lists (Overview) ---------- */
/* Five ranked rows per panel: a rank number, the creative's thumbnail where there is one, the name, then its figures
   in a row of small labelled cells. The rank is generated text (alt "") because the list is an <ol>: its order is
   already announced. */
.top-list { counter-reset: top; margin: 0; padding: 0; list-style: none; }
.top-item {
  counter-increment: top;
  display: grid; grid-template-columns: 24px minmax(0, 1fr); column-gap: var(--s-3); row-gap: var(--s-2);
  padding: var(--s-3) 0; border-top: 1px solid var(--line-neutral);
}
.top-item:first-child { border-top: 0; padding-top: 0; }
.top-item:last-child { padding-bottom: 0; }
.top-item:has(> .thumb) { grid-template-columns: 24px var(--thumb) minmax(0, 1fr); }
.top-item::before {
  grid-column: 1; grid-row: 1 / 3; padding-top: 2px;
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1.3; color: var(--text-3);
  content: counter(top, decimal-leading-zero); content: counter(top, decimal-leading-zero) / "";
}
.top-item > .thumb { grid-column: 2; grid-row: 1 / 3; }
/* Top creatives always keep the picture column, even for an ad with no picture (the thumb box then shows its dash),
   so the five names line up. */
.top-creatives .top-item { grid-template-columns: 24px var(--thumb) minmax(0, 1fr); }
/* Top campaigns and Top creatives sit side by side from 1012px and stretch to one height (the section grid). */
.overview-panels { align-items: stretch; }
.top-item > .top-name, .top-item > .top-figs { grid-column: -2 / -1; }
.top-item > .skeleton { grid-column: -2 / -1; height: 12px; }
.top-name {
  position: relative; justify-self: start; max-width: 100%;
  font-family: var(--sans); font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--text);
  overflow-wrap: anywhere; text-underline-offset: 3px;
}
@media (hover: hover) {
  .top-name:hover { color: var(--accent-hover); text-decoration: underline; }
}
.top-figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); gap: var(--s-1) var(--s-3); margin: 0; }
.top-fig { min-width: 0; }
.top-fig dt { color: var(--text-3); letter-spacing: 0.06em; }
.top-fig dd { margin: 2px 0 0; font-size: 13px; line-height: 1.3; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- toolbars, notes, selects, small tables, "Show more" ---------- */
/* A section's controls on the left (segments, a sort select), its count on the right ("216 CREATIVES · 41 WITH
   SPEND"); on a phone the controls take full rows and the count drops under them. */
.view-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); min-width: 0; }
/* The count takes the rest of its row and sets its text at the row's end, so its box is the same before and after the
   answer: shrunk to its text and pushed right, the empty box of a loading view sat at the far right and jumped left
   by its new width when the count landed (the Creatives count, 1192 → 1043 at 1440), and on a phone, on its own row,
   from the right edge to the left one. A line's height is kept while it is empty. On a phone it reads under the
   controls, so from the start of its row. */
.view-toolbar > .view-count { flex: 1 1 auto; min-height: calc(12px * 1.5); text-align: right; }
@media (max-width: 860px) { .view-toolbar > .view-count { text-align: left; } }
.view-count { font-size: 12px; line-height: 1.5; letter-spacing: 0.02em; color: var(--text-2); }
.view-note { font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.view-note:empty { display: none; }
/* A toolbar's group of controls (the Creatives filter and sort): 8px apart, wrapping as a pair. On a phone the group
   takes the whole row, so the four segments share its width and the sort fills its own row under them. */
.view-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); min-width: 0; }
@media (max-width: 860px) { .view-controls { flex: 1 1 100%; } }
/* The Creatives gallery stacks toolbar, note and cards: the note 8px under the toolbar, the cards 16px under what is
   above them (the grid's own gap), instead of all three touching. */
.creatives-gallery > .view-note { margin-top: var(--s-2); }
/* On a phone the count ("22 creatives · 20 with spend · 5 brought clients", about 350px) and the match note under it
   (about 620px) take two lines (the note three at 320px), but the count is empty and the note one skeleton line until
   the answers land: both hold the lines they will take, so the cards under them stay put (verify: the grid moved
   56px at 375px, a 0.017 shift on every load). A shorter text leaves the room empty rather than move the cards. */
@container main (max-width: 399.98px) {
  #creatives-count { min-height: calc(2 * 12px * 1.5); }
}
@container main (max-width: 639.98px) {
  #creatives-note { min-height: calc(2 * 13px * 1.5); }
}
@container main (max-width: 359.98px) {
  #creatives-note { min-height: calc(3 * 13px * 1.5); }
}
.creatives-gallery > :is(.creatives-state:not(:empty), .creative-grid, .creatives-filter-empty) { margin-top: var(--s-4); }
/* Inside a panel (Payments, Where leads came from, Recent leads) the table starts 12px under its toolbar, not on it. */
.panel-body > .view-toolbar + * { margin-top: var(--s-3); }

/* A native select dressed as the segmented track: the menu stays the platform's own (the right picker on a phone),
   only the closed control is styled. The chevron is a mask on the label, so it never takes a click from the select. */
.select { position: relative; display: inline-flex; align-items: center; min-width: 0; max-width: 100%; }
.select::after {
  content: ""; position: absolute; top: 50%; right: var(--s-3); width: 12px; height: 12px; margin-top: -6px;
  background: var(--text-2); pointer-events: none;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.select-input {
  width: 100%; min-width: 0; height: 36px; padding: 0 calc(var(--s-3) + 20px) 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); color: var(--text);
  font-family: var(--mono); font-size: 12px; font-weight: 500; letter-spacing: 0.04em;
  -webkit-appearance: none; appearance: none; cursor: pointer;
}
@media (hover: hover) {
  .select-input:hover { border-color: var(--accent); }
}

/* The sections' small tables: the main table's type and hairlines, not sticky, rows of --row-h, figures right-aligned
   in mono. Inside a panel the outer cells pad nothing, so the columns line up with the panel's title. */
.mini-table thead th { color: var(--text-2); }
/* The cell already pads; a sort button in a small table's header pads nothing more, so its label starts at the x of
   the column's cells (the Cash list's DATE and CLIENT were 12px to the right of their dates and names). */
.mini-table thead .sort { gap: var(--s-2); padding-inline: 0; }
.mini-table :is(th, td):first-child { padding-left: 0; }
.mini-table :is(th, td):last-child { padding-right: 0; }
.mini-table td { overflow-wrap: anywhere; }
.mini-table .num, .mini-table td.num { white-space: nowrap; overflow-wrap: normal; }
/* A date or a payment's client never wraps (on a phone "3 / Oct, / 22:36" made 82px rows, "Sample / client 31" 92px
   ones), and campaign or ad names keep room for a few words a line instead of one; the table scrolls in its panel. */
.mini-table :is(.num-date, .cash-date, .journey-open) { white-space: nowrap; }
#leads-sources-table tbody th { min-width: 13em; }
.mini-table .thumb { margin-right: var(--s-2); }
/* Where a lead or payment came from (Leads "Source", Cash "Came from"): the creative's thumbnail on the left and,
   beside it, views.js's span.src-text holding the ad's name over its match tag. The name keeps to one line and ends
   in an ellipsis, so a long Meta-style ad name no longer wraps the cell into a 92px phone row: a 32px picture beside
   an 18px name and a 20px tag is a 56px row on a phone, under the 64px it may take. The cap on the cell's width is
   what lets the ellipsis happen inside an auto-width table column; the full name is still the link's text, so a
   screen reader and the creative's own page have all of it. */
.source-cell { display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; max-width: 320px; vertical-align: middle; }
.source-cell > .thumb { margin-right: 0; }
.src-text { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; }
.src-text > :is(.link, .source-campaign, .muted) {
  display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
  font-size: 13px; line-height: 18px;
}
.src-text > .tag-method { margin-left: 0; }
/* The tags section gives a tag after a link 8px of left margin (a tag beside a name); stacked under the name here it
   starts at the name's own x, so this says it again with the weight to win over that rule. */
.source-cell .src-text > .tag-method { margin-left: 0; }
@media (max-width: 639px) {
  .source-cell { max-width: 220px; }
}
/* A list with a creative among its sources (Leads' recent responses, Cash's payments) takes the thumbnail rows of the
   Ads level and Clients for every row: 52px, the cells padding 4px top and bottom, so the picture or the name over its
   tag fits. Left to their content the rows alternated 40px (a "DIRECT" pill) and 57px (a creative) down the list. */
:is(#leads-recent-table, #cash-table):has(.source-cell > .thumb) tbody :is(td, th) {
  height: var(--row-h-thumb); padding-top: var(--s-1); padding-bottom: var(--s-1);
}
/* On a phone a small table wider than its card scrolls sideways, and the column cut at the card's edge read as whole:
   "A$4,541" for A$4,541.05, "AMOUNT (" for the header. Each edge of the scroll box fades over the card's padding, a
   band that holds nothing but the outer cells' own padding when the table is scrolled to that end, so a cut column
   fades out (there is more this way) and a whole one is never dimmed. A static mask, like the backdrop's. */
@container main (max-width: 639.98px) {
  .mini-wrap {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--card-pad), #000 calc(100% - var(--card-pad)), transparent);
    mask-image: linear-gradient(90deg, transparent, #000 var(--card-pad), #000 calc(100% - var(--card-pad)), transparent);
  }
  /* The Cash list never cuts an amount: the fade only marked the cut (at 375 the box showed 51 of A$4,541.05's 78px).
     Date, client and amount now share the scroll box (100cqi is the main column; the box is 2px narrower) and the
     client's name gives way, ending in an ellipsis; the journey it opens names them in full, as does its aria-label.
     The budget: the widest date ("30 May, 00:00", 96px), a five-figure amount (86px), the cells' 12px paddings, the
     outer card-pad and the right fade, which the amount stays clear of. The amount's header may take two lines
     ("AMOUNT" over "(A$)") so its column is only as wide as its figures. */
  #cash-table thead th.num .sort { white-space: normal; }
  #cash-table .journey-open {
    display: block; max-width: max(44px, calc(100cqi - 2 * var(--card-pad) - 232px));
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* The clip would cut the link's ::after band to its own line, so on touch the band is padding (hit-tested inside
     the clip) taken back by the same negative margin, like the source cell's name: 44px or more, the row unmoved. */
  @media (pointer: coarse) {
    #cash-table .journey-open { min-width: 44px; padding-block: 12px; margin-block: -12px; }
  }
}
/* A 320–360px phone: the date takes two lines, day over time (the column is set to "30 May," wide), which leaves
   the name 54px at 320 instead of nothing. */
@container main (max-width: 339.98px) {
  #cash-table th.cash-date { width: calc(var(--card-pad) + 58px + var(--s-3)); white-space: normal; }
  #cash-table .journey-open { max-width: max(44px, calc(100cqi - 2 * var(--card-pad) - 194px)); }
}
.mini-table td .skeleton { height: 12px; }
@media (hover: hover) {
  .mini-table tbody tr:hover :is(td, th) { background: none; }
}

/* "SHOW MORE": centred under its list on a wide screen, the full width of a phone. */
.btn-more { display: flex; width: fit-content; min-width: 160px; margin: var(--s-4) auto 0; }

/* ---------- Creatives: the gallery ---------- */
/* The client's main view: every creative as a card, picture first, then the two figures that matter (CLIENTS and
   REVENUE A$), then six smaller ones. Cards are drawn with a hairline and no corner brackets: 216 cards of eight
   gradient strokes each would be the most expensive thing on the page to paint. content-visibility skips the layout
   and paint of cards off screen; the intrinsic size is the card's height at a typical width (a 270px picture plus
   about 230px of text) and `auto` swaps in each card's real height once it has been drawn, so the scrollbar settles
   after one pass and filtering or sorting (hidden / reordering) does not re-lay the cards out of view. */
.creative-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--s-4);
  margin: 0; padding: 0; list-style: none;
}
/* The clip lets 8px past the edge through, so a People link's touch area in the first or last column of figures is
   not cut at the card's edge (it was 38px wide there); the picture's top corners are rounded on their own (below). */
.creative-card {
  position: relative; min-width: 0; overflow: clip; overflow-clip-margin: var(--s-2);
  border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--surface);
  content-visibility: auto; contain-intrinsic-size: auto 500px;
}
.creative-card.is-skeleton { pointer-events: none; }
@media (hover: hover) {
  .creative-card:not(.is-skeleton):hover { border-color: var(--line-ui); }
  .creative-card:not(.is-skeleton):hover .name { color: var(--accent-hover); }
}
/* The whole card is one link. Its focus ring goes on the card's own outline, which the card's clipping cannot cut. */
.creative-card:has(> .creative-link:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.creative-grid .creative-link:focus-visible { outline: none; box-shadow: none; }
.creative-link { display: flex; flex-direction: column; height: 100%; color: inherit; }

/* The square picture: the 320px proxy rendition at the card's width, hatch until it paints (the base .thumb rules). */
.thumb-lg, .thumb-xl { display: grid; }
.thumb-lg { width: 100%; height: auto; aspect-ratio: 1 / 1; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; }
/* At the top of a card the picture rounds its own top corners to the card's inner radius, since the card's clip now
   reaches past its edge. */
:is(.creative-card, .ov-creative) .thumb-lg { border-radius: calc(var(--r-xl) - 1px) calc(var(--r-xl) - 1px) 0 0; }
.thumb-xl { width: 160px; height: 160px; border-radius: var(--r-md); }
:is(.thumb-lg, .thumb-xl) .thumb-play { width: 28px; height: 28px; }
:is(.thumb-lg, .thumb-xl) .thumb-play svg { width: 16px; height: 16px; }
:is(.thumb-lg, .thumb-xl)[data-state="none"]::after { font-size: 16px; }
:is(.thumb-lg, .thumb-xl)[data-state="error"]::after { width: 24px; height: 24px; }

/* The card's text column is a size container, so its figures fit the room they actually have: the same card is
   268px wide in a desktop row and 217px beside an 88px picture on a phone. */
.creative-main {
  container: card / inline-size;
  display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--s-3); min-width: 0; padding: var(--s-4);
}
.creative-title { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-2); min-width: 0; }
/* Two lines at most, so the figures under every card in a row start at the same height. */
.creative-title .name {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; min-width: 0;
}
.creative-title .badge { flex: none; margin-top: 3px; }
.creative-path {
  margin: 0; font-size: 12px; line-height: 1.5; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.creative-title + .creative-path { margin-top: calc(var(--s-2) * -1); }
/* CLIENTS | REVENUE: an inset band with the two figures split by a hairline. The count takes only its own width, so
   the amount gets the rest of the band. */
.creative-hero {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start;
  padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.creative-kpi { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.creative-kpi + .creative-kpi { margin-left: var(--s-3); padding-left: var(--s-3); border-left: 1px solid var(--border); }
.creative-k { color: var(--text-2); }
/* Never broken: a cut amount ends in an ellipsis rather than wrapping into a different number. */
.creative-v {
  font-size: 22px; font-weight: 500; line-height: 1.1; letter-spacing: -0.02em; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.creative-kpi[data-k="revenue"] .creative-v { color: var(--accent-hover); }
.creative-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2) var(--s-3); margin: 0; }
.creative-stat { min-width: 0; }
.creative-stat dt { color: var(--text-3); letter-spacing: 0.06em; }
.creative-stat dd {
  margin: 2px 0 0; font-size: 13px; line-height: 1.3; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.creative-card.is-skeleton .skeleton { height: 12px; }
.creative-card.is-skeleton .creative-v .skeleton { height: 1.1em; width: 70%; }
@container card (max-width: 229.98px) {
  .creative-stat dd { font-size: 12px; }
}
/* Very narrow (a 320px phone's row card): the two hero figures stack, the stats go two across. */
@container card (max-width: 199.98px) {
  .creative-hero { grid-template-columns: minmax(0, 1fr); }
  .creative-kpi + .creative-kpi { margin: var(--s-2) 0 0; padding: var(--s-2) 0 0; border-left: 0; border-top: 1px solid var(--border); }
  .creative-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Phones: one column of row cards, the picture 88px on the left and the text beside it. */
@media (max-width: 639px) {
  .creative-grid { grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
  .creative-card { contain-intrinsic-size: auto 210px; }
  .creative-link { display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: start; gap: var(--s-3); padding: var(--s-3); }
  .creative-card .thumb-lg { width: 88px; height: 88px; border: 1px solid var(--border); border-radius: var(--r-sm); }
  .creative-card .creative-main { padding: 0; gap: var(--s-2); }
  .creative-title + .creative-path { margin-top: calc(var(--s-1) * -1); }
}

/* ---------- Creatives: one creative's detail ---------- */
.creative-detail { display: flex; flex-direction: column; gap: var(--view-gap); min-width: 0; }
.back-link { position: relative; align-self: flex-start; display: inline-flex; align-items: center; gap: var(--s-2); min-height: 32px; color: var(--accent-hover); }
.back-link::before { content: "\2190"; content: "\2190" / ""; }
@media (hover: hover) {
  .back-link:hover { text-decoration: underline; text-underline-offset: 3px; }
}
.detail-hero { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: var(--s-5); align-items: start; }
.detail-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2); min-width: 0; }
.detail-title { font-size: 20px; line-height: 1.25; letter-spacing: -0.015em; overflow-wrap: anywhere; }
.detail-head .creative-path { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
.detail-head .btn { margin-top: var(--s-2); }
/* A small phone keeps the picture beside the title, at 96px, so the title is not squeezed to a word a line. */
@media (max-width: 479px) {
  .detail-hero { grid-template-columns: 96px minmax(0, 1fr); gap: var(--s-4); }
  .thumb-xl { width: 96px; height: 96px; }
}

/* ---------- Leads ---------- */
/* Qualified leads per day as solid bars in the leads colour, the other responses stacked above as the same colour's
   outline over a quarter tint (the trend chart's new/repeat cash language), so the two differ by shape as well as
   fill. views.js draws to its own viewBox; the SVG scales to the panel's width. */
.leads-svg { display: block; width: 100%; height: auto; overflow: visible; user-select: none; -webkit-user-select: none; }
.leads-svg text { font-family: var(--mono); font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums; fill: var(--text-2); }
.leads-bar { fill: var(--series-leads); stroke: none; shape-rendering: crispEdges; }
.leads-bar.leads-bar-other { fill: var(--series-leads); fill-opacity: 0.25; stroke: var(--series-leads); stroke-width: 1px; }
.leads-tick { fill: var(--text-2); }
/* A day's column answers the pointer over its whole box, not just its painted bars, so its title (the day's counts)
   shows even over a short bar. */
.leads-col { pointer-events: bounding-box; }
.leads-svg :is(line, path):not([class]) { stroke: var(--border-strong); stroke-width: 1px; shape-rendering: crispEdges; }
/* The legend under the bars: 12px below the axis, each word after the trend tooltip's 8px swatch drawn like its bar
   (solid = qualified, an outline over a quarter tint = the other responses), 8px from its word, the two keys ~16px
   apart. The swatch spans had no rule, so the legend read as one phrase, "Qualified Other responses". */
.leads-legend { margin-top: var(--s-3); }
.leads-key { display: inline-block; width: 8px; height: 8px; margin-right: var(--s-2); background: var(--series-leads); }
.leads-key-o { margin-left: var(--s-3); background: rgba(25, 158, 176, 0.25); box-shadow: inset 0 0 0 1px var(--series-leads); }

/* Revenue bands: label and count on one line, a 6px bar under them scaled with transform (no width change, so a
   redraw is a compositor update). The track is the row's own pseudo-element; qualified bands fill in the leads
   colour, the others in the quiet --text-3. */
.band-list { display: flex; flex-direction: column; gap: var(--s-3); margin: 0; padding: 0; list-style: none; }
.band {
  position: relative; isolation: isolate;
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--s-1) var(--s-3);
  padding-bottom: calc(6px + var(--s-2)); font-size: 14px; line-height: 1.4; color: var(--text);
}
.band::after { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 6px; background: var(--surface-2); }
.band > :not(.band-fill):last-child:not(:first-child) { font-family: var(--mono); font-size: 13px; color: var(--text-2); white-space: nowrap; }
.band-fill {
  position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; height: 6px;
  background: var(--text-3); transform-origin: left center;
}
.band[data-qualified="true"] .band-fill { background: var(--series-leads); }
/* QUALIFIES right after its band's name and in one column: the names share a minimum width (the widest standard
   answer, "$20K-$50K"), and the tag's auto margin, not space-between, sends the count to the right. The rule under
   the list keeps 16px from the last bar. */
.band-label { min-width: 5.5em; }
.band > .tag { margin-right: auto; }
.band-list + .view-note { margin-top: var(--s-4); }

/* ---------- Cash ---------- */
/* New-client cash against repeat cash as one bar: new grows from the left edge, repeat from the right edge, each
   scaled to its share, so with the shares summing to one they meet exactly. New is the solid cash colour, repeat its
   outline over a quarter tint (the trend chart's language). The track is the box's own pseudo-element in the same
   grid cell; anything else views.js puts in the box (the labels) flows into the rows below. */
.cash-split { display: grid; grid-template-columns: minmax(0, 1fr); row-gap: var(--s-2); isolation: isolate; min-width: 0; }
.cash-split::before, .cash-split-new, .cash-split-repeat { grid-row: 1; grid-column: 1; height: 12px; }
.cash-split::before { content: ""; border: 1px solid var(--border); background: var(--surface-2); }
.cash-split-new { z-index: 1; display: block; background: var(--series-cash); transform-origin: left center; }
.cash-split-repeat {
  z-index: 1; display: block; transform-origin: right center;
  border: 1px solid var(--series-cash); background: rgba(60, 158, 44, 0.25);
}
/* The key under the bar: 12px of air below the track, one row per part 8px apart. The rows share the legend's two
   columns (subgrid), so the tag column is as wide as the widest tag, FIRST stretches to REPEAT's width, and both
   sentences start at one x, 12px after the tag, centred on it. */
.cash-split-legend {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-2) var(--s-3); margin-top: var(--s-3);
}
.cash-split-key { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; margin: 0; min-width: 0; }
.cash-split-key > .tag { justify-content: center; }

/* ---------- Data ---------- */
/* Connections: one row per source, an LED and the status word beside its name, then a sentence and a mono meta line
   ("checked 14:02 · key WHOP_PAYMENTS_API_KEY"). Two across where there is room. */
.conn-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: var(--s-3);
  margin: 0; padding: 0; list-style: none;
}
.conn {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--s-1) var(--s-2);
  min-width: 0; padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
  align-content: start; /* a card stretched to its row's height keeps its rows at the top: names level across a row */
}
.conn > .conn-detail, .conn > .conn-meta { grid-column: 1 / -1; }
/* The LED and the status word keep to the name's first line when the name wraps, rather than centring on the whole
   name (the Typeform row's LED was 11px low at 375px). The same holds for Data → Editing's setup row below. */
.conn { align-items: start; }
.conn > .led { margin-top: calc((14px * 1.4 - 6px) / 2); }
.conn > .conn-status { margin-top: calc((14px * 1.4 - 11px * 1.3) / 2); }
.conn-name { min-width: 0; font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.conn-detail { font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.conn-meta { font-size: 11px; line-height: 1.4; letter-spacing: 0.04em; color: var(--text-3); overflow-wrap: anywhere; }
.conn[data-status="ok"] .led { background: var(--led-on); }
.conn:is([data-status="needs_permission"], [data-status="needs_token"], [data-status="not_configured"]) .led { background: var(--led-warn); }
.conn[data-status="error"] .led { background: var(--led-bad); }
.conn:is([data-status="unknown"], [data-status="skipped"]) .led { background: transparent; border: 1px solid var(--led-off); }
.conn[data-status="ok"] .conn-status { color: var(--accent-hover); }
.conn:is([data-status="needs_permission"], [data-status="needs_token"], [data-status="not_configured"]) .conn-status { color: var(--warning-text); }
.conn[data-status="error"] .conn-status { color: var(--danger-text); }

/* The headline match rate in a panel, at the KPI figure's size. */
:is(#data-match, #data-leads) .stat-v { display: block; font-size: var(--fs-kpi); line-height: 1.1; letter-spacing: -0.02em; }
/* Match rate by method: label, a track with its fill scaled from the left, the count. The label column is fixed so
   every track starts at the same x. Each method has its own fill; the label beside it says which, so colour is never
   the only cue. "Name" is amber because name matches are only "likely". */
.method-bars { --method-k-w: 96px; display: flex; flex-direction: column; gap: var(--s-2); margin-top: var(--s-4); }
.method-bar { display: grid; grid-template-columns: var(--method-k-w) minmax(0, 1fr) auto; align-items: center; gap: var(--s-3); min-width: 0; }
/* The bars share one count column (subgrid), so every track ends at the same x and a fill is the same share of the
   same length in every row. Sized row by row, the widest count ("24 · 45%") cut its own track 14px short, and its
   fill could no longer be read against the others. */
@supports (grid-template-columns: subgrid) {
  .method-bars { display: grid; grid-template-columns: var(--method-k-w) minmax(0, 1fr) auto; gap: var(--s-2) var(--s-3); }
  .method-bar { grid-column: 1 / -1; grid-template-columns: subgrid; }
}
.method-k { min-width: 0; overflow-wrap: anywhere; }
.method-track { position: relative; height: 8px; overflow: hidden; border: 1px solid var(--border); background: var(--surface-2); }
.method-fill { position: absolute; inset: 0; background: var(--text-3); transform-origin: left center; }
.method-bar[data-method="whop"] .method-fill { background: var(--accent); }
.method-bar[data-method="email"] .method-fill { background: var(--series-leads); }
.method-bar[data-method="phone"] .method-fill { background: var(--series-calls); }
.method-bar[data-method="name"] .method-fill { background: var(--warning); }
.method-n { min-width: 4ch; font-size: 12px; line-height: 1.3; text-align: right; white-space: nowrap; color: var(--text); }
/* Lists and definition lists views.js writes into a panel without a class of their own (unmatched reasons, the rate
   in use, the attribution model). */
.panel-body :is(ul, ol):not([class]) { margin: var(--s-3) 0 0; padding-left: var(--s-5); font-size: 13px; line-height: 1.5; }
.panel-body :is(ul, ol):not([class]) li + li { margin-top: var(--s-1); }
.panel-body dl:not([class]) { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); gap: var(--s-2) var(--s-4); margin: 0; }
.panel-body dl:not([class]) dt {
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1.6; letter-spacing: 0.10em;
  text-transform: uppercase; color: var(--text-2);
}
.panel-body dl:not([class]) dd { margin: 0; color: var(--text); overflow-wrap: anywhere; }
/* Those lists now carry classes (reason-list, identity-list, rule-list, model-list), which the rules above skip, so
   they fell back to the browser's 40px indents and 1em margins, and the notes and "Not matched" sat on the blocks
   above them. One rhythm: 12px between blocks, a 20px bullet indent, 4px between items, the heading 16px clear. */
.panel-body :is(.reason-list, .identity-list, .rule-list) { margin: var(--s-3) 0 0; padding-left: var(--s-5); }
.panel-body :is(.reason-list, .identity-list, .rule-list) li + li { margin-top: var(--s-1); }
.method-bars + .view-note, :is(.reason-list, .identity-list) + .view-note { margin-top: var(--s-3); }
.panel-sub { margin-top: var(--s-4); }
/* The attribution model: each name with its IN USE tag 8px after it, its sentence under it at the same x (no 40px
   indent), 12px between the two models. */
.model-list { display: flex; flex-direction: column; gap: var(--s-3); margin: 0; }
.model dt { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); }
.model dd { margin: var(--s-1) 0 0; }
/* The exchange rate's figure on its own line with its source under it, like the match rates beside it (it ran into
   its sentence: "A$1.5152Sample rate"). */
#data-fx .stat-v { display: block; }
.match-big + .view-note { margin-top: var(--s-3); }
:is(.panel-body, .step) code {
  padding: 0 var(--s-1); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-2);
  font-family: var(--mono); font-size: 12.5px; color: var(--text); overflow-wrap: anywhere;
}

/* "Raise the match rate": numbered steps, the number in an outlined square on the left, each step's live count in an
   accent chip with a lit LED (the figure the client's team can watch rise). */
.steps { counter-reset: step; display: flex; flex-direction: column; gap: var(--s-4); margin: 0; padding: 0; list-style: none; }
/* The sentence over the steps keeps the 16px the panel's title keeps over it: step 01's square sat on its last line. */
.panel-body > .view-note + .steps { margin-top: var(--s-4); }
.step {
  counter-increment: step; position: relative; min-width: 0;
  padding: 0 0 var(--s-4) calc(32px + var(--s-3)); border-bottom: 1px solid var(--line-neutral);
}
.step:last-child { padding-bottom: 0; border-bottom: 0; }
.step::before {
  position: absolute; top: 0; left: 0; display: grid; place-items: center; width: 32px; height: 32px;
  border: 1px solid var(--line-ui); border-radius: var(--r-sm); color: var(--accent-hover);
  content: counter(step, decimal-leading-zero); content: counter(step, decimal-leading-zero) / "";
}
.step-title { min-height: 32px; padding-top: 6px; font-size: 15px; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.step p { max-width: 72ch; margin-top: var(--s-1); font-size: 14px; line-height: 1.55; color: var(--text-2); overflow-wrap: anywhere; }
.step .step-live {
  display: inline-flex; align-items: center; gap: var(--s-2); max-width: 100%; margin-top: var(--s-2); padding: 2px var(--s-2);
  border: 1px solid rgba(161, 249, 99, 0.40); border-radius: var(--r-sm); background: var(--accent-soft);
  font-size: 12px; line-height: 1.5; color: var(--accent-hover);
}
.step .step-live::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--led-on); }
.step .step-live:empty { display: none; }
/* The /book privacy note: the console's look with its own header word. */
.notice.notice-privacy { margin-top: var(--s-5); }
.notice-privacy::before { content: "PRIVACY"; content: "PRIVACY" / ""; }

/* ---------- tags: match method, payment type ---------- */
/* Label-style chips (the .tag base). The method says how sure a match is: WHOP and the payment's FIRST in the accent,
   EMAIL and PHONE neutral, NAME · LIKELY in the warning tones (a name is only matched when it is unique on both sides,
   and is still a guess). REPEAT is quiet, UNKNOWN an outline only. The words carry the meaning; the tints echo it. */
.tag-method { border-color: var(--border-strong); background: var(--surface-2); color: var(--text); }
.tag-method[data-method="whop"], .tag-first { border-color: rgba(161, 249, 99, 0.40); background: var(--accent-soft); color: var(--accent-hover); }
.tag-method[data-method="name"] { border-color: rgba(255, 194, 61, 0.40); background: var(--warning-soft); color: var(--warning-text); }
.tag-repeat { border-color: var(--border); background: var(--surface-hover); color: var(--text-2); }
.tag-unknown { border-style: dashed; border-color: var(--border-strong); background: transparent; color: var(--text-3); }
:is(.client-path, .link, a, .name, .thumb) + .tag-method { margin-left: var(--s-2); }

/* ---------- phone tab bar ---------- */
/* Five equal cells fixed to the bottom on phones: Overview, Campaigns, Creatives, Clients and More. Each is the
   56px bar's full height and at least 64px wide on a 320px phone, so every target clears 44 × 44. The icon sits in a
   52 × 28 square that takes the accent tint when its section is open (the More button lights for Leads, Cash and
   Data), and the label turns accent: the open section is said by the tint, the colour and aria-current. */
.tabbar { display: none; }
.tabbar-link {
  position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s-1);
  min-width: 0; height: var(--tabbar-h); padding: 0;
  border: 0; border-radius: 0; background: none; color: var(--text-2); cursor: pointer;
}
.tabbar-icon { display: grid; place-items: center; width: 52px; height: 28px; border-radius: var(--r-md); }
.tabbar-icon svg { width: 22px; height: 22px; }
.tabbar-label {
  max-width: 100%; padding: 0 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 11px; font-weight: 500; line-height: 16px; letter-spacing: 0.04em; text-transform: uppercase;
}
.tabbar-link[aria-current="page"], .tabbar-more[data-active="true"] { color: var(--accent-hover); }
.tabbar-link[aria-current="page"] .tabbar-icon, .tabbar-more[data-active="true"] .tabbar-icon { background: var(--accent-soft); }
.tabbar-more[aria-expanded="true"] .tabbar-icon { background: var(--surface-hover); }
.tabbar-more[aria-expanded="true"][data-active="true"] .tabbar-icon { background: var(--accent-soft); }
/* The count rides on the icon's top-right corner. */
.tabbar-badge {
  /* Anchored by its right edge just past the icon's corner and never closer than 2px to the cell's edge, so a
     three-digit count grows left over the icon instead of into the next tab. */
  position: absolute; top: 4px; right: max(2px, calc(50% - 34px));
  display: inline-flex; align-items: center; justify-content: center; height: 16px; min-width: 18px; padding: 0 3px;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-2); color: var(--text-2);
  font-size: 11px; font-weight: 500; line-height: 1; letter-spacing: 0;
}
.tabbar-link[aria-current="page"] .tabbar-badge { color: var(--accent-hover); }
.tabbar .tabbar-link:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
@media (hover: hover) {
  .tabbar-link:not([aria-current="page"], [data-active="true"]):hover { color: var(--text); }
}

/* ---------- the More sheet ---------- */
/* A bottom sheet like the picker's on phones: the opaque panel, the top corners rounded, a grab bar, a flat scrim
   (no blur: the scan band moves behind the page), and one slide-up on transform and opacity when it opens. The
   dialog's display is never set (the browser hides a closed dialog with display: none, and any display value would
   show it); its layout lives on .more-inner. Rows are 56px: icon, the section's name over its one-line description,
   its badge; then a divider and "Open in Whop". */
.more-sheet {
  inset: auto 0 0 0; margin: 0 auto; padding: 0;
  width: min(100%, 560px); max-width: 100%;
  max-height: 85vh; max-height: 85dvh;
  border: 1px solid var(--line-ui); border-bottom: 0; border-radius: var(--r-xl) var(--r-xl) 0 0;
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-pop);
  overflow: auto; overscroll-behavior: contain;
}
.more-sheet[open] { animation: sheet-up var(--dur-3) var(--ease); }
/* The custom property has a literal fallback: in older browsers ::backdrop does not inherit from the page. */
.more-sheet::backdrop { background: var(--scrim, rgba(3, 5, 4, 0.72)); }
.more-inner { padding: 0 0 calc(var(--s-3) + var(--safe-bottom)); }
.more-head {
  position: relative; display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-5) var(--s-2) var(--s-2) var(--s-5);
}
.sheet-grab { position: absolute; top: var(--s-2); left: calc(50% - 18px); width: 36px; height: 4px; border-radius: var(--r-full); background: var(--border-strong); }
.more-title { color: var(--text); }
.more-list { margin: 0; padding: 0 var(--s-2); list-style: none; }
.more-link {
  position: relative; display: flex; align-items: center; gap: var(--s-3);
  min-height: 56px; padding: var(--s-2) var(--s-3); border-radius: var(--r-md); color: var(--text);
}
.more-icon {
  display: grid; place-items: center; flex: none; width: 36px; height: 36px;
  border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text-2);
}
.more-icon svg { width: 20px; height: 20px; }
.more-text { display: flex; flex: 1 1 auto; flex-direction: column; gap: 2px; min-width: 0; }
.more-label { font-size: 15px; font-weight: 600; line-height: 1.3; }
.more-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 1.4; color: var(--text-2); }
.more-link[aria-current="page"] { background: var(--accent-soft); }
.more-link[aria-current="page"] .more-label { color: var(--accent-hover); }
.more-link[aria-current="page"] .more-icon { border-color: rgba(161, 249, 99, 0.40); color: var(--accent-hover); }
.more-link[aria-current="page"]::before { content: ""; position: absolute; top: var(--s-2); bottom: var(--s-2); left: 0; width: 2px; background: var(--accent); }
.more-ext { margin: var(--s-2) var(--s-2) 0; border-top: 1px solid var(--border); border-radius: 0; }
.more-sheet .more-link:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
@media (hover: hover) {
  .more-link:not([aria-current="page"]):hover { background: var(--surface-hover); }
}
/* Reduced motion: the sheet appears in place, like the picker (the global rule in section 7 already shortens every
   animation; this says it outright, after the rule that starts it). */
@media (prefers-reduced-motion: reduce) {
  .more-sheet[open] { animation: none; }
}

/* ---------- sections on phones ---------- */
@media (max-width: 860px) {
  .view { --view-gap: var(--s-4); margin-top: var(--s-4); }
  .section-grid { gap: var(--s-3); }
  .tabbar {
    position: fixed; right: 0; bottom: 0; left: 0; z-index: 30;
    display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
    height: calc(var(--tabbar-h) + var(--safe-bottom));
    padding: 0 env(safe-area-inset-right, 0px) var(--safe-bottom) env(safe-area-inset-left, 0px);
    border-top: 1px solid var(--border); background: var(--surface-bar);
  }
  /* The toolbars' segments size to their words (ALL is short, WITH CLIENTS is not), so four of them fit a 343px row
     without cutting a label; the select takes the rest of its row. */
  .view-body .seg-btn { flex: 1 1 auto; padding: 0 var(--s-2); }
  .view-toolbar .select { flex: 1 1 auto; }
  /* ALL alone was 39px wide at 320 (its word and 8px each side); the others give up a pixel or two of padding. */
  .view-body .seg-btn { min-width: 44px; }
  .select-input { height: 44px; }
  .btn-more { width: 100%; height: 44px; }
  /* Not the panel link: as a 44px box it pushed its panel's title 15px lower than in panels without one, and opened
     56px between a wrapped title and its note. On touch its ::before (below) is the 44px target, at no layout cost. */
  .back-link { min-height: 44px; }
  .top-figs { grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); }
  .method-bars { --method-k-w: 72px; }
  .panel-body dl:not([class]) { grid-template-columns: minmax(0, 1fr); gap: var(--s-1); }
  .panel-body dl:not([class]) dd + dt { margin-top: var(--s-2); }
  /* A small table that is not inside a scrolling wrapper scrolls on its own rather than widen the page. */
  .mini-table:not(:is(.table-wrap, .panel-body) > *) { display: block; overflow-x: auto; overscroll-behavior-x: contain; }
}
@media (max-width: 359px) {
  .tabbar-label { padding: 0 1px; letter-spacing: 0; }
  .view-body .seg-btn { letter-spacing: 0.02em; }
}
/* Touch: hit areas for the small text links, hanging off the line so the layout does not move. */
@media (pointer: coarse) {
  /* The links inside a note line ("how to raise this") get the same hit area, hung from their own box. A short link
     ("Data →", 43px) is widened to 44px about its centre as well. */
  .panel-note .link, .view-note .link, .panel-empty .link, .source-cell > .link { position: relative; }
  /* Such a link stays on one line: broken over two ("how to raise" / "this" at 320) it is two boxes, the band below
     had no one box to centre on, and the link was an 18px target. Whole, it moves to the next line when it must. */
  .panel-note .link, .view-note .link, .panel-empty .link { white-space: nowrap; }
  .top-name::after, .panel-link::before, .panel-note .link::after, .view-note .link::after, .panel-empty .link::after,
  .source-cell > .link::after {
    content: ""; position: absolute; top: 50%; right: min(0px, calc(50% - 22px)); left: min(0px, calc(50% - 22px));
    height: max(100%, 44px); transform: translateY(-50%);
  }
  /* In a panel head the band leans up, into the card's top padding, and ends 10px under the link: centred, its lower
     3px lay under the band of the first name below the head (Top campaigns at 375px), a 42px target. */
  .panel-head > .panel-link::before { top: auto; bottom: -10px; transform: none; }
  /* Any other text link in a table cell (the Clients "Came from" path: campaign / ad set / ad, a name in a small
     table) is an 18px line in a 48px or 52px row. The same band, 44px tall and centred on the link's own box, makes
     it a thumb-sized target without touching the row: it is absolutely positioned, so nothing is measured
     differently, and desktop (a fine pointer) never gets it. The links inside .client-path are flex items, so each
     is its own box, and the band centres on that box rather than on a first line fragment. A short link ("Ad 4",
     33px) is widened to 44px about its centre; the path's links are 24px apart (gap, "/", gap), so bands never meet. */
  :is(.table, .mini-table) :is(td, tbody th, tfoot th) .link:not(.src-text > .link) { position: relative; }
  :is(.table, .mini-table) :is(td, tbody th, tfoot th) .link:not(.src-text > .link)::after {
    content: ""; position: absolute; top: 50%; right: min(0px, calc(50% - 22px)); left: min(0px, calc(50% - 22px));
    height: max(100%, 44px); transform: translateY(-50%);
  }
  /* In the board a name whose LED wraps under it (a phone's 160px column) sits at the top of its cell, so there the
     area starts at the cell's 8px top padding rather than centring on the word: centred, it reached 5px past the
     first row's top edge, under the sticky header row, and that name was a 39px target. A name centred in its row is
     17px down, so the area still ends inside the row. */
  .grid-wrap .table .name-cell .link.name::after {
    top: calc(var(--s-2) * -1); height: max(calc(100% + 2 * var(--s-2)), 44px); transform: none;
  }
  /* The source cell's ad name clips for its ellipsis, and a clipping box would cut an ::after band to its own 18px.
     Padding is inside the clip and is hit-tested, so padding taken back by the same negative margin makes the name a
     44px target with the text, the tag under it and the row all where they were. The name sits 6px under its row's
     top (the name over its tag, centred in a 52px row), so the padding is 6px above and 20px below, over the tag:
     13px each way reached 7px into the row above, or the header's sort button over the first row. The tag comes later
     in the cell and would paint over that padding, so the name is lifted above it; the tag is a word, not a control,
     and loses nothing. A short name ("Ad 4", 34px) is held to 44px wide. */
  .src-text > .link {
    position: relative; z-index: 1; min-width: 44px;
    padding-top: 6px; padding-bottom: 20px; margin-top: -6px; margin-bottom: -20px;
  }
  /* A name with no tag under it is centred in its row, so the padding is even: 20px below reached 3px into the next
     row, under that row's name (Leads "Ad 10", a 41px target). */
  .src-text > .link:only-child { padding-top: 13px; padding-bottom: 13px; margin-top: -13px; margin-bottom: -13px; }
  .select-input { height: 44px; }
}

/* ---------- phones ---------- */
@media (max-width: 860px) {
  /* The actions drop under the title: the chip takes the row beside a 44×44 Refresh, the toggle fills the next. */
  .page-actions { flex: 1 1 100%; justify-content: stretch; }
  .range { flex: 1 1 100%; }
  .range-trigger { flex: 1 1 auto; height: 44px; }
  #refresh { flex: none; width: 44px; height: 44px; padding: 0; }
  /* The word goes visually; it stays in the DOM, so the button is still named "Refresh". */
  #refresh .btn-label {
    position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .seg { --seg-h: 44px; display: flex; flex: 1 1 100%; }
  .seg-btn { flex: 1 1 0; min-width: 0; }
  .ov-hero-row { --kpi-pad: var(--s-4); --kpi-gap: var(--s-3); }
  .kpi-value.is-na, .kpi-hero .kpi-value.is-na { font-size: 13px; }
  .board .tabs { display: flex; }
  .board .tab { flex: 1 1 0; padding: 0 var(--s-2); }
  .btn-icon { width: 44px; height: 44px; }

  /* The picker becomes a bottom sheet: full width, up to 90% of the screen, rounded only at the top, the presets a
     swiping row of chips, one month, the footer pinned to the sheet's bottom. picker.js anchors the desktop popover
     with inline top and left; on a phone the sheet ignores them, which only !important can say to an inline style. */
  .range-pop {
    top: auto !important; right: 0; bottom: 0; left: 0 !important;
    width: 100%; max-width: 100%;
    max-height: 90vh; max-height: 90dvh;
    border-bottom: 0; border-radius: var(--r-xl) var(--r-xl) 0 0;
  }
  .range-pop[open] { animation: sheet-up var(--dur-3) var(--ease); }
  /* The custom property has a literal fallback: in older browsers ::backdrop does not inherit from the page. */
  .range-pop::backdrop { background: var(--scrim, rgba(3, 5, 4, 0.72)); }
  .range-pop-inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "presets" "cal" "foot";
    padding-top: var(--s-4);
  }
  /* The grab bar: decorative (no drag-to-dismiss in this version). */
  .range-pop-inner::before {
    content: ""; position: absolute; top: var(--s-2); left: calc(50% - 18px); width: 36px; height: 4px;
    border-radius: var(--r-full); background: var(--border-strong);
  }
  .range-pop-head { padding: var(--s-1) var(--s-2) var(--s-2) var(--s-4); }
  .range-presets {
    flex-direction: row; gap: var(--s-2); overflow-x: auto; padding: var(--s-3) var(--s-4);
    border-right: 0; border-bottom: 1px solid var(--border);
    scroll-snap-type: x proximity; scroll-padding-inline: var(--s-4); overscroll-behavior-x: contain; scrollbar-width: none;
  }
  .range-presets::-webkit-scrollbar { display: none; }
  .range-presets .preset {
    flex: none; width: auto; height: 36px; padding: 0 var(--s-3);
    border: 1px solid var(--line-ui); border-radius: var(--r-md); scroll-snap-align: start;
  }
  /* The chip's label already says the range ("Last 7 days"); the hint would only repeat it in a row that must swipe. */
  .range-presets .preset-hint { display: none; }
  .range-presets .preset[aria-pressed="true"] { border-color: var(--accent); box-shadow: none; }
  /* 44px hit areas on 36px chips, hanging into the row's own padding. */
  .range-presets .preset::after { content: ""; position: absolute; inset: -5px 0; }
  .cal { justify-self: center; width: 100%; max-width: calc(7 * 44px + 2 * var(--s-4)); padding: var(--s-3) var(--s-4); }
  .cal-labels, .cal-months { column-gap: 0; }
  .cal-labels { min-height: 44px; }
  .cal-month { width: 100%; }
  .cal-month th { width: auto; }
  .cal-month td { width: auto; height: 44px; }
  .range-pop-foot {
    position: sticky; bottom: 0; z-index: 1;
    padding: var(--s-3) var(--s-4) calc(var(--s-3) + var(--safe-bottom));
  }
  .range-pop-status { flex-basis: 100%; }
  .range-pop-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); flex: 1 1 100%; margin-left: 0; }
  .range-pop-actions .btn { height: 44px; }

  /* On a phone the grid's box is exactly the screen between the two bars, less a 16px margin at each end, so that,
     scrolled into place, the header and the totals row are both in view and pinned inside the box (DESIGN.md, F8). */
  .grid-wrap {
    max-height: calc(100vh - var(--topbar-h) - var(--safe-top) - var(--tabbar-h) - var(--safe-bottom) - 2 * var(--s-4));
    max-height: calc(100dvh - var(--topbar-h) - var(--safe-top) - var(--tabbar-h) - var(--safe-bottom) - 2 * var(--s-4));
  }
}
/* The smallest phones: a 320px sheet less its padding is narrower than seven 44px days, so the month takes the width
   it has (each day about 42px, the row still one continuous tap target). */
@media (max-width: 359px) {
  .cal { padding-inline: var(--s-2); }
  .range-trigger { padding: 0 var(--s-2); }
}

/* ==========================================================================
   The Cortana build (CORTANA-PLAN.md, DESIGN-TECH section 13): the executive Overview (funnel, sources, latest
   clients, ranked campaigns and creatives, data health), the People links on every count of people, the source pills,
   the People modal and the customer journey dialog, and the custom dropdown that replaced the one native select.
   After the sections and the phone rules, so these win ties; before forced colours, which must win over everything.
   ========================================================================== */

/* ---------- Overview: the Whop ads funnel ---------- */
/* Impressions → Link clicks → Leads → Qualified → Booked calls → New clients. Six steps in a row split by hairlines
   from a 1012px main column, three by two from 640px, one column of rows below that. Each step: the Label, the count
   (a People link where the step is people), a 6px bar whose fill is scaled with transform from the left (app.js sets
   --fill on the step, a log scale so New clients is still a visible sliver beside 120,000 impressions), the
   conversion from the step before as a small chip, and what each one cost: the cost's key ("PER NEW CLIENT") stacked
   over its amount, because the two side by side ("PER NEW CLIENT A$5,936.91", 185px of mono) are wider than a
   six-across step at 1366 or 1440 and ran over the hairline into the next step. Where steps stand side by side they
   share their rows through subgrid, so a chip that wraps to two lines on one step lowers the cost line of all six
   together and the row still reads straight across. */
.funnel {
  /* One step as a column (640px and up): Label, 4px, count, 8px, bar, 8px, chip, 4px, then the cost's key, 2px and
     its amount. --funnel-row-h is one phone row: chip, 4px, Label, 4px, count, 4px, bar (the cost sits beside the
     Label and count, and its two lines are shorter than they are). */
  --funnel-step-h: calc(11px * 1.3 + var(--s-1) + 22px * 1.15 + var(--s-2) + 6px + var(--s-2) + 20px + var(--s-1)
    + 11px * 1.3 + 2px + 12px * 1.5);
  --funnel-row-h: calc(20px + var(--s-1) + 11px * 1.3 + var(--s-1) + 22px * 1.15 + var(--s-1) + 6px);
  display: grid; grid-template-columns: minmax(0, 1fr); margin: 0; padding: 0; list-style: none;
}
/* Held at the steps' height while it ships empty: six phone rows, each with 12px above and below except the first's
   top and the last's bottom, and five 1px hairlines (the wider layouts below reset it). */
.funnel:empty { min-height: calc(6 * var(--funnel-row-h) + 10 * var(--s-3) + 5px); }
.funnel-step {
  --step-c: var(--text-3);
  position: relative; display: grid; min-width: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "conv conv" "k cost" "v cost" "bar bar";
  align-items: center; gap: var(--s-1) var(--s-3);
  padding: var(--s-3) 0;
}
.funnel-step + .funnel-step { border-top: 1px solid var(--border); }
.funnel-step:first-child { padding-top: 0; }
.funnel-step:last-child { padding-bottom: 0; }
.funnel-step[data-step="leads"], .funnel-step[data-step="qualified"] { --step-c: var(--series-leads); }
.funnel-step[data-step="calls"] { --step-c: var(--series-calls); }
.funnel-step[data-step="clients"] { --step-c: var(--accent); }
/* break-word, not anywhere: a Label breaks between its words ("Booked / calls") and only inside a word that could not
   fit at all, so "NEW CLIENTS" is never cut to "NEW CLIEN / TS" on a 320px phone. */
.funnel-k { grid-area: k; min-width: 0; overflow-wrap: break-word; }
/* Not clipped: the count can be a People link, and its focus ring and underline must show. */
.funnel-v {
  grid-area: v; min-width: 0; white-space: nowrap;
  font-size: 22px; font-weight: 500; line-height: 1.15; letter-spacing: -0.02em; color: var(--text);
}
.funnel-v .skeleton { display: inline-block; vertical-align: middle; width: 5ch; height: 1em; }
.funnel-step.is-na .funnel-v { font-family: var(--sans); font-size: 14px; font-weight: 500; letter-spacing: normal; color: var(--text-2); }
.funnel-bar {
  grid-area: bar; position: relative; display: block; height: 6px; overflow: hidden;
  border: 1px solid var(--border); background: var(--surface-2);
}
.funnel-fill {
  position: absolute; inset: 0; display: block; background: var(--step-c);
  transform: scaleX(var(--fill, 0)); transform-origin: left center;
}
/* "4.2% of clicks": the step's conversion from the one before. Words carry it; the chip only frames it. The figure
   and its words are a mono space apart (1ch), not 4px, which read tighter than the spaces inside "of clicks". */
.funnel-conv {
  grid-area: conv; justify-self: start; display: inline-flex; align-items: center; gap: 1ch;
  max-width: 100%; min-height: 20px; padding: 0 6px;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-2);
  font-size: 11px; font-weight: 500; line-height: 1.3; color: var(--text);
}
.funnel-conv-k { color: var(--text-3); }
/* The first step has nothing before it: on a phone its chip row goes, elsewhere it keeps its room so the rows align. */
.funnel-conv:empty { display: none; }
/* The key on its own line above the amount, at every width: on a phone row the pair stays in a narrow right-hand
   column ("PER NEW CLIENT" is about 102px) and leaves the Label its room; in a column it fits the narrowest step. The
   amount never breaks; the key may, between its words, should a step ever be narrower than it. */
.funnel-cost { grid-area: cost; justify-self: end; text-align: right; font-size: 12px; line-height: 1.5; white-space: nowrap; color: var(--text-2); }
.funnel-cost-k {
  display: block; margin: 0 0 2px; font-size: 11px; line-height: 1.3; letter-spacing: 0.06em; text-transform: uppercase;
  white-space: normal; color: var(--text-3);
}
.funnel-cost .skeleton { display: inline-block; vertical-align: middle; width: 6ch; height: 12px; }
/* A step still waiting for its first answer has the amount's bar but no key ("PER NEW CLIENT") over it: the key's line
   is held here, so the steps are as tall loading as filled and the note under the funnel stays put (verify: the list
   was 16px short at 1440 and the note moved down when the figures landed). */
.funnel-step.is-skeleton .funnel-cost:not(:has(> .funnel-cost-k))::before {
  content: ""; display: block; height: calc(11px * 1.3); margin-bottom: 2px;
}
#funnel[aria-busy="true"] .funnel-step:not(:has(.skeleton)) { opacity: 0.55; }
/* The funnel could not be loaded: one empty state with Retry across the whole list. */
.funnel-error { grid-column: 1 / -1; list-style: none; }
/* The note under the funnel ("All sources: 56 qualified · 8 new clients (A$43,281) — see where they came from
   below."): it follows the list, so it takes the space above itself rather than the title row's pull-up. */
#ov-funnel-note { margin: var(--s-4) 0 0; }
@container main (min-width: 640px) {
  .funnel { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .funnel:empty { min-height: calc(2 * var(--funnel-step-h) + 2 * var(--s-4) + 1px); }
  .funnel-step {
    grid-template-columns: minmax(0, 1fr); grid-template-areas: "k" "v" "bar" "conv" "cost";
    align-items: start; gap: 0; padding: var(--s-4);
  }
  .funnel-v { margin-top: var(--s-1); }
  .funnel-bar { margin-top: var(--s-2); }
  .funnel-conv { margin-top: var(--s-2); }
  .funnel-conv:empty { display: inline-flex; visibility: hidden; }
  .funnel-cost { justify-self: start; margin-top: var(--s-1); text-align: left; }
  .funnel-step + .funnel-step { border-top: 0; border-left: 1px solid var(--border); }
  .funnel-step:nth-child(3n + 1) { padding-left: 0; border-left: 0; }
  .funnel-step:nth-child(3n) { padding-right: 0; }
  .funnel-step:nth-child(-n + 3) { padding-top: 0; }
  .funnel-step:nth-child(n + 4) { padding-bottom: 0; border-top: 1px solid var(--border); }
}
@container main (min-width: 1012px) {
  .funnel { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .funnel:empty { min-height: var(--funnel-step-h); }
  .funnel-step, .funnel-step:nth-child(n) { padding: 0 var(--s-4); border-top: 0; border-left: 1px solid var(--border); }
  .funnel-step:first-child { padding-left: 0; border-left: 0; }
  .funnel-step:last-child { padding-right: 0; }
}
/* Steps side by side share their five rows (Label, count, bar, chip, cost) with the list, so each row is as tall as
   the tallest step's and the six costs start at one y even when one chip wraps ("1.9% of impressions" is 143px, more
   than a step has at 1366). Each step spans five of the list's rows; the second row of three (640–1011px) is placed
   on the next five by auto-placement. Browsers without subgrid keep the stacked layout above, aligned in all but that
   case. */
@supports (grid-template-rows: subgrid) {
  @container main (min-width: 640px) {
    .funnel-step { grid-row: span 5; grid-template-rows: subgrid; row-gap: 0; }
  }
}

/* ---------- source pills, dots and fills ---------- */
/* One colour family per source (DESIGN-TECH 13.4): the text colour, a 12% tint and a border, set as three custom
   properties wherever a data-source attribute names one, and read by the pill, the dot and the share bar inside. The
   pill's word is the source's own label, so identity never rests on colour; Untracked and Not checked are outlines
   only (dashed, dotted), the "we cannot say" look the method tags already use. */
.src-pill, .src-dot, .src-row { --src-c: var(--text-2); --src-tint: var(--surface-2); --src-line: var(--border-strong); }
[data-source="whop_ad"] { --src-c: var(--src-whop); --src-tint: var(--accent-soft); --src-line: var(--line-ui); }
[data-source="meta_ad"] { --src-c: var(--src-meta); --src-tint: rgba(127, 178, 255, 0.12); --src-line: rgba(127, 178, 255, 0.35); }
[data-source="instagram"] { --src-c: var(--src-ig); --src-tint: rgba(245, 139, 192, 0.12); --src-line: rgba(245, 139, 192, 0.35); }
[data-source="facebook"] { --src-c: var(--src-fb); --src-tint: rgba(157, 190, 255, 0.08); --src-line: rgba(157, 190, 255, 0.30); }
[data-source="google"] { --src-c: var(--src-google); --src-tint: rgba(127, 217, 196, 0.12); --src-line: rgba(127, 217, 196, 0.35); }
[data-source="email"] { --src-c: var(--warning-text); --src-tint: var(--warning-soft); --src-line: rgba(255, 194, 61, 0.35); }
[data-source="referral"] { --src-c: var(--src-ref); --src-tint: rgba(194, 178, 255, 0.12); --src-line: rgba(194, 178, 255, 0.35); }
[data-source="direct"] { --src-c: var(--text-2); --src-tint: var(--surface-hover); --src-line: var(--line-neutral); }
[data-source="direct_link"] { --src-c: var(--text-2); --src-tint: var(--surface-2); --src-line: var(--border-strong); }
[data-source="untracked"] { --src-c: var(--text-3); --src-tint: transparent; --src-line: var(--border-strong); --src-style: dashed; }
[data-source="unknown"] { --src-c: var(--text-3); --src-tint: transparent; --src-line: var(--border-strong); --src-style: dotted; }
.src-pill {
  display: inline-flex; align-items: center; flex: none; max-width: 100%; height: 20px; padding: 0 var(--s-2);
  border: 1px var(--src-style, solid) var(--src-line); border-radius: var(--r-sm);
  background: var(--src-tint); color: var(--src-c);
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle;
}
.src-dot { display: inline-block; flex: none; width: 8px; height: 8px; border-radius: var(--r-full); background: var(--src-c); vertical-align: middle; }
.src-dot[data-source="untracked"], .src-dot[data-source="unknown"] { background: transparent; border: 1px var(--src-style) var(--src-c); }

/* ---------- Overview: where new clients came from ---------- */
/* "8 new clients · A$43,281", then one row per source that brought any: the pill (and the one client's detail, such
   as the Meta ad set, when it is a single client), the count (a People link), the A$, then a 4px share bar with its
   percentage. The figure columns are fixed widths in ch, so counts and amounts line up from row to row. */
.src-total { margin-bottom: var(--s-3); font-size: 13px; line-height: 1.5; color: var(--text); }
.src-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.src-row {
  display: grid; align-items: center; min-width: 0;
  grid-template-columns: auto minmax(0, 1fr) 6ch 11ch;
  grid-template-areas: "pill detail n amt" "bar bar bar pct";
  gap: var(--s-1) var(--s-3); padding: var(--s-2) 0;
  border-top: 1px solid var(--line-neutral);
  font-family: var(--mono); font-size: 13px; line-height: 1.4;
}
.src-row:first-child { border-top: 0; padding-top: 0; }
.src-row:last-child { padding-bottom: 0; }
.src-row > .src-pill { grid-area: pill; justify-self: start; }
.src-detail {
  grid-area: detail; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--sans); font-size: 12px; color: var(--text-3);
}
.src-n { grid-area: n; justify-self: end; color: var(--text); }
.src-amt { grid-area: amt; justify-self: end; white-space: nowrap; color: var(--text); }
.src-share { grid-area: bar; position: relative; display: block; height: 4px; overflow: hidden; background: var(--surface-2); }
.src-fill { position: absolute; inset: 0; display: block; background: var(--src-c); transform-origin: left center; }
.src-pct { grid-area: pct; justify-self: end; font-size: 11px; color: var(--text-2); }
.src-row > .skeleton { grid-column: 1 / -1; height: 12px; }
/* A 320px phone's panel is about 248px inside: tighter gutters, and the pill (which clips with an ellipsis, so its
   column may shrink) gives way before the figures do. */
@container main (max-width: 639.98px) {
  .src-row { grid-template-columns: auto minmax(0, 1fr) 5ch 10ch; column-gap: var(--s-2); }
}
/* Phones up to a 360px column: beside the pill and the figures the one client's detail had 31px at 320 ("Bro…"),
   71px at 360 and 86px at 375, which still cut "Broad AU 25-55", so it takes a line of its own under the pill; a row
   without one keeps its two lines. */
@container main (max-width: 359.98px) {
  .src-row:has(> .src-detail:not(:empty)) { grid-template-areas: "pill pill n amt" "detail detail detail detail" "bar bar bar pct"; }
  .src-row > .src-detail:empty { display: none; }
  /* The pill had 103px at 320 for "INSTAGRAM ORGANIC" (136px) and "DIRECT PAYMENT LINK" (150px) and cut them with no
     ellipsis in sight; it takes a second line instead, so the words read in full. */
  .src-row > .src-pill { height: auto; min-height: 20px; padding-block: 2px; line-height: 1.2; white-space: normal; }
}

/* ---------- Overview: latest new clients ---------- */
/* The last five, newest first, each one whole-row button that opens the journey: the matched creative's thumbnail
   (or, unmatched, a square holding the source's dot, so the names line up), the name over "3 Oct · SOURCE", and the
   first payment in A$ on the right. */
.latest-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.latest-item + .latest-item { border-top: 1px solid var(--line-neutral); }
.latest-open {
  position: relative; display: grid; grid-template-columns: var(--thumb) minmax(0, 1fr) auto; align-items: center;
  column-gap: var(--s-3); width: 100%; min-height: 56px; padding: var(--s-2) var(--s-2);
  border: 0; border-radius: var(--r-md); background: none; color: inherit; text-align: left; cursor: pointer;
}
.latest-open > .thumb, .latest-open > .src-dot { grid-column: 1; grid-row: 1 / 3; }
.latest-open > .src-dot {
  position: relative; width: var(--thumb); height: var(--thumb); border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-2);
}
.latest-open > .src-dot::after {
  content: ""; position: absolute; top: calc(50% - 4px); left: calc(50% - 4px); width: 8px; height: 8px;
  border-radius: var(--r-full); background: var(--src-c);
}
.latest-open > .src-dot:is([data-source="untracked"], [data-source="unknown"]) { border: 1px var(--src-style) var(--border-strong); }
.latest-open > .src-dot:is([data-source="untracked"], [data-source="unknown"])::after { background: transparent; border: 1px var(--src-style) var(--src-c); }
.latest-name {
  grid-column: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--sans); font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--text);
}
/* The date ("3 Oct · ", at most nine characters with its dot) holds nine characters, so the source pills start at one
   x down the list instead of following each date's width ("29 Sep" set its pill 7px right of "3 Oct"'s). */
.latest-meta {
  grid-column: 2; display: grid; grid-template-columns: 9ch minmax(0, max-content); align-items: center; gap: var(--s-2);
  min-width: 0; margin-top: 2px;
  font-size: 12px; line-height: 1.4; color: var(--text-2); white-space: nowrap;
}
.latest-meta .src-pill { min-width: 0; }
.latest-amt { grid-column: 3; grid-row: 1 / 3; justify-self: end; font-size: 14px; font-weight: 500; white-space: nowrap; color: var(--text); }
.latest-list .skeleton { height: 12px; }
.latest-list + .panel-link, #ov-latest-body > .panel-link { margin-top: var(--s-3); }
.latest-open:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
/* The smallest phones: beside the amount the date and pill had 117px, and the pill was cut to "WHOP AD"; the amount
   rides on the name's line instead, and the date and pill take the row's width under both. */
@container main (max-width: 327.98px) {
  .latest-amt { grid-row: 1; }
  .latest-meta { grid-column: 2 / -1; }
}
@media (hover: hover) {
  .latest-open:hover { background: var(--surface-hover); }
  .latest-open:hover .latest-name { color: var(--accent-hover); }
}

/* ---------- Overview: Top campaigns, a ranked table ---------- */
/* The five campaigns with the most spend: rank, name with its share of account spend as a 4px bar, then Spend, Leads,
   Qualified, Calls, CPL, Clients, Revenue and ROAS (mini table type: mono 13, right-aligned). Under a 640px main
   column the rows become cards: rank and name across the top, the figures in a three-column grid of label over value,
   the labels drawn from each cell's data-label. */
.ov-rank td.rank, .ov-rank th.rank { width: 3ch; font-family: var(--mono); font-size: 11px; color: var(--text-3); }
/* The rank stands on the name's first line, not in the middle of the name and its share bar (13.8px lower). */
.ov-rank tbody :is(td, th):is(.rank, .ov-name) { vertical-align: baseline; }
.ov-rank :is(td, th).ov-name { min-width: 200px; }
.ov-rank .top-name { display: block; }
.share {
  position: relative; display: inline-block; width: min(160px, 60%); height: 4px; margin-top: 6px; overflow: hidden;
  vertical-align: middle; background: var(--surface-2);
}
.share-fill { position: absolute; inset: 0; display: block; background: var(--series-spend); transform-origin: left center; }
/* The bars are pictures, not controls: letting taps fall through them keeps the 44px hit area of the count link
   above each bar working on a phone (elementFromPoint found the bar there, not the link). */
.funnel-bar, .funnel-fill, .src-share, .src-fill, .share, .share-fill { pointer-events: none; }
.share-pct { display: inline-block; margin: 6px 0 0 var(--s-2); vertical-align: middle; font-size: 11px; line-height: 1; color: var(--text-3); }
.ov-rank td.num { font-family: var(--mono); font-size: 13px; color: var(--text); }
@container main (max-width: 639.98px) {
  .ov-rank, .ov-rank tbody, .ov-rank tr, .ov-rank td, .ov-rank tbody th { display: block; }
  .ov-rank thead {
    position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .ov-rank tr {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2) var(--s-3);
    padding: var(--s-3) 0; border-top: 1px solid var(--line-neutral);
  }
  .ov-rank tbody tr:first-child { padding-top: 0; border-top: 0; }
  .ov-rank tr > :is(td, th) { height: auto; padding: 0; border: 0; text-align: left; }
  .ov-rank tr > .rank { grid-column: 1; grid-row: 1; width: auto; padding-top: 2px; }
  .ov-rank tr > .ov-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; padding-left: 4ch; }
  .ov-rank td.num { white-space: nowrap; }
  .ov-rank td[data-label]::before {
    content: attr(data-label); display: block; margin-bottom: 2px;
    font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1.3; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-3);
  }
  .ov-rank :is(.rank, .ov-name)[data-label]::before { content: none; }
  .ov-rank .share { width: min(160px, 70%); }
}
/* The smallest phones: three figures to a row left "A$4,541.05" 74px, and it ran 4px into ROAS beside it; two to a
   row give each figure about 115px. */
@container main (max-width: 327.98px) {
  .ov-rank tr { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Touch, cards: a figure is 33px (label over value), so at an 8px row gap two People links in one column were 41px
   apart and their 44px areas overlapped; a 12px gap keeps them clear. The cards never outgrow the panel, so its body
   does not scroll (or clip) here: its scroll box cut the area of a "1" in the first column, 4px from its edge, to 31px. */
@container main (max-width: 639.98px) {
  @media (pointer: coarse) {
    .ov-rank tr { row-gap: var(--s-3); }
    .panel-body:has(> .ov-rank) { overflow-x: visible; }
  }
}

/* ---------- Overview: Top creatives, a row of cards ---------- */
/* Up to five creatives ranked by matched revenue, then clients, then leads: the square picture with its rank in the
   corner and the name under it, all one link to the creative's detail, then the figures as a two-column list outside
   the link (People links may not sit inside an anchor). There are never more than five, and they are always one
   row: an auto-fill grid wrapped them 4 + 1 at a 960px main column (a 13" laptop, or 1920 wide at 150%) and 3 + 2 at
   1024 and 768, one card alone beside 700px of empty panel. So from a 900px main column it is exactly five equal
   columns (each at least 160px with the 12px gap, enough for "A$12,345" in a two-column figure list); between 640
   and 900 the five are one sideways rail that snaps card by card, the next card peeking at the edge to say there is
   more; under 640 they are one column of row cards, the picture 56px beside the name. */
.ov-creative-row {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--s-4);
  margin: 0; padding: 0; list-style: none;
}
@container main (max-width: 1099.98px) {
  .ov-creative-row { gap: var(--s-3); }
  .ov-creative-figs { column-gap: var(--s-2); }
}
/* The rail. Its 4px padding (taken back by the margin) is the room the focused card's 2px outline at a 2px offset
   needs, which the scroller would otherwise clip; scroll-padding keeps a snapped card's left edge on that line. */
@container main (min-width: 640px) and (max-width: 899.98px) {
  .ov-creative-row {
    grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(176px, 1fr);
    margin: -4px; padding: 4px 4px var(--s-2);
    overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scroll-padding-inline: 4px;
  }
  .ov-creative { scroll-snap-align: start; }
}
.ov-creative {
  position: relative; display: flex; flex-direction: column; min-width: 0; overflow: clip; overflow-clip-margin: var(--s-2);
  border: 1px solid var(--border); border-radius: var(--r-xl); background: var(--surface-2);
}
.ov-creative-link { position: relative; display: flex; flex-direction: column; min-width: 0; color: inherit; }
.ov-creative-link .thumb-lg { border-bottom-color: var(--border); }
.ov-creative-rank {
  position: absolute; top: var(--s-2); left: var(--s-2); z-index: 2;
  display: inline-flex; align-items: center; justify-content: center; height: 20px; min-width: 28px; padding: 0 6px;
  border: 1px solid var(--line-ui); border-radius: var(--r-sm); background: rgba(7, 10, 8, 0.85);
  font-size: 11px; font-weight: 600; line-height: 1; color: var(--accent-hover);
}
.ov-creative-name {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
  min-height: calc(2 * 14px * 1.35); margin: var(--s-3) var(--s-3) 0;
  font-family: var(--sans); font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--text); overflow-wrap: anywhere;
}
.ov-creative-figs {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-2) var(--s-3);
  margin: 0; padding: var(--s-3);
}
.ov-creative-figs > div { min-width: 0; }
/* No creative answer yet in a range that was not loaded: one quiet line with the link to Creatives. */
.panel-empty { margin: 0; padding: var(--s-3) 0; font-size: 13px; line-height: 1.5; color: var(--text-2); }
.ov-creative-figs dt { color: var(--text-3); letter-spacing: 0.06em; }
.ov-creative-figs dd { margin: 2px 0 0; font-size: 13px; line-height: 1.3; color: var(--text); white-space: nowrap; }
/* A figure wider than its column (a long amount in a 160px card) ends in an ellipsis inside it rather than running
   under the figure beside it; a cell holding a People link does not clip, so its underline, focus ring and touch area
   show whole (a count is a few digits and always fits). */
.ov-creative-figs dd:not(:has(.people-link)) { overflow: hidden; text-overflow: ellipsis; }
.ov-creative-figs .share { width: 100%; margin-top: var(--s-1); display: block; }
/* Under a full-width bar the "6% of spend" starts on its own line, so it starts flush with the figure, not 8px in. */
.ov-creative-figs .share-pct { margin-left: 0; }
.ov-creative-figs .skeleton { height: 12px; }
.ov-creative:has(> .ov-creative-link:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.ov-creative-link:focus-visible { outline: none; box-shadow: none; }
@media (hover: hover) {
  .ov-creative:hover { border-color: var(--line-ui); }
  .ov-creative-link:hover .ov-creative-name { color: var(--accent-hover); }
}
@container main (max-width: 639.98px) {
  .ov-creative-row { grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
  .ov-creative-link {
    display: grid; grid-template-columns: 56px minmax(0, 1fr); grid-template-rows: auto auto; align-items: center;
    column-gap: var(--s-3); row-gap: 2px; padding: var(--s-3) var(--s-3) 0;
  }
  .ov-creative-link > .thumb { grid-column: 1; grid-row: 1 / 3; width: 56px; height: 56px; border: 1px solid var(--border); border-radius: var(--r-sm); }
  .ov-creative-rank { position: static; grid-column: 2; grid-row: 1; justify-self: start; }
  .ov-creative-name { grid-column: 2; grid-row: 2; min-height: 0; margin: 0; }
  .ov-creative-figs { grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
}
/* The smallest phones: three to a row cut Spend's "6% of spend" to "6% of spen"; two to a row hold it. */
@container main (max-width: 327.98px) {
  .ov-creative-figs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Two to a row stacks the figures 41px apart, so a People link's 44px area reached 3px under the next one's and lost
     it (the calls figure was a 41px target). 12px between the rows clears them. */
  @media (pointer: coarse) {
    .ov-creative-figs { row-gap: var(--s-3); }
  }
}

/* ---------- Overview: the data-health strip ---------- */
/* One slim inset line, last on the page: Match rate · Leads linked · FX · Updated, each with an LED whose colour
   follows data-tone and the word beside it, then the door to Data. Four in a row where they fit, two by two on a
   phone. */
.ov-health {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-5);
  padding: var(--s-3) var(--s-4); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.ov-health-list {
  flex: 1 1 520px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--s-2) var(--s-5);
  min-width: 0; margin: 0;
}
.ov-health-item {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s-2); min-width: 0;
}
.ov-health-item::before {
  content: ""; flex: none; align-self: center; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--led-off);
}
.ov-health-item[data-tone="ok"]::before { background: var(--led-on); }
.ov-health-item[data-tone="info"]::before { background: var(--text-2); }
.ov-health-item[data-tone="warn"]::before { background: var(--led-warn); }
.ov-health-item:is([data-tone="bad"], [data-tone="error"])::before { background: var(--led-bad); }
.ov-health-item dt { flex: none; color: var(--text-3); }
.ov-health-item dd { min-width: 0; margin: 0; font-size: 12px; line-height: 1.5; color: var(--text); overflow-wrap: anywhere; }
.ov-health-item[data-tone="warn"] dd { color: var(--warning-text); }
.ov-health-item .skeleton { display: inline-block; vertical-align: middle; width: 8ch; height: 12px; }
.ov-health > .panel-link { margin-left: auto; }
/* Side by side, every value sits under its label (not under the LED), as it does on a phone: left beside its label
   where it fitted, one value that did not ("23 of 29 qualified" at 1440) dropped 18px below the three beside their
   labels, and the strip read as two baselines. Every first line is the label's 18px line, so the labels stay level
   along the strip and the LEDs sit on line one. */
@container main (min-width: 640px) {
  .ov-health-item { position: relative; padding-left: calc(6px + var(--s-2)); }
  .ov-health-item::before { position: absolute; top: 6px; left: 0; }
  .ov-health-item dt { line-height: 18px; }
  .ov-health-item dd { flex-basis: 100%; }
}
@container main (max-width: 639.98px) {
  .ov-health-list { flex-basis: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
  /* align-content: start, so an item stretched to a two-line neighbour's height (FX beside Updated at 320) keeps its
     value right under its label instead of sharing the spare height out between the two rows (9px lower). */
  .ov-health-item { display: grid; grid-template-columns: 6px minmax(0, 1fr); align-content: start; column-gap: var(--s-2); }
  .ov-health-item::before { grid-row: 1; margin-top: 4px; align-self: start; }
  .ov-health-item dd { grid-column: 2; }
}

/* ---------- People links: every count of people opens the list of who they are ---------- */
/* Cortana's underlined numbers. A reset button that keeps the text it sits in (font, size, colour, tabular figures),
   with a dotted underline in the control line colour; hover turns it accent and solid; focus is the page's ring.
   On a touch screen an invisible hit area hangs 12px above and below and 8px to the sides, so the target clears 44px
   without moving anything. Zero, "—" and pending figures are plain text (.people-zero), because there is no one to
   list. */
.people-link {
  position: relative; display: inline; margin: 0; padding: 0;
  border: 0; border-radius: var(--r-sm); background: none; color: inherit;
  font: inherit; letter-spacing: inherit; text-align: inherit; text-transform: inherit; font-variant-numeric: tabular-nums;
  text-decoration: underline dotted 1px; text-decoration-color: var(--line-ui); text-underline-offset: 3px;
  cursor: pointer;
}
.people-link:focus-visible { box-shadow: var(--focus); border-radius: var(--r-sm); }
.people-link-figure { line-height: inherit; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.people-zero { font-variant-numeric: tabular-nums; }
@media (hover: hover) {
  .people-link:hover { color: var(--accent-hover); text-decoration-style: solid; text-decoration-color: currentColor; }
}
/* The hit area is centred on the button and at least 44px each way: 12px above and below and 8px to the sides, or
   more when the button is small. The percentages resolve against the button itself, so a lone "1" (an 8px-wide
   button on an 18px line) gets 18px of room to each side and 13px above and below, and a wide figure keeps the fixed
   margin. */
@media (pointer: coarse) {
  .people-link::after {
    content: ""; position: absolute;
    top: min(-12px, calc(50% - 22px)); bottom: min(-12px, calc(50% - 22px));
    left: min(-8px, calc(50% - 22px)); right: min(-8px, calc(50% - 22px));
  }
  /* The totals row's cells are sticky, each its own layer, so the next cell painted over the right of a centred area
     (a "1" was a 38px target). The figures sit at their cells' right edge: the area ends 10px past the figure, inside
     the cell's 12px padding, and reaches left for the rest of its 44px. */
  .grid-wrap .table tfoot .people-link::after { right: -10px; left: min(-8px, calc(100% - 34px)); }
}
/* A figure box that clips (an ellipsis for a long amount) would cut a People link's underline, focus ring and hit
   area to its own line box. A count is a few digits and never needs the ellipsis, so the box holding one does not
   clip; fitKpiValues() still reads the hero figure's scrollWidth, which overflow does not change. */
.kpi-hero .kpi-value:has(> .people-link), .creative-v:has(> .people-link), .creative-stat dd:has(> .people-link) { overflow: visible; }

/* ---------- journey buttons ---------- */
/* A 32px outlined icon button beside a client's name (a route glyph), and a client's name that is itself a button
   (Cash). Both open the customer journey. On touch the icon button keeps its 32px box in a 52px row and gets the
   button's invisible hit area to reach 44px. */
.journey-btn { width: 32px; height: 32px; margin-left: var(--s-2); vertical-align: middle; }
.journey-btn svg { width: 16px; height: 16px; }
.link.journey-open { color: var(--text); font-weight: 600; text-decoration: underline dotted 1px; text-decoration-color: var(--line-ui); }
@media (hover: hover) {
  .link.journey-open:hover { color: var(--accent-hover); text-decoration-style: solid; text-decoration-color: currentColor; }
}
@media (pointer: coarse) {
  .journey-btn { width: 32px; height: 32px; }
  .journey-btn::after { inset: -7px; }
  .link.journey-open { position: relative; }
  .link.journey-open::after { content: ""; position: absolute; top: 50%; right: 0; left: 0; height: max(100%, 44px); transform: translateY(-50%); }
}

/* ---------- the People modal ---------- */
/* Cortana's large centred dark modal: 92vw × 88vh (at most 1400 × 900), the opaque panel colour, a strong 1px
   outline, a flat scrim behind (never a blur: the scan band moves behind the page) and one short rise on opacity and
   transform when it opens. people.js builds it on first use and opens it with showModal(), so the top layer, focus
   containment, the inert page and Escape come from the browser. The dialog's own display is never set (a closed
   dialog is display: none, and any display value would show it); the layout lives on .pm-inner. The body is the only
   scroller, with the column headings pinned at its top. On a phone (≤ 860px) it is the whole screen, the header takes
   two rows and every person is a stacked card. */
@keyframes modal-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes drawer-in { from { opacity: 0; transform: translateX(16px); } }
.people-modal {
  width: 92vw; height: 88vh; height: 88dvh; max-width: 1400px; max-height: 900px;
  margin: auto; padding: 0; overflow: hidden;
  border: 1px solid var(--border-strong); border-radius: var(--r-xl);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-pop);
}
.people-modal[open] { animation: modal-in var(--dur-3) var(--ease); }
/* The custom property has a literal fallback: in older browsers ::backdrop does not inherit from the page. */
.people-modal::backdrop { background: var(--scrim, rgba(3, 5, 4, 0.72)); }
.pm-inner { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.pm-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3);
  padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--border);
}
/* The title row: icon, metric name, the "Unique · Per Whop person" pill and the zone, wrapping as one group. */
.pm-titlerow { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); min-width: 0; }
.pm-icon {
  display: inline-grid; place-items: center; flex: none; width: 28px; height: 28px;
  border: 1px solid var(--line-ui); border-radius: var(--r-sm); color: var(--accent-hover);
}
.pm-icon svg { width: 16px; height: 16px; }
.pm-title { min-width: 0; font-size: 18px; font-weight: 600; line-height: 1.3; letter-spacing: -0.015em; color: var(--text); overflow-wrap: anywhere; }
.pm-title:focus { outline: none; }
.pm-pill {
  display: inline-flex; align-items: center; flex: none; height: 22px; padding: 0 var(--s-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm); background: var(--surface-2); color: var(--text-2);
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1; letter-spacing: 0.04em; white-space: nowrap;
}
.pm-tz { flex: none; font-size: 11px; line-height: 1.3; letter-spacing: 0.02em; color: var(--text-3); white-space: nowrap; }
.pm-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-left: auto; min-width: 0; }
.pm-count { font-size: 12px; line-height: 1.3; color: var(--text-2); white-space: nowrap; }
.pm-count .skeleton { display: inline-block; vertical-align: middle; width: 12ch; height: 12px; }
/* "Campaign · Scale Info Main — 27 Sep – 3 Oct 2026": its own line under the title row. */
.pm-sub { order: 10; flex: 1 1 100%; min-width: 0; font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.pm-search { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.pm-search-icon {
  position: absolute; top: 50%; left: var(--s-3); width: 14px; height: 14px; margin-top: -7px;
  color: var(--text-3); pointer-events: none;
}
.pm-search-input {
  width: 260px; max-width: 100%; height: 36px; padding: 0 var(--s-3) 0 calc(var(--s-3) + 14px + var(--s-2));
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); color: var(--text);
  font-family: var(--sans); font-size: var(--fs-input); font-weight: 400; line-height: 1.4; letter-spacing: normal; text-transform: none;
  -webkit-appearance: none; appearance: none;
}
.pm-search-input::placeholder { color: var(--text-3); opacity: 1; }
.pm-search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
@media (hover: hover) {
  .pm-search-input:hover { border-color: var(--accent); }
}
/* The server's notes ("101 leads from 96 people — 5 came back."): one quiet line each, under the header. */
.pm-notes { padding: var(--s-2) var(--s-5); border-bottom: 1px solid var(--border); background: var(--surface-2); }
.pm-notes p { font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.pm-notes p + p { margin-top: 2px; }
.pm-notes:empty { display: none; }
.pm-body {
  position: relative; flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scroll-padding-top: var(--head-h);
}
.pm-body:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
/* Fixed column widths (table-layout: fixed, one <col> per column from people.js). An auto table re-measures every row
   to size its columns, so each batch of rows appended to a 1,000-person list, and each search keystroke that hides
   rows, cost a layout of the whole list (74–156ms tasks at 1,029 rows); with fixed widths only the changed rows are
   laid out, a later batch with a longer email cannot shift the columns while they are read, and a long value ends in
   an ellipsis. The small columns are fixed; Contact, Email and Phone share what is left (about 195px each at 1440).
   Below 1100px of modal the body scrolls sideways rather than squeeze them further. */
.pm-table { border-collapse: separate; border-spacing: 0; width: 100%; min-width: 1100px; table-layout: fixed; }
.pm-col-check { width: 56px; }
/* Name and phone get fixed widths so the email, the longest of the three, takes what is left (about 240px at
   1440) instead of an equal third that cut ordinary addresses short while the phone column sat half empty. */
.pm-col-contact { width: 180px; }
.pm-col-phone { width: 176px; }
.pm-col-journey { width: 80px; }
.pm-col-value { width: 112px; }
.pm-col-attr { width: 200px; }
.pm-col-data { width: 100px; }
.pm-col-date { width: 188px; }
/* The text cells end in an ellipsis; the cells holding controls do not clip, so focus rings and touch areas show. */
.pm-table tbody :is(.pm-contact, .pm-email, .pm-phone, .pm-value, .pm-date) { overflow: hidden; text-overflow: ellipsis; }
.pm-table td.pm-attr .src-pill { max-width: calc(100% - 20px - var(--s-2)); }
.pm-table :is(th, td) { white-space: nowrap; }
.pm-table thead th { position: sticky; top: 0; z-index: 2; background: var(--surface-2); }
.pm-table :is(th, td):first-child { padding-left: var(--s-5); }
.pm-table :is(th, td):last-child { padding-right: var(--s-5); }
.pm-table svg { flex: none; width: 14px; height: 14px; margin-right: 6px; vertical-align: -2px; color: var(--text-3); }
.pm-check { width: 44px; }
.pm-check input[type="checkbox"] { width: 16px; height: 16px; margin: 0; vertical-align: middle; accent-color: var(--accent); cursor: pointer; }
.pm-table .pm-contact { min-width: 180px; max-width: 280px; overflow: hidden; text-overflow: ellipsis; font-weight: 600; color: var(--text); }
.pm-person { display: inline-flex; vertical-align: middle; color: var(--text-3); }
.pm-anon { font-weight: 400; font-style: italic; color: var(--text-3); }
.pm-text { vertical-align: middle; }
/* The modal's message slot (empty, error, needs a key): gone while it has nothing to say. */
.pm-state:empty { display: none; }
.pm-email, .pm-phone { max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
.pm-journey { width: 64px; text-align: center; }
.pm-journey-btn, .pm-attr-info {
  position: relative; display: inline-grid; place-items: center; padding: 0;
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: transparent; color: var(--text-2); cursor: pointer;
}
.pm-journey-btn { width: 32px; height: 28px; }
.pm-journey-btn svg { margin: 0; width: 16px; height: 16px; color: currentColor; }
/* 8px after the source pill, as a control beside a chip is everywhere else (4px read as part of the pill). */
.pm-attr-info { width: 20px; height: 20px; margin-left: var(--s-2); border-color: transparent; border-radius: var(--r-full); vertical-align: middle; }
.pm-attr-info svg { margin: 0; width: 14px; height: 14px; color: currentColor; }
.pm-value { text-align: right; font-size: 13px; color: var(--text); }
.pm-data-btn { font-size: 13px; color: var(--accent-hover); text-decoration: underline; text-decoration-color: rgba(163, 255, 102, 0.40); text-underline-offset: 3px; }
.pm-date { font-size: 12px; color: var(--text-2); }
/* The sort control is the whole Date heading; its arrow says which way, in the system's mono. */
.pm-sort {
  display: inline-flex; align-items: center; gap: var(--s-1); height: 100%; padding: 0;
  border: 0; background: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer;
}
.pm-sort .arrow { width: auto; }
.pm-table thead th[aria-sort] { color: var(--accent-hover); }
.pm-sort:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.pm-table tr.pm-skel td .skeleton { height: 12px; }
.pm-table tr.pm-skel :is(td, th) { height: var(--row-h); }
.pm-empty, .pm-error {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s-4);
  min-height: 240px; padding: var(--s-8) var(--s-6); text-align: center;
  font-size: 14px; line-height: 1.55; color: var(--text-2); overflow-wrap: anywhere;
}
.pm-error { color: var(--danger-text); }
.pm-error p { max-width: 52ch; }
.pm-foot {
  padding: var(--s-2) var(--s-5); border-top: 1px solid var(--border);
  font-size: 12px; line-height: 1.5; color: var(--text-3);
}
.pm-body[aria-busy="true"] .pm-table tbody:not(:has(.pm-skel)) { opacity: 0.55; }
@media (hover: hover) {
  .pm-table tbody tr:hover :is(td, th) { background: var(--surface-hover); }
  .pm-journey-btn:hover, .pm-attr-info:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-hover); }
  .pm-data-btn:hover { text-decoration-color: currentColor; }
}
/* Touch: the 32 × 28 journey button reaches 44 × 44 with 8px around it; the 20px info dot needs 12px; "View data" is a
   text link on a wide touch screen (a tablet on its side), so it gets a 44px band centred on its line. The rest of the
   desktop table is reached on a tablet too: the search box is 44px tall, and the heading row is 44px so the Date sort
   (the whole heading's height) and the select-all box are 44px targets. A hit area hanging above the sticky heading
   would be clipped by the scroller, so the row itself grows instead. */
@media (pointer: coarse) {
  .pm-journey-btn::after { content: ""; position: absolute; inset: -8px; }
  .pm-attr-info::after { content: ""; position: absolute; inset: min(-12px, calc(50% - 22px)); }
  .pm-data-btn { position: relative; }
  .pm-data-btn::after { content: ""; position: absolute; top: 50%; right: 0; left: 0; height: max(100%, 44px); transform: translateY(-50%); }
  .people-modal { --head-h: 44px; }
  .pm-search-input { height: 44px; }
  .pm-sort { min-width: 44px; height: var(--head-h); }
  /* A checkbox cannot carry a ::after hit area, and a native one is 16px. Here it is drawn instead: the input is a
     44px square whose 13px transparent border is the touch area (borders are hit-tested; the -14px margin keeps its
     layout box at 16px, so the 48px row and the 56px column do not move), and the visible 18px box is the padding
     box, outlined by an inset shadow, filled with the accent and a dark tick when checked, a dash when some rows are
     picked. */
  .pm-check input[type="checkbox"] {
    -webkit-appearance: none; appearance: none; box-sizing: border-box; flex: none;
    width: 44px; height: 44px; margin: -14px;
    border: 13px solid transparent; border-radius: 16px; background: transparent no-repeat center / 14px 14px;
    background-clip: padding-box; background-origin: padding-box;
    box-shadow: inset 0 0 0 1.5px var(--line-ui);
  }
  .pm-check input[type="checkbox"]:checked {
    background-color: var(--accent); box-shadow: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23070A08' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
  }
  .pm-check input[type="checkbox"]:indeterminate {
    background-color: var(--accent); box-shadow: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23070A08' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
  }
  /* Focus: the global ring would circle the whole 44px square; this one sits 2px outside the visible box. */
  .pm-check input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: -11px; box-shadow: inset 0 0 0 1.5px var(--line-ui); }
  .pm-check input[type="checkbox"]:is(:checked, :indeterminate):focus-visible { box-shadow: none; }
}
/* Phones and narrow tablets: the whole screen, a two-row header (the title row with Close; the search across the
   width beside an icon-only Export), the checkbox column gone and each person a stacked card: name and value, email,
   phone, source and date, then Journey and View data as two 44px buttons. */
@media (max-width: 860px) {
  .people-modal {
    inset: 0; width: 100vw; height: 100vh; height: 100dvh; max-width: none; max-height: none; margin: 0;
    border: 0; border-radius: 0;
  }
  .pm-head { padding: calc(var(--s-3) + var(--safe-top)) var(--s-4) var(--s-3); gap: var(--s-2); }
  .pm-titlerow { flex: 1 1 0; gap: var(--s-1) var(--s-2); }
  .pm-title { flex: 1 1 auto; font-size: 17px; }
  /* The tools' box goes so its four controls can be placed on the header's rows: the title row with Close, the scope
     line, the search across the width beside an icon-only Export, then the count. */
  .pm-tools { display: contents; }
  .pm-close { order: 1; flex: none; align-self: flex-start; }
  .pm-sub { order: 2; }
  .pm-search { order: 3; flex: 1 1 0; }
  .pm-search-input { width: 100%; height: 44px; }
  .pm-export { order: 3; flex: none; width: 44px; height: 44px; padding: 0; }
  .pm-count { order: 4; flex: 1 1 100%; }
  .pm-export .pm-export-text {
    position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .pm-notes { padding-inline: var(--s-4); }
  .pm-foot { padding: var(--s-2) var(--s-4) calc(var(--s-2) + var(--safe-bottom)); }
  .pm-table, .pm-table tbody, .pm-table tr, .pm-table :is(td, th) { display: block; }
  .pm-table { min-width: 0; }
  /* The column headings go entirely, not just out of sight: every card labels its own figures, and a visually hidden
     head row would leave its select-all box and Date sort in the Tab order as two invisible stops (Space on the first
     would quietly select every row). The column widths go with them. */
  .pm-table thead, .pm-table colgroup { display: none; }
  .pm-table tbody tr {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: var(--s-1) var(--s-3);
    grid-template-areas: "contact value" "email email" "phone phone" "attr date" "journey data";
    padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--border);
    /* A card out of view is neither laid out nor painted, so a 1,000-person list costs its first screen only; the
       intrinsic size is one card's height and `auto` keeps each card's real one once it has been drawn. Paint
       containment clips at the card's edge, inside which every focus ring and touch area already sits. */
    content-visibility: auto; contain-intrinsic-size: auto 172px;
  }
  .pm-table tbody tr > :is(td, th) {
    height: auto; min-width: 0; max-width: none; padding: 0; border: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .pm-table tbody tr > :is(td, th):first-child, .pm-table tbody tr > :is(td, th):last-child { padding: 0; }
  .pm-table tbody .pm-check { display: none; }
  .pm-table tbody .pm-contact { grid-area: contact; font-size: 15px; }
  .pm-table tbody .pm-value { grid-area: value; text-align: right; }
  .pm-table tbody .pm-email { grid-area: email; }
  .pm-table tbody .pm-phone { grid-area: phone; }
  .pm-table tbody .pm-attr { grid-area: attr; overflow: visible; }
  .pm-table tbody .pm-date { grid-area: date; text-align: right; }
  .pm-table tbody .pm-journey { grid-area: journey; width: auto; margin-top: var(--s-2); }
  .pm-table tbody .pm-data { grid-area: data; margin-top: var(--s-2); }
  .pm-table tbody :is(.pm-journey, .pm-data) { overflow: visible; }
  .pm-journey-btn, .pm-data-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2); width: 100%; height: 44px;
    border: 1px solid var(--line-ui); border-radius: var(--r-md); background: transparent;
    font-family: var(--mono); font-size: 12px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase;
    text-decoration: none; color: var(--text);
  }
  /* The journey button is an icon with an aria-label; on a card it gets its word too (generated, alt text "", so the
     label stays its name). */
  .pm-journey-btn::before { content: "Journey"; content: "Journey" / ""; order: 2; }
  .pm-table tr.pm-skel { display: block; padding: var(--s-3) var(--s-4); }
  .pm-table tr.pm-skel > :is(td, th) { display: none; }
  .pm-table tr.pm-skel > :is(td, th):nth-child(2), .pm-table tr.pm-skel > :is(td, th):nth-child(3) { display: block; height: auto; padding: var(--s-1) 0; }
}

/* ---------- the customer journey ---------- */
/* A drawer on the right on a wide screen (16px from the edges, at most 440px wide), the whole screen on a phone; its
   own top-layer dialog, so it opens over the People modal or alone, and Escape closes it first. Top to bottom: the
   title row (pinned while the timeline scrolls under it), the summary of the person it was opened for, then the
   timeline, newest first: a round icon per event on a vertical line, the tag ("Page View" orange, "Conversion" green,
   "Event" neutral) and the time, the title ("scaleinfo.com.au/confirm", "Appointment booked — …", "Cash collected —
   … · A$5,800.00"), "From: calendly.com", and the attribution details behind a disclosure. Host and path only: no
   query string ever reaches it. */
.journey-dialog {
  position: fixed; inset: 16px 16px 16px auto; margin: 0; padding: 0;
  width: min(440px, 92vw); height: calc(100vh - 32px); height: calc(100dvh - 32px); max-width: none; max-height: none;
  overflow: hidden;
  border: 1px solid var(--line-ui); border-radius: var(--r-xl);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-pop);
}
.journey-dialog[open] { animation: drawer-in var(--dur-3) var(--ease); }
/* Lighter than the modal's: opened over the People modal, the list behind should still be there. */
.journey-dialog::backdrop { background: rgba(3, 5, 4, 0.5); }
.jd-inner { display: flex; flex-direction: column; height: 100%; overflow: auto; overscroll-behavior: contain; }
.jd-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: var(--s-4) var(--s-3) var(--s-3) var(--s-5); border-bottom: 1px solid var(--border); background: var(--surface);
}
.jd-headtext { display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--s-1); min-width: 0; padding-top: 6px; }
.jd-head > .btn-icon { flex: none; }
.jd-title { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--text); }
.jd-title:focus { outline: none; }
.jd-sub {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); min-width: 0;
  font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere;
}
/* One row per fact (Source, Revenue band, Qualified, Booked, First paid, Paid to date, Campaign / Ad set / Ad), each
   its own two-column grid with a fixed label column, so the values start at one x. */
.jd-summary {
  display: flex; flex-direction: column; gap: var(--s-2);
  margin: 0; padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.jd-summary:focus { outline: none; }
.jd-summary:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.jd-row { display: grid; grid-template-columns: 112px minmax(0, 1fr); align-items: baseline; gap: var(--s-3); min-width: 0; }
.jd-summary dt { line-height: 1.6; color: var(--text-3); }
.jd-detail { font-size: 12px; color: var(--text-2); }
.src-pill + .jd-detail { margin-left: var(--s-1); } /* with the word space, ~7px from the pill instead of 3px */
.jd-summary dd { min-width: 0; margin: 0; font-size: 13px; line-height: 1.5; color: var(--text); overflow-wrap: anywhere; }
.jd-summary .thumb { margin-right: var(--s-2); }
/* The summary, the message slot, the timeline and its note, in one block that grows with them (the dialog's inner box
   is the scroller). */
.jd-body { flex: 1 0 auto; min-width: 0; }
.journey-list { position: relative; margin: 0; padding: var(--s-4) var(--s-5) var(--s-5); list-style: none; }
.journey-item {
  position: relative; display: grid; grid-template-columns: 28px minmax(0, 1fr); column-gap: var(--s-3);
  padding-bottom: var(--s-4);
}
/* The line joining one event's icon to the next: the item's own pseudo-element, gone after the last. */
.journey-item::before { content: ""; position: absolute; top: 28px; bottom: 0; left: 13.5px; width: 1px; background: var(--border-strong); }
.journey-item:last-child { padding-bottom: 0; }
.journey-item:last-child::before { display: none; }
.journey-dot {
  --tag-c: var(--text-2);
  position: relative; z-index: 1; display: grid; place-items: center; width: 28px; height: 28px;
  border: 1px solid var(--border-strong); border-radius: var(--r-full); background: var(--surface-2); color: var(--tag-c);
}
.journey-dot svg { width: 14px; height: 14px; }
.journey-item[data-tag="Page View"] .journey-dot { border-color: rgba(255, 173, 92, 0.35); background: rgba(255, 173, 92, 0.12); color: var(--tag-page); }
.journey-item[data-tag="Conversion"] .journey-dot { border-color: rgba(161, 249, 99, 0.40); background: var(--accent-soft); color: var(--accent-hover); }
/* Money is the event the whole journey leads to: a filled accent disc with dark ink (13.7:1). */
.journey-item[data-kind="payment"] .journey-dot { border-color: var(--accent-fill); background: var(--accent-fill); color: var(--accent-fill-text); }
.journey-main { min-width: 0; padding-top: 3px; }
.journey-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); min-width: 0; }
.journey-tag {
  display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm); background: var(--surface-2); color: var(--text-2);
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 1; letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap;
}
.journey-tag[data-tag="Page View"] { border-color: rgba(255, 173, 92, 0.35); background: rgba(255, 173, 92, 0.12); color: var(--tag-page); }
.journey-tag[data-tag="Conversion"] { border-color: rgba(161, 249, 99, 0.40); background: var(--accent-soft); color: var(--accent-hover); }
.journey-time { margin-left: auto; font-size: 11px; line-height: 1.5; color: var(--text-3); white-space: nowrap; }
.journey-title { margin-top: var(--s-1); font-size: 14px; font-weight: 500; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.journey-from { margin-top: 2px; font-size: 12px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.journey-more { margin-top: var(--s-1); }
.journey-more > summary {
  display: inline-flex; align-items: center; gap: var(--s-1); min-height: 28px;
  list-style: none; cursor: pointer; color: var(--accent-hover);
}
.journey-more > summary::-webkit-details-marker { display: none; }
.journey-more > summary::marker { content: ""; }
.journey-more > summary::after { content: "\25B8"; content: "\25B8" / ""; display: inline-block; transition: transform var(--dur-2) var(--ease); }
.journey-more[open] > summary::after { transform: rotate(90deg); }
/* Each fact is a div of label over value, two facts a row in equal halves: sized to the widest value (a campaign
   name), the first column left the second 31px on a phone and broke "Whop ads" into "Who / p ads". */
.journey-attr {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-1) var(--s-3);
  margin: var(--s-1) 0 0; padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.journey-attr dt { line-height: 1.6; color: var(--text-3); }
.journey-attr dd { min-width: 0; margin: 0; font-size: 12px; line-height: 1.5; color: var(--text); overflow-wrap: anywhere; }
.journey-list .skeleton { height: 12px; }
/* The dialog's message slot (no tracked journey, an error with Retry, a missing permission): centred, gone while empty. */
.jd-state:empty { display: none; }
.jd-state {
  display: flex; flex-direction: column; align-items: center; gap: var(--s-4);
  padding: var(--s-6) var(--s-5); text-align: center; font-size: 14px; line-height: 1.55; color: var(--text-2); overflow-wrap: anywhere;
}
.journey-item.is-skeleton .journey-main { display: flex; flex-direction: column; gap: var(--s-2); padding-top: 6px; }
.jd-note { padding: 0 var(--s-5) var(--s-4); font-size: 12px; line-height: 1.5; color: var(--text-3); }
.jd-inner :is(.pm-empty, .pm-error, .jd-empty, .jd-error) { min-height: 160px; }
.jd-empty, .jd-error { padding: var(--s-6) var(--s-5); text-align: center; font-size: 14px; line-height: 1.55; color: var(--text-2); }
.jd-error { color: var(--danger-text); }
@media (pointer: coarse) {
  .journey-more > summary { min-height: 44px; }
}
@media (max-width: 860px) {
  .journey-dialog {
    inset: 0; width: 100vw; height: 100vh; height: 100dvh; border: 0; border-radius: 0;
  }
  .jd-head { padding: calc(var(--s-3) + var(--safe-top)) var(--s-2) var(--s-3) var(--s-4); }
  .jd-summary, .journey-list { padding-inline: var(--s-4); }
  .journey-list { padding-bottom: calc(var(--s-5) + var(--safe-bottom)); }
  .jd-row { grid-template-columns: 104px minmax(0, 1fr); gap: var(--s-2); }
}

/* ---------- the dropdown (Creatives sort; any future select) ---------- */
/* The one native select drew its menu over its own box on macOS, which CSS cannot move. This is its replacement: a
   button in the select's old look (36px, 44px on touch, the control line, the inset fill, mono 12) showing the choice
   and a chevron, and a panel that opens 8px below it, left-aligned, at least the button's width, at most 320px tall
   with its own scroll, flipped above (.is-above) only when there is no room below. Options are 14px sans rows with
   10px vertical padding (44px on touch), a check in the accent on the chosen one, the active one in the accent tint.
   It opens on opacity and translateY only, instantly under reduced motion. */
@keyframes dd-in { from { opacity: 0; transform: translateY(-4px); } }
@keyframes dd-in-above { from { opacity: 0; transform: translateY(4px); } }
.dd { position: relative; display: inline-flex; min-width: 0; max-width: 100%; }
.dd-button {
  position: relative; display: inline-flex; align-items: center; gap: var(--s-2);
  min-width: 0; max-width: 100%; height: 36px; padding: 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); color: var(--text);
  font-family: var(--mono); font-size: 12px; font-weight: 500; letter-spacing: 0.04em; text-align: left; cursor: pointer;
}
.dd-button-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-chevron { flex: none; width: 12px; height: 12px; margin-left: auto; color: var(--text-2); transition: transform var(--dur-2) var(--ease); }
.dd.is-open .dd-chevron, .dd-button[aria-expanded="true"] .dd-chevron { transform: rotate(180deg); }
.dd.is-open .dd-button, .dd-button[aria-expanded="true"] { border-color: var(--accent); }
.dd-panel {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 40;
  min-width: 100%; max-width: min(360px, calc(100vw - 2 * var(--gutter))); max-height: 320px;
  overflow-y: auto; overscroll-behavior: contain; padding: var(--s-1);
  border: 1px solid var(--line-ui); border-radius: 8px;
  background: var(--surface); box-shadow: var(--shadow-pop);
  animation: dd-in var(--dur-2) var(--ease);
}
.dd-panel.is-above { top: auto; bottom: calc(100% + 8px); animation-name: dd-in-above; }
.dd-panel:focus { outline: none; }
.dd-option {
  display: flex; align-items: center; gap: var(--s-2); min-height: 40px; padding: 10px var(--s-3);
  border-radius: var(--r-md); color: var(--text); cursor: pointer; user-select: none;
  font-family: var(--sans); font-size: 14px; line-height: 1.3; white-space: nowrap;
}
.dd-check { display: inline-grid; place-items: center; flex: none; width: 14px; height: 14px; color: var(--accent); visibility: hidden; }
.dd-check svg { width: 14px; height: 14px; }
.dd-option-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.dd-option.is-selected { color: var(--accent-hover); }
.dd-option.is-selected .dd-check { visibility: visible; }
.dd-option.is-active { background: var(--accent-soft); }
@media (hover: hover) {
  .dd-button:hover { border-color: var(--accent); }
  .dd-option:hover { background: var(--accent-soft); }
}
@media (pointer: coarse), (max-width: 860px) {
  .dd-button { height: 44px; }
  .dd-option { min-height: 44px; }
}
@media (max-width: 860px) {
  .view-toolbar .dd { flex: 1 1 auto; }
  .view-toolbar .dd-button { width: 100%; }
}

/* ---------- Creatives cards with People links ---------- */
/* People links may not sit inside an anchor, so a card is the link (picture, title, path) followed by the figures in
   div.creative-figs. The link covers the whole card through a stretched ::after. The figures block is a size
   container, and containment makes it a stacking context painted after that overlay, so on its own it would sit on
   top of the link and swallow every click on the lower half of the card. It therefore lets the pointer through
   (pointer-events: none) and only its People links take it back, lifted above the overlay: a tap on a count opens
   the list, a tap anywhere else on the card opens the creative, as it did when the whole card was one link. Every
   card, the loading ones included, has this structure, so these rules need no :has() test (216 cards matched on
   each style pass of the Creatives switch). */
.creative-card { display: flex; flex-direction: column; }
.creative-card .creative-link { flex: none; height: auto; }
.creative-card .creative-link::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.creative-link > :is(.creative-title, .creative-path) { margin-inline: var(--s-4); }
.creative-link > .creative-title { margin-top: var(--s-4); }
.creative-link > .creative-path { margin-bottom: var(--s-3); }
/* The path 4px under its title. The old card's -8px pull-up assumed a 12px stack gap this column does not have, and
   laid the path 8px over the title. */
.creative-link > .creative-title + .creative-path { margin-top: var(--s-1); }
.creative-figs {
  container: card / inline-size;
  display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--s-3); min-width: 0; padding: 0 var(--s-4) var(--s-4);
  pointer-events: none;
}
.creative-figs .people-link { position: relative; z-index: 1; pointer-events: auto; }
@media (max-width: 639px) {
  .creative-card .thumb-lg { width: 56px; height: 56px; }
  .creative-card .creative-link > .thumb { grid-row: span 2; }
  /* The picture's spare height goes under the path (second row 1fr), so the title and its path stay 4px apart. */
  .creative-card .creative-link { grid-template-columns: 56px minmax(0, 1fr); grid-template-rows: auto 1fr; row-gap: 0; padding-bottom: var(--s-2); }
  .creative-link > :is(.creative-title, .creative-path) { margin: 0; }
  .creative-link > .creative-title + .creative-path { margin-top: var(--s-1); }
  .creative-figs { padding: 0 var(--s-3) var(--s-3); gap: var(--s-2); }
}

/* Reduced motion: the modal, the drawer and the dropdown appear in place (the global rule already shortens every
   animation; this says it outright, after the rules that start them). */
@media (prefers-reduced-motion: reduce) {
  .people-modal[open], .journey-dialog[open], .dd-panel { animation: none; }
  .notes-chev, .dd-chevron, .journey-more > summary::after { transition: none; }
}

/* ==========================================================================
   The edit build (EDIT-PLAN.md, DESIGN-TECH section 14): changing a daily budget, pausing and resuming, renaming,
   duplicating and launching from the dashboard. Nothing here is a new visual language: the switch is the segmented
   track's inset with an LED for a thumb, the row menu is the dropdown's panel, the dialogs are the picker's and the
   More sheet's, the toasts are the console's, and the Data panel is the connections list. edit.js emits every class
   below and app.js places them; every visual state of a board control is a class (is-on, is-busy, is-updated…), so
   app.js can tell a changed row from its classes and text alone. After the Cortana build so these win ties; before
   forced colours, which must win over everything.
   ========================================================================== */

/* ---------- the lockbox: "Unlock editing" / "Editing on · 12 min" + Lock, at the right of the crumbs row ---------- */
/* Absolutely placed in the board card, level with the path, and as wide as its widest state. While editing is on
   (#panel[data-edit="on"], set by app.js from the stored state before the first answer) the crumbs reserve that width
   on their right, so the path never moves when the box fills in or changes state. The padlock is drawn here (a mask
   on ::before, shut to unlock, open while on), so the box reads the same whatever icon markup edit.js has; on a phone
   the words go visually and the padlock carries the state, with the words still the controls' names. */
#panel { --lockbox-w: 296px; }
#panel[data-edit="on"] #crumbs { padding-right: calc(var(--card-pad) + var(--lockbox-w) + var(--s-3)); }
/* The box is 28px from 8px down (36px in all), and the path's one line ends at 31px: the row is held at 44px so the
   button keeps 8px clear of the header row or the parent bar under it (verify: the sticky header painted over its
   bottom border at 1440). The path stays on its first line, level with the button, and the room goes under it. The
   row is held whether editing is on or not: a tab with no stored flag learns that it is only after the first frame,
   and the board then moved 13px down at 1440. On touch the row is 48px already (the path's 44px segments), and the
   box's 44px fits it from 4px down; held at 44px there, the box hung 4px out under the row. */
@media (min-width: 640px) and (pointer: fine) {
  #crumbs { min-height: calc(var(--s-2) + 28px + var(--s-2)); align-content: flex-start; }
}
.edit-lockbox {
  position: absolute; top: var(--s-2); right: var(--card-pad); z-index: 1;
  display: flex; align-items: center; justify-content: flex-end; gap: var(--s-3);
  max-width: var(--lockbox-w); height: 28px;
}
.edit-lockbox svg { display: none; }
.edit-unlock { height: 28px; padding: 0 var(--s-3); }
.edit-unlock::before, .edit-unlocked::before {
  content: ""; flex: none; width: 14px; height: 14px; background: currentColor;
  -webkit-mask: var(--icon-lock) center / contain no-repeat; mask: var(--icon-lock) center / contain no-repeat;
}
.edit-unlocked { display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; white-space: nowrap; color: var(--accent-hover); }
.edit-unlocked::before { -webkit-mask-image: var(--icon-unlock); mask-image: var(--icon-unlock); }
/* Lock: a quiet text button in the Label voice, accent and underlined on hover, like the panel links. */
.link.edit-lock-btn { position: relative; color: var(--text-2); text-underline-offset: 3px; }
@media (hover: hover) {
  .link.edit-lock-btn:hover { color: var(--accent-hover); text-decoration: underline; }
}
/* SAMPLE: editing is simulated on made-up data (?mock=1). Amber, like the sample banner and the sample LED. */
.edit-sample {
  display: inline-flex; align-items: center; flex: none; height: 20px; padding: 0 var(--s-2);
  border: 1px solid rgba(255, 194, 61, 0.40); border-radius: var(--r-sm); background: var(--warning-soft);
  color: var(--warning-text); white-space: nowrap;
}
/* Touch: the crumbs row is 44px tall there, and the box is centred on it like the path's own segments. */
@media (pointer: coarse) {
  .edit-lockbox { top: var(--s-1); height: 44px; }
  .edit-unlock { height: 36px; }
  /* 6px to each side, or more for a short word: LOCK is 31px, so its area is held to 44px about its centre. */
  .link.edit-lock-btn::after {
    content: ""; position: absolute; top: 50%; right: min(-6px, calc(50% - 22px)); left: min(-6px, calc(50% - 22px));
    height: 44px; transform: translateY(-50%);
  }
}
/* Phones: the padlock alone (the words visually hidden, still the accessible names), then LOCK and SAMPLE. The box
   shares only the path's first line. Reserved down the whole right edge, its 136px squeezed every line of a drilled
   path to a word or two (six lines and 154px at 320px against 71px without it, pushing the table under the tab bar);
   so the crumbs flow as text here, and a float as tall as the box keeps the first line clear of it while the lines
   under it take the full width. Set from data-edit like the wide reserve, so nothing moves when the box fills in.
   The flex gap becomes each segment's right margin. */
@media (max-width: 639px) {
  #panel { --lockbox-w: 136px; }
  #panel[data-edit="on"] #crumbs { display: flow-root; padding-right: var(--card-pad); }
  #panel[data-edit="on"] #crumbs::before {
    content: ""; float: right; width: calc(var(--lockbox-w) + var(--s-3));
    height: calc(var(--s-2) + 28px - var(--s-3)); /* the box's foot, measured from the path's first line */
  }
  #panel[data-edit="on"] #crumbs > :not(:last-child) { margin-right: var(--s-2); }
  .edit-lockbox { gap: var(--s-2); }
  .edit-unlock { width: 36px; padding: 0; gap: 0; font-size: 0; letter-spacing: 0; }
  .edit-unlocked { gap: 0; font-size: 0; letter-spacing: 0; }
  .edit-unlocked::before { width: 16px; height: 16px; }
  :is(.edit-unlock, .edit-unlocked) > :not(svg) {
    position: absolute; left: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}
/* On touch the box and the path's first line are both 44px from the same 4px. A drilled path's first segment button
   is 44px and sets that line; a path at the root has none, and its words sat 12px above the padlock's middle, so the
   prompt is a 44px line of its own and the words beside it stand on its middle. */
@media (pointer: coarse) and (max-width: 639px) {
  #panel[data-edit="on"] #crumbs::before { height: 44px; }
}
@media (pointer: coarse) and (min-width: 360px) and (max-width: 639px) {
  #panel[data-edit="on"] #crumbs > .crumb-home { display: inline-block; line-height: 44px; }
}
/* The smallest phones: beside the box the first line is 98px at 320px, which holds "~/" and nothing after it (a
   drilled path then left an empty band beside the box), so the box takes a row of its own over the path. */
@media (max-width: 359px) {
  #panel[data-edit="on"] #crumbs::before { float: none; display: block; width: auto; }
}

/* ---------- the delivery switch ---------- */
/* On / off for a campaign or ad set that is active or paused. A 28 × 16 inset track with a 12px square thumb that is
   an LED: off on the left in --led-off, on at the right lit in the accent over the accent tint. The thumb moves by
   transform (--dur-2); the colours change at once, as every state colour does. It flips only once Whop has confirmed
   the change; until then it is busy and its thumb is a turning ring (a still "…" under reduced motion). Focus rings
   the track, not the button's box. On touch an invisible ::after makes it a 44px target, reaching only into the gap
   beside it, never over the name. */
@keyframes edit-spin { to { transform: rotate(360deg); } }
.edit-switch {
  position: relative; display: inline-grid; place-items: center; flex: none; width: 28px; height: 16px;
  margin: 0; padding: 0; border: 0; border-radius: var(--r-sm); background: none; color: inherit; cursor: pointer;
}
.edit-switch-track {
  position: relative; display: block; width: 28px; height: 16px;
  border: 1px solid var(--line-ui); border-radius: var(--r-sm); background: var(--surface-2);
}
.edit-switch-thumb {
  position: absolute; top: 1px; left: 1px; display: block; width: 12px; height: 12px;
  border-radius: 1px; background: var(--led-off);
  transition: transform var(--dur-2) var(--ease);
}
.edit-switch.is-off .edit-switch-thumb { transform: translateX(0); }
.edit-switch.is-on .edit-switch-track { border-color: var(--accent); background: var(--accent-soft); }
.edit-switch.is-on .edit-switch-thumb { background: var(--accent); transform: translateX(12px); }
.edit-switch[role="switch"]:focus-visible { box-shadow: none; }
.edit-switch[role="switch"]:focus-visible .edit-switch-track { box-shadow: var(--focus); }
/* Busy: the thumb stays on its side and its fill gives way to a ring with a gap, turned by its ::after. */
.edit-switch.is-busy, .edit-switch[aria-disabled="true"] { cursor: progress; }
.edit-switch.is-busy .edit-switch-thumb { background: transparent; }
.edit-switch.is-busy .edit-switch-thumb::after {
  content: ""; position: absolute; inset: 1px;
  border: 1.5px solid var(--accent); border-top-color: transparent; border-radius: var(--r-full);
  animation: edit-spin 0.8s linear infinite;
}
.edit-switch.is-busy:not(.is-on) .edit-switch-thumb::after { border-color: var(--text-2); border-top-color: transparent; }
@media (hover: hover) {
  .edit-switch:not(.is-busy, [aria-disabled="true"]):hover .edit-switch-track { border-color: var(--accent); }
}
@media (pointer: coarse) {
  .edit-switch::after { content: ""; position: absolute; inset: -14px -8px; }
}

/* ---------- the ⋯ button and the budget pencil ---------- */
/* 24px icon buttons in --text-3 that turn accent on hover and while their menu is open. On touch each gets a 44px
   hit area from ::after; the ⋯ in a name cell hangs it to the right, into the cell's own padding, so it never covers
   the name. Busy: dimmed with a progress cursor (aria-disabled says the same to a screen reader). Just changed
   (.is-updated, for 120 s after Whop confirmed): a 6px accent dot on the control's top-right corner. */
.edit-more, .edit-pencil {
  position: relative; display: inline-grid; place-items: center; flex: none; width: 24px; height: 24px;
  margin: 0; padding: 0; border: 0; border-radius: var(--r-md); background: none; color: var(--text-3);
  font-family: var(--mono); font-size: 16px; line-height: 1; cursor: pointer;
}
.edit-more svg { width: 16px; height: 16px; }
.edit-pencil svg { width: 14px; height: 14px; }
.edit-more.is-open { background: var(--accent-soft); color: var(--accent-hover); }
.edit-more.is-busy, .edit-pencil.is-busy, .edit-more[aria-disabled="true"], .edit-pencil[aria-disabled="true"] {
  opacity: 0.45; cursor: progress;
}
@media (hover: hover) {
  .edit-more:not(.is-busy, [aria-disabled="true"]):hover, .edit-pencil:not(.is-busy, [aria-disabled="true"]):hover {
    background: var(--accent-soft); color: var(--accent-hover);
  }
}
.edit-switch.is-updated::before, .edit-pencil.is-updated::before, .edit-more.is-updated::before {
  content: ""; position: absolute; top: -3px; right: -3px; z-index: 1; width: 6px; height: 6px;
  border-radius: var(--r-full); background: var(--accent);
}
/* On the switch the dot keeps to its own corner: the name starts 8px to its right. */
.edit-switch.is-updated::before { top: -4px; right: -3px; }
/* Pause, resume and launch mark the switch and the ⋯ alike, because under 640px the switch is hidden and the ⋯ is
   where they are done. Where the switch shows, its dot is the one: a second on the ⋯ of the same row (or of the parent
   bar, where the two are siblings too) only doubled the news. */
@media (min-width: 640px) {
  .edit-switch.is-updated ~ .edit-more.is-updated::before { content: none; }
}
@media (pointer: coarse) {
  .edit-more::after, .edit-pencil::after { content: ""; position: absolute; inset: -10px; }
  .name-cell.has-edit > .edit-more::after { inset: -10px -12px -10px -8px; }
}

/* ---------- the budget cell ---------- */
/* The label as the column always showed it (sans, --text-2), with the pencil 8px after it when the row owns a daily
   budget. One line: the grid's cells never wrap. */
.edit-budget { display: inline-flex; align-items: center; gap: var(--s-2); vertical-align: middle; white-space: nowrap; }
.edit-budget-label { color: var(--text-2); }

/* ---------- the name cell with editing on ---------- */
/* switch · name · status LED · ⋯. The ⋯ sits at the cell's right edge, centred on the row whatever the name does, in
   room the cell keeps for it, so a long name that wraps never pushes it onto a line of its own. From 640px the cell
   does not wrap: the name gives way and wraps inside its own box, with the switch before it and the LED after it on
   the same line. Left to wrap like the plain cell, a long name dropped under the switch, which then sat alone on a
   line of its own and doubled the row (88px instead of 52px). ≤ 639px the switch goes (the pinned column is 160px
   there), Pause / Resume are in the ⋯ menu instead, and the name and its LED wrap as they do without editing. */
.name-cell.has-edit { position: relative; padding-right: calc(24px + var(--s-2)); }
.name-cell.has-edit > .edit-more { position: absolute; top: 50%; right: 0; margin-top: -12px; }
@media (min-width: 640px) {
  .name-cell.has-edit { flex-wrap: nowrap; }
  .name-cell.has-edit > .name { flex: 0 1 auto; min-width: 0; }
  .name-cell.has-edit > .badge { flex: none; }
  /* A row with no switch (a draft, a campaign in review) keeps the switch's room, so the names of a level start on one
     line down the column and the switches read as a column of their own. */
  .name-cell.has-edit > .name:first-child { margin-left: calc(28px + var(--s-2)); }
}
@media (max-width: 639px) {
  .name-cell > .edit-switch { display: none; }
}
/* An ad: switch · thumbnail · name over its LED, the ⋯ at the right as on the other levels. An ad with no switch (in
   review, rejected) keeps the switch's room, so the thumbnails of a level stay in one column. */
@media (min-width: 640px) {
  .name-cell.has-thumb.has-switch { grid-template-columns: 28px var(--thumb) minmax(0, 1fr); }
  .name-cell.has-thumb.has-edit:not(.has-switch) { padding-left: calc(28px + var(--s-3)); }
}

/* A copy Whop is still making: amber, like "in review", with the word beside the dot. */
.badge.st-duplicating { color: var(--warning-text); }
.badge.st-duplicating::before { background: var(--led-warn); }

/* ---------- the parent bar (drilled into a campaign or ad set) ---------- */
/* The object the grid is inside, as one inset strip between the path and the grid: its level word, its name, its
   LED, then its budget (and pencil), switch and ⋯ together at the right. One line from 640px: the name gives way and
   wraps in its own box, so the budget, the switch and the ⋯ never break apart. ≤ 639px three lines: the level word
   with the LED, the name, then the budget and the ⋯ at the right; the switch goes, as it does in the rows, and Pause
   / Resume are in the ⋯ (wrapping freely, the ⋯ had ended up alone on a fourth line). */
.edit-parent { padding: var(--s-2) var(--card-pad) var(--s-3); }
.edit-parent-inner {
  display: flex; align-items: center; gap: var(--s-2) var(--s-3);
  min-width: 0; min-height: 44px; padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.edit-parent-inner > * { flex: none; }
.edit-parent-level { color: var(--text-3); }
.edit-parent-inner > .edit-parent-name {
  flex: 0 1 auto; min-width: 0; font-family: var(--sans); font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--text);
  overflow-wrap: anywhere;
}
.edit-parent-budget {
  display: inline-flex; align-items: center; gap: var(--s-2); margin-left: auto;
  font-size: 13px; line-height: 1.3; white-space: nowrap; color: var(--text-2);
}
.edit-parent-budget .edit-budget-label { font-family: var(--mono); }
@media (max-width: 639px) {
  .edit-parent-inner:not(.is-skeleton) {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--s-3); row-gap: var(--s-1);
  }
  /* The LED rides on the level word's line ("CAMPAIGN ○ PAUSED"), so the last line is the budget's and the ⋯'s
     alone. Beside the LED on that line the budget had a column of 124px at 320px: "Using campaign budget" (164px)
     ran out of it to the left and over the LED ("PAUSUsing campaign budget"), and a longer LED word ("payment
     failed") did the same at 375px. Now it has the line less the ⋯ (186px at 320px), and should a label still not
     fit, it wraps inside its own box, right-aligned, the pencil at the end of its last line. */
  .edit-parent-level { grid-area: 1 / 1; }
  .edit-parent-inner > .badge { grid-area: 1 / 2 / auto / -1; justify-self: start; }
  /* The name keeps to one line and ends in an ellipsis: the path right above the bar holds it in full, and a name
     that wrapped ("Stallion Acquisition Leads campaign 09/13" at 375px) made the bar 19px taller than the skeleton
     that held its place, so the board still moved when the drilled answer landed. */
  .edit-parent-inner > .edit-parent-name {
    grid-area: 2 / 1 / auto / -1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
  }
  .edit-parent-budget {
    grid-area: 3 / 1 / auto / 3; justify-self: end; min-width: 0; max-width: 100%; margin-left: 0;
    white-space: normal; text-align: right;
  }
  .edit-parent-budget > :is(.edit-budget, .muted) { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
  .edit-parent-inner > .edit-more { grid-area: 3 / 3; }
  .edit-parent-inner > .edit-switch { display: none; }
  /* The skeleton stands in for the three lines (the level word and LED, the one-line name, then the budget and ⋯ on
     the 24px of the ⋯), so the grid under the bar stays where it is when the drilled answer lands: at one line it was
     48px against the bar's 83px, and the board jumped 36px on every drill at 375px. */
  .edit-parent-inner.is-skeleton {
    min-height: calc(2px + 2 * var(--s-2) + 11px * 1.3 + var(--s-1) + 14px * 1.35 + var(--s-1) + 24px);
  }
}
/* While the drilled answer is on its way app.js holds the bar's height with one skeleton bar (pulsed with the other
   skeleton groups), so the grid under it does not jump when the bar fills in. */
.edit-parent-inner.is-skeleton .skeleton { height: 14px; }
@media (pointer: coarse) {
  .edit-parent-inner { min-height: 48px; }
}

/* ---------- the row menu ---------- */
/* The dropdown's panel (13.7) as a menu: a fixed layer 8px under its ⋯ (above it, .is-above, when there is no room
   below; edit.js sets top and left), --surface, the control line, radius 8, --shadow-pop, opening with dd-in. Items
   are sans 14 rows, 40px (44px on touch and phones). An item that does not apply stays in the list, focusable but
   inert: its label at 0.45 and, under it, the reason in 12px --text-3, kept at full strength so the reason reads
   (the focused one sits on --surface-hover, where --text-3 is 4.8:1; never on the accent tint). */
.edit-menu {
  position: fixed; z-index: 60;
  min-width: 220px; max-width: min(320px, calc(100vw - 2 * var(--s-4)));
  max-height: calc(100dvh - 2 * var(--s-4)); overflow-y: auto; overscroll-behavior: contain;
  padding: var(--s-1);
  border: 1px solid var(--line-ui); border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-pop);
  animation: dd-in var(--dur-2) var(--ease);
}
.edit-menu.is-above { animation-name: dd-in-above; }
.edit-menu:focus { outline: none; }
.edit-menu-item {
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px;
  width: 100%; min-height: 40px; padding: 10px var(--s-3);
  border: 0; border-radius: var(--r-md); background: none; color: var(--text); text-align: left; cursor: pointer;
  font-family: var(--sans); font-size: 14px; font-weight: 400; line-height: 1.3;
}
.edit-menu-label { min-width: 0; overflow-wrap: anywhere; }
.edit-menu-why { font-size: 12px; line-height: 1.4; color: var(--text-3); overflow-wrap: anywhere; }
.edit-menu-why:empty { display: none; }
.edit-menu-item:focus { outline: none; background: var(--accent-soft); }
.edit-menu .edit-menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.edit-menu-item[aria-disabled="true"] { cursor: not-allowed; }
.edit-menu-item[aria-disabled="true"] .edit-menu-label { opacity: 0.45; }
.edit-menu-item[aria-disabled="true"]:focus { background: var(--surface-hover); }
@media (hover: hover) {
  .edit-menu-item:not([aria-disabled="true"]):hover { background: var(--accent-soft); }
}
@media (pointer: coarse), (max-width: 860px) {
  .edit-menu-item { min-height: 44px; }
}

/* ---------- the PIN dialog and the edit dialog ---------- */
/* Modal <dialog>s opened with showModal() (top layer, focus containment, inert page, Escape), built by edit.js on
   first use. Desktop: a centred min(480px, 92vw) panel in --surface with the strong outline, --shadow-pop and a flat
   scrim, opening with modal-in. ≤ 860px: a bottom sheet like the picker's and the More sheet's (full width, up to 92%
   of the screen, top corners rounded, a grab bar, sheet-up, the footer clear of the home indicator), and its fields
   are 16px so iOS never zooms into them. The dialogs' own display is never set (a closed dialog is display: none);
   the layout lives on .ep-inner and .ed-inner. */
.edit-pin, .edit-dialog {
  width: min(480px, 92vw); max-width: calc(100vw - 2 * var(--s-4));
  max-height: calc(100vh - 2 * var(--s-4)); max-height: calc(100dvh - 2 * var(--s-4));
  margin: auto; padding: 0; overflow: auto; overscroll-behavior: contain;
  border: 1px solid var(--border-strong); border-radius: var(--r-xl);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow-pop);
}
.edit-pin[open], .edit-dialog[open] { animation: modal-in var(--dur-3) var(--ease); }
/* The custom property has a literal fallback: in older browsers ::backdrop does not inherit from the page. */
.edit-pin::backdrop, .edit-dialog::backdrop { background: var(--scrim, rgba(3, 5, 4, 0.72)); }
.ep-inner, .ed-inner { position: relative; display: flex; flex-direction: column; min-width: 0; }

/* PIN: title, one sentence, the numeric field (mono, spaced like a code), the error, Cancel and Unlock. */
.ep-inner { gap: var(--s-3); padding: var(--s-6) var(--s-5) 0; }
.ep-title { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--text); }
.ep-title:focus { outline: none; }
.ep-sub { font-size: 14px; line-height: 1.5; color: var(--text-2); }
.ep-input {
  display: block; width: 100%; min-width: 0; height: 44px; margin-top: var(--s-1); padding: 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); color: var(--text);
  font-family: var(--mono); font-size: 18px; font-weight: 500; line-height: 1; letter-spacing: 0.3em;
  -webkit-appearance: none; appearance: none;
}
.ep-input:disabled { opacity: 0.5; cursor: not-allowed; }
.ep-input[aria-invalid="true"] { border-color: var(--danger); }
@media (hover: hover) {
  .ep-input:not(:disabled, [aria-invalid="true"]):hover { border-color: var(--accent); }
}
.ep-error, .ed-error { font-size: 13px; font-weight: 500; line-height: 1.5; color: var(--danger-text); overflow-wrap: anywhere; }
.ep-error:not(:empty)::before, .ed-error:not(:empty)::before {
  content: ""; display: inline-block; width: 14px; height: 14px; margin-right: 6px; vertical-align: -2px;
  background: currentColor;
  -webkit-mask: var(--icon-alert) center / contain no-repeat; mask: var(--icon-alert) center / contain no-repeat;
}
.ep-foot, .ed-foot {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--s-2);
  padding: var(--s-3) var(--s-5); border-top: 1px solid var(--border); background: var(--surface);
}
.ep-foot { margin: var(--s-2) calc(var(--s-5) * -1) 0; }

/* Edit: the head (title, "Campaign · <name>", Close), the step's body, a status line, the error, then the footer
   pinned to the dialog's bottom so Confirm never scrolls away. */
.ed-head {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; column-gap: var(--s-3); row-gap: var(--s-1);
  padding: var(--s-4) var(--s-3) var(--s-3) var(--s-5); border-bottom: 1px solid var(--border);
}
.ed-title { grid-column: 1; padding-top: 6px; font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--text); }
.ed-title:focus { outline: none; }
.ed-sub { grid-column: 1; min-width: 0; font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.ed-head > .btn-icon { grid-column: 2; grid-row: 1 / span 2; }
.ed-body { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; padding: var(--s-5); }
.ed-body:empty { display: none; }
/* The status line ("Checking the current values in Whop…", "Applying in Whop…") and the error stay in the tree when
   empty (so the live region and the alert are always there to speak) and take no room. */
.ed-status:not(:empty) {
  display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-5) var(--s-4);
  font-size: 13px; line-height: 1.5; color: var(--text-2);
}
/* While checking, a hollow accent ring (the "working" LED of the system line); while a change is being applied, the
   same ring turns. Under reduced motion it stays still. */
.ed-body:is([data-step="checking"], [data-step="working"]) ~ .ed-status:not(:empty)::before {
  content: ""; flex: none; width: 12px; height: 12px;
  border: 1.5px solid var(--accent); border-radius: var(--r-full);
}
.ed-body[data-step="working"] ~ .ed-status:not(:empty)::before {
  border-top-color: transparent; animation: edit-spin 0.8s linear infinite;
}
.ed-error:not(:empty) { padding: 0 var(--s-5) var(--s-4); }
/* A step with nothing in its body (an error after the checks, or the check itself when edit.js puts its sentence in
   the status line alone): the first line that speaks takes the body's top padding, so it does not sit on the head's
   rule. */
.ed-body:empty + .ed-status:not(:empty), .ed-body:empty + .ed-status:empty + .ed-error:not(:empty) { padding-top: var(--s-5); }
/* edit.js also writes "Checking the current values in Whop…" into the body (p.ed-wait, hidden from screen readers),
   where it has the body's room: that copy is the one shown, with the still ring, and the live status line beside it
   is read out but not shown, so the sentence appears once. */
.ed-wait { display: flex; align-items: center; gap: var(--s-2); font-size: 13px; line-height: 1.5; color: var(--text-2); }
.ed-wait::before {
  content: ""; flex: none; width: 12px; height: 12px;
  border: 1.5px solid var(--accent); border-radius: var(--r-full);
}
.ed-body:has(> .ed-wait) ~ .ed-status {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.ed-foot { position: sticky; bottom: 0; z-index: 1; }

/* Form fields: a Label over each, the inset field look (40px, 44px on touch and phones), hints in --text-3. */
.ed-label { display: block; color: var(--text-2); }
.ed-input {
  display: block; width: 100%; min-width: 0; height: 40px; padding: 0 var(--s-3);
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); color: var(--text);
  font-family: var(--sans); font-size: var(--fs-input); font-weight: 400; line-height: 1.4;
  letter-spacing: normal; text-transform: none;
  -webkit-appearance: none; appearance: none;
}
.ed-input::placeholder { color: var(--text-3); opacity: 1; }
.ed-input[aria-invalid="true"] { border-color: var(--danger); }
@media (hover: hover) {
  .ed-input:not([aria-invalid="true"]):hover, .ed-money:not(:has(.ed-input[aria-invalid="true"])):hover { border-color: var(--accent); }
}
/* The amount: "A$" inside the field's left edge, the figure in mono 16, and the focus ring on the whole field. */
.ed-money {
  display: flex; align-items: center; min-width: 0; height: 44px;
  border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2);
}
.ed-money > span { flex: none; padding-left: var(--s-3); font-size: 14px; font-weight: 500; color: var(--text-2); }
/* The figure's input runs over the field's 1px border top and bottom (it has none of its own and is transparent), so
   the tap target is the field's whole 44px, not the 42px inside the border. */
.ed-money .ed-input {
  flex: 1 1 auto; height: calc(100% + 2px); margin-block: -1px; padding-left: var(--s-2); border: 0; background: transparent;
  font-family: var(--mono); font-size: 16px; font-weight: 500;
}
.ed-money .ed-input:focus-visible { box-shadow: none; }
.ed-money:focus-within { border-color: var(--accent); box-shadow: var(--focus); }
.ed-money:has(.ed-input[aria-invalid="true"]) { border-color: var(--danger); }
.ed-hint { font-size: 12px; line-height: 1.5; color: var(--text-3); overflow-wrap: anywhere; }
/* Straight under its field in the body's 12px rhythm, the count sits 4px below the name instead. */
.ed-body > .ed-count { margin-top: calc(var(--s-2) * -1); }
.ed-note { font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.ed-count { display: block; margin-top: var(--s-1); text-align: right; font-size: 11px; line-height: 1.4; color: var(--text-3); }
/* −20% −10% +10% +20%: small outlined chips in a row (44px targets on touch through the button's hit area). */
.ed-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.ed-chip {
  position: relative; display: inline-flex; align-items: center; justify-content: center; height: 28px; min-width: 52px;
  padding: 0 var(--s-2); border: 1px solid var(--line-ui); border-radius: var(--r-md); background: transparent;
  color: var(--text); font-size: 12px; font-weight: 500; line-height: 1; cursor: pointer;
}
@media (hover: hover) {
  .ed-chip:hover { border-color: var(--accent); background: var(--accent-soft); }
}
/* The copies stepper: − [ 3 ] +, square buttons either side of a centred figure, in one row (div.ed-stepper). */
.ed-stepper { display: flex; align-items: center; gap: var(--s-2); }
.ed-step {
  position: relative; display: inline-grid; place-items: center; width: 40px; height: 40px; padding: 0;
  vertical-align: middle; border: 1px solid var(--line-ui); border-radius: var(--r-md); background: transparent;
  color: var(--text); font-size: 16px; font-weight: 500; line-height: 1; cursor: pointer;
}
.ed-step:disabled { opacity: 0.45; cursor: not-allowed; }
@media (hover: hover) {
  .ed-step:not(:disabled):hover { border-color: var(--accent); background: var(--accent-soft); }
}
#edit-count.ed-input, #edit-count {
  display: inline-block; width: 64px; height: 40px; vertical-align: middle; text-align: center;
  font-family: var(--mono); font-size: 16px; font-weight: 500;
}
/* Outside a stepper row (an older edit.js), the three still keep the row's 8px apart. */
:not(.ed-stepper) > .ed-step + #edit-count, :not(.ed-stepper) > #edit-count + .ed-step { margin-left: var(--s-2); }
/* A checkbox line ("Keep the original posts' likes…", "I'm sure…"): the box beside its words, a 44px row on touch. */
.ed-check, .ed-sure {
  display: flex; align-items: flex-start; gap: var(--s-2); min-height: 24px;
  font-size: 14px; line-height: 1.45; color: var(--text); cursor: pointer;
}
.ed-check input, .ed-sure input { flex: none; width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--accent); cursor: pointer; }
/* "I'm sure": framed, because Confirm waits for it; the frame lights once it is ticked. */
.ed-sure { padding: var(--s-3); border: 1px solid var(--line-ui); border-radius: var(--r-md); background: var(--surface-2); }
.ed-sure:has(> input:checked) { border-color: var(--accent); background: var(--accent-soft); }
@media (pointer: coarse) {
  .ed-chip::after { content: ""; position: absolute; inset: -8px -4px; }
  .ed-chip { height: 36px; }
  .ed-step { width: 44px; height: 44px; }
  #edit-count.ed-input, #edit-count { height: 44px; }
  .ed-input { height: 44px; }
  .ed-check, .ed-sure { min-height: 44px; align-items: center; }
  .ed-check input, .ed-sure input { margin-top: 0; }
}

/* Review: the change in words as the headline, then the figures, mono, before → after, with the change chip and the
   US$ line under them; the warning box for a big jump, and "I'm sure". The arrow is drawn here, before the after
   figure, in --text-3 (alt text "", so a screen reader reads the two figures, which the headline already joins). */
.ed-words { font-size: 15px; font-weight: 600; line-height: 1.45; color: var(--text); overflow-wrap: anywhere; }
.ed-diff {
  display: flex; flex-direction: column; gap: var(--s-2); margin: 0; padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.ed-diff-row { display: grid; grid-template-columns: 112px minmax(0, 1fr); align-items: baseline; gap: var(--s-3); min-width: 0; }
.ed-diff dt { color: var(--text-3); }
.ed-diff dd {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-2); min-width: 0; margin: 0;
  font-size: 14px; line-height: 1.5; color: var(--text-3); overflow-wrap: anywhere;
}
.ed-from { color: var(--text-2); }
.ed-to { font-weight: 600; color: var(--text); }
.ed-to::before { content: "\2192"; content: "\2192" / ""; margin-right: var(--s-2); font-weight: 400; color: var(--text-3); }
/* A row with only the new value (nothing before it) gets no arrow. */
.ed-to:first-child::before { content: none; }
/* When edit.js writes the arrow itself (span.ed-arrow, hidden from screen readers), that one is the arrow, in the same
   quiet colour, and the drawn one stands down: a change never reads "→ →". */
.ed-arrow { font-weight: 400; color: var(--text-3); }
.ed-arrow ~ .ed-to::before { content: none; }
.ed-pct {
  display: inline-flex; align-items: center; height: 20px; padding: 0 6px;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface);
  font-size: 12px; font-weight: 500; line-height: 1; color: var(--text);
}
.ed-pct[data-dir="up"] { border-color: rgba(161, 249, 99, 0.40); color: var(--accent-hover); }
.ed-pct[data-dir="down"] { color: var(--text-2); }
.ed-usd { flex-basis: 100%; font-size: 12px; line-height: 1.5; color: var(--text-3); }
.ed-warn {
  padding: var(--s-2) var(--s-3);
  border: 1px solid rgba(255, 194, 61, 0.40); border-left: 2px solid var(--warning); border-radius: var(--r-md);
  background: var(--warning-soft); color: var(--warning-text);
  font-size: 13px; line-height: 1.5; overflow-wrap: anywhere;
}
.ed-warn:empty { display: none; }

/* Duplicate progress: one row per copy, an LED (amber while Whop makes it, lit once it is a draft, red if Whop gave
   up, a hollow amber ring when it is slow), the copy's name, and what is happening to it under the name. */
.ed-copies { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.ed-copy {
  display: grid; grid-template-columns: 6px minmax(0, 1fr); grid-template-areas: "led name" ". state";
  align-items: center; gap: 2px var(--s-3); padding: var(--s-2) 0; border-top: 1px solid var(--line-neutral);
}
.ed-copy:first-child { border-top: 0; padding-top: 0; }
.ed-copy-led { grid-area: led; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--led-warn); }
/* The name wraps rather than ending in an ellipsis: the copies differ only by the "(copy 2)" at the end of the name,
   and a phone's sheet cut exactly that off, leaving three identical rows. */
.ed-copy-name {
  grid-area: name; min-width: 0; overflow-wrap: anywhere;
  font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--text);
}
.ed-copy-state { grid-area: state; font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
.ed-copy[data-state="draft"] .ed-copy-led { background: var(--led-on); }
.ed-copy[data-state="draft"] .ed-copy-state { color: var(--accent-hover); }
.ed-copy[data-state="gone"] .ed-copy-led { background: var(--led-bad); }
.ed-copy[data-state="gone"] .ed-copy-state { color: var(--danger-text); }
.ed-copy[data-state="slow"] .ed-copy-led { background: transparent; border: 1px solid var(--led-warn); }
.ed-copy[data-state="slow"] .ed-copy-state { color: var(--warning-text); }

@media (max-width: 860px) {
  .edit-pin, .edit-dialog {
    inset: auto 0 0 0; width: 100%; max-width: 100%; margin: 0;
    max-height: 92vh; max-height: 92dvh;
    border-bottom: 0; border-radius: var(--r-xl) var(--r-xl) 0 0;
  }
  .edit-pin[open], .edit-dialog[open] { animation: sheet-up var(--dur-3) var(--ease); }
  /* The grab bar: decorative, like the picker's (no drag-to-dismiss). */
  .ep-inner::before, .ed-inner::before {
    content: ""; position: absolute; top: var(--s-2); left: calc(50% - 18px); width: 36px; height: 4px;
    border-radius: var(--r-full); background: var(--border-strong);
  }
  .ep-inner { padding: var(--s-6) var(--s-4) 0; }
  .ed-head { padding: var(--s-5) var(--s-2) var(--s-3) var(--s-4); }
  .ed-body { padding: var(--s-4); }
  .ed-status:not(:empty), .ed-error:not(:empty) { padding-inline: var(--s-4); }
  .ed-body:empty + .ed-status:not(:empty), .ed-body:empty + .ed-status:empty + .ed-error:not(:empty) { padding-top: var(--s-4); }
  .ep-foot { margin-inline: calc(var(--s-4) * -1); }
  /* Equal halves, 44px tall; a step with one button (Done, Close) gives it the whole width rather than leaving the
     other half empty, since edit.js hides the button a step does not use. */
  .ep-foot, .ed-foot {
    flex-wrap: nowrap;
    padding: var(--s-3) var(--s-4) calc(var(--s-3) + var(--safe-bottom));
  }
  .ep-foot .btn, .ed-foot .btn { flex: 1 1 0; min-width: 0; height: 44px; }
  .ed-input, .ed-money { height: 44px; }
  .ed-diff-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}

/* ---------- toasts ---------- */
/* The console's voice as a small stack at the bottom right (360px; ≤ 860px the width of the screen less the gutters,
   above the tab bar): opaque, the control line, an LED by tone (ok lit, warn amber, error red, with a matching left
   edge for the last two), the sentence, an optional action ("SHOW IN CAMPAIGNS") and Dismiss. They never take the
   pointer outside their own boxes and never take focus. Each enters on opacity and an 8px rise, not at all under
   reduced motion. The stack's right edge lines up with the briefing orb (jarvis.css) and its foot stands 12px above
   the orb's 44px slot: the orb lives outside .app, so it paints over anything in it, and a toast in the corner lost
   its Dismiss button under it. */
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
.edit-toasts {
  --toast-lift: calc(44px + var(--s-3));
  position: fixed; right: max(var(--s-6), env(safe-area-inset-right, 0px)); bottom: calc(var(--s-6) + var(--toast-lift)); z-index: 70;
  display: flex; flex-direction: column; align-items: stretch; gap: var(--s-2);
  width: min(360px, calc(100vw - 2 * var(--s-4))); pointer-events: none;
}
/* LED · sentence · Dismiss on the first line, the LED level with the sentence's first line and Dismiss at the top
   right; an action ("SHOW IN CAMPAIGNS") goes on a line of its own under the sentence, so a long sentence keeps the
   toast's width instead of being squeezed into a column beside it. */
.edit-toast {
  position: relative; display: grid; grid-template-columns: 6px minmax(0, 1fr) auto;
  grid-template-areas: "led text close" ". action .";
  align-items: start; column-gap: var(--s-3); row-gap: 0;
  min-width: 0; padding: var(--s-2) var(--s-2) var(--s-2) var(--s-4);
  border: 1px solid var(--line-ui); border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-pop);
  pointer-events: auto; animation: toast-in var(--dur-3) var(--ease);
}
.edit-toast::before {
  content: ""; grid-area: led; width: 6px; height: 6px; margin-top: calc(var(--s-1) + 13px * 0.75 - 3px);
  border-radius: var(--r-full); background: var(--led-on);
}
.edit-toast[data-tone="warn"] { border-left: 2px solid var(--warning); }
.edit-toast[data-tone="warn"]::before { background: var(--led-warn); }
.edit-toast[data-tone="error"] { border-left: 2px solid var(--danger); }
.edit-toast[data-tone="error"]::before { background: var(--led-bad); }
.edit-toast-text { grid-area: text; min-width: 0; padding: var(--s-1) 0; font-size: 13px; line-height: 1.5; color: var(--text); overflow-wrap: anywhere; }
.edit-toast-action {
  position: relative; grid-area: action; justify-self: start; height: 28px; margin-left: calc(var(--s-1) * -1);
  padding: 0 var(--s-1); border: 0; border-radius: var(--r-sm); background: none;
  color: var(--accent-hover); white-space: nowrap; cursor: pointer; text-underline-offset: 3px;
}
.edit-toast-close {
  position: relative; grid-area: close; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: var(--r-md); background: none; color: var(--text-2); font-size: 16px; line-height: 1; cursor: pointer;
}
.edit-toast-close svg { width: 14px; height: 14px; }
@media (hover: hover) {
  .edit-toast-action:hover { text-decoration: underline; }
  .edit-toast-close:hover { background: var(--surface-hover); color: var(--text); }
}
@media (pointer: coarse) {
  .edit-toast-action::after, .edit-toast-close::after { content: ""; position: absolute; inset: -8px; }
  .edit-toast-action::after { inset: -8px -4px; }
}
@media (max-width: 860px) {
  .edit-toasts {
    right: max(var(--gutter), env(safe-area-inset-right, 0px)); left: max(var(--gutter), env(safe-area-inset-left, 0px));
    bottom: calc(var(--tabbar-h) + var(--safe-bottom) + var(--s-3) + var(--toast-lift)); width: auto;
  }
}

/* ---------- Data → Editing ---------- */
/* The setup as one connection row (on, off and why, or the sample), the lock line, then the last 50 changes as a small
   table: when, who ("This browser" / "Session a1b2c3d4"), what (the change in words) and the result with an LED
   (applied lit, already neutral, rejected and failed red, timeout and conflict amber; the word always beside it).
   ≤ 639px each change is a card: when and result across the top, the words under them, who last. */
.edit-panel { min-width: 0; }
/* The panel follows views.js's body, which is empty before the section's first draw and then holds four skeleton
   panels (305px at 1440) until the status answer, which is never stored, lands and the section grows to its full
   height (2,200px or so). Shown meanwhile, the panel sat under the skeletons and was pushed a screen down: a 0.2
   layout shift on every reload of #/data. It waits until the body has drawn its figures; it is the last thing in the
   section, so coming in then moves nothing. The test is one :has() on one element, bounded by child combinators. */
#data-body:empty + .edit-panel, #data-body:has(> .section-grid > [aria-busy="true"]) + .edit-panel { display: none; }
.edp-setup {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--s-1) var(--s-2);
  min-width: 0; padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2);
}
.edp-setup > .edp-setup-meta { grid-column: 1 / -1; }
/* On the first line of a sentence that wraps (it sat 20px below it at 375px, centred on three lines). */
.edp-setup { align-items: start; }
.edp-setup > .led { margin-top: calc((14px * 1.4 - 6px) / 2); }
.edp-setup .led { background: var(--led-off); }
.edp-setup[data-state="on"] .led { background: var(--led-on); }
.edp-setup[data-state="mock"] .led { background: var(--led-warn); }
.edp-setup[data-state="off"] .led { background: transparent; border: 1px solid var(--led-off); }
.edp-setup-text { min-width: 0; font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.edp-setup-meta { font-family: var(--mono); font-size: 11px; line-height: 1.5; letter-spacing: 0.04em; color: var(--text-3); overflow-wrap: anywhere; }
.edp-lock {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); margin-top: var(--s-3);
  font-size: 13px; line-height: 1.5; color: var(--text-2);
}
.edp-lock:empty { display: none; }
.edp-table { margin-top: var(--s-4); font-size: 13px; }
.edp-table :is(th, td):first-child { padding-left: 0; }
.edp-table :is(th, td):last-child { padding-right: 0; }
.edp-table thead th { color: var(--text-2); }
.edp-when, .edp-who { font-size: 12px; white-space: nowrap; color: var(--text-2); }
.edp-what { min-width: 12em; color: var(--text); overflow-wrap: anywhere; }
/* Whop's own words when a change was refused or failed, after the change in words. */
.edp-error { color: var(--danger-text); }
.edp-result { white-space: nowrap; color: var(--text-2); }
.edp-result::before {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-right: var(--s-2); vertical-align: 1px;
  border-radius: var(--r-full); background: var(--led-off);
}
.edp-row[data-result="applied"] .edp-result { color: var(--accent-hover); }
.edp-row[data-result="applied"] .edp-result::before { background: var(--led-on); }
.edp-row:is([data-result="rejected"], [data-result="failed"]) .edp-result { color: var(--danger-text); }
.edp-row:is([data-result="rejected"], [data-result="failed"]) .edp-result::before { background: var(--led-bad); }
.edp-row:is([data-result="timeout"], [data-result="conflict"]) .edp-result { color: var(--warning-text); }
.edp-row:is([data-result="timeout"], [data-result="conflict"]) .edp-result::before { background: var(--led-warn); }
.edp-empty, .edp-note { margin-top: var(--s-3); font-size: 13px; line-height: 1.5; color: var(--text-2); overflow-wrap: anywhere; }
@media (hover: hover) {
  .edp-table tbody tr:hover :is(td, th) { background: none; }
}
@container main (max-width: 639.98px) {
  .edp-table, .edp-table tbody, .edp-table tr, .edp-table td { display: block; }
  .edp-table thead { display: none; }
  .edp-table .edp-row {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "when result" "what what" "who who";
    gap: var(--s-1) var(--s-3); padding: var(--s-3) 0; border-top: 1px solid var(--line-neutral);
  }
  .edp-table .edp-row:first-child { padding-top: 0; border-top: 0; }
  .edp-table .edp-row > td { height: auto; min-width: 0; padding: 0; border: 0; }
  .edp-table .edp-when { grid-area: when; }
  .edp-table .edp-result { grid-area: result; text-align: right; }
  .edp-table .edp-what { grid-area: what; min-width: 0; }
  .edp-table .edp-who { grid-area: who; color: var(--text-3); }
}

/* Reduced motion: the menu, the dialogs and the toasts appear in place, the thumb jumps, and the busy ring is a still
   "…" in the thumb's place (the global rule in section 7 already stops every animation; this says it outright, after
   the rules that start them, so nothing is left turning). */
@media (prefers-reduced-motion: reduce) {
  .edit-menu, .edit-pin[open], .edit-dialog[open], .edit-toast { animation: none; }
  .edit-switch-thumb { transition: none; }
  .edit-switch.is-busy .edit-switch-thumb::after {
    content: "\2026"; content: "\2026" / ""; inset: -3px -2px; display: grid; place-items: center;
    border: 0; border-radius: 0; animation: none;
    font-family: var(--mono); font-size: 11px; line-height: 1; color: var(--text-2);
  }
  .ed-body[data-step="working"] ~ .ed-status:not(:empty)::before { animation: none; border-top-color: var(--accent); }
}

/* ---------- forced colours ---------- */
/* Panels keep their borders and lose the brackets, the backdrop goes, LEDs, swatches and chart marks become
   CanvasText, and every selected state (tab, segment, preset, range end) gets a Highlight outline, since the tints
   that showed it are dropped. */
@media (forced-colors: active) {
  .card::before, .empty-state-icon::before, .range-pop::before, .ambient, .scan, .range-pop-inner::before,
  .stat-strip > .stat::before { display: none; }
  .nav-link[aria-current="page"], .tab.active, .tab[aria-pressed="true"], .tab[aria-selected="true"],
  .seg-btn[aria-pressed="true"], .preset[aria-pressed="true"], .cal-day.is-start, .cal-day.is-end,
  .tabbar-link[aria-current="page"], .tabbar-more[data-active="true"], .more-link[aria-current="page"] {
    outline: 2px solid Highlight; outline-offset: -2px;
  }
  .nav-badge, .tabbar-badge, .creative-card, .conn, .tag { border-color: CanvasText; }
  .nav-badge[data-tone="warn"]::before, .step .step-live::before { forced-color-adjust: none; background: CanvasText; }
  .band-fill, .method-fill, .cash-split-new { forced-color-adjust: none; background: CanvasText; }
  .cash-split-repeat { forced-color-adjust: none; background: transparent; border-color: CanvasText; }
  .band::after, .cash-split::before, .method-track { border: 1px solid CanvasText; }
  .leads-bar { fill: CanvasText; stroke: CanvasText; }
  .leads-bar.leads-bar-other { fill: Canvas; }
  .creative-card:has(> .creative-link:focus-visible) { outline-color: Highlight; }
  .cal-day.is-in-range { outline: 1px dotted Highlight; outline-offset: -3px; }
  .badge { border: 1px solid CanvasText; }
  .badge[class*="st-"]::before, .led, .trend-tip-sw { forced-color-adjust: none; background: CanvasText; }
  .skeleton { background: GrayText; }
  .trend-bar, .trend-swatch { fill: CanvasText; stroke: CanvasText; }
  .trend-cross, .trend-hits .trend-hit:focus-visible { stroke: Highlight; }
  .trend-dot { fill: Highlight; }
  .trend-hl { fill: transparent; }
  .spark-line, .spark-last { stroke: CanvasText; }
  .spark-area { fill: transparent; }
  .thumb { border-color: CanvasText; }
  /* The Cortana build: pills, tags and dots keep their words with a plain outline; bars and fills become CanvasText on
     an outlined track; the dropdown's active row and the modal's outline use the system colours. */
  .src-pill, .journey-tag, .pm-pill, .funnel-conv, .ov-creative, .ov-health, .notes-summary, .journey-dot,
  .people-modal, .journey-dialog, .dd-panel, .dd-button { border-color: CanvasText; }
  .src-dot, .funnel-fill, .src-fill, .share-fill, .ov-health-item::before, .journey-item::before {
    forced-color-adjust: none; background: CanvasText;
  }
  .funnel-bar, .src-share, .share { border: 1px solid CanvasText; }
  .dd-option.is-active { outline: 2px solid Highlight; outline-offset: -2px; }
  .people-link { text-decoration-color: LinkText; }
  /* The edit build: the switch is an outlined track with a CanvasText thumb (Highlight when on), the menu, dialogs,
     toasts and strips keep a CanvasText outline, the focused menu item a Highlight one, and every dot is CanvasText. */
  .edit-switch-track, .edit-menu, .edit-pin, .edit-dialog, .edit-toast, .edit-parent-inner, .edp-setup, .ed-diff,
  .ed-money, .ed-sure, .ed-warn, .edit-sample, .ed-pct { border-color: CanvasText; }
  .edit-switch-thumb, .edit-switch.is-updated::before, .edit-pencil.is-updated::before, .edit-more.is-updated::before,
  .ed-copy-led, .edit-toast::before, .edp-result::before { forced-color-adjust: none; background: CanvasText; }
  .edit-switch.is-on .edit-switch-thumb { background: Highlight; }
  .edit-switch.is-busy .edit-switch-thumb { background: transparent; }
  .edit-menu-item:focus, .edit-more.is-open { outline: 2px solid Highlight; outline-offset: -2px; }
  .edit-unlock::before, .edit-unlocked::before { forced-color-adjust: none; background: CanvasText; }
}

/* ==========================================================================
   Perf: fallback fonts. Keep this block last: it wins ties with everything above.
   ========================================================================== */
/* The "Early" faces: the very latin files Google's stylesheet names today (DM Sans v17, JetBrains Mono v24, one
   variable file each for 400–600), declared here so they need no round trip for that stylesheet, and preloaded by
   index.html, which lets Chrome hold the first paint a moment for them. The Clients table used to be laid out in the
   stand-ins before Google's sheet arrived; a long "Came from" path then wrapped to three lines and went back to two
   when DM Sans landed (CLS 0.0135 on an empty-cache load at 1440). Google's own families stay first in --sans and
   --mono and win once their sheet lands: the same file, so nothing moves. If Google moves to a new version these still
   load (then swap to the new one, as before); if a file ever 404s they are skipped and the stand-ins hold the layout.
   Refresh both URLs, here and in index.html's preloads, from the latin blocks of the css2 URL in index.html. */
@font-face {
  font-family: "DM Sans Early"; font-style: normal; font-weight: 400 600; font-display: swap;
  src: url(https://fonts.gstatic.com/s/dmsans/v17/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K6z9mXg.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono Early"; font-style: normal; font-weight: 400 600; font-display: swap;
  src: url(https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwgknk-4.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* DM Sans is loaded from Google Fonts (index.html); until it lands, text is set in a stand-in stretched to match it:
   Arial (iOS, macOS, Windows, most Linux) or Roboto (Android), scaled to DM Sans's average width and given its line
   metrics, so when DM Sans arrives the letters change shape but line breaks and heights don't. Same unicode-range as
   DM Sans's latin file, so symbols it lacks (→ ≥ …) still come from the system font. */
@font-face {
  font-family: "DM Sans Fallback"; font-weight: 400;
  src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("Arimo");
  size-adjust: 103.8%; ascent-override: 95.6%; descent-override: 29.9%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans Fallback"; font-weight: 500;
  src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("Arimo");
  size-adjust: 104.4%; ascent-override: 95%; descent-override: 29.7%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans Fallback"; font-weight: 600 900;
  src: local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold"), local("Arimo Bold");
  size-adjust: 100%; ascent-override: 99.2%; descent-override: 31%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans Fallback Android"; font-weight: 400;
  src: local("Roboto"), local("Roboto Regular"), local("Roboto-Regular");
  size-adjust: 104.4%; ascent-override: 95%; descent-override: 29.7%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans Fallback Android"; font-weight: 500;
  src: local("Roboto Medium"), local("Roboto-Medium");
  size-adjust: 104.5%; ascent-override: 94.9%; descent-override: 29.7%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans Fallback Android"; font-weight: 600 900;
  src: local("Roboto Bold"), local("Roboto-Bold");
  size-adjust: 106.2%; ascent-override: 93.4%; descent-override: 29.2%; line-gap-override: 0%;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono stand-ins. A monospace only has to match one advance width, so the swap is exact: JetBrains Mono is
   600/1000 em wide, ascent 1020, descent 300, no line gap, and the overrides are divided by size-adjust so the line
   box is the real font's. Widths do not change with weight in a monospace, so 500 and 600 synthesise from these
   faces without reflow. Windows first: Courier New is installed there too and would otherwise win the face below. */
@font-face {
  font-family: "JetBrains Mono Fallback Consolas";
  src: local("Consolas");
  size-adjust: 109.13%; ascent-override: 93.47%; descent-override: 27.49%; line-gap-override: 0%;
}
/* Menlo (macOS, iOS), DejaVu Sans Mono (Linux), Droid Sans Mono (Android), Courier New: all about 0.600 em wide. */
@font-face {
  font-family: "JetBrains Mono Fallback";
  src: local("Menlo"), local("Menlo-Regular"), local("DejaVu Sans Mono"), local("Droid Sans Mono"), local("Courier New");
  size-adjust: 99.65%; ascent-override: 102.36%; descent-override: 30.11%; line-gap-override: 0%;
}

/* Meta ads (assets/meta.js): the Overview card and the Data card. */
.ov-meta { margin-top: var(--s-5); }
.ov-meta .panel-head { row-gap: var(--s-1); }
.ov-meta .panel-meta { flex: 1 1 auto; text-align: right; }
.meta-panel { margin-top: var(--s-5); }
.meta-strip { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.meta-strip .stat-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--s-2); }
.meta-strip .skeleton { display: block; height: 1em; margin-top: var(--s-2); }
.meta-bars { margin: var(--s-5) 0 0; }
.meta-bars-cap { font-size: 11px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-2); margin-bottom: var(--s-2); }
.meta-bars-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 4px; height: 64px; padding-bottom: 1px; border-bottom: 1px solid var(--border); }
.meta-bar { position: relative; flex: 1 1 0; min-width: 2px; max-width: 40px; height: 100%; display: flex; align-items: flex-end; justify-content: center; border-radius: 2px; }
.meta-bar:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.meta-bar-fill { display: block; width: 70%; min-width: 2px; border-radius: 2px 2px 0 0; background: color-mix(in srgb, var(--accent, currentColor) 30%, transparent); transition: background-color .15s; }
.meta-bar-fill.has-leads { background: color-mix(in srgb, var(--accent, currentColor) 70%, transparent); }
.meta-bar:hover .meta-bar-fill { background: var(--accent, currentColor); }
.ov-meta .panel-body > .panel-note, .meta-panel .panel-body > .panel-note { margin: var(--s-3) 0 0; }
.meta-panel .panel-body > .meta-tools + .panel-note { margin-top: 0; }
.meta-bars-axis { display: flex; justify-content: space-between; margin-top: var(--s-1); font-size: 11px; color: var(--text-2); }
.ov-meta .table-wrap, .meta-panel .table-wrap { margin-top: var(--s-4); }
.meta-table .meta-name { max-width: 34ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.meta-tools { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-bottom: var(--s-4); }
.meta-panel .total-row td { font-weight: 600; border-top: 1px solid var(--border, currentColor); }
.meta-error { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); }
.meta-retry { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; text-decoration: underline; }
@media (max-width: 640px) {
  .ov-meta .panel-meta { text-align: left; flex-basis: 100%; }
  .meta-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .meta-table .meta-name { max-width: 18ch; }
}
