/* public/dashboard-assets/dashboard.css
 *
 * The Agency dashboard's ONE shared stylesheet - every server-rendered
 * page under webapp/functions/dashboard/*.ts links to this exact file
 * (functions-shared/html.ts's own renderPage()), so editing THIS file
 * changes how the whole dashboard looks, everywhere, consistently.
 *
 * ---------------------------------------------------------------
 * 2026-08-18 - VISUAL RESTYLE onto the Pic2Vdo brand direction
 * "Signal" (external designer round, Pic2Vdo-dashboard-restyle-2026-08-20).
 * Only property VALUES changed plus new rules; the set of class/#id
 * selectors is byte-identical to the previous version except for two
 * additions (.sortable-table, .probability-input) that were ALREADY
 * present in the server-rendered markup and simply had no styling yet.
 * Nothing was renamed or removed, so every dashboard.js /
 * analytics-charts.js / quality-check.js binding still resolves.
 *
 * WHAT CHANGED, AND WHY:
 *   - Theme flipped dark -> light, per direct decision (2026-08-22 -
 *     "for the sake of simplicity, let's use the light mode for
 *     everyone" - a per-Agency dark/light toggle was considered and
 *     explicitly dropped in favor of one theme, one file, no branching).
 *     The previous dark theme's accent was #03dac5, Android's stock
 *     Material teal, which appears nowhere in the Pic2Vdo brand. The
 *     deck and the website both run Signal: near-black ink and #1B45E8
 *     blue on a warm off-white.
 *   - Everything runs off custom properties (:root below). Change a
 *     token, the whole dashboard follows.
 *   - Spacing was ad-hoc (0.25/0.4/0.5/0.6/0.75/0.9/1.1/1.25rem, chosen
 *     per-rule). It now uses one 8px-based scale, so cards, tables and
 *     forms share a rhythm instead of each having their own.
 *   - Type had almost no hierarchy (h1 1.4rem against 1rem body). The
 *     scale below separates page title / section / body / meta properly.
 *   - Buttons dropped the 999px pill for a 6px radius. The pill was the
 *     single strongest "generic Material app" tell in the old UI.
 *
 * 2026-08-22 - RECONCILIATION pass (this codebase's own, not the
 * external designer's). The designer's snapshot predates four features
 * built here afterward (grids-with-scrollbar, invoicing) and their own
 * self-audit claim of "0 selectors removed" was independently checked
 * and found wrong for two of them - both silently dropped from their
 * file despite being live, JS-bound selectors:
 *   - `.grid-scroll` (Analytics' three grids + payment-history.ts) - re-
 *     added below, expressed on this file's own tokens.
 *   - `.chart-title` (analytics-charts.js's per-chart heading) - re-added.
 *   - The Chrome autofill fix on login inputs - re-added.
 *   - `section.card.print-block, .card.print-block` (Invoice print-view
 *     specificity fix, 2026-08-21, postdates the designer's snapshot) -
 *     re-added inside @media print below.
 * Also added `.accent-text`/`.accent-border` (general-purpose, so the
 * ~11 hardcoded `style="color:#03dac5"`/`#ccc`/`#ffd98a` literals still
 * sitting directly in the .ts route files - which no stylesheet change
 * can ever reach - could be converted to real classes that track this
 * file's own tokens; see Developments/ for the full file list).
 *
 * FOR A DESIGNER WORKING ON THIS FILE:
 *   - This is the ONLY file you should need to edit for a visual
 *     restyle of the dashboard.
 *   - You may freely change property VALUES on any existing rule and
 *     ADD new rules/media queries. Prefer changing a token in :root
 *     over editing an individual rule.
 *   - Do NOT rename, remove, or add any selector that doesn't already
 *     exist here (class names and #ids are relied on by both the
 *     server-rendered pages and public/dashboard-assets/dashboard.js /
 *     analytics-charts.js / quality-check.js - renaming one silently
 *     breaks a specific feature, often invisibly, e.g. a lightbox that
 *     never opens or a chart that renders unstyled).
 *   - Do NOT edit any other file in this project.
 *   - Media stages (.logo-preview, .thumb img, #corner-marker-canvas img)
 *     deliberately keep a DARK backdrop even on this light theme -
 *     agency logos are usually transparent PNGs with white artwork, and
 *     a white-on-white preview shows nothing at all.
 */

:root {
  color-scheme: light;

  /* ---- Signal brand tokens (assets/brand-direction-signal.html) ---- */
  /* --page/--elevated darkened 2026-08-23 (per direct report: "I do not
     see any difference in the pages background color to enhance
     visibility. I need the background in all pages to be a bit
     darker") - was #F7F7F6/#F1F0EE, barely distinguishable from
     --surface's own #FFFFFF card background at a glance. Both shifted
     down together (not just --page alone) to keep --elevated's own
     purpose intact - it's used dashboard-wide as a HOVER/emphasis state
     (table rows, secondary buttons, dropzones - see this file's own
     other --elevated usages), which needs to stay visibly darker than
     the page's own resting background or hovering would show no
     contrast at all. --surface stays pure white - cards popping clearly
     against a darker page background is the whole point of this
     change. */
  --page: #EFEEEC;
  --surface: #FFFFFF;
  --elevated: #E8E6E3;
  --ink: #0A0A0A;
  --muted-ink: #5C5C5C;
  --border: #DDDCDA;
  --border-strong: #C4C3C0;

  --accent: #1B45E8;
  --accent-deep: #1533B8;
  --accent-tint: #E6EBFE;

  /* Status colours re-picked for a light background. The old greens and
     teals were chosen against #0a0a0a and drop to ~2:1 contrast on white. */
  --danger: #C81E1E;
  --danger-tint: #FDECEC;
  --success: #0F7A3D;
  --success-tint: #E7F5EC;
  --warning: #8A5A00;
  --warning-tint: #FFF4DF;

  /* ---- 8px spacing scale ---- */
  --s1: 0.5rem;
  --s2: 1rem;
  --s3: 1.5rem;
  --s4: 2.5rem;
  --s5: 4rem;

  /* ---- type scale ---- */
  --t1: 0.75rem;
  --t2: 0.875rem;
  --t3: 1rem;
  --t4: 1.25rem;
  --t5: 1.75rem;

  /* ---- shape ---- */
  --r: 10px;
  --r-sm: 6px;
  --max: 1240px;
  --shadow: 0 1px 2px rgba(10, 10, 10, 0.04), 0 1px 3px rgba(10, 10, 10, 0.06);
  --ring: 0 0 0 3px var(--accent-tint);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: var(--s4) var(--s3) var(--s5);
  background: var(--page);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--t3);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

main { max-width: var(--max); margin: 0 auto; }

/* Per CLAUDE.md "Next tasks" (2026-08-22) - shared strip above every page's
   own content (functions-shared/html.ts's own renderPage(), the ONE place
   this reaches all 17 dashboard routes at once). Matches main's own
   max-width/centering so the logo lines up with page content below it,
   not the browser edge. */
/* Strengthened 2026-08-23 (per direct report: "Help button with bigger
   font (now not visible enough)" + "darker background for better
   contrast") - was see-through (inherited --page's own near-white
   background) with a thin bottom border only, and the Help link was
   --muted-ink grey at --t2 (14px) - both easy to miss. Now its own
   --elevated band (one step darker than the page, still light enough
   that the logo's own hard-white background - it's a plain JPEG, no
   transparency - doesn't read as a jarring box) with a real border, and
   Help is styled as an actual secondary button (reusing button.secondary's
   own look, not a new pattern) instead of a plain small link. */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s2) var(--s2);
  margin-bottom: var(--s3);
  background: var(--elevated);
  border-bottom: 1px solid var(--border-strong);
}
.site-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  max-width: var(--max);
  margin: 0 auto;
  width: 100%;
}
.site-header-logo { height: 28px; width: auto; display: block; background: var(--surface); padding: 0.3rem 0.6rem; border-radius: var(--r-sm); }
.site-header-help {
  display: inline-flex;
  align-items: center;
  font-size: var(--t3);
  font-weight: 700;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  padding: 0.4rem 1rem;
  text-decoration: none;
}
.site-header-help:hover { color: #FFFFFF; background: var(--accent); border-color: var(--accent); }

h1 {
  font-size: var(--t5);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0 0 var(--s1);
}
h2 {
  font-size: var(--t4);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  margin: var(--s4) 0 var(--s2);
}
h3 { font-size: var(--t3); font-weight: 700; letter-spacing: -0.005em; }

/* .muted is the workhorse - it is a paragraph of helper text, a table
   cell, AND an inline pill (the campaign "Ended" badge, which the page
   markup gives its own inline display/padding/radius). vertical-align
   fixes that pill: an inline-block with padding aligned on its baseline
   rides visibly higher than the plain text beside it in the same row. */
.muted {
  color: var(--muted-ink);
  font-size: var(--t2);
  vertical-align: middle;
}

/* Originally analytics.ts's own explanatory paragraphs only ("Scoped to:",
   the unknown-CTA-status note, "Billing methodology" - bumped above the
   base .muted size since those notes explain how money is calculated and
   were the smallest text on the page). Per CLAUDE.md "Next tasks"
   (2026-08-22, "make the font of explanatory wording a bit larger for
   better visibility") - generalized dashboard-wide to every genuinely
   explanatory/reasoning paragraph (why something works the way it does, a
   consequence of an action, a caveat), applied ALONGSIDE .muted
   (`class="muted analytics-note"`, same combination already used here and
   in payment-history.ts) wherever that pattern already existed. Left
   .muted itself untouched - it still carries timestamps, "Agency: X"
   labels, empty-state one-liners, status pills and short field hints
   dashboard-wide, none of which this request was about, and touching it
   directly risked breaking the layout of every inline pill/badge that
   relies on its current size. */
.analytics-note { font-size: var(--t3); line-height: 1.55; color: #45474A; max-width: 78ch; }

section.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s3);
  margin-bottom: var(--s3);
  box-shadow: var(--shadow);
}

label {
  display: block;
  font-size: var(--t2);
  font-weight: 500;
  color: var(--ink);
  margin-bottom: var(--s1);
}

/* Plain block flow, NOT flex-column. The gap here is 0, so for an ordinary
   stacked form the two are visually identical - but flex-direction:column
   used to leak onto pages that set their own display:flex inline and expect
   a ROW. analytics.ts's time-frame filter bar is the visible case: it sets
   display:flex + align-items:flex-end inline, inherited column from here,
   and so rendered as a right-aligned stack with a wide empty card beside it.
   Block flow leaves those pages' inline display:flex to mean what they meant. */
form { display: block; max-width: 420px; }
/* Per CLAUDE.md ("instead of a 4 rows by 1 column display it
   as a 2 by 2") - opt-in wider form + a 2-column row, used by
   campaigns.ts's "New campaign" AND "Edit campaign" forms
   (name+timezone, then starts+ends); other forms keep the default
   single-column layout above untouched. */
form.wide { max-width: 680px; }
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s3); }
.form-row-2 > div { min-width: 0; }

input[type="text"], input[type="number"], input[type="email"], input[type="password"], select {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--t3);
  margin-bottom: var(--s2);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* The old theme had no focus style at all - keyboard users had no idea
   which field they were in. */
input[type="text"]:focus-visible, input[type="number"]:focus-visible,
input[type="email"]:focus-visible, input[type="password"]:focus-visible,
select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}
input:disabled, select:disabled {
  opacity: 1;
  background: var(--elevated);
  color: var(--muted-ink);
  cursor: not-allowed;
}
/* Per CLAUDE.md "Next tasks" ("the 'horrible' background color in the
   input fields in agency login page - see agency_login.JPG") - that's
   Chrome's own autofill styling (a saved email/password gets
   autofilled), not a bug in this stylesheet's own input background -
   Chrome forces a pale-yellow input background + black text on any
   autofilled field via its internal UA stylesheet, at a specificity
   normal CSS can't override directly. The standard, only-working fix:
   an absurdly large inset box-shadow the same color as the real
   background, which Chrome renders THROUGH (autofill only overrides
   background-color, not box-shadow) - `-webkit-text-fill-color` likewise
   overrides the color Chrome forces on the TEXT specifically (plain
   `color` alone is not enough once autofilled). Firefox/Safari don't
   have this quirk - harmless no-op there. Reconciled forward onto this
   theme's own --surface/--ink tokens (2026-08-22) - the fix itself is
   theme-independent, only the color values change. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--surface) inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
}

button, .button {
  padding: 0.5rem 0.95rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: #FFFFFF;
  font-family: inherit;
  font-weight: 600;
  font-size: var(--t2);
  line-height: 1.35;
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
button:hover, .button:hover { background: var(--accent-deep); border-color: var(--accent-deep); opacity: 1; }
button:focus-visible, .button:focus-visible { outline: none; box-shadow: var(--ring); }

/* Secondary is now a bordered ghost rather than a grey fill. On the old
   dark theme the grey fill read as roughly equal in weight to the
   primary, so rows of actions had no obvious main action. */
button.secondary, .button.secondary {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--ink);
}
button.secondary:hover, .button.secondary:hover { background: var(--elevated); border-color: var(--muted-ink); }

/* Destructive stays unmistakably red but sits at ghost weight until
   hovered. In the campaigns table every row previously showed a filled
   teal Duplicate next to a filled red Delete, so the loudest thing on
   the page was the one action you least want mis-clicked. */
button.danger, .button.danger {
  background: var(--surface);
  border-color: var(--danger);
  color: var(--danger);
}
button.danger:hover, .button.danger:hover { background: var(--danger); border-color: var(--danger); color: #FFFFFF; }

/* Shared alert/pill rule. These same three classes serve two jobs: a
   full-width banner (a <p> that dashboard.js re-classes after a fetch)
   and an inline status pill in a table row, where the page markup
   supplies its own display/padding/radius inline. No margin is declared
   here on purpose - the banner case inherits the <p> default, and
   declaring one would push the pill off its row baseline. */
.error, .success, .warning {
  padding: 0.7rem 0.9rem;
  border-radius: var(--r-sm);
  border: 1px solid;
  font-size: var(--t2);
  font-weight: 500;
  vertical-align: middle;
}
.error { background: var(--danger-tint); border-color: var(--danger); color: #8C1414; }
.success { background: var(--success-tint); border-color: var(--success); color: #0B5C2E; }
.warning { background: var(--warning-tint); border-color: var(--warning); color: #6B4600; }

table { width: 100%; border-collapse: collapse; }
th {
  padding: var(--s1) 0.75rem;
  background: var(--elevated);
  border-bottom: 1px solid var(--border);
  font-size: var(--t1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted-ink);
  text-align: left;
  white-space: nowrap;
}
/* The row separator lives on the ROW, not the cell, on purpose. Several
   pages render a status cell as <td style="display:inline-block"> to get
   a pill shape; an inline-block td leaves the table's row layout, so a
   border-bottom declared on td draws a stray underline around just that
   badge instead of continuing the row line. Declaring it on tr keeps one
   clean line per row whatever the cells do. */
td {
  padding: 0.7rem 0.75rem;
  border-bottom: none;
  text-align: left;
  font-size: var(--t2);
  vertical-align: middle;
}
tbody tr { border-bottom: 1px solid var(--border); }
tbody tr:hover { background: var(--elevated); }
tbody tr:last-child { border-bottom: none; }

/* Per CLAUDE.md "Next tasks" (analytics optimization item 1) - "Engagement
   per target, Overlay watch duration and CTA submission grids should
   implement a fixed size with scroll down bar" - a tall grid (many
   targets/overlays/submissions) no longer pushes the rest of the page
   down indefinitely; it scrolls within its own fixed-height box instead.
   Wraps analytics.ts's own three grids and payment-history.ts's own
   table (a caller opts in by wrapping its <table> in this div) - every
   OTHER table dashboard-wide (Manage Campaigns, Brands, submissions
   elsewhere) is UNCHANGED. `thead` is pinned via position:sticky so
   column headers (incl. the sortable-table click targets) stay visible
   while scrolling a long grid - a plain background color (not
   transparent) is required for sticky to actually look right over
   scrolled-past rows, matching section.card's own background so it reads
   as part of the same card. Reconciled forward onto this theme's own
   border/surface tokens (2026-08-22) - independently confirmed missing
   from the external designer's returned file despite their own audit
   claiming no selectors were dropped. */
.grid-scroll { max-height: 420px; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--r-sm); }
.grid-scroll table { margin: 0; }
.grid-scroll thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
.grid-scroll tbody tr:last-child td { border-bottom: none; }

/* Gallery grid (2026-08-27, campaign-grid.ts) - one column per Symbolic
   Image can genuinely run wide (up to plan-limit brands x targets across
   every gallery), so this modifier adds HORIZONTAL scroll on top of
   .grid-scroll's own vertical one - the page body itself must never
   scroll sideways.
   FOUR fixed columns (2026-08-27, CLAUDE.md "Next tasks" - "must have four
   fixed columns: name and status, Start, End, Load. The other columns
   should have horizontal sliding bar") - Gallery/Start/End pinned to the
   LEFT edge (sticky, cumulative offsets so each sits right after the one
   before it), Load pinned to the RIGHT edge - only the Symbolic-Image
   columns in between actually scroll underneath them, same idea as the
   sticky header row above (top+left/right compose fine on the same cell -
   the header row's own corner cells need both). */
.p2v-grid-scroll-x { overflow-x: auto; }
/* ROOT CAUSE (2026-08-27, CLAUDE.md "Next tasks" - "add HORIZONTAL slide
   bar... make sure the grid cells are readable") - the CSS fixed-table-
   layout algorithm sizes columns from the `width` property of cells in
   the FIRST row, NOT `min-width` - the Symbolic-Image `<th>`s only ever
   had `min-width: 140px` set (below), no `width` at all, so the algorithm
   never counted them toward "how wide does this table actually need to
   be." Per the CSS2 table spec, a table's own rendered width is the
   GREATER of its specified `width` and the sum of its column widths - with
   every scrolling column contributing 0 toward that sum, the table just
   sat at its container's own width, .p2v-grid-scroll-x's overflow-x:auto
   had nothing to ever actually scroll, and the columns were left to be
   squeezed however the browser happened to distribute the (too-small)
   remainder - exactly what made them hard to read. Fixed by giving every
   column a real, explicit `width` below (same as the 4 fixed columns
   already had via their own nth-child/last-child rules, which still
   correctly override this for those 4) - the sum now genuinely exceeds
   the container once there are enough Symbolic-Image columns, so the
   table grows past it and the scrollbar engages, same idea as the sticky
   header row above. `width: auto` (not the base `table` rule's 100%) lets
   the table size itself from that real column-width sum instead of being
   pinned to exactly the container's width regardless of it; `min-width:
   100%` keeps a small (few-column) grid still filling the full card width
   rather than leaving a gap on the right. */
.p2v-grid-table { table-layout: fixed; width: auto; min-width: 100%; }
.p2v-grid-table th, .p2v-grid-table td { width: 160px; min-width: 160px; }
/* Brand-name column headers WRAP (2026-08-27, CLAUDE.md "Next tasks" -
   "the column title should contain only the Brand name (wrapped)") -
   the base `th` rule above is white-space:nowrap (every OTHER table in
   this dashboard wants a header that never breaks), which would instead
   overflow or get clipped now that a header can be a genuinely long Brand
   name on its own, with no shorter Target name below it to anchor the
   column's width. */
.p2v-grid-table th { white-space: normal; overflow-wrap: break-word; }
.p2v-grid-table th:nth-child(-n + 3), .p2v-grid-table td:nth-child(-n + 3),
.p2v-grid-table th:last-child, .p2v-grid-table td:last-child {
  position: sticky;
  background: var(--surface);
  z-index: 1;
}
.p2v-grid-table th:nth-child(1), .p2v-grid-table td:nth-child(1) { left: 0; width: 190px; min-width: 190px; }
.p2v-grid-table th:nth-child(2), .p2v-grid-table td:nth-child(2) { left: 190px; width: 110px; min-width: 110px; }
.p2v-grid-table th:nth-child(3), .p2v-grid-table td:nth-child(3) { left: 300px; width: 110px; min-width: 110px; border-right: 1px solid var(--border); }
.p2v-grid-table th:last-child, .p2v-grid-table td:last-child { right: 0; width: 90px; min-width: 90px; border-left: 1px solid var(--border); }
.p2v-grid-table thead th { z-index: 2; }
.p2v-grid-table thead th:nth-child(-n + 3), .p2v-grid-table thead th:last-child { z-index: 3; }

/* Product Campaign timeline (2026-08-28, product-campaign-timeline.ts) -
   a plain CSS Gantt, no charting library needed at this scale (a handful
   of Product Campaigns per Agency) - each row's own bar is positioned/
   sized via inline left/width percentages the route computes server-side
   from real calendar dates. */
.p2v-timeline { padding: 0.5rem 0; }
.p2v-timeline-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--accent, #4a90d9); z-index: 1; }
.p2v-timeline-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.6rem; }
.p2v-timeline-label { width: 160px; flex: 0 0 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9em; }
.p2v-timeline-track { position: relative; flex: 1 1 auto; height: 28px; background: var(--elevated); border-radius: var(--r-sm); }
.p2v-timeline-bar {
  position: absolute;
  top: 3px;
  bottom: 3px;
  background: var(--muted-ink);
  border-radius: 4px;
  color: #fff;
  font-size: 0.75em;
  padding: 0 0.4rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  line-height: 22px;
}
.p2v-timeline-bar--live { background: var(--success, #2e7d32); }

/* .sortable-table is applied by the server-rendered pages and wired by
   dashboard.js, but had no styling at all - the headers were clickable
   with nothing to say so. */
.sortable-table th { cursor: pointer; user-select: none; }
.sortable-table th:hover { color: var(--ink); background: var(--border); }

.dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: var(--r);
  padding: var(--s3);
  text-align: center;
  color: var(--muted-ink);
  font-size: var(--t2);
  background: var(--surface);
  cursor: pointer;
  margin-bottom: var(--s2);
  transition: border-color 0.12s ease, background-color 0.12s ease, color 0.12s ease;
}
.dropzone:hover { border-color: var(--muted-ink); background: var(--elevated); }
.dropzone.dragover { border-color: var(--accent); background: var(--accent-tint); color: var(--accent-deep); }

.thumb-grid { display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s2); }
.thumb { position: relative; width: 100px; }
/* Dark media stage kept on purpose - see the header note. */
.thumb img, .thumb video {
  width: 100px; height: 100px; object-fit: cover;
  border-radius: var(--r-sm); background: var(--ink);
  border: 1px solid var(--border); cursor: zoom-in;
}
.thumb .remove {
  position: absolute; top: -6px; right: -6px;
  background: var(--danger); color: #fff;
  border: 2px solid var(--surface); border-radius: 999px;
  width: 22px; height: 22px; padding: 0; line-height: 1;
  cursor: pointer; font-weight: 700; font-size: var(--t2);
}
.thumb .remove:hover { background: #A81717; color: #fff; }

.checkbox-row { display: flex; align-items: center; gap: var(--s1); margin-bottom: var(--s1); }
.checkbox-row label { margin: 0; color: var(--ink); font-size: var(--t2); }
input[type="checkbox"] { accent-color: var(--accent); width: 1rem; height: 1rem; }

.prob-total { font-weight: 700; }
.prob-total.bad { color: var(--danger); }
.prob-total.ok { color: var(--success); }
/* Also already in the markup, previously unstyled - kept narrow so a
   column of percentage inputs lines up instead of stretching full-width. */
.probability-input { max-width: 6rem; }

.logo-preview {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--s2) var(--s3);
  margin-bottom: var(--s2);
}
.logo-preview img { max-height: 8rem; max-width: 60vw; object-fit: contain; cursor: zoom-in; }

#lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgba(10, 10, 10, 0.88);
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  cursor: zoom-out;
}
#lightbox.open { display: flex; }
#lightbox-frame { max-width: 90vw; max-height: 80vh; background: #000; border-radius: var(--r-sm); display: flex; align-items: center; justify-content: center; }
#lightbox-frame img, #lightbox-frame video { width: 100%; height: 100%; }
#lightbox-caption { position: absolute; bottom: var(--s3); left: 0; right: 0; text-align: center; color: #C9C9C7; font-size: var(--t2); }

#corner-marker-modal { display: none; position: fixed; inset: 0; z-index: 60; background: rgba(10, 10, 10, 0.88); align-items: center; justify-content: center; padding: var(--s4); }
#corner-marker-modal.open { display: flex; }
#corner-marker-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); padding: var(--s3);
  max-width: 90vw; max-height: 90vh;
  display: flex; flex-direction: column; gap: var(--s2);
  box-shadow: 0 12px 32px rgba(10, 10, 10, 0.28);
}
#corner-marker-canvas {
  position: relative;
  max-width: 80vw;
  max-height: 65vh;
  line-height: 0;
  /* align-self:flex-start opts this flex item out of #corner-marker-panel's
     default cross-axis stretch, so it shrink-wraps to the <img>'s own
     natural size - without this the absolutely-positioned SVG overlay
     (sized to 100%/100% of this box) renders well to the right of the
     image instead of on top of it. Ported from admin/templates/base.html -
     see that file's own comment for the exact bug this fixes. */
  align-self: flex-start;
}
#corner-marker-canvas img { display: block; max-width: 80vw; max-height: 65vh; width: auto; height: auto; background: var(--ink); border-radius: var(--r-sm); }
#corner-marker-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: default; }
#corner-marker-svg .corner-poly { fill: rgba(27, 69, 232, 0.18); stroke: var(--accent); stroke-width: 2; }
/* With the aspect ratio locked, dragging inside the rectangle moves the
   whole area (dashboard.js) - show a move cursor so that's discoverable. */
#corner-marker-svg.locked .corner-poly { cursor: move; }
#corner-marker-svg .corner-handle { fill: var(--accent); stroke: #FFFFFF; stroke-width: 2; cursor: grab; }
#corner-marker-svg .corner-handle:active { cursor: grabbing; }
#corner-marker-controls { display: flex; justify-content: flex-end; gap: var(--s1); }
/* Standard-ratio presets + aspect-lock toggle (2026-09-01) */
#corner-marker-ratios { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
#corner-marker-ratios button[data-corner-ratio] { padding: 0.2rem 0.55rem; font-size: var(--t1); }
.thumb .mark-corners-btn { display: block; width: 100%; margin-top: 0.35rem; padding: 0.3rem 0.4rem; font-size: var(--t1); }

/* public/dashboard-assets/quality-check.js's own per-image result -
   .quality-check-warning only appears when that script actually finds
   something worth flagging. */
.quality-check { margin-top: 0.4rem; line-height: 1.35; }
.quality-check-warning { color: var(--warning); font-size: var(--t1); margin-top: 0.2rem; }

/* Per direct request ("display in a nicely ordered, well aligned grid
   format") - a reusable card-grid pattern for entity LISTS whose items
   carry more than a single thumbnail (campaigns, brands): each
   .entity-card is a full bordered box with an optional logo, a title,
   freeform meta lines, and a button row PINNED TO THE BOTTOM
   (margin-top: auto) so every card in a row lines up evenly regardless
   of how much meta text or how many buttons any one of them has.
   auto-fill/minmax means it reflows cleanly from a phone-width single
   column up to as many columns as fit, with no fixed count to maintain. */
.entity-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--s2); margin-bottom: var(--s2); }
.entity-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s2);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  box-shadow: var(--shadow);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.entity-card:hover { border-color: var(--border-strong); box-shadow: 0 2px 6px rgba(10, 10, 10, 0.08); }
.entity-card-logo { width: 100%; height: 76px; object-fit: contain; background: var(--ink); border-radius: var(--r-sm); cursor: zoom-in; }
.entity-card-logo-placeholder { width: 100%; height: 76px; display: flex; align-items: center; justify-content: center; background: var(--elevated); border: 1px dashed var(--border-strong); border-radius: var(--r-sm); color: var(--muted-ink); font-size: var(--t1); text-align: center; }
.entity-card-title { font-weight: 700; font-size: var(--t3); letter-spacing: -0.01em; }
.entity-card-meta { font-size: var(--t2); color: var(--muted-ink); display: flex; flex-direction: column; gap: 0.15rem; }
.entity-card-actions { margin-top: auto; padding-top: var(--s1); display: flex; flex-wrap: wrap; gap: 0.4rem; }
.entity-card-actions .button, .entity-card-actions button { padding: 0.3rem 0.6rem; font-size: var(--t1); }

.stat-row { display: flex; gap: var(--s2); margin: var(--s3) 0; flex-wrap: wrap; }
.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s2) var(--s3);
  box-shadow: var(--shadow);
}
.stat-tile .n { display: block; font-size: var(--t5); font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat-tile .l { display: block; margin-top: 0.35rem; font-size: var(--t1); font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-ink); line-height: 1.35; }
/* Fixed 3-column grid variant of .stat-row/.stat-tile above - per
   dashboard/analytics.ts's own Summary section, a caller can leave a
   cell deliberately blank with a bare empty div (occupies its grid
   position, renders nothing) rather than every row needing exactly 3
   real tiles. Reuses .stat-tile itself unchanged. */
.stat-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); margin: var(--s3) 0; }
/* Marks the billing tiles inside a .stat-grid-3. Border and number colour
   only - deliberately NOT a filled tint. This started as one row of 3; the
   2026-08-23 CTA breakdown made it NINE tiles, at which point a tinted fill
   stopped reading as "these ones are special" and just turned two thirds of
   the summary blue. An accent edge groups them at a glance and still lets the
   engagement tiles above hold their own weight. */
.stat-tile-financial { border-color: var(--accent); }
.stat-tile-financial .n { color: var(--accent-deep); }

/* General-purpose accent utilities (added 2026-08-22, reconciliation
   pass) - for one-off inline highlights across functions/dashboard/*.ts
   that used to be hardcoded `style="color:#03dac5"` (Android Material
   teal, pre-restyle) directly in the server-rendered markup. An inline
   style attribute always wins over any stylesheet rule, so those never
   responded to the restyle at all - converting each one to a real class
   here lets it track this file's own --accent token like everything
   else, instead of staying stuck at a fixed color forever. */
.accent-text { color: var(--accent); }
.accent-border { border-color: var(--accent); }

.top-bar { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s3); }
.top-bar form { margin: 0; max-width: none; }
.top-bar button {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  color: var(--ink);
  padding: 0.4rem 0.8rem;
  font-weight: 500;
  font-size: var(--t2);
}
.top-bar button:hover { background: var(--elevated); border-color: var(--muted-ink); }

/* CLAUDE.md "Next tasks" (Data owner round follow-up) - "make the sign
   out button more visible" on the Data Owner page specifically
   (functions/dashboard/data-owner.ts's own two Sign out buttons - not a
   change to .top-bar button generally, every OTHER page's own Sign out
   stays exactly as before). Opt-in via class="prominent", restoring the
   plain accent-colored primary-button look .top-bar button above
   deliberately downgrades away from by default. */
.top-bar button.prominent { background: var(--accent); border-color: var(--accent); color: #FFFFFF; }
.top-bar button.prominent:hover { background: var(--accent-deep); border-color: var(--accent-deep); }

/* Applied by dashboard.js's wireUnsavedChangeWarning() to a form's
   containing card the moment any field in it is edited - a "red frame"
   (per direct request) so unsaved changes are never silently lost by
   navigating away without hitting Save. */
.unsaved-warning { border: 2px solid var(--danger) !important; }

/* Applied by dashboard.js's setBusy() to any button/dropzone mid-request
   (form submit, delete/duplicate button, upload) so an action never looks
   like it silently did nothing while the request is in flight. */
.busy { opacity: 0.65; cursor: wait !important; pointer-events: none; }
.busy::after {
  content: '';
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-left: 0.5em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  vertical-align: -0.15em;
  animation: dashboard-spin 0.6s linear infinite;
}
@keyframes dashboard-spin {
  to { transform: rotate(360deg); }
}

/* analytics.ts's Grid/Chart-over-time/Chart-by-share toggle
   (dashboard-assets/analytics-charts.js's wireViewToggles()) - a plain
   row of secondary buttons, the active one flipped to the primary
   button look so the current view is unambiguous at a glance. */
.view-toggle { display: flex; gap: 0.35rem; margin: var(--s2) 0; flex-wrap: wrap; }
.view-toggle button {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--muted-ink);
  font-weight: 500;
}
.view-toggle button:hover { background: var(--elevated); color: var(--ink); border-color: var(--muted-ink); }
.view-toggle button.active { background: var(--accent); border-color: var(--accent); color: #FFFFFF; font-weight: 600; }

/* Chart container - analytics-charts.js renders an <svg> + legend +
   download button into one of these per chart. */
.chart-box { position: relative; margin: var(--s2) 0 var(--s3); }
/* Per CLAUDE.md "Next tasks" (analytics optimization item 2) - the
   on-screen counterpart to the title analytics-charts.js's own
   downloadSvgAsPng() bakes into the exported PNG (see that function's
   own comment) - shown here too so the chart is just as self-explanatory
   in the dashboard as in the download, not only once saved. Reconciled
   forward onto this theme's own type/ink tokens (2026-08-22) -
   independently confirmed missing from the external designer's returned
   file despite their own audit claiming no selectors were dropped. */
.chart-title { margin: 0 0 var(--s1); font-size: var(--t3); font-weight: 600; color: var(--ink); }
.chart-wrap { position: relative; }
.chart-wrap svg { display: block; width: 100%; height: auto; }
.chart-wrap-pie svg { width: 220px; height: 220px; }
/* analytics-charts.js paints its chart furniture with SVG PRESENTATION
   ATTRIBUTES chosen for the old dark theme - gridlines #262626, the axis
   #333, the crosshair #555, and #131313 (the old card colour) as the ring
   around data points and pie slices, so a marker punched a hole in the card
   behind it. On white those all read as heavy black: the gridlines came out
   stronger than the data series they sit behind.
   Presentation attributes sit below every real CSS rule in the cascade, so
   these override them without touching the script. One shared grey serves
   gridlines, axis and crosshair - light enough to sit behind the data, dark
   enough that the crosshair still reads when it appears. The rings become
   the surface colour, which is the same intent the script had, just on a
   light background. Pie slice strokes are scoped to .chart-wrap-pie
   deliberately: in a line chart the data series are <path> too, and a blanket
   path rule would erase every series colour. */
.chart-wrap svg line { stroke: #B4B3B0; }
.chart-wrap svg circle { stroke: var(--surface); }
.chart-wrap-pie svg path { stroke: var(--surface); }
/* Line-chart crosshair tooltip follows the pointer (left/right flipped
   past the chart's own midpoint so it never runs off the card); the
   pie chart's own tooltip (.chart-tooltip-static) instead sits in
   fixed flow right under the chart - see analytics-charts.js's own
   per-slice pointerenter handler. */
.chart-tooltip {
  position: absolute;
  top: var(--s1);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: var(--s1) 0.65rem;
  font-size: var(--t2);
  color: var(--ink);
  box-shadow: 0 4px 12px rgba(10, 10, 10, 0.12);
  pointer-events: none;
  white-space: nowrap;
  z-index: 2;
}
.chart-tooltip-static { position: static; display: inline-block; margin-top: var(--s1); white-space: normal; box-shadow: none; }
.chart-tooltip-date { color: var(--muted-ink); font-size: var(--t1); margin-bottom: 0.25rem; }
.chart-tooltip-row { display: flex; align-items: center; gap: 0.4rem; }
.chart-tooltip-row strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.chart-tooltip-key { display: inline-block; width: 0.6rem; height: 0.6rem; border-radius: 2px; flex: none; }
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s2); margin: var(--s1) 0; font-size: var(--t2); color: var(--muted-ink); }
.chart-legend-item { display: inline-flex; align-items: center; gap: 0.35rem; }
.chart-legend-swatch { display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 2px; flex: none; }
.chart-download-btn { margin-top: 0.4rem; padding: 0.3rem 0.6rem; font-size: var(--t1); }

/* Reusable print utility - per CLAUDE.md "Next tasks" (custom-domain
   DNS instructions need to be "printable for easy future reference") -
   a "no-print" class any page can put on chrome (nav, sign-out, banners,
   buttons) that shouldn't appear on a printed page, and a "print-block"
   class for content that should get real paper-friendly styling. Now
   that the dashboard itself is light, printing is far closer to
   what's on screen, but the overrides below still strip shadows and
   force pure white so nothing wastes toner. */
@media print {
  .no-print { display: none !important; }
  body { background: #fff; padding: 0; }
  section.card, .entity-card, .stat-tile { box-shadow: none; }
  .print-block { background: #fff; color: #111; border: 1px solid #ccc; }
  /* invoice.ts's own receipts section uses `<section class="card print-
     block">` (2026-08-21, postdates the external designer's snapshot,
     found while building admin/'s own mirrored invoice view - CLAUDE.md
     "Next tasks": "I could not find a way to actually view/download and
     print the generated invoices") - section.card's OWN rule above is
     element+class (specificity 0-1-1), which beats plain .print-block
     (0-1-0) regardless of this @media print block's source order, so
     that section's card background would silently win over the
     "readable black-on-white" print intent .print-block exists for.
     This rule's own selector matches or exceeds that specificity so it
     actually wins here. Reconciled forward (2026-08-22) - independently
     confirmed missing from the external designer's returned file. */
  section.card.print-block, .card.print-block { background: #fff; color: #111; border: 1px solid #ccc; }
  .print-block code, .print-block input { color: #111; background: #f3f3f3; border-color: #bbb; }
  .print-block .muted { color: #444; }
}

.dns-field { display: flex; align-items: center; gap: var(--s1); margin-bottom: var(--s1); flex-wrap: wrap; }
.dns-field label { flex: 0 0 6rem; margin: 0; font-weight: 600; font-size: var(--t2); color: var(--muted-ink); }
.dns-field input { margin: 0; flex: 1 1 260px; font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: var(--t2); }
.dns-step {
  padding: var(--s3);
  margin-bottom: var(--s2);
  border-radius: var(--r);
  background: var(--surface);
  border: 1px solid var(--border);
}
.dns-step h3 { margin: 0 0 0.25rem; font-size: var(--t3); }
.dns-step > p.muted { margin-top: 0; margin-bottom: var(--s2); }

/* ---- Responsive. The old sheet had exactly one breakpoint (a
   .stat-grid-3 collapse at 640). These keep the wide tables and stat
   grids usable between a laptop and a phone. ---- */
@media (max-width: 960px) {
  .stat-grid-3 { grid-template-columns: repeat(2, 1fr); }
  .form-row-2 { grid-template-columns: 1fr; gap: 0; }
  form.wide { max-width: 100%; }
}
@media (max-width: 640px) {
  body { padding: var(--s3) var(--s2) var(--s4); }
  h1 { font-size: var(--t4); }
  h2 { font-size: var(--t3); margin-top: var(--s3); }
  .stat-grid-3 { grid-template-columns: 1fr; }
  .stat-row { gap: var(--s1); }
  .stat-tile { flex: 1 1 100%; }
  section.card { padding: var(--s2); }
  .top-bar { flex-direction: column; align-items: stretch; }
  /* Wide data tables scroll sideways in their own container rather than
     forcing the whole page to scroll horizontally. Deliberately NOT
     scoped to section.card: analytics.ts renders its "CTA form
     submissions" table bare in <main>, outside any card, and that one is
     the widest table in the whole dashboard. */
  table { display: block; overflow-x: auto; white-space: nowrap; }
  form { max-width: 100%; }
}

/* ===========================================================================
 * v2 ADDITIONS — everything above this line is the partner's own
 * dashboard.css, byte-identical. Nothing below renames, removes or
 * re-declares any of his selectors; every new class is namespaced `p2v-`
 * and every new custom property is namespaced `--p2v-`.
 *
 * Token source: work/dashboard-ui/v2/tokens.css (frozen seam).
 * Skeleton source: work/dashboard-ui/v2/SHELL.md (frozen seam).
 * Measured blueprint: work/dashboard-ui/v2/DESIGN-DNA.md.
 *
 * Scoping rule: the shell only exists on `<body class="p2v">`. Every
 * override of one of his rules is prefixed `body.p2v`, so a page that does
 * NOT carry that class renders exactly as it does today.
 * ======================================================================== */

:root {
  /* ---- shell geometry ---- */
  --p2v-sidebar-w: 224px;
  --p2v-sidebar-w-sm: 0px;
  --p2v-topbar-h: 56px;
  --p2v-header-h: 64px;
  --p2v-content-pad: var(--s3);
  --p2v-panel-max: 1400px;
  /* fixed height of a .grid-scroll box (~10 rows at the v2 52px row) */
  --p2v-grid-scroll-h: 520px;

  /* ---- sidebar ---- */
  --p2v-sidebar-bg: #F1F0EE;
  --p2v-sidebar-border: var(--border);
  --p2v-nav-size: 0.875rem;
  --p2v-nav-weight: 500;
  --p2v-nav-color: var(--ink);
  --p2v-nav-pad: 8px 10px;
  --p2v-nav-h: 34px;
  --p2v-nav-gap: 4px;
  --p2v-nav-radius: var(--r-sm);
  --p2v-nav-hover-bg: rgba(10, 10, 10, 0.045);
  --p2v-nav-active-bg: var(--accent-tint);
  --p2v-nav-active-color: var(--accent-deep);
  --p2v-nav-active-weight: 700;
  --p2v-nav-section-size: 0.6875rem;

  /* ---- wordmark ---- */
  --p2v-wordmark-font: "Archivo Black", system-ui, -apple-system, sans-serif;
  --p2v-wordmark-size: 0.9375rem;
  --p2v-wordmark-tracking: -0.04em;
  --p2v-wordmark-color: var(--ink);

  /* ---- pills ---- */
  --p2v-pill-size: 0.6875rem;
  --p2v-pill-weight: 700;
  --p2v-pill-pad: 3px 10px;
  --p2v-pill-h: 22px;
  --p2v-pill-radius: 999px;
  --p2v-pill-live-bg: var(--accent-tint);
  --p2v-pill-live-color: var(--accent-deep);
  --p2v-pill-neutral-bg: #F1F0EE;
  --p2v-pill-neutral-color: var(--muted-ink);
  --p2v-pill-outline-border: var(--border);
  --p2v-pill-outline-color: var(--muted-ink);

  /* ---- pill-shaped control ---- */
  --p2v-control-pill-pad: 7px 14px;
  --p2v-control-pill-h: 32px;

  /* ---- tables ---- */
  --p2v-th-size: 0.6875rem;
  --p2v-th-weight: 700;
  --p2v-th-tracking: 0.08em;
  --p2v-th-color: var(--muted-ink);
  --p2v-th-pad: 0 12px 8px;
  --p2v-row-h: 52px;
  --p2v-td-pad: 0 12px;
  --p2v-row-hover-bg: #F7F6F4;

  /* ---- stat tiles ---- */
  --p2v-tile-pad: 20px;
  --p2v-tile-radius: var(--r);
  --p2v-tile-border: 1px solid var(--border);
  --p2v-tile-label-size: 0.6875rem;
  --p2v-tile-label-weight: 600;
  --p2v-tile-label-tracking: 0.06em;
  --p2v-tile-label-color: var(--muted-ink);
  --p2v-tile-number-size: 2rem;
  --p2v-tile-number-weight: 700;
  --p2v-tile-delta-size: 0.75rem;
  --p2v-tile-delta-color: var(--muted-ink);
  --p2v-tile-accent-bar: 3px;

  /* ---- cards ---- */
  --p2v-card-border: 1px solid var(--border);
  --p2v-card-radius: var(--r);
  --p2v-card-pad: 20px;
  --p2v-card-shadow: none;
  --p2v-popover-shadow: 0 6px 24px rgba(10, 10, 10, 0.10);

  /* ---- buttons ---- */
  --p2v-btn-h: 36px;
  --p2v-btn-h-sm: 28px;
  --p2v-btn-pad: 9px 16px;
  --p2v-btn-pad-sm: 5px 10px;
  --p2v-btn-radius: var(--r-sm);
  --p2v-btn-size: 0.875rem;
  --p2v-btn-weight: 700;
  --p2v-btn-primary-bg: var(--accent);
  --p2v-btn-primary-color: #FFFFFF;
  --p2v-btn-quiet-color: var(--accent-deep);
  --p2v-btn-quiet-hover-bg: var(--accent-tint);
  --p2v-btn-danger-color: var(--danger);
  --p2v-btn-danger-hover-bg: var(--danger-tint);

  /* ---- wizard stepper ---- */
  --p2v-step-dot: 26px;
  --p2v-step-gap: var(--s2);
  --p2v-step-line: var(--border);
  --p2v-step-done-bg: var(--accent);
  --p2v-step-done-color: #FFFFFF;
  --p2v-step-current-ring: 0 0 0 3px var(--accent-tint);
  --p2v-step-current-color: var(--accent-deep);
  --p2v-step-todo-bg: #F1F0EE;
  --p2v-step-todo-color: var(--muted-ink);
  --p2v-step-label-size: 0.75rem;

  /* ---- breadcrumb ---- */
  --p2v-crumb-size: 0.75rem;
  --p2v-crumb-color: var(--muted-ink);
  --p2v-crumb-current-color: var(--ink);
  --p2v-crumb-sep-color: var(--border-strong);

  /* ---- forms ---- */
  --p2v-field-max: 520px;
  --p2v-label-size: 0.8125rem;
  --p2v-label-weight: 600;
  --p2v-help-color: var(--muted-ink);

  /* ---- shell type ramp ---- */
  --p2v-title-size: 1.75rem;
  --p2v-section-size: 1.25rem;
  --p2v-body-size: 0.9375rem;
  --p2v-meta-size: 0.8125rem;
}

/* =========================================================================
 * 1. SHELL LAYOUT
 * ====================================================================== */

body.p2v {
  margin: 0;
  padding: 0;
  display: flex;
  align-items: stretch;
  min-height: 100vh;
  /* The sidebar is position:sticky/100vh, so on a long page its own box ends
     one viewport down. Painting the sidebar colour on the body and his --page
     on the content column instead gives a grey rail the full height of the
     document, with no extra element. */
  background: var(--p2v-sidebar-bg);
  font-size: var(--p2v-body-size);
}

/* His two page-chrome blocks are replaced by the shell's own sidebar +
   topbar. renderPage() drops them server-side; hiding them here covers the
   transition so a page that still emits them looks right either way. Every
   href / form action inside them stays in the DOM, so nothing he binds to
   disappears. */
body.p2v .site-header,
body.p2v .top-bar { display: none !important; }

.p2v-skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  padding: var(--s1) var(--s2);
  font-size: var(--p2v-meta-size);
  font-weight: 700;
  text-decoration: none;
}
.p2v-skip:focus { left: var(--s2); top: var(--s2); }

/* ---- sidebar ---- */
.p2v-sidebar {
  flex: 0 0 var(--p2v-sidebar-w);
  width: var(--p2v-sidebar-w);
  background: var(--p2v-sidebar-bg);
  border-right: 1px solid var(--p2v-sidebar-border);
  padding: var(--s3) 14px var(--s2);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 30;
}
.p2v-wordmark {
  font-family: var(--p2v-wordmark-font);
  font-size: var(--p2v-wordmark-size);
  letter-spacing: var(--p2v-wordmark-tracking);
  color: var(--p2v-wordmark-color);
  text-transform: uppercase;
  line-height: 1.2;
  padding: 4px 10px var(--s3);
}
.p2v-wordmark-play { color: var(--accent); margin: 0 6px; }
/* Real logo image (2026-08-27) - hidden by default (its own onload
   handler, html.ts's own platformLogoHtml(), reveals it once a real
   upload actually loads; onerror removes the <img> entirely when none
   is set, leaving .p2v-wordmark-text - already visible by default -
   showing with no flash either way). Capped height, not a fixed box, so
   a wide OR tall uploaded logo both still fit the sidebar's own narrow
   column without needing to know its real aspect ratio in advance. */
.p2v-wordmark-logo { display: none; max-height: 32px; max-width: 100%; width: auto; vertical-align: middle; }

.p2v-nav { display: flex; flex-direction: column; gap: var(--p2v-nav-gap); }
.p2v-nav-item {
  display: flex;
  align-items: center;
  min-height: var(--p2v-nav-h);
  padding: var(--p2v-nav-pad);
  border-radius: var(--p2v-nav-radius);
  font-size: var(--p2v-nav-size);
  font-weight: var(--p2v-nav-weight);
  color: var(--p2v-nav-color);
  text-decoration: none;
}
.p2v-nav-item:hover { background: var(--p2v-nav-hover-bg); }
.p2v-nav-item.is-active {
  background: var(--p2v-nav-active-bg);
  color: var(--p2v-nav-active-color);
  font-weight: var(--p2v-nav-active-weight);
}
.p2v-nav-item:focus-visible { outline: none; box-shadow: var(--ring); }
/* .p2v-nav-foot removed 2026-08-27 - Help moved out of its own pinned-
   to-bottom strip into the normal nav list, right after Settings (see
   html.ts's own SIDEBAR_NAV_ITEMS comment). */

.p2v-scrim {
  position: fixed;
  inset: 0;
  z-index: 25;
  background: rgba(10, 10, 10, 0.42);
  border: 0;
  padding: 0;
}
.p2v-scrim[hidden] { display: none; }

/* ---- content column ---- */
.p2v-shell {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--page);
}

/* ---- topbar ---- */
.p2v-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  min-height: var(--p2v-topbar-h);
  padding: 0 var(--p2v-content-pad);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 20;
}
.p2v-burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  flex: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  font-size: 0;
  cursor: pointer;
}
.p2v-burger::before {
  content: "";
  display: block;
  width: 16px;
  height: 10px;
  border-top: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  box-sizing: content-box;
  position: relative;
}
.p2v-burger:hover { background: var(--elevated); border-color: var(--muted-ink); }

.p2v-agency {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  font-size: var(--p2v-meta-size);
}
.p2v-agency-name {
  font-weight: 700;
  font-size: var(--t2);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* flex:0 1 auto + min-width:0 (not flex:none) so a long account email can
   shrink and ellipsise instead of pushing the topbar past the viewport.
   Folded in from _additions-analytics.css "6c" at the 2026-08-25 merge:
   the data owner ("Aurora Skincare" + privacy@auroraskincare.example) ran
   24px past the right edge at 390 with the old flex:none / 46vw pair. */
.p2v-account { position: relative; flex: 0 1 auto; min-width: 0; }
.p2v-account-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-height: var(--p2v-control-pill-h);
  padding: 0 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--p2v-meta-size);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.p2v-account-btn:hover { background: var(--elevated); border-color: var(--muted-ink); }
.p2v-account-btn:focus-visible { outline: none; box-shadow: var(--ring); }
.p2v-account-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 40;
  min-width: 220px;
  padding: var(--s1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--p2v-popover-shadow);
}
.p2v-account-menu[hidden] { display: none; }
.p2v-account-menu form { display: block; margin: 0; max-width: none; }
.p2v-menu-item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 32px;
  padding: 0 var(--s1);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--p2v-meta-size);
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.p2v-menu-item:hover { background: var(--p2v-nav-hover-bg); color: var(--ink); border-color: transparent; }
.p2v-menu-item:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- breadcrumb ---- */
.p2v-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  padding: var(--s2) var(--p2v-content-pad) 0;
  font-size: var(--p2v-crumb-size);
  color: var(--p2v-crumb-color);
}
.p2v-crumbs a { color: var(--p2v-crumb-color); text-decoration: none; }
.p2v-crumbs a:hover { color: var(--ink); text-decoration: underline; }
.p2v-crumb-sep { margin: 0 var(--s1); color: var(--p2v-crumb-sep-color); }
.p2v-crumb-current { color: var(--p2v-crumb-current-color); font-weight: 600; }

/* ---- stepper ---- */
.p2v-stepper {
  display: flex;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: var(--s2) var(--p2v-content-pad) 0;
  overflow-x: auto;
}
.p2v-step {
  display: flex;
  align-items: center;
  gap: var(--s1);
  flex: none;
  font-size: var(--p2v-step-label-size);
  font-weight: 600;
  color: var(--p2v-step-todo-color);
  white-space: nowrap;
}
.p2v-step + .p2v-step::before {
  content: "";
  display: block;
  width: 40px;
  height: 1px;
  margin: 0 var(--s2);
  background: var(--p2v-step-line);
  flex: none;
}
.p2v-step a { color: inherit; text-decoration: none; }
.p2v-step a:hover { text-decoration: underline; }
.p2v-step-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--p2v-step-dot);
  height: var(--p2v-step-dot);
  flex: none;
  border-radius: 999px;
  background: var(--p2v-step-todo-bg);
  color: var(--p2v-step-todo-color);
  font-size: var(--t1);
  font-weight: 700;
  line-height: 1;
}
.p2v-step.is-done { color: var(--ink); }
.p2v-step.is-done .p2v-step-dot { background: var(--p2v-step-done-bg); color: var(--p2v-step-done-color); }
.p2v-step.is-current { color: var(--p2v-step-current-color); }
.p2v-step.is-current .p2v-step-dot {
  background: var(--surface);
  color: var(--p2v-step-current-color);
  border: 1px solid var(--accent);
  box-shadow: var(--p2v-step-current-ring);
}

/* ---- panel ---- */
body.p2v main.p2v-panel {
  flex: 1 1 auto;
  max-width: var(--p2v-panel-max);
  margin: 0;
  padding: var(--s3) var(--p2v-content-pad) var(--s4);
}
.p2v-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-bottom: var(--s3);
}
.p2v-panel-title {
  margin: 0;
  font-size: var(--p2v-title-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.p2v-panel-sub {
  margin: 4px 0 0;
  max-width: 78ch;
  color: var(--muted-ink);
  font-size: var(--p2v-meta-size);
  line-height: 1.4;
}
.p2v-panel-actions { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }

/* =========================================================================
 * 2. BUTTONS
 * ====================================================================== */

body.p2v .p2v-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: var(--p2v-btn-h);
  padding: var(--p2v-btn-pad);
  border: 1px solid transparent;
  border-radius: var(--p2v-btn-radius);
  background: transparent;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--p2v-btn-size);
  font-weight: var(--p2v-btn-weight);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
/* Per-gallery Duplicate button (2026-09-01, CLAUDE.md "Next tasks") is the
   first .p2v-btn to ever render with a plain `disabled` attribute (its
   `title` explains why - either this gallery is empty, or there is
   nowhere for a copy to land) - no rule existed for that state before. */
body.p2v .p2v-btn:disabled { opacity: 0.5; cursor: not-allowed; }
body.p2v .p2v-btn--primary {
  background: var(--p2v-btn-primary-bg);
  border-color: var(--p2v-btn-primary-bg);
  color: var(--p2v-btn-primary-color);
}
body.p2v .p2v-btn--primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
body.p2v .p2v-btn--secondary {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--ink);
}
body.p2v .p2v-btn--secondary:hover { background: var(--elevated); border-color: var(--muted-ink); }
body.p2v .p2v-btn--quiet {
  min-height: var(--p2v-btn-h-sm);
  padding: var(--p2v-btn-pad-sm);
  background: transparent;
  border-color: transparent;
  color: var(--p2v-btn-quiet-color);
  font-size: var(--p2v-meta-size);
}
body.p2v .p2v-btn--quiet:hover { background: var(--p2v-btn-quiet-hover-bg); border-color: transparent; color: var(--accent-deep); }
body.p2v .p2v-btn--danger {
  min-height: var(--p2v-btn-h-sm);
  padding: var(--p2v-btn-pad-sm);
  background: transparent;
  border-color: transparent;
  color: var(--p2v-btn-danger-color);
  font-size: var(--p2v-meta-size);
}
body.p2v .p2v-btn--danger:hover { background: var(--p2v-btn-danger-hover-bg); border-color: transparent; color: var(--danger); }
body.p2v .p2v-btn--sm { min-height: var(--p2v-btn-h-sm); padding: var(--p2v-btn-pad-sm); font-size: var(--p2v-meta-size); }
body.p2v .p2v-btn:focus-visible { outline: none; box-shadow: var(--ring); }

.p2v-actions { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }
body.p2v .p2v-panel section.card > .p2v-actions { margin-top: var(--s2); }

/* Pill-shaped control wrapper for his existing <select> filters. */
.p2v-control-pill {
  display: inline-flex;
  align-items: center;
  min-height: var(--p2v-control-pill-h);
  padding: 0 4px 0 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: var(--p2v-meta-size);
  color: var(--ink);
}
body.p2v .p2v-control-pill select {
  width: auto;
  margin: 0;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  font-size: var(--p2v-meta-size);
}

/* =========================================================================
 * 3. PILLS
 * ====================================================================== */

body.p2v .p2v-pill {
  border-radius: var(--p2v-pill-radius);
  border: 1px solid transparent;
  font-size: var(--p2v-pill-size);
  font-weight: var(--p2v-pill-weight);
  line-height: 16px;
  letter-spacing: 0.02em;
  vertical-align: middle;
  white-space: nowrap;
}
body.p2v .p2v-pill--live { background: var(--p2v-pill-live-bg); color: var(--p2v-pill-live-color); border-color: transparent; }
body.p2v .p2v-pill--neutral { background: var(--p2v-pill-neutral-bg); color: var(--p2v-pill-neutral-color); border-color: transparent; }
body.p2v .p2v-pill--outline { background: transparent; color: var(--p2v-pill-outline-color); border-color: var(--p2v-pill-outline-border); }
/* Standalone pills (not sitting on one of his inline-styled status spans)
   get the full token box. */
body.p2v span.p2v-pill:not([style]) {
  display: inline-flex;
  align-items: center;
  min-height: var(--p2v-pill-h);
  padding: var(--p2v-pill-pad);
}

/* =========================================================================
 * 4. STAT TILES
 * ====================================================================== */

.p2v-tiles {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s2);
  margin-bottom: var(--s3);
  align-items: stretch;
}
.p2v-tile {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: var(--p2v-tile-border);
  border-radius: var(--p2v-tile-radius);
  padding: var(--p2v-tile-pad);
  box-shadow: none;
}
.p2v-tile-label {
  font-size: var(--p2v-tile-label-size);
  font-weight: var(--p2v-tile-label-weight);
  letter-spacing: var(--p2v-tile-label-tracking);
  text-transform: uppercase;
  color: var(--p2v-tile-label-color);
  line-height: 1.4;
}
.p2v-tile-number {
  margin: var(--s1) 0 4px;
  font-size: var(--p2v-tile-number-size);
  font-weight: var(--p2v-tile-number-weight);
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.p2v-tile-delta {
  margin-top: auto;
  font-size: var(--p2v-tile-delta-size);
  color: var(--p2v-tile-delta-color);
  line-height: 1.4;
}

/* =========================================================================
 * 5. CARDS + TABLES (his markup, restyled inside the panel)
 * ====================================================================== */

body.p2v .p2v-panel section.card {
  background: var(--surface);
  border: var(--p2v-card-border);
  border-radius: var(--p2v-card-radius);
  padding: var(--p2v-card-pad);
  margin-bottom: var(--s3);
  box-shadow: var(--p2v-card-shadow);
}
body.p2v .p2v-panel section.card > h2 {
  font-size: var(--p2v-section-size);
  font-weight: 700;
  letter-spacing: -0.01em;
  /* A second heading inside the same card (his "Add a user" under the users
     table) needs air above it; the first one sits on the card padding. */
  margin: var(--s3) 0 var(--s1);
}
body.p2v .p2v-panel section.card > h2:first-child { margin-top: 0; }
body.p2v .p2v-panel section.card > h2 + .analytics-note,
body.p2v .p2v-panel section.card > h2 + p.muted { margin-top: 0; }
body.p2v .p2v-panel .analytics-note { font-size: var(--p2v-meta-size); line-height: 1.5; }

/* --- tables: uppercase hairline header, tall rows, no zebra, no grey block --- */
body.p2v .p2v-panel table { width: 100%; border-collapse: collapse; }
body.p2v .p2v-panel th {
  background: transparent;
  border-bottom: 1px solid var(--border);
  padding: var(--p2v-th-pad);
  font-size: var(--p2v-th-size);
  font-weight: var(--p2v-th-weight);
  letter-spacing: var(--p2v-th-tracking);
  text-transform: uppercase;
  color: var(--p2v-th-color);
  text-align: left;
  white-space: nowrap;
}
body.p2v .p2v-panel .sortable-table th:hover { background: transparent; color: var(--ink); }
body.p2v .p2v-panel td {
  height: var(--p2v-row-h);
  padding: var(--p2v-td-pad);
  border-bottom: none;
  font-size: var(--t2);
  vertical-align: middle;
}
body.p2v .p2v-panel tbody tr { border-bottom: 1px solid var(--border); }
body.p2v .p2v-panel tbody tr:hover { background: var(--p2v-row-hover-bg); }
body.p2v .p2v-panel tbody tr:last-child { border-bottom: none; }
body.p2v .p2v-panel .p2v-num { text-align: right; font-variant-numeric: tabular-nums; }
body.p2v .p2v-panel td.p2v-cell-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s1);
  height: var(--p2v-row-h);
  white-space: nowrap;
}
body.p2v .p2v-panel th.p2v-cell-actions { text-align: right; }

/* His opt-in fixed-height scroll grid, RE-PINNED (2026-08-25 merge).
   The partner asked for fixed-size grids with their own scrollbar, so the
   cap stays. His own value is 420px, sized for the old dark theme's ~30px
   rows; against the v2 52px row that showed 8 rows, so the cap is expressed
   as one token he can change in one place.
   The sticky thead needs three things to work over scrolling rows: an opaque
   background (his tables are transparent), a z-index above the cells, and a
   real bottom rule - a `border-bottom` on a sticky th is painted with the
   cell and scrolls away under border-collapse, so it is drawn with a
   box-shadow instead. */
body.p2v .p2v-panel .grid-scroll {
  max-height: var(--p2v-grid-scroll-h);
  overflow-y: auto;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
body.p2v .p2v-panel .grid-scroll thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
  box-shadow: inset 0 -1px 0 var(--border-strong);
}
body.p2v .p2v-panel .grid-scroll th { padding: var(--s1) 12px; }

/* Gallery grid (campaign-grid.ts): the first three headers (Gallery/Start/
   End) and the Load header are ALSO pinned to the left/right edge, not
   just the top - their left/right offsets come from the .p2v-grid-table
   nth-child rules further up. But the rule just above re-sets EVERY thead
   th to z-index:2 at a specificity the base grid's own z-index:3 (line
   ~492) can't beat, so on horizontal scroll the scrolling brand-column
   headers (also z-index:2, and later in the DOM) were painting OVER the
   pinned corner headers - the reported "titles of the first three columns
   are not always visible when the horizontal slider is moved" (2026-08-30,
   CLAUDE.md "Next tasks"). Re-lift them here at matching specificity. */
body.p2v .p2v-panel .grid-scroll .p2v-grid-table thead th:nth-child(-n + 3),
body.p2v .p2v-panel .grid-scroll .p2v-grid-table thead th:last-child {
  z-index: 4;
}

/* =========================================================================
 * 6. FORMS
 * ====================================================================== */

.p2v-form-col { max-width: var(--p2v-field-max); }
body.p2v .p2v-panel form.p2v-form-col { max-width: var(--p2v-field-max); }
body.p2v .p2v-panel label {
  font-size: var(--p2v-label-size);
  font-weight: var(--p2v-label-weight);
  color: var(--ink);
  margin-bottom: 6px;
}
body.p2v .p2v-panel .p2v-help {
  margin: 0 0 var(--s2);
  max-width: 78ch;
  font-size: var(--p2v-meta-size);
  color: var(--p2v-help-color);
  line-height: 1.5;
}
body.p2v .p2v-panel .dropzone { background: var(--surface); border-radius: var(--r); }

.p2v-note {
  margin: 0 0 var(--s3);
  max-width: 78ch;
  font-size: var(--p2v-meta-size);
  color: var(--muted-ink);
  line-height: 1.5;
}

/* Key/value meta pairs (active-campaign card). */
.p2v-kv {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--s2);
  margin: 0;
  align-items: start;
}
.p2v-kv-label {
  display: block;
  font-size: var(--p2v-tile-label-size);
  font-weight: var(--p2v-tile-label-weight);
  letter-spacing: var(--p2v-tile-label-tracking);
  text-transform: uppercase;
  color: var(--muted-ink);
  line-height: 1.4;
}
.p2v-kv-value {
  /* flex + a fixed min-height so a value holding a pill is exactly as tall as
     a value holding plain text - otherwise the four cells render ragged. */
  display: flex;
  align-items: center;
  min-height: calc(var(--p2v-pill-h) + 2px); /* pill height + its 1px hairline top and bottom */
  margin-top: 4px;
  font-size: var(--t2);
  color: var(--ink);
  line-height: 1.4;
}

/* =========================================================================
 * 7. EMPTY STATE + CHECKLIST
 * ====================================================================== */

.p2v-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s1);
  padding: var(--s4) var(--s3);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r);
  background: var(--surface);
}
.p2v-empty-ico { display: block; width: 40px; height: 40px; color: var(--border-strong); }
.p2v-empty-title { margin: 0; font-size: var(--t3); font-weight: 700; color: var(--ink); line-height: 1.3; }
.p2v-empty-text { margin: 0; max-width: 48ch; font-size: var(--p2v-meta-size); color: var(--muted-ink); line-height: 1.5; }
.p2v-empty .p2v-btn { margin-top: var(--s1); }
/* Inside one of his cards the empty state drops its own frame, so the card's
   hairline is the only box. */
body.p2v .p2v-panel section.card > .p2v-empty { border: 0; background: transparent; padding: var(--s3) var(--s2); }

/* Visually-hidden label — keeps a <select> inside a .p2v-control-pill
   labelled for screen readers without printing the word twice. */
.p2v-sr-label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.p2v-checklist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s1); }
.p2v-checklist-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  min-height: 40px;
  padding: var(--s1) var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.p2v-checklist-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 999px;
  background: var(--p2v-step-todo-bg);
  color: var(--muted-ink);
  border: 1px solid var(--border);
  font-size: var(--t1);
  font-weight: 700;
  line-height: 1;
}
.p2v-checklist-item.is-done .p2v-checklist-tick { background: var(--success); border-color: var(--success); color: #FFFFFF; }
.p2v-checklist-text { flex: 1 1 auto; min-width: 0; font-size: var(--t2); color: var(--ink); line-height: 1.4; }
.p2v-checklist-item.is-done .p2v-checklist-text { color: var(--muted-ink); }

/* =========================================================================
 * 8. COMPONENT SHEET (components.html only)
 * ====================================================================== */

.p2v-sheet { margin: 0 0 var(--s4); }
.p2v-sheet-title {
  margin: 0 0 var(--s1);
  padding-bottom: var(--s1);
  border-bottom: 1px solid var(--border);
  font-size: var(--p2v-tile-label-size);
  font-weight: 700;
  letter-spacing: var(--p2v-th-tracking);
  text-transform: uppercase;
  color: var(--muted-ink);
  line-height: 1.4;
}
.p2v-sheet-note { margin: 0 0 var(--s2); font-size: var(--p2v-meta-size); color: var(--muted-ink); line-height: 1.5; }

/* =========================================================================
 * 9. RESPONSIVE — off-canvas drawer below 960px
 * ====================================================================== */

@media (max-width: 960px) {
  .p2v-sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100%;
    transform: translateX(-100%);
    transition: transform 0.18s ease;
    box-shadow: none;
  }
  .p2v-sidebar.is-open { transform: translateX(0); box-shadow: var(--p2v-popover-shadow); }
  .p2v-burger { display: inline-flex; }
  .p2v-shell { width: 100%; }
  .p2v-tiles { grid-template-columns: repeat(2, 1fr); }
  .p2v-step + .p2v-step::before { width: var(--s3); margin: 0 var(--s1); }
}

@media (max-width: 640px) {
  body.p2v { padding: 0; }
  .p2v-tiles { grid-template-columns: 1fr; }
  .p2v-panel-head { flex-direction: column; align-items: stretch; }
  .p2v-panel-actions { justify-content: flex-start; }
  .p2v-panel-title { font-size: var(--t4); }
  body.p2v main.p2v-panel { padding: var(--s2) var(--s2) var(--s4); }
  .p2v-topbar { padding: 0 var(--s2); }
  .p2v-crumbs, .p2v-stepper { padding-left: var(--s2); padding-right: var(--s2); }
  body.p2v .p2v-panel section.card { padding: var(--s2); }
  body.p2v .p2v-panel td { padding: var(--s1) 12px; height: auto; }
  /* out-specifies the standalone-pill rule above (0-3-2) */
  body.p2v .p2v-topbar .p2v-agency span.p2v-pill { display: none; }
  .p2v-agency-name { max-width: 34vw; }
}

/* =========================================================================
 * 10. PRINT — his @media print rules above still apply; this only removes
 *     the new shell chrome so a printed page is the content alone.
 * ====================================================================== */

@media print {
  body.p2v { display: block; }
  .p2v-sidebar, .p2v-topbar, .p2v-crumbs, .p2v-stepper, .p2v-scrim, .p2v-skip { display: none !important; }
  body.p2v main.p2v-panel { padding: 0; max-width: none; }
  .p2v-tile, .p2v-checklist-item, .p2v-empty { box-shadow: none; }
}


/* =========================================================================
 * ==  v2 PAGE-GROUP SECTIONS, MERGED 2026-08-25  ==========================
 *
 * Sections 11-13 below were built as three separate sheets while three
 * agents worked in parallel (_additions-wizard.css, _additions-analytics.css,
 * _additions-account.css) and are folded in here so the partner installs
 * ONE stylesheet and ONE script. The originals are archived unchanged at
 * work/dashboard-ui/v2/build/archive-merged/.
 *
 * Resolved at merge time:
 *   - the two sheets' own `:root` blocks are merged into section 11a below;
 *     no token was renamed and none collided (section 10a).
 *   - the analytics sheet's "6c" topbar shrink fix was moved to its real
 *     home in section 3 (SHELL / topbar) rather than left in a page sheet.
 *   - the analytics sheet's "6b" .grid-scroll UNPIN was dropped: the cap is
 *     re-pinned in section 5 above, which is what the partner asked for.
 *   - the two segmented controls (.p2v-seg on 08c, his .view-toggle on the
 *     analytics pages) were drawn differently by two agents. They are one
 *     control now: see 13.4.
 * ======================================================================== */

/* -------------------------------------------------------------------------
 * 10a. PAGE-GROUP TOKENS (merged from the two sheets that declared :root)
 * ---------------------------------------------------------------------- */

:root {
  /* segmented control (his .view-toggle + our .p2v-seg) */
  --p2v-seg-h: 28px;
  --p2v-seg-h-sm: 24px;
  /* analytics / deck slide 10 */
  --p2v-chart-title-size: 0.9375rem;   /* 15px - deck chart card title */
  --p2v-stat-number-size: 2rem;        /* 32px - deck stat number, real-UI scale */
  --p2v-stat-label-size: 0.6875rem;    /* 11px uppercase label above the number */
  --p2v-stat-label-tracking: 0.06em;
  --p2v-billing-bar: 3px;
  /* article pages (11 help, 12 white-label) */
  --p2v-toc-w: 200px;
  --p2v-doc-measure: 70ch;
  /* left accent bar on the Finance CPS tiles */
  --p2v-fin-bar: var(--p2v-tile-accent-bar);
}

/* =========================================================================
 * 11. WIZARD, BRAND + TARGET DETAIL PAGES
 *     (was _additions-wizard.css - 04, 04_wizard, 05, 05_wizard, 06,
 *      06_wizard, 06b, 06c, wizard-review)
 * ====================================================================== */

/* ---- 11.1 wizard mode ---------------------------------------------------
 * On a wizard route the stepper IS the location indicator, so the
 * breadcrumb would say the same thing twice. Hidden, never removed: every
 * crumb href stays in the DOM for the guard. */
body.p2v-wizard .p2v-crumbs { display: none; }

/* ---- 11.2 the Continue bar at the foot of a wizard step ---------------- */
.p2v-continue {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}
.p2v-continue-note {
  margin: 0;
  margin-right: auto;
  max-width: 60ch;
  font-size: var(--p2v-meta-size);
  color: var(--muted-ink);
}

/* ---- 11.3 two-column detail layout (>=960px) --------------------------- */
.p2v-two-col { display: grid; gap: var(--s3); align-items: start; }
@media (min-width: 960px) {
  .p2v-two-col { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
}
.p2v-col { min-width: 0; }
body.p2v .p2v-panel .p2v-col > section.card { margin-bottom: var(--s3); }
body.p2v .p2v-panel .p2v-col > section.card:last-child { margin-bottom: 0; }

/* ---- 11.4 native <details> for the advanced recognition fields ---------
 * HTML/CSS only. The selects and the number input stay inside the form, so
 * they submit whether the disclosure is open or shut, and dashboard.js /
 * quality-check.js never query them by id. */
.p2v-advanced {
  margin: var(--s2) 0 var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--elevated);
}
.p2v-advanced > summary {
  cursor: pointer;
  list-style: none;
  padding: 10px 14px;
  font-size: var(--p2v-label-size);
  font-weight: var(--p2v-label-weight);
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: var(--s1);
}
.p2v-advanced > summary::-webkit-details-marker { display: none; }
.p2v-advanced > summary::before {
  content: "\25B8";
  color: var(--muted-ink);
  font-size: 0.75rem;
  transition: transform 120ms ease;
}
.p2v-advanced[open] > summary::before { transform: rotate(90deg); }
.p2v-advanced > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Chromium keeps a laid-out box for a closed <details>'s children
   (content-visibility, not display:none), which made the collapsed fields
   overlap the rest of the form. Collapsing it explicitly. display:none does not
   stop a field submitting, and querySelector still finds it, so dashboard.js and
   the form POST are both unaffected. */
.p2v-advanced:not([open]) .p2v-advanced-body { display: none; }
.p2v-advanced-body { padding: 0 14px 14px; }
.p2v-advanced-body > .p2v-help:first-child { margin-top: 0; }

/* ---- 11.5 the CTA field group, collapsed until the CTA is enabled ------
 * DECISION: :has(input[disabled]).
 * His wireEngagementToggles() only flips input.disabled and sets an inline
 * opacity on the group; it never changes visibility, and dashboard-shell.js
 * is frozen, so no JS could be added. `disabled` is a reflected attribute,
 * so setting the property live also adds/removes the attribute and the
 * selector re-evaluates on every change with no script at all. The class is
 * added to HIS existing #engagement-group-NN div; his id and inline style
 * are untouched. */
.p2v-cta-group { overflow: hidden; }
.p2v-cta-group:has(input[disabled]) {
  max-height: 0;
  margin-top: 0;
  margin-bottom: 0;
  visibility: hidden;
}
.p2v-cta-hint {
  margin: 0 0 var(--s2);
  max-width: 60ch;
  font-size: var(--p2v-meta-size);
  color: var(--muted-ink);
}
.p2v-cta-group:has(input:not([disabled])) + .p2v-cta-hint { display: none; }

/* ---- 11.6 tab bar under the panel head (brand pages) ------------------- */
.p2v-tabs {
  display: flex;
  gap: var(--s3);
  margin: 0 0 var(--s3);
  border-bottom: 1px solid var(--border);
}
.p2v-tab {
  padding: 0 0 10px;
  font-size: var(--p2v-meta-size);
  font-weight: 600;
  color: var(--muted-ink);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.p2v-tab:hover { color: var(--ink); }
.p2v-tab.is-active { color: var(--accent-deep); border-bottom-color: var(--accent); }
.p2v-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- 11.7 first-win card (wizard step 4) ------------------------------- */
body.p2v .p2v-panel section.card.p2v-firstwin {
  border: 1px solid var(--accent);
  background: var(--accent-tint);
}
.p2v-firstwin-body { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: flex-start; }
.p2v-firstwin-text { flex: 1 1 260px; min-width: 0; }
.p2v-firstwin-text > p { max-width: 60ch; }
.p2v-firstwin-qr { flex: 0 0 auto; }
body.p2v .p2v-panel .p2v-firstwin-qr .logo-preview { margin: 0; padding: var(--s2); }
.p2v-firstwin-row { display: flex; gap: var(--s1); align-items: center; max-width: 420px; margin-bottom: var(--s2); }

/* ---- 11.8 review tree (wizard step 5) ---------------------------------- */
.p2v-tree, .p2v-tree ul { list-style: none; margin: 0; padding: 0; }
.p2v-tree ul {
  margin-left: 10px;
  padding-left: var(--s3);
  border-left: 1px solid var(--border);
}
.p2v-tree-item { padding: 8px 0; }
.p2v-tree-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
.p2v-tree-name { font-weight: 600; font-size: var(--p2v-body-size); color: var(--ink); }
.p2v-tree-kind {
  font-size: var(--p2v-tile-label-size);
  font-weight: 600;
  letter-spacing: var(--p2v-th-tracking);
  text-transform: uppercase;
  color: var(--muted-ink);
}
.p2v-tree-note { margin: 4px 0 0; font-size: var(--p2v-meta-size); color: var(--muted-ink); }

/* ---- 11.9 overlay card head ------------------------------------------- */
.p2v-overlay-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--s2);
}
.p2v-overlay-title { font-weight: 700; font-size: var(--p2v-section-size); }

/* ---- 11.10 media stage for an overlay video ---------------------------- */
body.p2v .p2v-panel .p2v-stage {
  background: var(--ink);
  border-radius: var(--r);
  padding: var(--s2);
  display: flex;
  justify-content: center;
  margin-bottom: var(--s2);
}
body.p2v .p2v-panel .p2v-stage .thumb { width: 180px; }

/* ---- 11.11 pass-2 fixes (see SELF-REVIEW.md "B2a") --------------------- */

/* At 390 the 5-step stepper was clipped at the panel edge, so steps 4 and 5
   were unreachable on a phone. Scoped to wizard pages so no other page's
   stepper changes. */
@media (max-width: 960px) {
  body.p2v-wizard .p2v-stepper {
    overflow-x: auto;
    flex-wrap: nowrap;
    scrollbar-width: none;
  }
  body.p2v-wizard .p2v-stepper::-webkit-scrollbar { display: none; }
  body.p2v-wizard .p2v-step { flex: 0 0 auto; }
}

/* The action row at the foot of a capped-width form sat flush against the
   last field. */
body.p2v .p2v-panel form.p2v-form-col > .p2v-actions { margin-top: var(--s3); }

/* The details column is much shorter than the media column, so on wide
   screens it followed the scroll instead of leaving a dead grey block. */
@media (min-width: 960px) {
  .p2v-col--sticky { position: sticky; top: var(--s2); }
}

/* The overlay video stage was rendering as a full-column black slab. Capped
   so it reads as a media object next to its own controls. */
body.p2v .p2v-panel .p2v-stage { max-width: 340px; }

/* His .accent-border lost its blue edge inside the panel: the shell's own
   `body.p2v .p2v-panel section.card` rule (0-3-1) out-specifies `.accent-border`
   (0-1-0). Re-asserted at the same weight; his class and its meaning are
   unchanged. */
body.p2v .p2v-panel section.card.accent-border { border-color: var(--accent); }


/* =========================================================================
 * 12. ANALYTICS + DATA OWNER
 *     (was _additions-analytics.css - 07, 13). Its "6b" grid-scroll unpin
 *     and "6c" topbar fix are NOT here - see the merge note above.
 * ====================================================================== */

/* =========================================================================
 * 12.1 FILTER TOOLBAR — his GET form, restyled as a row of pill controls
 *    Sits as the first row UNDER the panel head (see SELF-REVIEW "B2b").
 * ====================================================================== */

body.p2v .p2v-panel section.card.p2v-filterbar {
  padding: var(--s1) var(--s2);
  margin-bottom: var(--s3);
}
body.p2v .p2v-panel .p2v-filterbar-form {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s1);
  max-width: none;
  margin: 0;
}
body.p2v .p2v-panel .p2v-filterbar-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s1);
  min-width: 0;
}
/* One label+control pill. Same box as .p2v-control-pill in dashboard.css,
   but it carries a visible label because a bare "All time" reads as nothing. */
body.p2v .p2v-panel .p2v-field {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--p2v-control-pill-h);
  max-width: 100%;
  padding: 0 4px 0 14px;
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
}
body.p2v .p2v-panel .p2v-field label {
  margin: 0;
  flex: none;
  font-size: var(--p2v-stat-label-size);
  font-weight: 600;
  letter-spacing: var(--p2v-stat-label-tracking);
  text-transform: uppercase;
  color: var(--muted-ink);
  white-space: nowrap;
}
body.p2v .p2v-panel .p2v-field select,
body.p2v .p2v-panel .p2v-field input {
  width: auto;
  min-width: 0;
  max-width: 100%;
  margin: 0;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: var(--p2v-meta-size);
}
body.p2v .p2v-panel .p2v-field select:focus-visible,
body.p2v .p2v-panel .p2v-field input:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 999px; }
/* Apply sits at the far end of the toolbar row. */
body.p2v .p2v-panel .p2v-filterbar-form > button[type="submit"] { margin-left: auto; }

/* =========================================================================
 * 12.2 SECTION HEAD — his <h2> and its view toggle on one row
 * ====================================================================== */

body.p2v .p2v-panel .p2v-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  margin: var(--s4) 0 var(--s2);
}
body.p2v .p2v-panel .p2v-section-title {
  margin: 0;
  font-size: var(--p2v-section-size);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
/* Variant used inside one of his cards (data owner's CTA submissions). */
body.p2v .p2v-panel section.card > .p2v-section-head--incard {
  margin: 0 0 var(--s2);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--border);
}
body.p2v .p2v-panel .p2v-section-head .p2v-csv-row { margin: 0; }

/* =========================================================================
 * 12.3 VIEW TOGGLES — his [data-view-btn] buttons as a segmented control
 *    Attributes, ids and the .active class his JS sets are untouched;
 *    this is presentation only.
 * ====================================================================== */

body.p2v .p2v-panel .view-toggle {
  display: inline-flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 2px;
  margin: 0;
  padding: 3px;
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
}
body.p2v .p2v-panel .view-toggle button {
  display: inline-flex;
  align-items: center;
  min-height: var(--p2v-seg-h);
  padding: 0 14px;
  flex: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--muted-ink);
  font-family: inherit;
  font-size: var(--p2v-meta-size);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
}
body.p2v .p2v-panel .view-toggle button:hover {
  background: var(--elevated);
  border-color: transparent;
  color: var(--ink);
}
body.p2v .p2v-panel .view-toggle button.active {
  background: var(--accent-tint);
  border-color: transparent;
  color: var(--accent-deep);
  font-weight: 700;
}
body.p2v .p2v-panel .view-toggle button:focus-visible { outline: none; box-shadow: var(--ring); }
/* Second tier: the by-target / by-brand group-by switches nested inside a
   panel. Same control, one notch quieter so it never competes with the
   section-level toggle sitting above it. */
body.p2v .p2v-panel .view-toggle.p2v-seg--sub {
  background: transparent;
  border-color: var(--border-strong);
  margin: 0 0 var(--s2);
}
body.p2v .p2v-panel .view-toggle.p2v-seg--sub button {
  min-height: var(--p2v-seg-h-sm);
  padding: 0 12px;
  font-size: var(--t1);
}

/* =========================================================================
 * 12.4 STAT TILES — deck slide 10: label above, number below, hairline box
 *    His .stat-grid-3 keeps its 3 columns and its deliberate empty cell.
 * ====================================================================== */

body.p2v .p2v-panel .stat-grid-3 {
  gap: var(--s2);
  margin: 0 0 var(--s3);
  align-items: stretch;
}
body.p2v .p2v-panel .stat-grid-3 .stat-tile {
  /* column-reverse puts .l (the label) above .n (the number) without
     touching his DOM order, which analytics.ts emits number-first. */
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  padding: var(--p2v-tile-pad);
  border: var(--p2v-tile-border);
  border-radius: var(--p2v-tile-radius);
  box-shadow: none;
}
body.p2v .p2v-panel .stat-tile .l {
  margin: 0;
  font-size: var(--p2v-stat-label-size);
  font-weight: 600;
  letter-spacing: var(--p2v-stat-label-tracking);
  color: var(--muted-ink);
  line-height: 1.4;
}
body.p2v .p2v-panel .stat-tile .n {
  margin-top: var(--s1);
  font-size: var(--p2v-stat-number-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink);
}
/* Billing tiles: a 3px accent bar groups the row instead of a full accent
   outline (which turned two thirds of the summary blue). */
body.p2v .p2v-panel .stat-grid-3 .stat-tile-financial {
  border-color: var(--border);
  border-left: var(--p2v-billing-bar) solid var(--accent);
}
body.p2v .p2v-panel .stat-grid-3 .stat-tile-financial .n { color: var(--ink); }
/* "Free Trial" is a status word, not a figure — demoted to label weight so
   it stops reading as the biggest number on the page. */
body.p2v .p2v-panel .stat-tile .n.p2v-stat-text {
  font-size: var(--p2v-body-size);
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.4;
  color: var(--muted-ink);
}

/* =========================================================================
 * 12.5 NOTE BLOCK — his two .analytics-note paragraphs as one muted block
 * ====================================================================== */

body.p2v .p2v-panel .p2v-notebox {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  margin: 0 0 var(--s3);
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
body.p2v .p2v-panel .p2v-notebox .analytics-note { margin: 0; color: var(--muted-ink); }

/* Data owner's scope line, directly under the panel head. */
body.p2v .p2v-panel .p2v-scope { margin: 0 0 var(--s3); color: var(--muted-ink); }

/* Data owner's standing caveat. Uppercase because the source page is named
   for it; a warning-toned bar so it cannot be mistaken for live billing. */
body.p2v .p2v-panel .p2v-demo-banner {
  margin: 0 0 var(--s3);
  padding: var(--s1) var(--s2);
  background: var(--warning-tint);
  border: 1px solid var(--border);
  border-left: var(--p2v-billing-bar) solid var(--warning);
  border-radius: var(--r-sm);
  color: var(--warning);
  font-size: var(--p2v-meta-size);
  line-height: 1.5;
}
body.p2v .p2v-panel .p2v-demo-banner strong {
  letter-spacing: var(--p2v-stat-label-tracking);
  margin-right: var(--s1);
}

/* =========================================================================
 * 12.6 CHART CARDS — deck slide 10: bold title, muted subtitle, hairline box
 *    analytics-charts.js clears and refills .chart-box on load, so every
 *    rule here targets the container or an element the script itself
 *    creates (.chart-title, .chart-wrap, .chart-legend, .chart-download-btn).
 * ====================================================================== */

body.p2v .p2v-panel .chart-box {
  margin: 0 0 var(--s3);
  padding: var(--p2v-tile-pad);
  background: var(--surface);
  border: var(--p2v-tile-border);
  border-radius: var(--p2v-tile-radius);
  box-shadow: none;
}
body.p2v .p2v-panel .chart-box .chart-title {
  margin: 0;
  font-size: var(--p2v-chart-title-size);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  line-height: 1.35;
}
/* His per-panel "most recent 90 days" caption is the muted subtitle line. */
body.p2v .p2v-panel .p2v-chart-note {
  margin: 0 0 var(--s2);
  max-width: 78ch;
  font-size: var(--p2v-meta-size);
  color: var(--muted-ink);
  line-height: 1.5;
}
body.p2v .p2v-panel .chart-box .chart-wrap { margin-top: var(--s2); }
body.p2v .p2v-panel .chart-box .chart-wrap-pie svg { margin: 0 auto; }
body.p2v .p2v-panel .chart-box .chart-legend {
  margin: var(--s2) 0 0;
  gap: var(--s1) var(--s2);
  font-size: var(--p2v-meta-size);
  color: var(--muted-ink);
}
body.p2v .p2v-panel .chart-box .chart-download-btn {
  min-height: var(--p2v-btn-h-sm);
  margin-top: var(--s2);
  padding: var(--p2v-btn-pad-sm);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--p2v-btn-radius);
  color: var(--ink);
  font-size: var(--p2v-meta-size);
  font-weight: 700;
}
body.p2v .p2v-panel .chart-box .chart-download-btn:hover { background: var(--elevated); border-color: var(--muted-ink); }
/* Chart furniture. dashboard.css already re-points the gridline/axis/marker
   STROKES (its .chart-wrap svg line / circle / .chart-wrap-pie svg path
   block) off the old dark theme. The one thing it does not cover is the
   axis LABEL colour: analytics-charts.js writes fill="#888" as a
   presentation attribute, which is 3.5:1 on white. Presentation attributes
   lose to any real CSS rule, so this fixes it without touching the script. */
body.p2v .p2v-panel .chart-wrap svg text { fill: var(--muted-ink); }
/* Gridlines one step further back than dashboard.css's shared #B4B3B0: at
   that value they read stronger than several of the data series in front of
   them. --border is the same hairline every card, table and tile on the page
   already uses, which is the point of having one. The crosshair is a <line>
   too and goes with them; it is never the only hover feedback (the per-series
   dots and the tooltip both appear with it). */
body.p2v .p2v-panel .chart-wrap svg line { stroke: var(--border); }
/* A 220px pie stranded in a 1160px card is mostly empty card. The pie
   chart-boxes are the ones carrying data-pie, so no :has() needed. */
body.p2v .p2v-panel .chart-box[data-pie] { max-width: 520px; }
/* .grid-scroll brings its own border + radius, which inside one of his cards
   draws a second hairline 20px in from the first. The card is the frame. */
body.p2v .p2v-panel section.card > .grid-scroll { border: 0; border-radius: 0; }

/* Numeric grid columns are right-aligned (deck slide 10 / dataviz). The td
   carries dashboard.css's .p2v-num; the header follows it off his own
   data-sort-type, which is already on every sortable th.
   PARTNER: add class="p2v-num" to the numeric <td>s in analytics.ts's row
   template — that is the only server-side change this file assumes. */
body.p2v .p2v-panel .sortable-table th[data-sort-type="number"] { text-align: right; }

/* Download-CSV row under a grid. */
body.p2v .p2v-panel .p2v-csv-row { margin: var(--s2) 0 0; }

/* =========================================================================
 * 12.7 RESPONSIVE
 * ====================================================================== */

@media (max-width: 900px) {
  /* His deliberate empty grid cell only makes sense at 3 columns; below
     that it leaves a hole in the middle of the summary. */
  body.p2v .p2v-panel .stat-grid-3 > div:not([class]) { display: none; }
}

@media (max-width: 640px) {
  body.p2v .p2v-panel .p2v-section-head { align-items: flex-start; }
  body.p2v .p2v-panel .p2v-filterbar-form > button[type="submit"] { margin-left: 0; }
  body.p2v .p2v-panel .p2v-field { width: 100%; }
  body.p2v .p2v-panel .p2v-field select,
  body.p2v .p2v-panel .p2v-field input { flex: 1 1 auto; }
  body.p2v .p2v-panel .p2v-filterbar-group { width: 100%; }
  body.p2v .p2v-panel .chart-box { padding: var(--s2); }
  /* Let the grid headers wrap on a phone. His th is white-space:nowrap, which
     pushes a 5-column grid ~60px wider than the card and leaves the last
     column only reachable by sideways-scrolling inside a page that already
     scrolls. Wrapped, every grid fits the card at 390. */
  body.p2v .p2v-panel .grid-scroll th { white-space: normal; }
  body.p2v .p2v-panel .grid-scroll th,
  body.p2v .p2v-panel .grid-scroll td { padding-left: var(--s1); padding-right: var(--s1); }
  body.p2v .p2v-topbar .p2v-agency-name { max-width: 30vw; }
  /* Data owner only: at 390 the brand name plus a 29-character account email
     cannot both sit in the topbar without one of them being ellipsised.
     The brand is already named in the scope line under the panel head, the
     email is not named anywhere else, so the brand is the one that goes. */
  body.p2v-dataowner .p2v-topbar .p2v-agency { display: none; }
}

/* =========================================================================
 * 12.8 PRINT — the toolbar and the toggles are chrome, not content.
 * ====================================================================== */

@media print {
  body.p2v .p2v-panel section.card.p2v-filterbar,
  body.p2v .p2v-panel .view-toggle,
  body.p2v .p2v-panel .chart-download-btn { display: none !important; }
  body.p2v .p2v-panel .chart-box, body.p2v .p2v-panel .stat-tile { box-shadow: none; }
}


/* =========================================================================
 * 13. ACCOUNT, FINANCE, ARTICLE + LOGIN PAGES
 *     (was _additions-account.css - 01, 08, 08b, 08c, 09, 10, 11, 12)
 * ====================================================================== */

/* =========================================================================
 * 13.1 LOGIN  (01_login_page.html, body class "p2v p2v-auth")
 *    Logged out: no sidebar, no topbar. body.p2v's own flex row is
 *    replaced by a centring grid so the card sits on his --page.
 * ====================================================================== */

body.p2v.p2v-auth {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: var(--s4) var(--s3);
  background: var(--page);
}
body.p2v.p2v-auth main {
  width: 100%;
  max-width: 380px;
  margin: 0;
}
.p2v-auth-wordmark {
  display: block;
  margin: 0 0 var(--s3);
  font-family: var(--p2v-wordmark-font);
  font-size: var(--t4);
  letter-spacing: var(--p2v-wordmark-tracking);
  color: var(--p2v-wordmark-color);
  text-align: center;
}
.p2v-auth-wordmark .p2v-wordmark-play { color: var(--accent); margin: 0 6px; }
body.p2v.p2v-auth h1 {
  margin: 0 0 var(--s1);
  font-size: var(--t5);
  line-height: 1.2;
  text-align: center;
}
body.p2v.p2v-auth main > p.muted {
  margin: 0 0 var(--s3);
  font-size: var(--p2v-meta-size);
  text-align: center;
}
body.p2v.p2v-auth section.card {
  margin: 0;
  padding: var(--s3);
  border: var(--p2v-card-border);
  border-radius: var(--p2v-card-radius);
}
body.p2v.p2v-auth section.card form { max-width: none; }
body.p2v.p2v-auth section.card label {
  display: block;
  margin-bottom: var(--s1);
  font-size: var(--p2v-label-size);
  font-weight: var(--p2v-label-weight);
}
body.p2v.p2v-auth section.card input { width: 100%; }
body.p2v.p2v-auth section.card button[type="submit"] { width: 100%; margin-top: var(--s2); }

/* =========================================================================
 * 13.2 FINANCE  (08_finance.html, main class "p2v-panel p2v-finance")
 * ====================================================================== */

/* His .stat-row is a flex wrap; a 2-up grid keeps the two CPS tiles equal
   width AND equal height (ragged tiles are the loudest "clunky" tell). */
body.p2v .p2v-finance .stat-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s2);
  margin: var(--s2) 0 var(--s3);
}
body.p2v .p2v-finance .stat-tile {
  position: relative;
  padding-left: calc(var(--s3) + var(--p2v-fin-bar));
  box-shadow: none;
}
body.p2v .p2v-finance .stat-tile::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--p2v-fin-bar);
  background: var(--accent);
  border-radius: var(--r) 0 0 var(--r);
}
/* "Free Trial" is a status word, not a figure - demoted off the 28px
   number size so it stops shouting louder than the invoice totals. */
body.p2v .p2v-finance .stat-tile .n {
  font-size: var(--t3);
  font-weight: 700;
  letter-spacing: 0;
}
body.p2v .p2v-finance .stat-tile .l { text-transform: none; letter-spacing: 0; }
@media (max-width: 640px) {
  body.p2v .p2v-finance .stat-row { grid-template-columns: 1fr; }
}

/* His filter form: a quiet toolbar strip above the invoices grid. */
body.p2v .p2v-finance form[data-warn-on-change] {
  padding: var(--s2);
  margin-bottom: var(--s2);
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
body.p2v .p2v-finance form[data-warn-on-change] label {
  display: block;
  margin-bottom: 4px;
}
body.p2v .p2v-finance form[data-warn-on-change] select,
body.p2v .p2v-finance form[data-warn-on-change] input[type="date"] { margin: 0; }

/* A control that has been PROMOTED into the panel head keeps its markup
   (and therefore its href) in the DOM down here, hidden, so nothing the
   partner's server or the binding guard looks for ever disappears.
   !important because his `.button` rule sets display:inline-flex, which
   would otherwise out-specify the UA's own [hidden] rule. */
body.p2v .p2v-hide { display: none !important; }

/* Section heading between cards (his bare <h2>Invoices</h2>). */
body.p2v .p2v-finance > h2 {
  margin: var(--s4) 0 var(--s2);
  font-size: var(--p2v-section-size);
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* A key-value card for the rate metadata that used to be two stray
   muted paragraphs under the tiles. */
/* Used together with the shell's own .p2v-kv grid - adds only the rule
   line that separates the pairs from the tiles above them. */
.p2v-kv-row {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}

/* =========================================================================
 * 13.3 INVOICE  (08b, main class "p2v-panel p2v-invoice")
 *    His three .print-block boxes are wrapped, never rewritten, so the
 *    @media print rules in his sheet still hit exactly the same elements.
 *    Everything below is SCREEN styling only.
 * ====================================================================== */

.p2v-invoice-doc {
  max-width: 720px;
  background: var(--surface);
  border: var(--p2v-card-border);
  border-radius: var(--p2v-card-radius);
  padding: var(--s3);
}
.p2v-invoice-doc > .print-block,
.p2v-invoice-doc > .dns-step {
  border: 0;
  background: transparent;
}
.p2v-invoice-doc > .dns-step { padding: 0 0 var(--s1); }
/* Amounts must never break across two lines in an invoice. Desktop only:
   at 390 the three columns cannot all hold their content on one line, and
   forcing them pushes the document past its own padding. */
@media (min-width: 641px) {
  .p2v-invoice-doc td, .p2v-invoice-doc th { white-space: nowrap; }
  .p2v-invoice-doc td:first-child, .p2v-invoice-doc th:first-child { white-space: normal; }
}
.p2v-invoice-doc > .print-block:first-child {
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--border);
}
.p2v-invoice-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s2);
  flex-wrap: wrap;
}
/* The totals block carries his own inline border-radius:10px, so a plain
   border-top renders as a rounded 2px line with curled end caps. A filled
   panel uses that radius instead of fighting it. */
.p2v-invoice-doc > .print-block:last-of-type {
  margin-top: var(--s3);
  background: var(--page);
}
.p2v-invoice-doc h3 { font-size: var(--t2); color: var(--muted-ink); font-weight: 600; }

/* Print: the wrapper must add nothing of its own to the page, and his own
   @media print intent (each .print-block white with #111 text) has to
   survive the screen rules above - this file loads after his, so it is
   restated here rather than left to source order. The shell's body paints
   the sidebar grey; on paper that becomes white. */
@media print {
  body.p2v { background: #fff; }
  .p2v-invoice-doc { max-width: none; border: 0; padding: 0; background: #fff; }
  .p2v-invoice-doc > .print-block,
  .p2v-invoice-doc > .dns-step.print-block { background: #fff; color: #111; }
  .p2v-invoice-doc > .print-block:first-child { border-bottom-color: #ccc; }
  .p2v-invoice-doc > .print-block:last-of-type { background: #fff; border-top: 2px solid #111; }
  .p2v-invoice-doc h3 { color: #111; }
}

/* =========================================================================
 * 13.4 SEGMENTED CONTROL  (08c payment history: All / By brand)
 *    Two <a>s using his own hrefs, drawn as one control.
 *
 * MERGE 2026-08-25: this and his .view-toggle (section 12.3) were two
 * different-looking drawings of the same control - one white-pill-on-grey,
 * one accent-tint-on-white. They are one control now, matching 12.3: white
 * track with a hairline, accent-tint active pill. Only the geometry differs,
 * because .p2v-seg holds links and .view-toggle holds buttons.
 * ====================================================================== */

.p2v-seg {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
}
body.p2v .p2v-seg-item {
  display: inline-flex;
  align-items: center;
  min-height: var(--p2v-seg-h);
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: var(--p2v-meta-size);
  font-weight: 600;
  line-height: 1.2;
  color: var(--muted-ink);
  text-decoration: none;
  white-space: nowrap;
}
body.p2v .p2v-seg-item:hover { background: var(--elevated); color: var(--ink); }
body.p2v .p2v-seg-item.is-active {
  background: var(--accent-tint);
  color: var(--accent-deep);
  font-weight: 700;
}
body.p2v .p2v-seg-item:focus-visible { outline: none; box-shadow: var(--ring); }

/* =========================================================================
 * 13.5 CUSTOM DOMAIN  (09, main class "p2v-panel p2v-domain")
 *    His `.stat-tile.stat-tile-financial` holds the status word in `.n`
 *    and the URL in `.l`. Restyled ADJACENTLY into a status row - his
 *    markup is untouched, so if he ever swaps the word Live for another
 *    state the pill still reads.
 * ====================================================================== */

body.p2v .p2v-domain .stat-tile-financial {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  border-color: var(--border);
  box-shadow: none;
}
body.p2v .p2v-domain .stat-tile-financial .n {
  display: inline-flex;
  align-items: center;
  min-height: var(--p2v-pill-h);
  padding: var(--p2v-pill-pad);
  border-radius: var(--p2v-pill-radius);
  background: var(--success-tint);
  color: #0B5C2E; /* same pairing his own .success banner uses */
  font-size: var(--p2v-pill-size);
  font-weight: var(--p2v-pill-weight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
body.p2v .p2v-domain .stat-tile-financial .l {
  flex: 1 1 240px;
  margin-top: 0;
  font-size: var(--p2v-body-size);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}
body.p2v .p2v-domain form[action="/dashboard/custom-domain/remove"] { margin-top: var(--s3); }

/* =========================================================================
 * 13.6 ARTICLE PAGES  (11 help, 12 white-label; main class "p2v-panel p2v-doc")
 *    Sticky in-page contents on the left at >=1100px, article measure
 *    capped so the prose stops running the full panel width.
 * ====================================================================== */

.p2v-doc-grid { display: block; }
/* Body column when the article must keep its own full width (the
   white-label explainer's 4-up phone grid). */
.p2v-doc-col { min-width: 0; }
.p2v-toc { display: none; }
.p2v-doc-body { max-width: var(--p2v-doc-measure); }
body.p2v .p2v-doc .p2v-doc-body section.card > h2 { font-size: var(--t4); }
body.p2v .p2v-doc .p2v-doc-body p,
body.p2v .p2v-doc .p2v-doc-body li { font-size: var(--p2v-body-size); line-height: 1.65; }
body.p2v .p2v-doc .p2v-doc-body ul { padding-left: var(--s3); }
body.p2v .p2v-doc .p2v-doc-body li + li { margin-top: 6px; }
/* His inline <strong> terms are UI labels ("Time frame", "Download as
   CSV"). Letting one break across two lines both reads badly and makes
   its bounding box span the full column, which the layout audit then
   reports as a genuine text overlap at 390px. The widest of them is
   128px against a 296px measure, so nowrap can never overflow here. */
body.p2v .p2v-doc .p2v-doc-body p > strong,
body.p2v .p2v-doc .p2v-doc-body li > strong { white-space: nowrap; }

@media (min-width: 1100px) {
  .p2v-doc-grid {
    display: grid;
    grid-template-columns: var(--p2v-toc-w) minmax(0, 1fr);
    gap: var(--s4);
    align-items: start;
  }
  .p2v-toc {
    display: block;
    position: sticky;
    top: var(--s3);
  }
  /* His own "On this page" card is the mobile fallback for the same
     links - one of the two is always visible, never both. */
  .p2v-toc-inline { display: none; }
}
/* The back-to-previous banner is his markup with its own inline
   margin-bottom; promoted into the panel head it needs that margin off. */
body.p2v .p2v-panel-actions > #back-to-previous { margin-bottom: 0 !important; }

/* A single short form should not sit in a card the full width of the
   panel - the card ends where the fields do. */
body.p2v .p2v-account-page section.card { max-width: 600px; }

.p2v-toc-title {
  margin: 0 0 var(--s1);
  font-size: var(--p2v-tile-label-size);
  font-weight: 700;
  letter-spacing: var(--p2v-th-tracking);
  text-transform: uppercase;
  color: var(--muted-ink);
}
.p2v-toc-list { list-style: none; margin: 0; padding: 0; }
body.p2v .p2v-toc-list a {
  display: block;
  padding: 6px var(--s1);
  border-left: 2px solid var(--border);
  font-size: var(--p2v-meta-size);
  line-height: 1.35;
  color: var(--muted-ink);
  text-decoration: none;
}
body.p2v .p2v-toc-list a:hover { color: var(--accent-deep); border-left-color: var(--accent); }
body.p2v .p2v-toc-list a:focus-visible { outline: none; box-shadow: var(--ring); }

/* =========================================================================
 * 13.7 WHITE-LABEL EXPLAINER  (12, body class "p2v p2v-wl")
 *    This page ships its OWN <style> block with its OWN :root - including
 *    a prefers-color-scheme:dark override that no other dashboard page
 *    has. Left in place (it is his file), but the shell's tokens are
 *    re-pinned on the body so one page cannot flip the whole dashboard
 *    dark on a dark-mode laptop. Light Signal is the locked single theme.
 *    Specificity, not source order, is what wins here: body.p2v.p2v-wl
 *    (0,3,0) outranks his :root and his prefers-color-scheme block
 *    (0,1,0). 12 ALSO links dashboard.css a second time after its own
 *    <style> so the belt-and-braces order holds if he ever lowers the
 *    specificity here. Verified in the browser: --page #EFEEEC.
 * ====================================================================== */

body.p2v.p2v-wl {
  --page: #EFEEEC;
  --surface: #FFFFFF;
  --elevated: #E8E6E3;
  --ink: #0A0A0A;
  --muted-ink: #5C5C5C;
  --border: #DDDCDA;
  --border-strong: #C4C3C0;
  --accent: #1B45E8;
  --accent-deep: #1533B8;
  --accent-tint: #E6EBFE;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--p2v-body-size);
  line-height: 1.55;
}
/* His own page header duplicates the shell's sidebar and topbar. Hidden,
   never removed - both of its hrefs stay in the DOM. */
body.p2v.p2v-wl .wrap > header.top { display: none; }
body.p2v.p2v-wl .wrap { max-width: none; margin: 0; padding: 0; }
body.p2v.p2v-wl .prose { max-width: var(--p2v-doc-measure); }
body.p2v.p2v-wl .foot { margin-top: var(--s4); }

/* 14. Date/time inputs (added 2026-08-25, wizard step 1) — same look as his text inputs */
body.p2v input[type="datetime-local"], body.p2v input[type="date"] {
  display: block; width: 100%; box-sizing: border-box; font: inherit; font-size: var(--t3);
  color: var(--ink); background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r-sm); padding: 0.6rem 0.75rem; margin: 0 0 var(--s2);
}
body.p2v input[type="datetime-local"]:focus-visible, body.p2v input[type="date"]:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* =========================================================================
 * 15. PRODUCT CAMPAIGNS - by-URL/QR group sections
 *   product-campaigns.ts renders #p2v-pc-scope with one .p2v-pc-section
 *   per distinct link (the shared Agency `?a=<id>` one first, then one
 *   per Semi/Total Brand Labeled Brand). Per CLAUDE.md "Next tasks"
 *   (2026-08-29): "assign a separate background to each group to
 *   emphasize that they indeed belong to different groups (different
 *   sections)". Each section is a padded panel with its own pale tint
 *   and a matching 4px left rule; the tints cycle every 4 so an Agency
 *   with many Brands still gets clear separation. The tint values are
 *   literal one-offs (no other use in this file), kept deliberately
 *   faint so they sit quietly inside the white card. Light Signal is
 *   the locked single theme (see 13.7), so no dark-mode variant.
 * ====================================================================== */
body.p2v #p2v-pc-scope .p2v-pc-section {
  padding: var(--s2);
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--r-sm);
  margin-bottom: var(--s3);
}
body.p2v #p2v-pc-scope .p2v-pc-section:last-child { margin-bottom: 0; }
body.p2v #p2v-pc-scope .p2v-pc-section:nth-of-type(4n + 1) { background: #EEF2FD; border-left-color: #1B45E8; }
body.p2v #p2v-pc-scope .p2v-pc-section:nth-of-type(4n + 2) { background: #ECF6EF; border-left-color: #0F7A3D; }
body.p2v #p2v-pc-scope .p2v-pc-section:nth-of-type(4n + 3) { background: #FBF3E4; border-left-color: #8A5A00; }
body.p2v #p2v-pc-scope .p2v-pc-section:nth-of-type(4n + 4) { background: #F4EFFA; border-left-color: #6B3FA0; }
/* Spacing for the shared section's per-Brand sub-blocks (<h4> + table),
   with a hairline rule between them so several Brands under the one
   Agency link still read apart without competing with the section tint. */
body.p2v #p2v-pc-scope .p2v-pc-section .p2v-brand-group { margin-top: var(--s2); }
body.p2v #p2v-pc-scope .p2v-pc-section .p2v-brand-group + .p2v-brand-group {
  border-top: 1px solid var(--border);
  padding-top: var(--s1);
}
