/* ============================================================
   VOCTOS Client Portal — shared design system
   Brand: taken from voctos.com — #0D172D ink · #7616F4 violet · #A1CB3A lime
   ============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Unbounded:wght@300;400;500;600;700&display=swap');

/* ---------------------------------------------------------------------------
   THE PALETTE IS THE WEBSITE'S, NOT THE LOGO'S.

   An earlier version of this file sampled `public/img/voctos-logo.png` pixel by
   pixel and built the palette from the three colours in the mark: #124F70
   wordmark, #00A7B9 arc, #7FC241 arrowhead. That was a defensible way to pick
   colours and it produced a product that looked nothing like voctos.com, which
   is what people see before they ever log in. The site's own ink, violet and
   lime replaced them.

   The NAMES were kept deliberately. `--teal` now holds a violet. Renaming it
   would mean touching ~140 call sites across 69 views for a change that is
   entirely about value, and every rename is a chance to miss one and leave a
   stray old colour behind — so the names are historical and the values are
   current. If that bothers you, rename them in one pass with the render harness
   (`npm run page <page>`) checking both roles, not incrementally.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   AUGUST 2026 — the palette now comes from voctos.com, not from the logo.

   Read in a browser off the live homepage, not sampled from an image:
     #0D172D  every h1/h2/h3          #7616F4  "Request a Call" + every eyebrow
     #9BB725  "Book a consultation"   #EFF0F4  main.site-main
     Unbounded (display) + Inter (body)

   THE NAMES ARE UNCHANGED ON PURPOSE. `--teal` is violet now and `--navy` is
   the site's ink. Renaming them would mean touching ~140 call sites across 67
   views for a change that is entirely about VALUE, and every rename is a chance
   to miss one and leave a stray old colour behind. One file changes; the whole
   portal follows. See previews/brand-persona.md for the measurements.

   The one thing NOT copied: the site puts white text on its lime button, which
   measures 2.28:1 against a 4.5 requirement. --on-accent is ink here instead.
   --------------------------------------------------------------------------- */
:root{
  /* --green is the site's lime. It is used for fills and rings only, never for
     text on white — lime text on white is 2.28:1, which is why the site's own
     lime button gets INK on it here rather than the white the site ships. */
  --navy:#0D172D; --blue:#2563EB; --teal:#7616F4; --teal-600:#5B0FC0; --green:#9BB725;
  --teal-700:#4A0C9E; --lime:#A1CB3A;
  --grad:linear-gradient(90deg,#0D172D 0%,#7616F4 52%,#A1CB3A 100%);
  /* The half of the sweep that can carry white text — the full one ends on lime
     and cannot. Used for the primary button. */
  /* The site's primary is a flat violet, not a sweep — kept as a "gradient"
     token only because ~40 call sites already say var(--grad-btn). */
  --grad-btn:linear-gradient(92deg,#7616F4 0%,#7616F4 100%);
  --grad-btn-hover:linear-gradient(92deg,#5B0FC0 0%,#5B0FC0 100%);
  --grad-soft:linear-gradient(120deg,#E9E2FB,#F1ECFE 55%,#EFF5E2);

  /* Neutrals re-tuned to the cyan hue instead of the violet one, so the greys
     sit under the accent rather than arguing with it. */
  --ink:#0D172D; --ink-2:#3C465D;
  --paper:#E6E9EF; --surface:#FFFFFF; --surface-2:#F6F7FA;
  --line:rgba(13,23,45,.16); --line-2:rgba(13,23,45,.07);
  /* --muted-2 was #93A1AC: 2.4:1 on white. It is used for "not measured" and for
     table headers, so it was failing AA on text people are meant to read. */
  /* 5.93:1 and 5.15:1 on white; --muted-2 also clears AA on --paper, which is
     the darker of the two backgrounds it sits on. */
  --muted:#5B6478; --muted-2:#5F6678;

  --teal-tint:#F0E8FE; --navy-tint:#E7E9EF; --green-tint:#F0F6E1;
  --pos:#0E7A37; --neg:#C0272C; --warn:#E8A13A; --info:#7616F4;
  --pos-tint:rgba(23,163,74,.10); --neg-tint:rgba(229,72,77,.10); --warn-tint:rgba(232,161,58,.13);
  /* THE SOLID "SELECTED" FILL, AND THE TEXT THAT GOES ON IT.
   *
   * Seventeen rules used to say `background:var(--navy); color:#fff` — every
   * selected segment, active range pill, lead button, chat bubble, avatar and
   * open-addbox summary in the app. That is right in light mode and 17.83:1.
   *
   * In dark mode --navy is #C3CADB: the token is INVERTED there so it can serve
   * as text on a dark ground. Used as a fill it becomes a pale pill, and the
   * `#fff` next to it does not invert with it — white on #C3CADB is 1.64:1.
   * Every one of those seventeen controls was unreadable, and each looked like
   * its own small bug rather than one shared one.
   *
   * A fill and the text on it are a PAIR. Naming the pair is what stops them
   * being changed apart: in dark mode the pill inverts to near-white with dark
   * text, which is the same "solid block, opposite of the page" the light theme
   * gets. */
  --fill-strong:#0D172D; --fill-strong-hover:#26324D; --on-fill-strong:#FFFFFF;

  /* Button elevation. Small and tight: these sit ON a card, so the shadow only
     has to lift them off it, not off the page. Anything larger and a row of five
     starts reading as five little cards. */
  /* The amber that marks the "not measured" notice. --warn itself is #E8A13A,
     which is 2.2:1 on a white card — under the 3:1 a graphic needs. This is the
     same amber darkened until it clears, and it is already the colour the
     Google-access icon uses, so the two notices match. */
  --voff-rule:#B5791E;

  --btn-shadow:0 1px 2px rgba(13,23,45,.07), 0 4px 10px -4px rgba(13,23,45,.14);
  --btn-shadow-hover:0 2px 4px rgba(13,23,45,.09), 0 8px 18px -6px rgba(13,23,45,.20);

  /* Text that sits ON the accent colour. Overridden per workspace in layout.ejs,
     where the readable value is computed from the client's own accent. */
  --on-accent:#FFFFFF;
  /* Filled-accent button background. Same as the accent by default; layout.ejs
     darkens it for workspaces whose accent needs white text. */
  --accent-strong:#7616F4;
  /* Primary-button fill. Overridden per workspace in layout.ejs — see there. */
  --btn-primary:var(--grad-btn);
  --btn-primary-hover:var(--grad-btn-hover);
  --btn-primary-ink:#FFFFFF;

  /* Measured: the homepage box is radius 30 / padding 32, with NO border and
     NO shadow. --r-lg is what .card uses, so that is where 30 goes. */
  --r-sm:8px; --r:12px; --r-lg:30px; --r-xl:30px;
  /* THE HERO RADIUS IS NOT THE CARD RADIUS.
     30px is what voctos.com uses, and it is right there — its panels are
     500-600px wide, so 30px is about 5% of the width and reads as a soft edge.
     A KPI card here is roughly 200px wide, where the same 30px is 15% and the
     corners eat the box: it stops looking like a card and starts looking like a
     pill. Scaling the radius with the surface is what keeps the same OPTICAL
     corner, which is the thing being copied — not the number.
     The ratio only sets a range, not a number: one token serves both a 200px KPI
     card and an 1100px content card, so no single value matches 5% on both. 16px
     is the compromise — 8% on the small ones, crisp on the wide ones, and in the
     same family as the 12px folder tiles.
        --r-lg   30px  the notched panel and other full-width heroes
        --r-card 16px  the ordinary card
        --r      12px  inner boxes, and the folder tiles */
  --r-card:16px;
  --shadow:none;
  --shadow-lg:0 20px 50px rgba(13,23,45,.05);
  --tile-shadow:0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.1);
  /* A white card on the #EFF0F4 paper is a 1.14:1 step — for practical purposes
     no edge at all, so the panels dissolved into the page. The site solves this
     with elevation rather than a border: a tight contact shadow to seat the card,
     and a wide soft one to lift it. Deliberately lighter than --tile-shadow,
     because a card is a much larger surface than a 126px tile and the same
     shadow on it reads as heavy. */
  /* Close and slightly firmer, rather than wide and soft. A 24px blur on a
     200px card puts most of the shadow under the neighbouring card and reads as
     a haze; 8px sits under the edge and reads as a card lying on paper. */
  --card-shadow:0 1px 2px rgba(13,23,45,.06), 0 4px 8px -4px rgba(13,23,45,.10);
  --card-shadow-hover:0 1px 2px rgba(13,23,45,.07), 0 10px 20px -8px rgba(13,23,45,.18);
  --sb-w:254px;
}
/* Dark mode carries the same hues, lifted. The old dark --navy was #5B9BD8, a
   violet-blue — the same colour that was wrong in light mode, wrong again. The
   brand hues here are the logo's, raised in value until they read on a dark
   surface; the hue angles (201° / 185° / 91°) are unchanged. */
:root[data-theme="dark"]{
  --navy:#C3CADB; --blue:#7FA6FF; --teal:#A46BFF; --teal-600:#BE96FF; --green:#B7D95C;
  --teal-700:#CDB0FF; --lime:#D4EE69;
  --grad-soft:linear-gradient(120deg,#122C3A,#123A42 55%,#1D3320);
  /* Dark mode inverts the button rather than just lightening it.
   *
   * Lifting the light-mode gradient enough to stand out from a dark surface
   * pushes it past the point where white text still reads — white on #0E8DA0 is
   * 3.94, under AA. So the dark button goes light with DARK text on it, the
   * same trade the accent chips already make here. Ink on the darkest stop is
   * 5.42, and the button clears the surface behind it by the same margin. */
  /* #9350FF put white text at 4.35:1 — just under AA, and "just under" is still
     under. #8A3BFF is 5.06:1. It sits closer in luminance to the dark surface,
     which is fine: a button is found by its fill and its shape, and the text on
     it has to be readable regardless of how much the fill pops. */
  --grad-btn:linear-gradient(92deg,#8A3BFF 0%,#8A3BFF 100%);
  --grad-btn-hover:linear-gradient(92deg,#9C5AFF 0%,#9C5AFF 100%);
  --ink:#E9EDF7; --ink-2:#C3CADB;
  --paper:#070C18; --surface:#111A2E; --surface-2:#18223B;
  --line:rgba(255,255,255,.10); --line-2:rgba(255,255,255,.06);
  /* --muted-2 was #63788A: 3.2:1 on the dark surface. Same failure as light mode. */
  --muted:#9AA3BC; --muted-2:#8B94AE;
  --teal-tint:rgba(164,107,255,.16); --navy-tint:rgba(255,255,255,.07); --green-tint:rgba(161,203,58,.16);
  --pos:#8FE388; --neg:#FF8B8B; --warn:#EDB25A; --info:#A46BFF;
  --pos-tint:rgba(51,192,106,.14); --neg-tint:rgba(240,101,106,.14); --warn-tint:rgba(237,178,90,.16);
  /* These were #2DC2D2 and #04222A — a teal accent with near-black text on it,
     left over from a palette this brand no longer uses. Two things were wrong
     with it. The accent is violet, not teal, and it was the one place the old
     hue still surfaced. And --btn-primary-ink was the dark #04222A while
     --grad-btn is #8A3BFF: dark ink on that violet measures 3.27:1, under AA for
     body text. The comment above argues for a LIGHT button with dark text, but
     the button that actually ships is dark violet, so the text on it has to be
     white — which is the 5.06:1 that same comment worked out. */
  /* The pair inverts together. Near-white block, dark text on it: 16.67:1, and
     the same reading as light mode's dark-block-on-light-page. */
  --fill-strong:#E9EDF7; --fill-strong-hover:#FFFFFF; --on-fill-strong:#070C18;
  /* A drop shadow on a dark ground is invisible — there is nothing darker for it
     to be. The lift has to come from a hairline of light along the edge, with
     the shadow kept underneath only to seat the button. Same token, different
     technique, so no call site has to know which theme it is in. */
  /* On the dark card the light-mode amber is too dark to see, so the token
     flips to --warn, which measures 9.18:1 there. */
  --voff-rule:#EDB25A;
  --btn-shadow:0 0 0 1px rgba(255,255,255,.07), 0 4px 12px rgba(0,0,0,.40);
  --btn-shadow-hover:0 0 0 1px rgba(255,255,255,.14), 0 8px 20px rgba(0,0,0,.55);
  --on-accent:#FFFFFF; --accent-strong:#8A3BFF;
  --btn-primary:var(--grad-btn); --btn-primary-hover:var(--grad-btn-hover); --btn-primary-ink:#FFFFFF;
  --shadow:0 1px 2px rgba(0,0,0,.4); --shadow-lg:0 6px 24px rgba(0,0,0,.35);
}

*{box-sizing:border-box; margin:0; padding:0}
html,body{background:var(--paper); color:var(--ink);
  font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  -webkit-font-smoothing:antialiased; line-height:1.45; font-size:14px}
.disp{font-family:'Unbounded',sans-serif}
.tnum{font-variant-numeric:tabular-nums}
a{color:inherit; text-decoration:none}
button{font-family:inherit}
::-webkit-scrollbar{width:10px; height:10px}
::-webkit-scrollbar-thumb{background:var(--line); border-radius:10px; border:2px solid var(--paper)}

/* ---------- BRAND LOCKUP ---------- */
.brand{display:flex; align-items:center; gap:11px}
.brand .mark{width:36px; height:36px; flex:none; display:block}
.brand .wm{font-family:'Unbounded',sans-serif; font-weight:500; font-size:19px; letter-spacing:.01em;
  background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent}
.brand .wm .tm{font-size:9px; vertical-align:super; -webkit-text-fill-color:var(--muted-2)}
.brand-logo-img{height:auto; width:auto; max-width: 150px; display:block}
.brand .tagline{font-size:10px; color:var(--muted); letter-spacing:.02em}

/* ---------- APP LAYOUT ---------- */
.app{display:flex; min-height:100vh}
/* ============================================================
   THE SIDEBAR IS DARK
   The navy gradient is what voctos.com actually looks like — its
   hero, its footer, its whole identity. The portal had it nowhere,
   which is why a new palette and new card shapes still read as
   "almost no change": the largest constant surface on every screen
   was still plain white.

   Everything below exists because a rule further up assumed a light
   sidebar. Left alone they produce navy-on-navy badges, white text
   in white popovers, and pale hover plates. Contrast measured
   against BOTH ends of the gradient (#091122 and #182748); the
   lighter end is the harder test and the numbers quoted are its.
   ============================================================ */
.sidebar{width:var(--sb-w); flex:none; background:linear-gradient(160deg,#091122,#182748);
  color:#fff; border-right:0;
  position:sticky; top:0; height:100vh; display:flex; flex-direction:column; overflow:hidden}

/* items — 9.94:1, and 6.55:1 for the 10.5px caps, both past AA */
.sidebar .nav-item{color:rgba(255,255,255,.80)}
.sidebar .nav-item:hover{background:rgba(255,255,255,.08); color:#fff}
.sidebar .nav-item.active{background:rgba(255,255,255,.12); color:#fff; font-weight:600}
.sidebar .nav-item.active::before{background:var(--green)}      /* lime, 6.46:1 — violet is 2.86:1 here and unreadable */
.sidebar .nav-item.active .ic{color:var(--green); opacity:1}
.sidebar .nav-group .gl{color:rgba(255,255,255,.62)}
.sidebar .nav-group + .nav-group{border-top-color:rgba(255,255,255,.10)}
.sidebar .nav-item.locked:hover{background:rgba(255,255,255,.08)}
.sidebar .nav-item.locked .lockic{color:rgba(255,255,255,.55)}
.sidebar .nav-item.locked:hover .lockic{color:var(--green)}

/* counts — lime plate, ink numerals, 7.81:1 */
.sidebar .nav-item .cnt{background:var(--green); color:#0D172D; border-color:transparent}
.sidebar .nav-item.active .cnt{background:var(--green); color:#0D172D}
.sidebar .nav-item .cnt.hot{background:var(--neg); color:#fff}

/* switcher — its badge was var(--navy), i.e. navy on navy */
.sidebar .switcher{background:rgba(255,255,255,.07); border-color:rgba(255,255,255,.14)}
.sidebar .switcher:hover{border-color:rgba(255,255,255,.30); box-shadow:none}
.sidebar .switcher .cl-badge{background:rgba(255,255,255,.16); color:var(--on-fill-strong)}
.sidebar .switcher .cl-name{color:#fff}
.sidebar .switcher .cl-sub,.sidebar .switcher .chev{color:rgba(255,255,255,.62)}

/* the floating menus stay light — they overlay the page, not the rail.
   Without these they inherit color:#fff and go white-on-white. */
.sidebar .switch-menu,.sidebar .lock-pop{color:var(--ink)}
.sidebar .switch-menu .switch-head{color:var(--muted)}
.sidebar .switch-menu .cl-badge{background:var(--fill-strong); color:var(--on-fill-strong)}
.sidebar .switch-item:hover{background:var(--surface-2)}

/* foot */
.sidebar .sb-foot{border-top-color:rgba(255,255,255,.10)}
.sidebar .am-card{background:rgba(255,255,255,.07)}
.sidebar .am-card:hover{box-shadow:none; background:rgba(255,255,255,.11)}
.sidebar .am-card .am-nm{color:#fff}
.sidebar .am-card .am-role,.sidebar .am-card .msg{color:rgba(255,255,255,.62)}
/* THE SIDEBAR IS ALWAYS DARK, IN BOTH THEMES — so nothing inside it may use a
   token that flips with the theme.
   `.staff-link` sets `background:var(--navy-tint)` in the base rule. On the
   navy rail that resolves to #E7E9EF in LIGHT mode: a near-white pill carrying
   white-at-80% text, which measured about 1.2:1 — the "Take the tour" button was
   effectively invisible, which is exactly how it looked. In dark mode the same
   token is a 7% white wash, so the button was merely faint rather than gone, and
   the bug read as two unrelated problems.
   The background is stated here, in white alphas that work on the rail whatever
   the rest of the page is doing. 92% white on this navy is ~12:1. */
.sidebar .staff-link{background:rgba(255,255,255,.10); color:rgba(255,255,255,.92)}
.sidebar .staff-link:hover{background:rgba(255,255,255,.18); color:#fff}
.sidebar .staff-link.staff{background:rgba(255,255,255,.16); color:var(--on-fill-strong)}   /* was var(--navy) on navy */

/* toggle + the rail's own scrollbar */
.sidebar .sb-toggle{color:rgba(255,255,255,.62)}
.sidebar .sb-toggle:hover{background:rgba(255,255,255,.10); color:#fff; border-color:transparent}
.sidebar .nav::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16); border-color:transparent}

/* collapsed rail — same corrections, its own selectors */
html[data-sidebar="collapsed"] .sidebar .nav-item.active{background:rgba(255,255,255,.14); box-shadow:inset 0 0 0 1px rgba(255,255,255,.16)}
html[data-sidebar="collapsed"] .sidebar .nav-item .cnt{background:var(--green)}
html[data-sidebar="collapsed"] .sidebar .nav-group + .nav-group{border-top-color:rgba(255,255,255,.10)}
html[data-sidebar="collapsed"] .sidebar .am-card:hover,
html[data-sidebar="collapsed"] .sidebar .staff-link:hover{background:rgba(255,255,255,.08)}

.sb-top{padding:16px 16px 10px}

/* ============================================================
   COLLAPSED SIDEBAR
   An icon rail, for when the work is in the page rather than in
   the menu — PPC tables and the client list both run wider than
   a laptop screen. The state is on <html> so it is set before
   first paint (see partials/head.ejs); toggling it here would
   show the wide sidebar for a frame and then snap.

   Every size below is deliberate. The first pass kept the
   expanded paddings and card borders, which left a 72px column
   of empty boxes with small icons rattling around inside them —
   narrower than before and no easier to read. A rail works when
   the icon IS the item: one size, one rhythm, no chrome.
   ============================================================ */
.sb-toggle{position:absolute; top:12px; right:10px; z-index:2; width:26px; height:26px; padding:0;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  border:1px solid transparent; border-radius:8px; background:transparent; color:var(--muted-2);
  transition:background .12s ease, color .12s ease, border-color .12s ease}
.sb-toggle:hover{background:var(--surface-2); color:var(--ink); border-color:var(--line)}
.sb-toggle svg{width:15px; height:15px}

/* `html[...]`, not `[...]`.
 *
 * A later :root block further down this file redefines --sb-w. A bare attribute
 * selector has the same specificity as :root, so source order decided it and the
 * rail kept the full width — labels gone, nothing narrower. Adding the element
 * makes this (0,1,1) and it wins wherever it sits in the file. */
html[data-sidebar="collapsed"]{--sb-w:60px}
/* Belt and braces: the sidebar's own width, so nothing downstream can widen it
   back by redefining the variable again. */
html[data-sidebar="collapsed"] .sidebar{width:60px}
html[data-sidebar="collapsed"] .sb-toggle{top:10px; right:auto; left:50%; transform:translateX(-50%); width:30px; height:28px}
html[data-sidebar="collapsed"] .sb-toggle svg{transform:scaleX(-1)}   /* points the other way */

/* Words go. `display:none` rather than `visibility`, or the space stays booked
   and the rail is narrow in name only. */
html[data-sidebar="collapsed"] .brandwrap .wm,
html[data-sidebar="collapsed"] .brandwrap .brand,
html[data-sidebar="collapsed"] .switcher > div,
html[data-sidebar="collapsed"] .switcher .chev,
html[data-sidebar="collapsed"] .nav-group .gl,
html[data-sidebar="collapsed"] .nav-item > span:not(.cnt),
html[data-sidebar="collapsed"] .am-card > div,
html[data-sidebar="collapsed"] .am-card .msg,
html[data-sidebar="collapsed"] .lockic{display:none}
/* The staff links are bare text beside an inline <svg>, with no element around
   the words to hide. Zeroing the font on the link and restoring it on the icon
   removes the label without removing the link. */
html[data-sidebar="collapsed"] .staff-link{font-size:0}
html[data-sidebar="collapsed"] .staff-link .ic{font-size:initial}

/* ---- header: the mark alone, small ---- */
html[data-sidebar="collapsed"] .sb-top{padding:44px 8px 8px}
html[data-sidebar="collapsed"] .brandwrap{justify-content:center; gap:0}
html[data-sidebar="collapsed"] .brand-logo-img{max-width:30px; max-height:30px; object-fit:contain; margin:0 auto}

/* The switcher loses its card entirely — a bordered box around one 28px badge
   reads as an empty input, not as the client you are looking at. */
html[data-sidebar="collapsed"] .switcher{margin-top:10px; padding:0; border:0; background:none; justify-content:center}
html[data-sidebar="collapsed"] .switcher:hover{border:0; box-shadow:none}
html[data-sidebar="collapsed"] .switcher .cl-badge{width:30px; height:30px}
html[data-sidebar="collapsed"] .switch-menu{left:auto; right:-4px; width:230px}

/* ---- the items: one square, one rhythm ---- */
html[data-sidebar="collapsed"] .nav{padding:4px 6px 10px}
html[data-sidebar="collapsed"] .nav-group{margin-top:6px}
html[data-sidebar="collapsed"] .nav-group + .nav-group{border-top:1px solid var(--line-2); padding-top:6px}
html[data-sidebar="collapsed"] .nav-item{justify-content:center; gap:0; padding:0; height:38px; border-radius:9px}
html[data-sidebar="collapsed"] .nav-item .ic{width:18px; height:18px}
/* The badge can't sit beside a hidden label, so it becomes a dot on the corner —
   still says "something is waiting here", without a number nobody can read. */
html[data-sidebar="collapsed"] .nav-item .cnt{position:absolute; top:5px; right:7px; min-width:7px; width:7px; height:7px;
  padding:0; font-size:0; border-radius:50%; background:var(--teal)}
html[data-sidebar="collapsed"] .nav-item .cnt.hot{background:var(--neg)}
html[data-sidebar="collapsed"] .nav-item.active{background:var(--teal-tint); box-shadow:inset 0 0 0 1px rgba(118,22,244,.22)}

/* ---- foot: same square, no card ---- */
html[data-sidebar="collapsed"] .sb-foot{padding:8px 6px; gap:4px; align-items:stretch}
html[data-sidebar="collapsed"] .am-card,
html[data-sidebar="collapsed"] .staff-link{display:flex; justify-content:center; align-items:center;
  height:38px; padding:0; width:100%; border-radius:9px; border:0; background:transparent; box-shadow:none}
html[data-sidebar="collapsed"] .am-card:hover,
html[data-sidebar="collapsed"] .staff-link:hover{background:var(--surface-2); transform:none; box-shadow:none}
html[data-sidebar="collapsed"] .staff-link .ic{width:18px; height:18px}
html[data-sidebar="collapsed"] .am-card .av{width:28px; height:28px; font-size:11px}
/* The staff panel button keeps its colour — it is the one thing here that
   leaves the client's portal, and losing that signal makes it a trap. */
html[data-sidebar="collapsed"] .staff-link.staff{background:var(--fill-strong); color:var(--on-fill-strong)}
html[data-sidebar="collapsed"] .staff-link.staff:hover{background:var(--fill-strong-hover); filter:none}

/* The locked-module popover needs room the rail hasn't got; the `title` set by
   the toggle script carries the name instead. */
html[data-sidebar="collapsed"] .nav-lock .lock-pop{display:none}


.switcher{margin-top:14px; display:flex; align-items:center; gap:10px; padding:9px 11px;
  border:1px solid var(--line); border-radius:var(--r); cursor:pointer; background:var(--surface)}
.switcher:hover{border-color:var(--muted-2)}
.switcher .cl-badge{width:27px; height:27px; border-radius:7px; background:var(--fill-strong); color:var(--on-fill-strong);
  display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; flex:none; font-family:'Unbounded',sans-serif}
.switcher .cl-name{font-size:13.5px; font-weight:600; line-height:1.1}
.switcher .cl-sub{font-size:11px; color:var(--muted-2)}
.switcher .chev{margin-left:auto; color:var(--muted-2)}

/* Client switcher dropdown (staff only). Pure <details>, no JS. */
.switch-wrap{position:relative}
.switch-wrap>summary{list-style:none; cursor:pointer}
.switch-wrap>summary::-webkit-details-marker{display:none}
.switch-wrap[open]>summary{border-color:var(--teal)}
.switch-wrap[open] .chev{transform:rotate(180deg)}
.switch-menu{position:absolute; z-index:40; left:0; right:0; margin-top:6px; padding:6px;
  background:var(--surface); border:1px solid var(--line); border-radius:12px;
  box-shadow:0 10px 30px rgba(20,32,43,.14); max-height:320px; overflow:auto}
.switch-head{font-size:10.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted-2); padding:7px 9px 5px}
.switch-item{display:flex; align-items:center; gap:9px; padding:8px 9px; border-radius:8px;
  font-size:13px; color:var(--ink); text-decoration:none}
.switch-item:hover{background:var(--surface-2)}
.switch-item.on{font-weight:600}
.switch-item .cl-badge{width:22px; height:22px; border-radius:6px; background:var(--fill-strong); color:var(--on-fill-strong);
  display:flex; align-items:center; justify-content:center; font-size:9.5px; font-weight:700; flex:none}
/* The client's own logo in place of their initials, wherever a client is
   identified: the switcher, the list inside it, and the Clients table. Initials
   are the fallback, not the design — two letters is what you show when there is
   nothing better.

   `contain` on a light plate, never `cover`: these are logos people have signed
   off, and cropping one to fill a square is how a wordmark loses its last
   letter. A wide logo simply sits smaller in the chip. */
.cl-badge.cl-logo{background:var(--surface); border:1px solid var(--line); padding:2px; overflow:hidden}
.cl-badge.cl-logo img{width:100%; height:100%; object-fit:contain; display:block}
/* Backstop. `.cl-badge` sets a size in three different contexts (switcher, menu
   row, table row); if a fourth appears without one, an uploaded logo would fill
   the page rather than the chip. max-width/height:100% cannot be forgotten. */
.cl-badge img{max-width:100%; max-height:100%}
/* Inside a table row, where the chip has to sit on the text baseline. */
.cl-badge.cl-row{width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center;
  vertical-align:middle; margin-right:8px; border-radius:6px; color:#fff; font-size:10px; font-weight:700; flex:none}

.switch-tick{margin-left:auto; color:var(--teal-600); font-weight:700}
.switch-back{margin-top:4px; border-top:1px solid var(--line-2); border-radius:0 0 8px 8px;
  color:var(--teal-600); font-weight:600}

.nav{flex:1; overflow-y:auto; padding:6px 12px 12px}
.nav-group{margin-top:13px}
.nav-group .gl{font-size:10.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); padding:0 10px 6px}
.nav-item{display:flex; align-items:center; gap:11px; padding:8px 10px; border-radius:9px; color:var(--muted);
  font-weight:500; font-size:13.5px; cursor:pointer; position:relative}
.nav-item:hover{background:var(--surface-2); color:var(--ink)}
.nav-item.active{background:var(--teal-tint); color:var(--navy); font-weight:600}
.nav-item.active::before{content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--teal)}
.nav-item .ic{width:16px; height:16px; flex:none; opacity:.9}
.nav-item .cnt{margin-left:auto; font-size:11px; font-weight:700; color:var(--muted); background:var(--surface-2);
  border:1px solid var(--line); padding:1px 7px; border-radius:100px}
.nav-item.active .cnt{background:#fff; border-color:transparent; color:var(--teal-600)}
:root[data-theme="dark"] .nav-item.active .cnt{background:var(--teal); color:var(--on-accent)}
.nav-item .cnt.hot{background:var(--neg); color:#fff; border-color:transparent}

.sb-foot{padding:12px; border-top:1px solid var(--line); display:flex; flex-direction:column; gap:8px}
.am-card{display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:var(--r); background:var(--surface-2)}
.am-card .av{width:30px; height:30px; border-radius:50%; background:linear-gradient(135deg,#0D172D,#7616F4); color:#fff; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; flex:none}
.am-card .am-nm{font-size:12.5px; font-weight:600; line-height:1.15}
.am-card .am-role{font-size:11px; color:var(--muted)}
.am-card .msg{margin-left:auto; color:var(--muted); cursor:pointer}
.staff-link{display:flex; align-items:center; gap:9px; padding:9px 10px; border-radius:var(--r); font-size:12.5px;
  font-weight:600; color:var(--navy); background:var(--navy-tint); cursor:pointer}
.staff-link.staff{color:var(--on-fill-strong); background:var(--fill-strong)}

.main{flex:1; min-width:0; display:flex; flex-direction:column}
/* 82% was letting the page show THROUGH the bar: anything scrolling under it
   came out half-washed and half-sharp, which reads as an element sliced in two
   rather than as one passing behind glass. The frost stays, but at 96% the
   content underneath disappears cleanly instead of half-appearing. */
.topbar{position:sticky; top:0; z-index:20; height:68px; background:color-mix(in srgb,var(--paper) 96%, transparent);
  backdrop-filter:blur(10px); border-bottom:0; display:flex; align-items:center; gap:14px; padding:0 30px}
.search{flex:1; max-width:440px; display:flex; align-items:center; gap:9px; padding:8px 13px; background:var(--surface);
  border:0; border-radius:100px; color:var(--muted-2); font-size:13px; cursor:text;
  box-shadow:0 1px 2px rgba(13,23,45,.05), 0 4px 12px -6px rgba(13,23,45,.12)}
.search kbd{margin-left:auto; font-size:10.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:5px; padding:1px 6px; color:var(--muted); font-family:inherit}
/* The box is now a real input; it inherits the wrapper's look so nothing about
   the topbar changes visually. `position:relative` is what the results hang off. */
.search{position:relative}
.search input{flex:1; min-width:0; border:0; outline:0; background:transparent; padding:0;
  font:inherit; font-size:13px; color:var(--ink)}
.search input::placeholder{color:var(--muted-2)}
.search input::-webkit-search-cancel-button{-webkit-appearance:none}
.search:focus-within{box-shadow:0 0 0 3px var(--teal-tint)}

.sres{position:absolute; top:calc(100% + 7px); left:0; right:0; z-index:800; max-height:min(70vh,520px);
  overflow-y:auto; background:var(--surface); border:1px solid var(--line); border-radius:12px;
  box-shadow:var(--shadow-lg,0 18px 46px rgba(11,32,39,.16)); padding:6px}
.sres .sgrp{font-size:10.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted-2); padding:9px 11px 5px}
.sres a{display:block; padding:8px 11px; border-radius:8px; text-decoration:none; color:inherit}
.sres a:hover,.sres a.on{background:var(--surface-2)}
.sres .st{font-size:13px; font-weight:600; color:var(--ink); line-height:1.35}
.sres .ss{font-size:11.5px; color:var(--muted); margin-top:2px}
.sres .smsg{padding:14px 12px; font-size:12.5px; color:var(--muted); text-align:center}
.tb-right{margin-left:auto; display:flex; align-items:center; gap:8px}
.live{display:flex; align-items:center; gap:7px; font-size:12px; font-weight:600; color:var(--teal-600); background:var(--teal-tint); padding:6px 11px; border-radius:100px}
.live .pulse{width:7px; height:7px; border-radius:50%; background:var(--teal); position:relative}
.live .pulse::after{content:""; position:absolute; inset:-4px; border-radius:50%; border:2px solid var(--green); opacity:.55}
/* Not connected, or never synced — the chip must be able to say so. */
.live.off{color:var(--muted); background:var(--surface-2)}
.live.off .pulse{background:var(--muted-2)}
.live.off .pulse::after{display:none}
.tb-icon{width:38px; height:38px; border-radius:50%; border:0; background:var(--surface);
  box-shadow:0 1px 2px rgba(13,23,45,.05), 0 4px 12px -6px rgba(13,23,45,.12);
  display:flex; align-items:center; justify-content:center; color:var(--muted); cursor:pointer; position:relative}
.tb-icon:hover{color:var(--ink); background:var(--surface-2)}
.tb-icon .badge{position:absolute; top:-3px; right:-3px; width:8px; height:8px; border-radius:50%; background:var(--neg); border:2px solid var(--paper)}
.tb-ava{width:36px; height:36px; border-radius:50%; background:var(--fill-strong); color:var(--on-fill-strong); font-weight:700; font-size:13px; display:flex; align-items:center; justify-content:center; cursor:pointer}

/* The account menu behind the avatar. A <details> rather than a script: it
   opens, closes and traps focus on its own, and it works before any JavaScript
   has loaded — which matters for the one control that signs you out. */
.ava-wrap{position:relative}
.ava-wrap>summary{list-style:none}
.ava-wrap>summary::-webkit-details-marker{display:none}
.ava-wrap[open]>summary{box-shadow:0 0 0 3px var(--teal-tint)}
.ava-menu{position:absolute; z-index:80; right:0; top:calc(100% + 8px); width:230px; padding:6px;
  background:var(--surface); border:1px solid var(--line); border-radius:12px;
  box-shadow:0 12px 32px rgba(16,24,40,.16)}
.ava-head{padding:9px 10px 10px; border-bottom:1px solid var(--line-2); margin-bottom:6px}
.ava-nm{font-size:13px; font-weight:600; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.ava-em{font-size:11.5px; color:var(--muted-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.ava-item{display:flex; align-items:center; gap:9px; padding:9px 10px; border-radius:8px;
  font-size:13px; font-weight:500; color:var(--ink); text-decoration:none}
.ava-item:hover{background:var(--surface-2)}
.ava-item svg{color:var(--muted-2); flex:none}
.ava-out{color:var(--neg)} .ava-out svg{color:var(--neg)}

/* ---------- NOTIFICATIONS ----------
   The bell reuses the avatar menu's shell — same <details>, same panel — so the
   two controls sitting next to each other open the same way. Only the width and
   the row shape differ, because a notification is two lines and a menu item is
   one. */
.notif-wrap>summary{cursor:pointer}
.notif-bell{position:relative}
/* The badge is only drawn when there is something to count, so it needs no
   empty state. It sits half off the icon's corner, which is where a count is
   read without being mistaken for part of the glyph. */
.notif-bell .badge.on{position:absolute; top:-3px; right:-3px; min-width:16px; height:16px;
  padding:0 4px; border-radius:100px; background:var(--neg); color:#fff;
  font-size:9.5px; font-weight:700; line-height:16px; text-align:center;
  border:2px solid var(--surface); box-sizing:content-box}
.notif-menu{width:330px; max-height:min(70vh,460px); overflow:auto}
.notif-none{padding:14px 10px; font-size:12px; color:var(--muted-2); line-height:1.55}
.notif-item{display:flex; gap:9px; padding:9px 10px; border-radius:8px; text-decoration:none;
  color:var(--ink); align-items:flex-start}
.notif-item:hover{background:var(--surface-2)}
/* Unread is carried by weight and a tinted ground, not by colour alone — the
   dot already spends the colour channel on what KIND of news it is. */
.notif-item.unread{background:var(--teal-tint)}
.notif-item.unread .notif-t{font-weight:600}
.notif-tx{display:flex; flex-direction:column; gap:2px; min-width:0}
.notif-t{font-size:12.5px; line-height:1.4; color:var(--ink)}
.notif-m{font-size:11px; color:var(--muted-2)}
/* Kind, in one dot: green rose, red wants a decision, grey moved. */
.notif-dot{width:7px; height:7px; border-radius:50%; flex:none; margin-top:5px; background:var(--muted-2)}
.notif-dot.pos{background:var(--pos)} .notif-dot.neg{background:var(--neg)}
.lnk{background:none; border:0; padding:0; font:inherit; font-size:11.5px; font-weight:600;
  color:var(--teal-600); cursor:pointer}
.lnk:hover{text-decoration:underline}

/* The full page. */
.nlist{display:flex; flex-direction:column}
.nrow{display:flex; gap:11px; align-items:flex-start; padding:12px 4px; text-decoration:none;
  color:var(--ink); border-top:1px solid var(--line-2)}
.nlist>.nrow:first-child{border-top:0}
.nrow:hover{background:var(--surface-2)}
/* What was still unread when the page opened. Left-barred rather than filled:
   the page marks everything read as it loads, so this is a record of what you
   had missed, not a live state — and it should not shout once you have read it. */
.nrow.was-new{border-left:2px solid var(--teal); padding-left:10px; border-radius:0}
.nrow-b{display:flex; flex-direction:column; gap:3px; min-width:0; flex:1}
.nrow-t{font-size:13px; font-weight:600; line-height:1.4}
.nrow-d{font-size:12px; color:var(--muted); line-height:1.55}
.nrow-m{font-size:11px; color:var(--muted-2); display:flex; align-items:center; gap:5px; flex-wrap:wrap}
.nrow .notif-dot{margin-top:6px}

.content{padding:24px; max-width:1460px; width:100%; margin:0 auto}

/* ---------- PAGE HEADER ---------- */
.pg-head{display:flex; align-items:flex-start; gap:20px; margin-bottom:26px; flex-wrap:wrap}
.pg-head .eyebrow{font-size:11.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--teal-600); margin-bottom:6px}
.pg-head .greet,.pg-head h1{font-family:'Unbounded',sans-serif; font-size:30px; font-weight:400; letter-spacing:-.022em; line-height:1.15}
.pg-head .sub{color:var(--muted); font-size:13.5px; margin-top:4px; max-width:680px}
.pg-head .sub b{color:var(--pos); font-weight:600}
/* The rule above is green whatever the number says, so a fall would be
   highlighted in the colour of a win. Falls get their own class. */
.pg-head .sub b.down{color:var(--neg)}
.pg-head .ph-right{margin-left:auto; display:flex; align-items:center; gap:10px}

/* segmented control */
.seg{display:inline-flex; background:var(--surface); border:1px solid var(--line); border-radius:10px; padding:3px}
.seg button{border:0; background:transparent; font-size:12.5px; font-weight:600; color:var(--muted); padding:6px 13px; border-radius:7px; cursor:pointer}
.seg button.on{background:var(--fill-strong); color:var(--on-fill-strong)}

/* ---------- BUTTONS ---------- */
.btn{display:inline-flex; align-items:center; justify-content:center; gap:8px; font-size:13px; font-weight:600;
  padding:9px 14px; border-radius:10px; border:1px solid var(--line); background:var(--surface); color:var(--ink); cursor:pointer}
.btn:hover{border-color:var(--muted-2)}
.btn .ic{width:15px; height:15px}
/* The one filled action per section.
 *
 * A flat violet rather than a sweep. The full brand gradient ends on lime,
 * which white text cannot sit on — white on #A1CB3A is 1.89:1 — so the button
 * takes the middle stop alone. White on #7616F4 is 6.58:1.
 *
 * `--btn-primary` is a token rather than a literal because of white-label:
 * layout.ejs replaces it with a flat fill of the CLIENT's accent when they have
 * one. A workspace branded purple must not get a VOCTOS teal gradient. */
.btn-primary{background:var(--btn-primary); color:var(--btn-primary-ink); border-color:transparent}
.btn-primary:hover{background:var(--btn-primary-hover)}
.btn-teal{background:var(--accent-strong); color:var(--on-accent); border-color:var(--accent-strong)}
/* The dark half of the sweep, not the whole thing. White on the lime end is
   1.89:1, and a button label spans the full width. Min contrast here is 6.58:1. */
.btn-grad{background:linear-gradient(90deg,#0D172D,#7616F4); color:#fff; border:0}
.btn-sm{font-size:12.5px; padding:8px 12px}
.btn-ghost{background:transparent}

/* ---------- CHIPS / BADGES ---------- */
.chip{display:inline-flex; align-items:center; gap:4px; font-size:11.5px; font-weight:700; padding:3px 8px; border-radius:100px}
.chip.pos{background:var(--pos-tint); color:var(--pos)} .chip.neg{background:var(--neg-tint); color:var(--neg)}
.chip.warn{background:var(--warn-tint); color:#B5791E} .chip.flat{background:var(--surface-2); color:var(--muted)}
.chip.teal{background:var(--teal-tint); color:var(--teal-600)} .chip.navy{background:var(--navy-tint); color:var(--navy)}
.tag{font-size:11px; font-weight:700; padding:3px 9px; border-radius:100px}
.tag.pending{background:var(--warn-tint); color:#B5791E} .tag.paid{background:var(--pos-tint); color:var(--pos)}
.tag.open{background:var(--teal-tint); color:var(--teal-600)} .tag.draft{background:var(--surface-2); color:var(--muted)}
.tag.overdue{background:var(--neg-tint); color:var(--neg)}
.pri{font-size:10.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; padding:2px 8px; border-radius:100px}
.pri.high{background:var(--neg-tint); color:var(--neg)} .pri.med{background:var(--warn-tint); color:#B5791E} .pri.low{background:var(--surface-2); color:var(--muted)}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block}

/* ---------- CARDS ----------
   The site's box: white, radius 30, padding 32, NO border and NO shadow. The
   separation is white-on-#EFF0F4 and nothing else — adding a hairline back is
   the single change that stops it looking like voctos.com. */
.card{background:var(--surface); border:0; border-radius:var(--r-card); box-shadow:var(--card-shadow)}
.card-h{display:flex; align-items:center; gap:10px; padding:20px 22px 0}
/* A card head that is followed by a filter bar needs air under it. `.card-h` has
   no bottom padding because it is normally followed by `.card-p`, which brings
   its own 22px — but `.rangebar` is a band with its own background, so without
   this the title sits directly on the edge of the filter and the two read as one
   crowded strip rather than a heading and a control. */
.card-h + .rangebar{margin-top:16px}
.card-h h3{font-family:'Unbounded',sans-serif; font-size:17px; font-weight:400; letter-spacing:-.01em}
.card-h .hint{font-size:12px; color:var(--muted-2)}
.card-h .more{margin-left:auto; color:var(--teal-600); font-size:12.5px; font-weight:600; cursor:pointer}
.card-p{padding:22px}

/* grids */
.grid{display:grid; gap:16px}
.g-2{grid-template-columns:repeat(2,minmax(0,1fr))} .g-3{grid-template-columns:repeat(3,minmax(0,1fr))} .g-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.g-main{grid-template-columns:minmax(0,1fr) 336px; align-items:start}
.col{display:flex; flex-direction:column; gap:16px}

/* ---------- KPI ---------- */
.kpis{display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:24px}
.kpi{padding:20px 22px}
/* The ⓘ used to sit immediately after the label text, which made it read as
   part of the title. It belongs at the end of the row: the label owns the left,
   the reading's chip sits next to it, and the icon closes the line. */
.kpi .top{display:flex; align-items:center; gap:8px; margin-bottom:12px}
.kpi .top .lbl{margin-right:auto}
.kpi .top .ihelp{order:9}

/* ============================================================
   THE READINGS ARE ONE PANEL, NOT FIVE CARDS

   Every round of this was spent trying to make five small white boxes read
   against a page that was almost the same white — a 1.14:1 step, which no
   border or shadow can rescue. The answer was not a better card. It was fewer
   surfaces: the five readings become five CELLS of one panel, divided by
   hairlines, the way the shelf underneath already worked.

   The page got darker at the same time (#EFF0F4 -> #E6E9EF), taking the step
   from 12.8% to 18.7%. Two large white panels on a grey page are legible in a
   way that eleven small ones never were. --muted-2 was darkened to #5F6678 to
   keep small text at 4.72:1 on the new paper; at the old value it would have
   dropped to 4.38:1 and quietly failed.

   Geometry still from Semrush's tokens: 8px radius, their 7% card shadow.
   ============================================================ */
.kpis{display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:0;
  background:var(--surface); border:1px solid var(--line); border-radius:8px;
  box-shadow:0 0 1px 0 rgba(13,23,45,.07), 0 1px 3px 0 rgba(13,23,45,.07)}
.kpi{background:transparent; border:0; border-right:1px solid var(--line-2);
  border-radius:0; box-shadow:none; padding:18px 20px 19px; position:relative}
.kpi:last-child{border-right:0}

/* the score colours the meter — the element that already is the score */
.r-good .meter>span{background:#0E7A37}
.r-ok .meter>span{background:#B8770B}
.r-bad .meter>span{background:#C0272C}

/* the direction tints its own CELL. Inside a shared panel a tinted cell reads
   as one column highlighted, which is what it is — no border to tint now, so
   the wash carries it alone and can be a touch stronger without shouting. */
.t-up  {background:#EFF7F2}
.t-down{background:#FBF1F1}
.t-up .meter,.t-down .meter{background:rgba(13,23,45,.08)}
/* Same retune as the shelf tints, and for the same reason: these were mixed
   against the surface the duplicate dark block was declaring, so on the real
   card they sat at 1.05:1 — a rise and a fall that looked identical to a plain
   row. 1.38:1 now, with the label still at 4.99:1. */
:root[data-theme="dark"] .t-up  {background:#183A26}
:root[data-theme="dark"] .t-down{background:#572325}
.kpis .kpi:first-child{border-radius:8px 0 0 8px}
.kpis .kpi:last-child{border-radius:0 8px 8px 0}

/* ---- the secondary shelf, same panel treatment one level quieter ---- */
.card.strip{background:var(--surface); box-shadow:none;
  border:1px solid var(--line); border-radius:8px; overflow:visible}
.card.strip .cell{padding:17px 20px; border-right:1px solid var(--line-2)}
.card.strip .cell:last-child{border-right:0}
.card.strip .cl{color:var(--muted); font-size:11.5px}
.card.strip .cv{font-size:21px; margin-top:5px}

/* THE SECONDARY ROW IS NOT CARDS.
   Five KPI cards stacked on six more, identical white, identical weight, is a
   slab with no order to read it in. Those six are secondary counts, so they are
   plain figures on the paper divided by hairlines — eleven boxes become five.

   (An earlier version also put a 3px colour bar at the top edge of each KPI
   card. It looked detached, and the reason is instructive: the card's own edge
   against the paper is a 1.14:1 step, so a mark ON that edge has nothing to sit
   against and reads as a tick floating above a card. Removed.) */
.k-violet{--k:#7616F4} .k-lime{--k:#A1CB3A}

/* the secondary row: figures on the paper, not a card */

.kpi .lbl{font-size:12px; color:var(--muted); font-weight:500}
.kpi .val{font-family:'Unbounded',sans-serif; font-weight:400; font-size:30px; letter-spacing:-.02em; line-height:1}
.kpi .val small{font-size:15px; color:var(--muted-2); font-weight:500}
.kpi .foot{font-size:11.5px; color:var(--muted-2); margin-top:9px}
.meter{height:6px; background:var(--surface-2); border-radius:100px; overflow:hidden; margin-top:13px}
.meter>span{display:block; height:100%; border-radius:100px; background:var(--teal)}
/* Was var(--grad) — a three-stop navy→violet→lime sweep. Under a flat card it
   read as decoration competing with the number above it. A meter is a quantity,
   so it gets one colour. */
.meter>span.grad{background:var(--teal)}
.spark{margin-top:11px; display:block}
.stack{height:8px; border-radius:100px; overflow:hidden; display:flex; margin-top:14px}
.stack i{display:block; height:100%}
.legend-s{display:flex; gap:14px; margin-top:10px; font-size:11.5px; color:var(--muted); flex-wrap:wrap}
.legend-s span{display:flex; align-items:center; gap:5px}
.sd{width:8px; height:8px; border-radius:2px; display:inline-block}

/* stat strip */
.strip{display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); overflow:hidden}
.strip .cell{padding:19px 22px; border-right:1px solid var(--line-2)}
.strip .cell:last-child{border-right:0}
.strip .cl{font-size:11.5px; color:var(--muted); font-weight:500}
.strip .cv{font-family:'Unbounded',sans-serif; font-size:27px; font-weight:400; margin-top:7px; letter-spacing:-.01em; display:flex; align-items:baseline; gap:7px}
.strip .cv .mini{font-size:11px; font-weight:700}
.mini.pos{color:var(--pos)} .mini.neg{color:var(--neg)} .mini.muted{color:var(--muted-2)}

/* ---------- PLATFORM BARS ---------- */
.plat{padding:6px 18px 14px}
.plat-row{display:grid; grid-template-columns:170px minmax(0,1fr) 66px; align-items:center; gap:14px; padding:9px 0; border-bottom:1px solid var(--line-2)}
.plat-row:last-child{border-bottom:0}
.plat-name{display:flex; align-items:center; gap:10px; min-width:0}
.mono{width:27px; height:27px; border-radius:7px; background:var(--surface-2); border:1px solid var(--line); display:flex; align-items:center; justify-content:center; font-size:9.5px; font-weight:700; color:var(--ink-2); flex:none}

/* The engine marks. Same tile the letter codes used, so nothing shifts on the
   pages where one replaced the other — and the abbreviation fallback inherits
   it too, which is why an engine we have no mark for still lines up. */
.emk{width:27px; height:27px; border-radius:7px; background:var(--surface-2);
  border:1px solid var(--line); display:inline-flex; align-items:center;
  justify-content:center; flex:none; vertical-align:middle}
.emk-sm{width:20px; height:20px; border-radius:5px}
.emk-lg{width:32px; height:32px; border-radius:8px}
/* Inside a chip the tile would be a box within a box. The right margin is here
   rather than on the parent because a bare mark is dropped straight in front of
   a label in half a dozen places, and each of those would otherwise need its own
   gap rule — one of which would be forgotten. */
.emk-bare{background:none; border:0; width:auto; height:auto; margin-right:5px}
/* In the legend and the forecast rows the mark sits between the colour swatch
   and the name, in text flow rather than a flex row — so it is aligned to the
   text baseline instead of being laid out, and the existing widths are left
   alone. */
.lgi .emk-bare, .fc-n .emk-bare{margin-right:4px}
.lgi .emk-bare svg, .fc-n .emk-bare svg{vertical-align:-3px}
/* A real logo file dropped into public/img/engines/. `contain` because those
   files arrive at whatever aspect ratio the company ships — a square box would
   stretch a wordmark — and the tile already provides the padding. */
.emk-img{display:block; object-fit:contain}
/* An inlined logo. The file's own width and height were removed on load, so the
   size is set here from --emk-g — the same glyph size the drawn marks use, so a
   tile looks identical whether its engine has a real file or not. `color` is
   what a one-colour logo binds to: the file's flat black or white was swapped
   for currentColor, so this is the line that makes a white OpenAI knot show up
   on a light tile and turn light again in dark mode. */
.emk-svg{display:inline-flex; color:var(--ink)}
.emk-svg svg{width:var(--emk-g,17px); height:var(--emk-g,17px); display:block}
.emk-abbr{font-size:9.5px; font-weight:700; color:var(--ink-2); letter-spacing:.02em}
.emk-sm .emk-abbr{font-size:8px}
.plat-name .nm{font-size:13px; font-weight:600; white-space:nowrap}
.plat-name .mc{font-size:11px; color:var(--muted-2)}
.bar{height:8px; background:var(--surface-2); border-radius:100px; overflow:hidden}
.bar>span{display:block; height:100%; background:var(--teal); border-radius:100px}
.bar>span.grad{background:var(--teal)}
.plat-val{text-align:right; font-family:'Unbounded',sans-serif; font-weight:400; font-size:15px}
.plat-val .d{font-size:10.5px; font-weight:700; display:block; margin-top:1px}
.d.pos{color:var(--pos)} .d.neg{color:var(--neg)} .d.flat{color:var(--muted-2)}

/* ---------- CHART ---------- */
.chart{width:100%; height:auto; display:block}
/* A viewBox with height:auto scales with the column, so on a wide screen a
   150-unit chart renders 250px tall with most of it empty. These have no text
   inside, so they can be pinned to a real height and stretched to fit. */
.chart.c-sm{height:120px} .chart.c-md{height:150px} .chart.c-lg{height:190px}
.legend{display:flex; gap:18px; margin-bottom:6px; font-size:12px; color:var(--muted); flex-wrap:wrap}
.legend span{display:flex; align-items:center; gap:7px}
.lg{width:16px; height:0; border-top:2.5px solid var(--teal)} .lg.navy{border-color:var(--navy)} .lg.dash{border-top:2px dashed var(--muted-2)}
.axis-x{display:flex; justify-content:space-between; font-size:11px; color:var(--muted-2); margin-top:8px}

/* date-range filter above a chart */
.rangebar{display:flex; flex-wrap:wrap; align-items:center; gap:10px 16px;
  padding:11px 18px; border-bottom:1px solid var(--line); background:var(--surface-2)}
.rangebar .presets{display:inline-flex; gap:4px; background:var(--surface);
  border:1px solid var(--line); border-radius:10px; padding:3px}
.rangebar .rp{font-size:12px; font-weight:600; color:var(--muted); padding:5px 11px;
  border-radius:7px; text-decoration:none; white-space:nowrap}
.rangebar .rp:hover{background:var(--surface-2); color:var(--ink)}
.rangebar .rp.on{background:var(--fill-strong); color:var(--on-fill-strong)}
.rangebar .custom{display:inline-flex; align-items:center; gap:7px; margin-left:auto; flex-wrap:wrap}
.rangebar .custom label{font-size:12px; color:var(--muted-2)}
.rangebar .custom input[type=date]{font-family:inherit; font-size:12.5px; color:var(--ink);
  background:var(--surface); border:1px solid var(--line); border-radius:9px; padding:6px 9px}
.rangebar .custom input[type=date]:focus{outline:none; border-color:var(--teal)}
.rangebar .rp.clear{border:1px solid var(--line); background:var(--surface)}
@media (max-width:720px){ .rangebar .custom{margin-left:0} }

/* ---------- TABLES ---------- */
.tbl{width:100%; border-collapse:collapse; font-size:13px}
.tbl th{text-align:left; font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--muted-2); padding:11px 14px; border-bottom:1px solid var(--line); white-space:nowrap}
.tbl td{padding:12px 14px; border-bottom:1px solid var(--line-2); vertical-align:middle}
.tbl tr:last-child td{border-bottom:0}
.tbl tbody tr:hover{background:var(--surface-2)}
.tbl .num{font-variant-numeric:tabular-nums; font-family:'Unbounded',sans-serif; font-weight:600}
.tbl .strong{font-weight:600}
/* Row actions.
   `Edit · Portal · Mission Control` as bare links read as one run-on sentence
   and gave a click target the height of the text. These are buttons: the same
   three, in the same place on every row, aligned right so the eye finds them
   once and then knows where they are. */
.rowact-cell{text-align:right; white-space:nowrap; width:1%}
.rowact{display:inline-flex; gap:6px; justify-content:flex-end}
.rowact-b{display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px;
  border:1px solid var(--line); background:var(--surface); color:var(--muted);
  font-size:12px; font-weight:600; text-decoration:none; line-height:1;
  transition:border-color .12s ease, color .12s ease, background .12s ease}
.rowact-b:hover{border-color:var(--teal); color:var(--teal-600); background:var(--teal-tint)}
.rowact-b svg{width:14px; height:14px; flex:none}
/* Below this the labels would push the numbers off the page, so the icons carry
   it — each button keeps its title, so the meaning survives. */
@media (max-width:1180px){
  .rowact-b{padding:7px}
  .rowact-b span{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap}
}

.rank{display:inline-flex; width:24px; height:24px; border-radius:7px; background:var(--surface-2); align-items:center; justify-content:center; font-family:'Unbounded',sans-serif; font-weight:400; font-size:12px}
.rank.win{background:var(--pos-tint); color:var(--pos)} .rank.lose{background:var(--neg-tint); color:var(--neg)}

/* tabs */
.tabs{display:flex; gap:4px; border-bottom:1px solid var(--line); margin-bottom:0}
.tab{padding:11px 14px; font-size:13px; font-weight:600; color:var(--muted); cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px}
.tab:hover{color:var(--ink)}
.tab.on{color:var(--navy); border-bottom-color:var(--teal)}
.tab .pilln{font-size:11px; background:var(--surface-2); border-radius:100px; padding:1px 7px; margin-left:6px; color:var(--muted)}

/* ---------- RECOMMENDATIONS ---------- */
.recs{display:flex; flex-direction:column; gap:10px}
.rec{display:flex; gap:13px; padding:14px; border:1px solid var(--line); border-radius:var(--r); background:var(--surface)}
.rec .ricon{width:32px; height:32px; border-radius:9px; flex:none; display:flex; align-items:center; justify-content:center}
.rec.alert .ricon{background:var(--neg-tint); color:var(--neg)}
.rec.opp .ricon{background:var(--teal-tint); color:var(--teal-600)}
.rec.content .ricon{background:var(--warn-tint); color:#B5791E}
.rec.win .ricon{background:var(--pos-tint); color:var(--pos)}
.rec .rtitle{font-size:13px; font-weight:600; margin-bottom:3px}
.rec .rbody{font-size:12.5px; color:var(--muted); line-height:1.5}
.rec .rmeta{display:flex; align-items:center; gap:10px; margin-top:9px; flex-wrap:wrap}
.rec .act{font-size:12px; font-weight:600; color:var(--teal-600); cursor:pointer; margin-left:auto}

/* right rail cards */
.rr .rr-lbl{font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted-2)}
.rr .rr-title{font-size:15px; font-weight:600; margin-top:9px; font-family:'Unbounded',sans-serif}
.rr .rr-meta{font-size:12.5px; color:var(--muted); margin-top:4px}
.rr-actions{display:flex; gap:8px; margin-top:14px}
/* the three delivery figures inside an .rr card, so they share the invoice's
   shape without needing three cards of their own */
.rr-3{display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; margin-top:12px}
.rr-3-v{font-family:'Unbounded',sans-serif; font-weight:400; font-size:26px; letter-spacing:-.02em; line-height:1}
.rr-3-v small{font-size:13px; color:var(--muted-2); margin-left:1px}
.rr-3-l{font-size:12px; color:var(--muted); margin-top:6px}
.row-between{display:flex; align-items:center; justify-content:space-between}
.inv-amt{font-family:'Unbounded',sans-serif; font-size:26px; font-weight:400; letter-spacing:-.01em}

/* ---------- ACTIVITY FEED ---------- */
.feed{padding:6px 18px 8px}
.fi{display:flex; gap:11px; padding:11px 0; border-bottom:1px solid var(--line-2)}
.fi:last-child{border-bottom:0}
.fav{width:28px; height:28px; border-radius:50%; flex:none; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#fff}
.fi .ft{font-size:12.5px; line-height:1.45} .fi .ft b{font-weight:600}
.fi .ftime{font-size:11px; color:var(--muted-2); margin-top:2px}
.feed-foot{padding:12px 18px; border-top:1px solid var(--line); text-align:center; font-size:12.5px; font-weight:600; color:var(--teal-600); cursor:pointer}

/* ---------- TIMELINE / ROADMAP ---------- */
.timeline{display:flex; flex-direction:column; gap:0}
.tl-month{display:grid; grid-template-columns:120px minmax(0,1fr); gap:20px; padding:18px 0; border-bottom:1px solid var(--line-2)}
.tl-month:last-child{border-bottom:0}
.tl-label{font-family:'Unbounded',sans-serif; font-weight:600; font-size:14px}
.tl-label .sub{font-size:11.5px; color:var(--muted-2); font-weight:400; margin-top:3px}
.tl-items{display:flex; flex-direction:column; gap:9px}
.tl-item{display:flex; align-items:center; gap:12px; padding:11px 13px; border:1px solid var(--line); border-radius:var(--r); background:var(--surface)}
.tl-check{width:20px; height:20px; border-radius:6px; flex:none; display:flex; align-items:center; justify-content:center; font-size:11px; color:#fff}
.tl-check.done{background:var(--pos)} .tl-check.prog{background:var(--teal)} .tl-check.todo{background:var(--surface-2); border:1px solid var(--line)}
.tl-item .ti-name{font-size:13px; font-weight:600} .tl-item .ti-meta{font-size:11.5px; color:var(--muted-2)}
.tl-prog{margin-left:auto; display:flex; align-items:center; gap:10px}
.pbar{width:90px; height:6px; background:var(--surface-2); border-radius:100px; overflow:hidden}
.pbar>span{display:block; height:100%; background:var(--teal); border-radius:100px}

/* ---------- KANBAN ---------- */
.kanban{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; align-items:start}
.kcol{background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r-lg); padding:12px}
.kcol-h{display:flex; align-items:center; gap:8px; padding:2px 4px 10px; font-size:12.5px; font-weight:700}
.kcol-h .kc{font-size:11px; color:var(--muted); background:var(--surface); border:1px solid var(--line); padding:0 7px; border-radius:100px; margin-left:auto}
.kcard{background:var(--surface); border:1px solid var(--line); border-radius:var(--r); padding:12px; margin-bottom:9px; box-shadow:var(--shadow)}
.kcard:last-child{margin-bottom:0}
.kcard .kt{font-size:12.5px; font-weight:600; line-height:1.4; margin-bottom:9px}
.kcard .kmeta{display:flex; align-items:center; gap:8px; flex-wrap:wrap}
.ktag{font-size:10px; font-weight:700; padding:2px 7px; border-radius:5px; letter-spacing:.03em; text-transform:uppercase}
.ktag.seo{background:var(--teal-tint); color:var(--teal-600)} .ktag.geo{background:var(--navy-tint); color:var(--navy)}
.ktag.content{background:var(--warn-tint); color:#B5791E} .ktag.tech{background:var(--surface-2); color:var(--muted)} .ktag.links{background:var(--green-tint); color:#4c7a24}
.kav{width:22px; height:22px; border-radius:50%; color:#fff; font-size:9.5px; font-weight:700; display:flex; align-items:center; justify-content:center; margin-left:auto}

/* ---------- CHAT / TICKETS ---------- */
.chat{display:flex; flex-direction:column; gap:14px; padding:18px}
.msg-row{display:flex; gap:11px; max-width:78%}
.msg-row.me{align-self:flex-end; flex-direction:row-reverse}
.msg-av{width:30px; height:30px; border-radius:50%; flex:none; color:#fff; font-size:11px; font-weight:700; display:flex; align-items:center; justify-content:center}
/* The site never nests a box inside a box — inner cells are bare, and structure
   comes from type and space. No border here; the tint alone is enough. */
.bubble{background:var(--surface-2); border:0; border-radius:14px; padding:12px 15px; font-size:13px; line-height:1.5}
.msg-row.me .bubble{background:var(--fill-strong); color:var(--on-fill-strong); border-color:var(--fill-strong)}
.msg-time{font-size:10.5px; color:var(--muted-2); margin-top:4px}

/* ---------- PROGRESS / GOALS ---------- */
.goal{padding:16px 18px}
.goal .g-top{display:flex; align-items:baseline; justify-content:space-between; margin-bottom:6px}
.goal .g-name{font-size:14px; font-weight:600}
.goal .g-val{font-family:'Unbounded',sans-serif; font-weight:400; font-size:15px}
.gbar{height:10px; background:var(--surface-2); border-radius:100px; overflow:hidden; margin-top:10px}
.gbar>span{display:block; height:100%; border-radius:100px; background:var(--teal)}
.goal .g-foot{font-size:11.5px; color:var(--muted-2); margin-top:8px}
.goal .g-foot b{color:var(--pos)}

/* score ring alt: big number */
.score-hero{display:flex; align-items:center; gap:26px; flex-wrap:wrap}
.score-num{font-family:'Unbounded',sans-serif; font-weight:400; font-size:64px; letter-spacing:-.03em; line-height:1}
.score-num small{font-size:22px; color:var(--muted-2)}
.breakdown{display:grid; grid-template-columns:repeat(var(--bd-cols,4),minmax(0,1fr)); gap:12px; flex:1; min-width:280px}
/* Narrow screens take two whatever the count. */
@media(max-width:1100px){ .breakdown{--bd-cols:2 !important} }
.bd{padding:12px 13px; border:1px solid var(--line); border-radius:var(--r); background:var(--surface); position:relative}
/* Label left, ⓘ hard right, both on one line. It was sitting immediately after
   the text, which at 11px reads as part of the label rather than as a control —
   and on the shorter labels it landed in a different place on every card, so the
   row of icons was ragged. Pinned to the end, they line up down the row. */
.bd .bl{font-size:11px; color:var(--muted); font-weight:500;
  display:flex; align-items:center; justify-content:space-between; gap:8px}
.bd .bl .ihelp{flex:none; margin-left:auto}
.bd .bv{font-family:'Unbounded',sans-serif; font-weight:400; font-size:19px; margin-top:5px}
.bd .bm{height:4px; background:var(--surface-2); border-radius:100px; margin-top:8px; overflow:hidden}
.bd .bm>span{display:block; height:100%; background:var(--teal); border-radius:100px}

/* ---- info boxes ----
 * A number nobody can explain is a number nobody trusts. Every figure gets a
 * plain-language "where did this come from". Built on <details> so it needs no
 * JS — the CSP is script-src 'self' and this must work even if app.js fails. */
.info{position:relative; display:inline-block; vertical-align:middle}
.info>summary{list-style:none; cursor:help; width:15px; height:15px; border-radius:50%;
  border:1px solid var(--line); background:var(--surface-2); color:var(--muted-2);
  font-size:10px; font-weight:700; line-height:13px; text-align:center; display:inline-block;
  font-family:'Unbounded',sans-serif; user-select:none}
.info>summary::-webkit-details-marker{display:none}
.info>summary:hover{border-color:var(--muted-2); color:var(--ink)}
.info[open]>summary{background:var(--fill-strong); color:var(--on-fill-strong); border-color:var(--fill-strong)}
.info-pop{position:absolute; z-index:20; top:22px; right:0; width:260px; padding:12px 14px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r);
  box-shadow:var(--shadow-lg); font-size:12px; line-height:1.6; color:var(--muted);
  font-weight:400; text-align:left; white-space:normal}
.info-pop b{color:var(--ink)}
.info-pop .src{display:block; margin-top:8px; padding-top:8px; border-top:1px solid var(--line);
  font-size:11px; color:var(--muted-2)}
.info-left .info-pop{right:auto; left:0}

/* A wider, always-visible explainer for a whole page or section. */
.note{display:flex; gap:11px; align-items:flex-start; padding:13px 15px; margin-bottom:16px;
  background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r);
  font-size:12.5px; line-height:1.65; color:var(--muted)}
.note .ic{flex:none; width:19px; height:19px; border-radius:50%; background:var(--fill-strong); color:var(--on-fill-strong);
  font-size:11px; font-weight:700; line-height:19px; text-align:center;
  font-family:'Unbounded',sans-serif}
.note b{color:var(--ink)}

/* report folders — a grid you click into, like a file manager */
.folders{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px}
@media (max-width:1180px){ .folders{grid-template-columns:repeat(3,minmax(0,1fr))} }
@media (max-width:860px){ .folders{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:520px){ .folders{grid-template-columns:1fr} }

.folder{display:block; position:relative; padding:18px 16px 15px; background:var(--surface); border:1px solid var(--line);
  border-radius:var(--r-lg); box-shadow:var(--shadow); text-decoration:none; color:var(--ink);
  transition:border-color .12s, box-shadow .12s, transform .12s}
.folder:hover{border-color:var(--teal); box-shadow:var(--shadow-lg); transform:translateY(-1px)}
.folder .fico{color:var(--teal); width:46px; height:46px; margin-bottom:11px}
.folder .fico svg{width:46px; height:46px; display:block}
/* top-right of the card, clear of the folder icon */
.folder .fbadge{position:absolute; top:14px; right:14px; white-space:nowrap}
/* `line-clamp` alongside the -webkit- one. The prefixed property is what every
   current browser actually honours, but it is no longer a draft — the standard
   name is what a browser that drops the prefix will read, and shipping only the
   prefix is how a truncation silently becomes an unbounded block later. */
.folder .fname{font-weight:600; font-size:14px; line-height:1.35; margin-bottom:5px;
  display:-webkit-box; -webkit-line-clamp:2; line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
.folder .fmeta{font-size:11.5px; color:var(--muted-2)}
.folder .chip{margin-top:10px}

/* drag-and-drop upload target */
.dz{border:1.5px dashed var(--line-2); border-radius:var(--r); padding:18px; text-align:center;
  cursor:pointer; transition:background .12s,border-color .12s; background:var(--surface-2)}
.dz:hover{border-color:var(--teal)}
.dz.over{border-color:var(--teal); background:var(--teal-tint)}
.dz.busy{opacity:.65; pointer-events:none}
.dz.failed{border-color:var(--neg)}
.dz .dz-msg{font-size:12.5px; color:var(--muted); font-weight:600}
.dz .dz-sub{font-size:11.5px; color:var(--muted-2); margin-top:3px}
.dz input[type=file]{display:none}
/* The real input lives in its own form OUTSIDE the dropzone (so the dropzone can
   submit it), so hiding inputs "inside .dz" wasn't enough — a stray native
   "Choose files" button sat in the middle of the file list. */
/* Visually hidden rather than display:none — a display:none input can refuse a
   programmatic .click(), and clicking the dropzone is exactly how the picker opens. */
.dz-form{position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap}

/* a file inside a report folder */
/* One report, one card, four to a row. These used to be rows separated by
   hairlines inside a single panel, which read as one object with lines through
   it — and the buttons at the right belonged to no particular row at a glance.
   Each item is a thing you open, so each item is its own box.

   The columns are a fixed count rather than auto-fit: a folder holding three
   reports should still lay them out as three cards in a row of four, not
   stretch them across the page. */
.filelist{display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:12px; align-items:stretch}
@media (max-width:1400px){ .filelist{grid-template-columns:repeat(3, minmax(0,1fr))} }
@media (max-width:1050px){ .filelist{grid-template-columns:repeat(2, minmax(0,1fr))} }
@media (max-width:620px){ .filelist{grid-template-columns:1fr} }

.filerow{display:flex; flex-direction:column; gap:4px; padding:14px 15px 13px;
  border:1px solid var(--line); border-radius:var(--r); background:var(--surface);
  box-shadow:var(--shadow); transition:border-color .12s ease, box-shadow .12s ease}
.filerow:hover{border-color:var(--muted-2); box-shadow:var(--shadow-lg)}
.filerow .fr-top{display:flex; align-items:flex-start; justify-content:space-between; gap:8px}
.filerow .ftype{flex:none; width:42px; height:34px; border-radius:8px; color:#fff; font-size:9.5px;
  font-weight:700; display:flex; align-items:center; justify-content:center; letter-spacing:.02em}
/* Two lines then ellipsis: names wrap in a narrow card, but a card that grows
   to fit one long filename breaks the row it sits in. */
.filerow .fname{margin-top:9px; font-size:13.5px; font-weight:600; line-height:1.35;
  display:-webkit-box; -webkit-line-clamp:2; line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
.filerow .fr-meta{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
/* Pushed to the bottom so the buttons line up across the row however tall the
   names are. */
.filerow .fr-act{display:flex; gap:7px; margin-top:auto; padding-top:11px}
.filerow .fr-act .btn{flex:1; justify-content:center}
/* The remove control stays quiet until you're on the card it belongs to. */
.filerow .icon-btn{opacity:.4; transition:opacity .12s ease}
.filerow:hover .icon-btn,.filerow .icon-btn:focus{opacity:1}

/* The act-on-this-folder controls, above the list rather than after it. */
.rowtools{display:flex; flex-wrap:wrap; align-items:flex-start; gap:10px; margin-bottom:16px}
.rowtools>details{flex:1 1 100%}
.rowtools>details:not([open]){flex:0 0 auto}

/* file/doc grid */
.doc{display:flex; align-items:center; gap:12px; padding:13px 14px; border:1px solid var(--line); border-radius:var(--r); background:var(--surface)}
.doc .dic{width:36px; height:36px; border-radius:9px; background:var(--surface-2); display:flex; align-items:center; justify-content:center; color:var(--teal-600); flex:none}
.doc .dn{font-size:13px; font-weight:600} .doc .dm{font-size:11.5px; color:var(--muted-2)}

/* integrations */
.intg{display:flex; align-items:center; gap:12px; padding:15px; border:1px solid var(--line); border-radius:var(--r-lg); background:var(--surface)}
.intg .ilogo{width:40px; height:40px; border-radius:10px; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:13px; color:#fff; flex:none; font-family:'Unbounded',sans-serif}
.intg .inm{font-size:13.5px; font-weight:600} .intg .ist{font-size:11.5px; color:var(--muted-2)}

/* misc */
.divider{height:1px; background:var(--line); margin:0}
.muted{color:var(--muted)} .center{text-align:center}
.foot-note{text-align:center; color:var(--muted-2); font-size:11.5px; margin:26px 0 6px}
.section-title{font-family:'Unbounded',sans-serif; font-size:17px; font-weight:400; margin:4px 0 2px}

/* ---------- RESPONSIVE ---------- */
@media(max-width:1180px){
  .g-main{grid-template-columns:1fr} .kpis{grid-template-columns:repeat(2,minmax(0,1fr))}
  .strip{grid-template-columns:repeat(3,minmax(0,1fr))} .g-4{grid-template-columns:repeat(2,minmax(0,1fr))}
  .kanban{grid-template-columns:repeat(2,minmax(0,1fr))} .breakdown{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:820px){
  .sidebar{display:none} .g-2,.g-3{grid-template-columns:1fr} .kpis{grid-template-columns:1fr}
  .strip{grid-template-columns:repeat(2,minmax(0,1fr))} .kanban{grid-template-columns:1fr}
}

/* ============================================================
   v2 — PREMIUM POLISH (global-agency grade). Layered on top.
   ============================================================ */
:root{
  /* Shadows tinted with the ink's own hue rather than a generic blue-grey. */
  --paper:#F4F8F9;
  --line:#E2EAEC; --line-2:#EFF4F5;
  --shadow:0 1px 2px rgba(14,33,41,.04), 0 2px 6px rgba(14,33,41,.04);
  --shadow-md:0 6px 18px rgba(14,33,41,.07), 0 2px 6px rgba(14,33,41,.04);
  --shadow-hover:0 16px 38px rgba(14,33,41,.12), 0 6px 12px rgba(14,33,41,.06);
  --ring:0 0 0 3px rgba(118,22,244,.24);
  /* NOTE: the collapsed rail overrides this with `html[data-sidebar]`, which
     outranks :root regardless of order. Changing this line is fine; removing
     that qualifier is not — see the COLLAPSED SIDEBAR block above. */
  --sb-w:256px;
}
/* THIS BLOCK ADDS TOKENS. IT NO LONGER REDEFINES THE PALETTE.
 *
 * It used to restate --paper, --surface, --surface-2, --line and --line-2 with a
 * second, teal-tinted set (#0A151A / #121F27 / #1A2B33, hairlines #243740). Two
 * :root[data-theme="dark"] blocks have equal specificity, so source order
 * decided it and this one won — silently. Every contrast figure worked out
 * against the navy set six hundred lines up was therefore computed against
 * surfaces that never shipped, and the dark theme came out a teal-green that
 * argues with a navy-and-violet brand.
 *
 * One palette, declared once, up there. Anything genuinely additional lives
 * here. (The same trap as --sb-w and .vd-orbit: equal specificity, later wins,
 * and nothing warns you.) */
:root[data-theme="dark"]{
  /* On a dark ground a shadow cannot be seen, so the separation has to come from
     the surface itself plus a hairline of light along the top edge. */
  --card-shadow:0 0 0 1px rgba(255,255,255,.06), 0 8px 24px rgba(0,0,0,.45);
  --card-shadow-hover:0 0 0 1px rgba(255,255,255,.10), 0 14px 32px rgba(0,0,0,.55);
  /* The tints are our --pos/--neg washed over WHITE, so on a dark ground they
     would be near-white blocks. Same idea, washed over the dark surface. */
  --kpi-up:#183A26;   --kpi-up-line:#2A5B3C;
  --kpi-down:#572325; --kpi-down-line:#7A3639;
  --shadow-md:0 8px 22px rgba(0,0,0,.45);
  --shadow-hover:0 18px 44px rgba(0,0,0,.6);
  --ring:0 0 0 3px rgba(164,107,255,.34);
}
html{scroll-behavior:smooth}
body{letter-spacing:-.006em; font-feature-settings:"cv05","ss01"}
.disp,.pg-head h1,.pg-head .greet{letter-spacing:-.022em}
.section-title,.score-num,.inv-amt,.kpi .val,.strip .cv,.rr-title{letter-spacing:-.02em}
.eyebrow{font-size:11px; color:var(--teal-600)}

/* elevation + gentle motion */
/* Elevation is static. Only a card you can actually click lifts — a KPI panel
   that responds to the pointer is telling the reader it does something, and
   most of these do nothing. */
.card{border:0}
a.card,.card.is-link{transition:box-shadow .2s ease, transform .2s cubic-bezier(.2,.6,.2,1)}
a.card:hover,.card.is-link:hover{box-shadow:var(--card-shadow-hover); transform:translateY(-2px)}
.tile,.doc,.intg{transition:box-shadow .2s ease, transform .2s cubic-bezier(.2,.6,.2,1), border-color .2s ease}
.tile:hover,.doc:hover,.intg:hover{box-shadow:var(--shadow-lg); transform:translateY(-2px); border-color:transparent}
.rec{transition:box-shadow .2s ease, border-color .2s ease}
.rec:hover{border-color:var(--muted-2)}
.kbd,kbd{font-feature-settings:normal}

/* sidebar refinement */
.nav-item{transition:background .15s ease, color .15s ease}
.switcher{transition:border-color .15s, box-shadow .15s}
.sb-foot .am-card{transition:background .18s ease}

/* topbar */
.topbar{box-shadow:none; border-bottom:0}
.tb-icon,.tb-ava,.btn{transition:transform .16s cubic-bezier(.2,.6,.2,1), box-shadow .16s ease, border-color .16s ease, background .16s ease}
.tb-ava{box-shadow:var(--shadow)}

/* buttons — premium */
.btn{box-shadow:none}
.btn:hover{transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
/* A gradient has no automatic states, so they are spelled out. `active` flattens
   to a solid so the press is felt, and `disabled` is a flat grey rather than a
   faded gradient — a translucent gradient reads as broken, not as unavailable. */
.btn-primary{box-shadow:none}
.btn-primary:hover{filter:none; box-shadow:0 6px 18px rgba(118,22,244,.28)}
.btn-primary:active{background:var(--teal-700); box-shadow:none}
.btn-primary[disabled],.btn-primary:disabled,.btn-primary.is-off{
  background:#CDD1DB; color:#FFFFFF; box-shadow:none; cursor:not-allowed; transform:none}
.btn-grad{box-shadow:none}
:root[data-theme="dark"] .btn-primary:active{background:#7A2BE8}
:root[data-theme="dark"] .btn-primary[disabled],
:root[data-theme="dark"] .btn-primary:disabled,
:root[data-theme="dark"] .btn-primary.is-off{background:#2A2438; color:#9A93AC}

/* focus + selection */
a:focus-visible,button:focus-visible,input:focus-visible{outline:none; box-shadow:var(--ring)}
.input:focus{outline:none; border-color:var(--teal); box-shadow:var(--ring)}
::selection{background:rgba(118,22,244,.18)}
::-webkit-scrollbar-thumb{background:var(--line); border:2px solid transparent; background-clip:padding-box}

/* numbers + chips + tables */
.kpi .val{font-size:34px}
.chip{border:1px solid transparent} .chip.flat{border-color:var(--line)}

/* "MEASURED, AND IT DID NOT MOVE" — an equals sign, drawn.
 *
 * This started as the character ▬ (U+25AC BLACK RECTANGLE), which failed twice
 * over: at this size it is a heavy filled block that reads beside a figure as a
 * redaction bar — something covered up rather than something flat — and being a
 * glyph it depends on the font carrying it. Unbounded does not, so the browser
 * substituted whatever that particular machine had.
 *
 * Drawing it fixed the font problem and not the reading problem. ONE horizontal
 * line is a dash whatever you make it out of, and a dash is the one shape this
 * mark must not be: — is already spent on `.noval`, meaning NOT MEASURED.
 * "Measured and unchanged" versus "never measured" is the distinction this
 * interface works hardest to hold, and two marks that differ only in weight
 * would collapse it everywhere at once.
 *
 * TWO lines cannot be misread as one. An equals sign says "the same as before",
 * which is exactly the claim, and it sits with ▲ and ▼ as a third state rather
 * than as an absence. Sized in ems so it tracks the figure beside it, and
 * `currentColor` so it inherits the muted tone each context already sets —
 * including the light-on-dark panels. */
/* A DASH — "measured, and it did not move".
 *
 * This went through the block ▬, a bar, an equals sign and a sideways arrow
 * before landing back on the plainest option, which is where it should have
 * stayed: a dash is what people already read as "no change", and cleverer marks
 * only made the reader stop and work out what was meant.
 *
 * Drawn rather than typed, so it does not depend on the font carrying a glyph —
 * that was the original fault with ▬, which Unbounded does not have and every
 * machine substituted differently.
 *
 * THE ONE THING TO WATCH: `.noval` uses — for NOT MEASURED. The two are kept
 * apart by weight and by placement — this one is short, thick and muted, and it
 * always sits directly after a figure, while .noval appears alone in an
 * otherwise empty cell. If a place ever needs both in the same column, that
 * column needs a label, not a third icon. */
.flatbar{display:inline-block; vertical-align:middle; width:.75em; height:2.5px;
  border-radius:2px; background:currentColor; opacity:.5}
/* Inside a chip the dash is the whole content, so it carries a little more
   weight — too light alone in a pill and it reads as a rendering fault. */
.chip.flat .flatbar{opacity:.7}
.tbl th{font-size:10.5px; letter-spacing:.07em}
.tbl tbody tr{transition:background .13s ease}
.card-h h3{letter-spacing:-.012em; font-size:17px; font-weight:400}
.pg-head .greet,.pg-head h1{font-size:30px; font-weight:400}

/* ============================================================
   MESSAGES / TEAM CHAT
   ============================================================ */
.msgs{display:flex; height:calc(100vh - 61px); background:var(--surface); border-radius:var(--r-card); overflow:hidden; box-shadow:var(--card-shadow)}
/* The pane inside the bento. `100vh - 61px` assumed the chat owned the whole
   viewport under the topbar, which it did while the page was full-bleed; with a
   claim, a qbar and a row of counts above it that height runs the composer off
   the bottom of the window and the Send button becomes unreachable without
   scrolling the document. Clamped instead: it takes what is left, never
   collapses to a strip on a short window, and stops growing on a tall one
   because a 1200px-high message list is not more readable than a 760px one. */
.bento > .msgs{height:clamp(480px, calc(100vh - 340px), 760px)}
.chat-list{width:320px; flex:none; border-right:1px solid var(--line-2); display:flex; flex-direction:column; background:var(--surface)}
.chat-list .clh{padding:18px 16px 14px; border-bottom:1px solid var(--line)}
.chat-list .clh .row-between h2{font-family:'Unbounded',sans-serif; font-size:19px; font-weight:600; letter-spacing:-.01em}
.chat-search{margin-top:13px; display:flex; align-items:center; gap:8px; padding:9px 12px; background:var(--surface-2); border:1px solid var(--line); border-radius:10px; color:var(--muted-2); font-size:12.5px}
.seg-mini{display:flex; gap:4px; margin-top:12px}
.seg-mini button{flex:1; border:1px solid var(--line); background:var(--surface); border-radius:8px; font-size:11.5px; font-weight:600; color:var(--muted); padding:6px; cursor:pointer; font-family:inherit}
.seg-mini button.on{background:var(--fill-strong); color:var(--on-fill-strong); border-color:var(--fill-strong)}
.chat-items{flex:1; overflow-y:auto; padding:8px}
.chat-item{display:flex; gap:12px; padding:11px; border-radius:12px; cursor:pointer; align-items:center; border:1px solid transparent}
.chat-item:hover{background:var(--surface-2)}
.chat-item.active{background:var(--teal-tint); border-color:rgba(118,22,244,.2)}
.avatar{width:42px; height:42px; border-radius:50%; flex:none; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:700; font-size:14px; position:relative; font-family:'Unbounded',sans-serif}
.avatar .presence{position:absolute; right:-1px; bottom:-1px; width:12px; height:12px; border-radius:50%; border:2.5px solid var(--surface)}
/* THE PHOTO INSIDE ANY AVATAR — see partials/avatar.ejs.
   It rounds itself with border-radius:inherit so the parent never needs
   overflow:hidden. Clipping the parent is the obvious approach and it is wrong
   here: every presence dot sits at right:-1px, outside the circle on purpose,
   and a clipped parent slices it in half. */
.ava-img{width:100%; height:100%; object-fit:cover; border-radius:inherit; display:block}

/* THE HOVER READOUT on the visibility chart — see public/js/app.js.
   The guide is a hairline, not a highlight: it says which run you are reading,
   and anything heavier would compete with the three lines it sits behind. */
.ch-guide{stroke:var(--muted-2); stroke-width:1; stroke-dasharray:3 3; opacity:.55; pointer-events:none}
.ch-read{position:absolute; top:6px; z-index:6; pointer-events:none;
  background:var(--surface); border:1px solid var(--line); border-radius:10px;
  box-shadow:var(--shadow-lg, 0 12px 28px -10px rgba(13,23,45,.25));
  padding:8px 10px; min-width:132px; font-size:11.5px; line-height:1.5}
.ch-read .ch-d{font-weight:700; color:var(--ink); margin-bottom:4px}
.ch-read .ch-f{font-weight:500; color:var(--muted-2)}
.ch-read .ch-r{display:flex; align-items:center; gap:7px}
.ch-read .ch-r i{width:8px; height:8px; border-radius:2px; flex:none}
.ch-read .ch-r span{color:var(--muted); margin-right:auto}
.ch-read .ch-r b{color:var(--ink); font-weight:700}
/* The chart is the hover target, so it needs to receive the pointer. */
svg.chart{cursor:crosshair}
/* THE TWO AXIS LINES. A shade stronger than the gridlines so the plot has an
   edge rather than floating between hairlines.

   vector-effect:non-scaling-stroke is not decoration. Several of these charts
   are drawn with preserveAspectRatio="none", which stretches x and y by
   different factors — without it the vertical axis and the horizontal axis come
   out at visibly different thicknesses on the same chart. */
.chart .ax{stroke:var(--line); stroke-width:1; vector-effect:non-scaling-stroke}
/* A repeated date keeps its tick but not its word — see the note in
   ai-visibility.ejs. The dot marks that a run happened there. */
.axis-x .rep{color:var(--muted-2); opacity:.5}
/* The two halves of the AI Visibility card's sentence: the method, then the
   result. Each is its own line at every width. */
.vd-l1, .vd-l2{display:block}
/* Inside a stored engine answer — see h.answer() in middleware/locals.js. The
   containers are white-space:pre-wrap, so these only need to look right inline. */
.bubble a, .ans a{color:var(--teal-600); text-decoration:underline; text-underline-offset:2px; word-break:break-word}
.bubble code, .ans code{background:var(--surface-3, rgba(13,23,45,.06)); border-radius:4px; padding:1px 5px; font-size:.92em; font-family:ui-monospace,'SFMono-Regular',Menlo,monospace}
.bubble b, .ans b{font-weight:700; color:var(--ink)}
.presence.on{background:#17A34A} .presence.away{background:#E8A13A} .presence.off{background:#8FA1A7}
.chat-item .ci-body{flex:1; min-width:0}
.chat-item .ci-top{display:flex; justify-content:space-between; align-items:baseline; gap:8px}
.chat-item .ci-name{font-weight:600; font-size:13.5px}
.chat-item .ci-time{font-size:11px; color:var(--muted-2); flex:none}
.chat-item .ci-prev{font-size:12px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.chat-item .ci-unread{background:var(--teal); color:#fff; font-size:10.5px; font-weight:700; min-width:19px; height:19px; border-radius:100px; display:flex; align-items:center; justify-content:center; padding:0 5px; flex:none}
.chat-role{font-size:10.5px; color:var(--muted-2); font-weight:600}
.chat-main{flex:1; display:flex; flex-direction:column; min-width:0}
.chat-head{height:66px; flex:none; box-shadow:inset 0 -1px 0 var(--line); display:flex; align-items:center; gap:13px; padding:0 22px}
.chat-head .ch-name{font-weight:600; font-size:15px}
.chat-head .ch-status{font-size:12px; color:var(--muted)}
.chat-head .ch-status .presence{position:static; display:inline-block; width:8px; height:8px; border:0; margin-right:5px; vertical-align:middle}
.chat-head .ch-actions{margin-left:auto; display:flex; gap:8px}
.chat-scroll{flex:1; overflow-y:auto; padding:24px 26px; display:flex; flex-direction:column; gap:3px; background:var(--paper)}
.day-sep{align-self:center; font-size:11px; color:var(--muted-2); background:var(--surface); border:1px solid var(--line); padding:4px 13px; border-radius:100px; font-weight:600; margin:12px 0}
.m{max-width:62%; display:flex; flex-direction:column; gap:3px; margin-top:9px}
.m.me{align-self:flex-end; align-items:flex-end}
.m .mb{padding:11px 15px; border-radius:16px; font-size:13.5px; line-height:1.5; box-shadow:var(--shadow)}
.m.them .mb{background:var(--surface); border:1px solid var(--line); border-bottom-left-radius:5px}
.m.me .mb{background:linear-gradient(180deg,#17b1c8,#0E8194); color:#fff; border-bottom-right-radius:5px}
.m .mmeta{font-size:10.5px; color:var(--muted-2); padding:0 5px}
.m .mfile{display:flex; align-items:center; gap:10px; padding:10px 13px; border:1px solid var(--line); border-radius:12px; background:var(--surface); font-size:12.5px; box-shadow:var(--shadow)}
.composer{flex:none; box-shadow:inset 0 1px 0 var(--line); padding:15px 22px; display:flex; align-items:center; gap:12px; background:var(--surface)}
.composer .cbox{flex:1; display:flex; align-items:center; gap:10px; background:var(--surface-2); border:1px solid var(--line); border-radius:12px; padding:12px 15px; color:var(--muted-2); font-size:13.5px}
.composer .cbtn{width:40px; height:40px; border-radius:11px; border:1px solid var(--line); background:var(--surface); color:var(--muted); display:flex; align-items:center; justify-content:center; cursor:pointer; flex:none}
.chat-ctx{width:296px; flex:none; box-shadow:inset 1px 0 0 var(--line); padding:24px 22px; overflow-y:auto; background:var(--surface)}
.ctx-hero{display:flex; flex-direction:column; align-items:center; text-align:center; gap:4px; padding-bottom:20px; border-bottom:1px solid var(--line)}
.ctx-hero .avatar{width:64px; height:64px; font-size:20px; margin-bottom:8px}
@media(max-width:1180px){.chat-ctx{display:none}}
@media(max-width:900px){.chat-list{width:100%; max-width:340px}}

/* ---- app runtime additions ---- */
.content.flush{padding:0;max-width:none}
.flash{position:fixed;bottom:24px;right:24px;z-index:60;display:flex;flex-direction:column;gap:8px;max-width:min(460px,calc(100vw - 320px))}
.flash .fl{padding:11px 15px;border-radius:10px;font-size:13px;font-weight:600;line-height:1.5;box-shadow:var(--shadow-md);border:1px solid var(--line);background:var(--surface);white-space:pre-wrap;overflow-wrap:anywhere;max-height:40vh;overflow:auto}
.flash .fl.ok{background:var(--pos-tint);color:var(--pos)}
.flash .fl.err{background:var(--neg-tint);color:var(--neg)}
@media(max-width:820px){.sidebar.open{display:flex;position:fixed;z-index:70;height:100vh;width:260px}}

/* ---- inline create-forms (Phase 2 CRUD) ---- */
.addbox{box-shadow:var(--card-shadow); margin-bottom:16px}
/* An add-box whose opener lives somewhere else — in a panel's bite. The
   <details> is kept for its open state and its script-free reveal; only the
   summary goes, and when it is closed the whole thing takes no room at all. */
.addbox.nosum>summary{display:none}
.addbox.nosum:not([open]){margin:0; padding:0; box-shadow:none; background:none}
.addbox>summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:8px;font-size:13px;font-weight:600;padding:9px 14px;border-radius:10px;border:1px solid var(--line);background:var(--surface);color:var(--ink);box-shadow:var(--shadow)}
.addbox>summary::-webkit-details-marker{display:none}
.addbox>summary:hover{border-color:var(--muted-2)}
.addbox[open]>summary{background:var(--fill-strong);color:var(--on-fill-strong);border-color:var(--fill-strong)}
.addform{margin-top:12px;padding:18px;border:1px solid var(--line);border-radius:var(--r-lg);background:var(--surface);box-shadow:var(--shadow);display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end}
.addform{align-items:flex-end}

/* Field styling lives on .fld itself, so any form — inside an add-box, a card,
   or a page — looks the same without repeating inline styles. */
.fld{display:flex;flex-direction:column;gap:6px;min-width:0}
.fld>label,.addform label{font-size:11.5px;font-weight:600;color:var(--muted);display:block}
.fld input,.fld select,.fld textarea,
.addform input,.addform select,.addform textarea{
  padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--surface);
  font-size:13.5px;color:var(--ink);font-family:inherit;width:100%;max-width:100%}
.fld input:focus,.fld select:focus,.fld textarea:focus,
.addform input:focus,.addform select:focus,.addform textarea:focus{outline:none;border-color:var(--teal);box-shadow:var(--ring)}
/* Same drawn chevron on every form select, so nothing falls back to the OS arrow. */
.fld select,.addform select{
  appearance:none; -webkit-appearance:none; -moz-appearance:none; padding-right:32px;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%237B8A99' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 11px center}
.fld select[multiple]{background-image:none; padding-right:12px}
.fld input[type=checkbox],.fld input[type=radio]{width:auto}
.fld input[type=file]{padding:8px 10px;font-size:12.5px;background:var(--surface-2)}
.fld select[multiple]{padding:6px;height:auto}
.fld input::placeholder,.addform input::placeholder{color:var(--muted-2)}
.fld input:disabled,.fld select:disabled{background:var(--surface-2);color:var(--muted-2)}

/* Card-based forms: a wrapping row of fields with a full-width action row. */
.formrow{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end}
.formrow .actions{flex:1 1 100%;display:flex;gap:10px;align-items:center;margin-top:2px}

/* Client / filter picker. Matches .btn height so it lines up beside buttons, with
   a drawn chevron instead of the OS default arrow. */
.picker{
  appearance:none; -webkit-appearance:none; -moz-appearance:none;
  font-family:inherit; font-size:12.5px; font-weight:600; color:var(--ink); line-height:1.25;
  padding:8px 32px 8px 12px; border-radius:10px; border:1px solid var(--line);
  background-color:var(--surface); cursor:pointer; max-width:230px;
  text-overflow:ellipsis; white-space:nowrap;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%237B8A99' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 12px center}
.picker:hover{border-color:var(--muted-2)}
.picker:focus{outline:none; border-color:var(--teal); box-shadow:var(--ring)}
.picker-sm{font-size:12.5px; padding:8px 30px 8px 12px; background-position:right 10px center}
/* Label sitting in front of a picker, e.g. "Client" */
.picker-wrap{display:inline-flex; align-items:center; gap:8px}
.picker-wrap>label{font-size:11.5px; font-weight:600; color:var(--muted); white-space:nowrap}

/* A list row you can act on: change status, expand to edit, or delete. */
.editrow{padding:5px 0;border-bottom:1px solid var(--line-2)}
.editrow:last-child{border-bottom:0}
.minisel{padding:3px 6px;border:1px solid var(--line);border-radius:7px;background:var(--surface);
  font-size:11.5px;color:var(--muted);font-family:inherit;cursor:pointer}
.minisel:focus{outline:none;border-color:var(--teal)}
.editrow details[open]>summary{color:var(--teal-600)}
.editform{display:flex;flex-direction:column;gap:9px;padding:12px;margin-top:8px;
  border:1px solid var(--line);border-radius:10px;background:var(--surface-2);min-width:240px}
.editform button{align-self:flex-start}

/* Inline code — .mono is the little square engine badge, not this. */
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;
  background:var(--surface-2);border:1px solid var(--line);border-radius:6px;
  padding:2px 6px;color:var(--ink-2);white-space:nowrap}
.code.wrap{white-space:normal;word-break:break-all}
.icon-btn{width:26px;height:26px;border-radius:7px;border:1px solid var(--line);background:var(--surface);color:var(--muted-2);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:1}
.icon-btn:hover{color:var(--neg);border-color:var(--neg)}
/* .icon-btn's hover is RED because every one of them used to be a delete. An
   icon that turns red on hover is a warning, and putting one on "show me this
   prompt's answers" would make a harmless action look destructive. */
.icon-btn.act:hover{color:var(--teal-600); border-color:var(--teal-600)}
.icon-btn.on{color:#fff; background:var(--teal); border-color:var(--teal)}
.icon-btn.on:hover{color:#fff; border-color:var(--teal)}

/* Two controls, one baseline. A bare <a> and a <button> inside a <form> sit on
   different baselines by default, which is why the replay icon rode higher than
   the delete beside it. The cell owns the alignment so neither has to. */
.row-acts{display:flex; align-items:center; gap:6px; justify-content:flex-end}
.row-acts form{display:flex; margin:0}

/* NOT `.row-open` — that name was already taken, further down this file, by the
   underline-on-hover style Content Center puts on its first column. Reusing it
   here made every cell of the open row underline on hover, which looked like a
   bug in the table rather than a collision in the stylesheet. */
tr.prow-on > td{background:var(--teal-tint)}
tr.prow-on > td:first-child{box-shadow:inset 3px 0 0 var(--teal)}
/* The answers, in a row of their own directly beneath their prompt. */
tr.prow-ans > td{background:var(--teal-tint); padding:0 0 16px 0}
tr.prow-ans .prow-inner{background:var(--surface); border:1px solid var(--line); border-radius:var(--r-card); margin:0 14px; padding:16px 18px}
/* The prompt text is the way into its answers. Underlined only on hover, so the
   first column stays scannable instead of becoming a column of links. */
.prow-link{color:inherit; text-decoration:none}
.prow-link:hover{color:var(--teal-600); text-decoration:underline; text-underline-offset:2px}
.inline-form{display:inline}
.viewbar{background:var(--navy-tint);color:var(--navy);font-size:12.5px;font-weight:600;padding:9px 24px;display:flex;align-items:center;gap:9px;box-shadow:inset 0 -1px 0 var(--line)}
.viewbar a{margin-left:auto;color:var(--teal-600);font-weight:600}
.ic{width:30px; height:30px;}
/* ---- Rich text editor (public/js/editor.js) ---- */
.rte{border:1px solid var(--line); border-radius:10px; background:var(--surface); overflow:hidden}
.rte:focus-within{border-color:var(--teal); box-shadow:var(--ring)}
.rte-bar{display:flex; align-items:center; gap:4px; flex-wrap:wrap; padding:6px 8px;
  background:var(--surface-2); border-bottom:1px solid var(--line)}
.rte-btn{font-size:12px; font-weight:600; line-height:1; color:var(--ink-2); cursor:pointer;
  background:transparent; border:1px solid transparent; border-radius:7px; padding:6px 9px; font-family:inherit}
.rte-btn:hover{background:var(--surface); border-color:var(--line)}
.rte-btn.on{background:var(--fill-strong); color:var(--on-fill-strong); border-color:var(--fill-strong)}
.rte-area{min-height:200px; max-height:460px; overflow:auto; padding:14px 16px;
  font-size:14px; line-height:1.65; color:var(--ink); outline:none}
.rte-area:empty::before{content:attr(data-placeholder); color:var(--muted-2)}
.rte-area>*:first-child{margin-top:0}
.rte-area p{margin:0 0 10px}
.rte-area h2{font-size:19px; font-weight:700; margin:18px 0 8px; line-height:1.3}
.rte-area h3{font-size:16px; font-weight:700; margin:15px 0 7px; line-height:1.35}
.rte-area ul,.rte-area ol{margin:0 0 10px; padding-left:22px}
.rte-area li{margin:3px 0}
.rte-area a{color:var(--teal-600); text-decoration:underline}
.rte-area blockquote{margin:10px 0; padding:6px 0 6px 14px; border-left:3px solid var(--teal);
  color:var(--ink-2); font-style:italic}
.rte-area code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12.5px;
  background:var(--surface-2); padding:1px 5px; border-radius:5px}
.rte .rte-source{width:100%; min-height:220px; padding:14px 16px; border:0; outline:none;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12.5px;
  line-height:1.6; color:var(--ink); background:var(--surface); resize:vertical}


/* ============================================================
   PAID MEDIA (PPC) module
   Everything else reuses the classes above (.card .kpi .tbl .chip
   .rec .tabs .meter …). Nothing existing is modified.
   ============================================================ */

/* ---- money ---- */
.money{font-family:'Unbounded',sans-serif;font-variant-numeric:tabular-nums;font-weight:600}
.saving{color:var(--pos);font-weight:700}
.burn{color:var(--neg);font-weight:700}

/* ---- row flags on keywords / search terms / campaigns ---- */
.flag{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:700;
  letter-spacing:.03em;text-transform:uppercase;padding:2px 8px;border-radius:100px}
.flag.waste{background:var(--neg-tint);color:var(--neg)}
.flag.winner{background:var(--pos-tint);color:var(--pos)}
.flag.watch{background:var(--warn-tint);color:#B5791E}
.flag.scale{background:var(--teal-tint);color:var(--teal-600)}
tr.is-waste td:first-child{box-shadow:inset 3px 0 0 var(--neg)}
tr.is-winner td:first-child{box-shadow:inset 3px 0 0 var(--pos)}

/* ---- quality score pip ---- */
.qs{display:inline-flex;width:22px;height:22px;border-radius:6px;align-items:center;justify-content:center;
  font-family:'Unbounded',sans-serif;font-weight:400;font-size:11.5px}
.qs.good{background:var(--pos-tint);color:var(--pos)}
.qs.ok{background:var(--warn-tint);color:#B5791E}
.qs.bad{background:var(--neg-tint);color:var(--neg)}

/* ---- channel pill ---- */
.chan{font-size:10px;font-weight:700;padding:2px 7px;border-radius:5px;letter-spacing:.03em;text-transform:uppercase}
.chan.search{background:var(--teal-tint);color:var(--teal-600)}
.chan.pmax{background:var(--navy-tint);color:var(--navy)}
.chan.shopping{background:var(--green-tint);color:#4c7a24}
.chan.display{background:var(--surface-2);color:var(--muted)}
.chan.social{background:var(--warn-tint);color:#B5791E}
.chan.video{background:var(--neg-tint);color:var(--neg)}

/* ---- budget pacing bar (target marker + forecast overshoot) ---- */
.pace{position:relative;height:34px;background:var(--surface-2);border:1px solid var(--line);
  border-radius:10px;overflow:hidden;margin-top:14px}
.pace .spent{position:absolute;inset:0 auto 0 0;background:var(--teal);border-radius:9px 0 0 9px}
.pace .fore{position:absolute;top:0;bottom:0;background:repeating-linear-gradient(135deg,
  var(--neg-tint),var(--neg-tint) 6px,transparent 6px,transparent 12px);border-left:0}
.pace .mark{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--ink);opacity:.55}
.pace .mark::after{content:attr(data-label);position:absolute;top:-19px;left:50%;transform:translateX(-50%);
  font-size:10px;font-weight:700;color:var(--muted);white-space:nowrap}
.pace-legend{display:flex;gap:16px;flex-wrap:wrap;margin-top:22px;font-size:11.5px;color:var(--muted)}
.pace-legend span{display:flex;align-items:center;gap:6px}

/* ---- ad creative (RSA) approval card ---- */
.rsa{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--surface);overflow:hidden;box-shadow:var(--shadow)}
.rsa-h{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line-2)}
.rsa-h .rsa-t{font-size:13px;font-weight:600}
.rsa-h .rsa-m{font-size:11.5px;color:var(--muted-2)}
.rsa-body{padding:16px}
/* the SERP preview — clients approve what they can picture */
.serp{border:1px solid var(--line);border-radius:var(--r);padding:13px 15px;background:var(--surface-2)}
.serp .sp-badge{font-size:10.5px;font-weight:700;color:var(--ink-2);border:1px solid var(--muted-2);
  border-radius:4px;padding:0 5px;margin-right:7px}
.serp .sp-url{font-size:12px;color:var(--muted)}
.serp .sp-title{font-size:16px;color:#1a4fd8;line-height:1.3;margin:4px 0 3px}
:root[data-theme="dark"] .serp .sp-title{color:#7fa9ff}
.serp .sp-desc{font-size:12.5px;color:var(--muted);line-height:1.5}
.asset-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
.asset{font-size:11.5px;padding:4px 9px;border-radius:7px;background:var(--surface-2);
  border:1px solid var(--line);color:var(--ink-2)}
.asset.low{border-color:var(--neg);color:var(--neg)}
.rsa-foot{display:flex;align-items:center;gap:9px;padding:12px 16px;border-top:1px solid var(--line-2);background:var(--surface-2)}
.rsa-foot .btn{box-shadow:none}

/* ---- waste banner on the overview ---- */
.wastecard{background:linear-gradient(120deg,var(--neg-tint),transparent 70%);
  border:1px solid rgba(229,72,77,.25)}
.oppcard{background:linear-gradient(120deg,var(--teal-tint),transparent 70%);
  border:1px solid rgba(118,22,244,.25)}
.bigmoney{font-family:'Unbounded',sans-serif;font-weight:400;font-size:34px;letter-spacing:-.02em;line-height:1}

/* ---- methodology disclaimer ---- */
.disclaim{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--surface-2);padding:17px 19px}
.disclaim .dh{display:flex;align-items:center;gap:9px;font-size:13px;font-weight:700;margin-bottom:4px}
.disclaim .dh svg{color:var(--muted);flex:none}
.disclaim .dsub{font-size:12px;color:var(--muted-2);margin-bottom:13px}
.disclaim ol{list-style:none;counter-reset:d;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:11px 22px}
.disclaim li{counter-increment:d;position:relative;padding-left:26px;font-size:12.5px;color:var(--muted);line-height:1.6}
.disclaim li::before{content:counter(d);position:absolute;left:0;top:1px;width:18px;height:18px;border-radius:6px;
  background:var(--surface);border:1px solid var(--line);color:var(--ink-2);font-size:10.5px;font-weight:700;
  display:flex;align-items:center;justify-content:center}
.disclaim li b{color:var(--ink)}
@media(max-width:900px){ .disclaim ol{grid-template-columns:1fr} }

/* the small inline note that sits under a modelled figure */
.modelled{font-size:11.5px;color:var(--muted-2);line-height:1.55;margin-top:12px;padding-top:11px;
  border-top:1px dashed var(--line)}

/* the rule that produced a recommendation, shown verbatim */
.ruleline{display:flex;gap:8px;align-items:flex-start;margin-top:9px;padding:8px 10px;border-radius:8px;
  background:var(--surface-2);border:1px solid var(--line-2);font-size:11.5px;color:var(--muted);line-height:1.55}
.ruleline b{color:var(--ink-2);font-weight:600;flex:none}

/* ---- selectable metric slot (for anything not every client can measure) ---- */
.metricsel{appearance:none;-webkit-appearance:none;-moz-appearance:none;font-family:inherit;font-size:12px;
  font-weight:500;color:var(--muted);background:transparent;border:1px solid transparent;border-radius:7px;
  padding:2px 20px 2px 6px;margin-left:-6px;cursor:pointer;max-width:170px;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%2393A1AC' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 5px center}
.metricsel:hover{border-color:var(--line);background-color:var(--surface-2)}
.metricsel:focus{outline:none;border-color:var(--teal);box-shadow:var(--ring)}
.metricsel option:disabled{color:var(--muted-2)}

/* a metric the client cannot measure yet — shown, not hidden, with the reason */
.kpi.locked .val{color:var(--muted-2)}
.kpi.locked .meter>span{background:var(--line)}
.lockwhy{font-size:11.5px;color:var(--muted);line-height:1.5;margin-top:9px}
.lockwhy a{color:var(--teal-600);font-weight:600}

/* ---- "Where the money went": channel → campaign → asset group ---- */
.spend-list{display:flex;flex-direction:column;margin-top:15px}
.schan{border-top:1px solid var(--line-2)}
.schan:first-child{border-top:0}
.schan>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:9px;padding:9px 2px;
  font-size:12.5px;border-radius:8px}
.schan>summary::-webkit-details-marker{display:none}
.schan>summary:hover{background:var(--surface-2)}
.schan .sn{font-weight:600;flex:1;min-width:0}
.schan .sv{font-family:'Unbounded',sans-serif;font-weight:400;font-variant-numeric:tabular-nums}
.schan .sp{color:var(--muted-2);font-size:11.5px;width:40px;text-align:right}
.schan .chev{color:var(--muted-2);transition:transform .18s ease;flex:none}
.schan[open]>summary .chev{transform:rotate(180deg)}
.skids{padding:2px 0 10px 22px;display:flex;flex-direction:column;gap:1px}
.skid{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:8px;font-size:12.5px;color:var(--ink-2)}
.skid:hover{background:var(--surface-2);color:var(--ink)}
.skid .kn{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.skid .kv{font-family:'Unbounded',sans-serif;font-weight:600;font-variant-numeric:tabular-nums;flex:none}
.skid .go{color:var(--muted-2);flex:none}
.skid:hover .go{color:var(--teal-600)}
.subnote{font-size:11px;color:var(--muted-2);line-height:1.55;padding:8px 10px;margin:4px 0 2px;
  background:var(--surface-2);border:1px solid var(--line-2);border-radius:8px}
.subnote b{color:var(--ink-2)}

/* rows that navigate */
tr.linked{cursor:pointer}
tr.linked td:first-child a{color:inherit}
tr.linked:hover td:first-child a{color:var(--teal-600);text-decoration:underline}

/* breadcrumb on a detail page */
/* THE ARTICLE BODY — the one place in this app that renders prose written
   elsewhere, so it is the one place that needs type defaults rather than a class
   on every element. Measure is capped at 72ch: this is the only screen someone
   reads top to bottom, and a 1400px line is the fastest way to make them stop.
   The tag list matches what the sanitiser allows, so nothing here styles an
   element that can never arrive. */
.article-body{max-width:72ch; font-size:15.5px; line-height:1.75; color:var(--ink)}
.article-body > *:first-child{margin-top:0}
.article-body p{margin:0 0 1.1em}
.article-body h2{font-family:'Unbounded',sans-serif; font-size:22px; font-weight:400;
  letter-spacing:-.02em; margin:1.9em 0 .6em}
.article-body h3{font-size:17px; font-weight:700; margin:1.6em 0 .5em}
.article-body h4{font-size:15px; font-weight:700; margin:1.4em 0 .4em; color:var(--ink-2)}
.article-body ul,.article-body ol{margin:0 0 1.1em; padding-left:1.3em}
.article-body li{margin:0 0 .45em}
.article-body a{color:var(--teal-600); font-weight:600; text-decoration:underline; text-underline-offset:2px}
.article-body blockquote{margin:1.4em 0; padding:2px 0 2px 18px; box-shadow:inset 3px 0 0 var(--line);
  color:var(--muted); font-style:italic}
.article-body code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.9em;
  background:var(--surface-2); padding:2px 6px; border-radius:5px}
.article-body pre{background:var(--surface-2); padding:14px 16px; border-radius:var(--r);
  overflow-x:auto; margin:0 0 1.1em}
.article-body pre code{background:none; padding:0}

/* A table row whose first cell opens the thing. Underlined on hover only — a
   permanently underlined first column turns a scannable table into a wall of
   links, and in Content Center every row is one. */
.row-open{color:inherit; text-decoration:none}
.row-open:hover{color:var(--teal-600); text-decoration:underline; text-underline-offset:2px}

.crumb{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted-2);margin-bottom:9px}
.crumb a{color:var(--teal-600);font-weight:600}

/* impression-share split bar */
.isbar{display:flex;height:26px;border-radius:9px;overflow:hidden;margin-top:12px;border:1px solid var(--line)}
.isbar i{display:flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:700;color:#fff}

/* ---- staff-only strip ---- */
.staffonly{border:1px dashed var(--navy);background:var(--navy-tint);border-radius:var(--r-lg);padding:14px 16px}
.staffonly .so-l{font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--navy)}

@media(max-width:1180px){ .g-main{grid-template-columns:1fr} }

/* ============================================================
   LOCKED MODULES
   A module the client doesn't buy stays in the sidebar rather than
   disappearing. Hiding it means nobody ever asks for it.
   ============================================================ */
.nav-lock{position:relative}
.nav-item.locked{opacity:.55}
.nav-item.locked:hover{opacity:.95; background:var(--surface-2)}
.nav-item.locked .lockic{margin-left:auto; flex:none; color:var(--muted-2)}
.nav-item.locked:hover .lockic{color:var(--teal-600)}

.lock-pop{
  position:absolute; left:0; right:0; top:calc(100% + 5px); z-index:60;
  background:var(--surface); border:1px solid var(--line); border-radius:12px;
  box-shadow:0 12px 32px rgba(16,24,40,.16); padding:12px 13px;
  opacity:0; visibility:hidden; transform:translateY(-4px);
  transition:opacity .14s ease, transform .14s ease, visibility .14s;
}
.nav-lock:hover .lock-pop, .nav-lock:focus-within .lock-pop{opacity:1; visibility:visible; transform:translateY(0)}
.lock-pop .lp-h{display:flex; align-items:center; gap:7px; font-size:12px; font-weight:700; color:var(--ink); margin-bottom:6px}
.lock-pop .lp-h svg{color:var(--muted-2); flex:none}
.lock-pop .lp-b{font-size:11.5px; color:var(--muted); line-height:1.55}

/* The page behind a locked module — an explanation and a way to ask, not a 403. */
.locked-hero{background:var(--grad-soft); border-color:var(--line)}
.locked-hero .lh-inner{display:flex; gap:30px; padding:34px 32px; align-items:flex-start; flex-wrap:wrap}
.locked-hero .lh-lock{width:112px; height:112px; flex:none; border-radius:26px;
  background:var(--surface); border:1px solid var(--line); box-shadow:var(--shadow-md);
  display:flex; align-items:center; justify-content:center; color:var(--navy)}
.locked-hero .lh-lock svg{width:62px; height:62px}
.locked-hero .lh-body{flex:1; min-width:280px}
.locked-hero .lh-title{font-family:'Unbounded',sans-serif; font-size:19px; font-weight:600; letter-spacing:-.015em}
.locked-hero .lh-sub{font-size:13.5px; color:var(--ink-2); line-height:1.65; margin-top:8px; max-width:640px}
@media(max-width:640px){ .locked-hero .lh-inner{padding:24px 20px; gap:20px} .locked-hero .lh-lock{width:80px;height:80px;border-radius:20px} .locked-hero .lh-lock svg{width:44px;height:44px} }

/* ============================================================
   DATE RANGE PICKER
   One control shared by every page. Pure <details> so it works
   with no JavaScript, like the client switcher.
   ============================================================ */
.rangepick{position:relative}
.rangepick>summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:8px;
  font-size:12.5px;font-weight:600;color:var(--ink);padding:8px 12px;border-radius:10px;
  border:1px solid var(--line);background:var(--surface);box-shadow:var(--shadow);white-space:nowrap}
.rangepick>summary::-webkit-details-marker{display:none}
.rangepick>summary:hover{border-color:var(--muted-2)}
.rangepick[open]>summary{border-color:var(--teal);box-shadow:var(--ring)}
.rangepick>summary svg{color:var(--muted);flex:none}
.rangepick .rp-chev{transition:transform .18s ease}
.rangepick[open] .rp-chev{transform:rotate(180deg)}
/* A dot means the page can't answer for an arbitrary window with every figure. */
.rangepick .rp-dot{width:6px;height:6px;border-radius:50%;background:var(--warn);flex:none}

.rp-menu{position:absolute;z-index:60;right:0;top:calc(100% + 6px);width:270px;padding:8px;
  background:var(--surface);border:1px solid var(--line);border-radius:12px;
  box-shadow:0 14px 36px rgba(16,24,40,.16)}
.rp-head{font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted-2);padding:7px 9px 5px}
.rp-item{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:8px;
  font-size:12.5px;color:var(--ink);text-decoration:none}
.rp-item:hover{background:var(--surface-2)}
.rp-item.on{font-weight:600;background:var(--teal-tint);color:var(--navy)}
.rp-tick{margin-left:auto;color:var(--teal-600);font-weight:700}
.rp-custom{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;padding:4px 9px 2px}
.rp-custom label{display:flex;flex-direction:column;gap:4px;flex:1 1 108px;
  font-size:11px;font-weight:600;color:var(--muted)}
.rp-custom input{padding:7px 9px;border:1px solid var(--line);border-radius:8px;background:var(--surface);
  font-size:12.5px;color:var(--ink);font-family:inherit;width:100%}
.rp-custom input:focus{outline:none;border-color:var(--teal);box-shadow:var(--ring)}
.rp-custom button{flex:1 1 100%;margin-top:2px}
.rp-foot{padding:9px 9px 4px;margin-top:4px;border-top:1px solid var(--line-2);
  font-size:11px;color:var(--muted-2);line-height:1.5}
.rp-foot b{color:#B5791E}

/* Says, next to a figure, that this one didn't move with the range. */
.periodonly{font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;
  color:var(--muted-2);border:1px solid var(--line);border-radius:100px;padding:1px 7px;white-space:nowrap}

/* ---- Sortable table headers ------------------------------------------------
   The arrow only appears on the column actually in force. Showing a faint arrow
   on every sortable column reads as "these are all sorted", which is the one
   thing a sort indicator must never say. */
.tbl th.sortable{padding:0}
.tbl th.sortable>a{
  display:block; padding:11px 14px; color:inherit; text-decoration:none;
  font:inherit; letter-spacing:inherit; text-transform:inherit; white-space:nowrap;
}
.tbl th.sortable>a:hover{color:var(--ink); background:var(--surface-2)}
.tbl th.sortable.on>a{color:var(--ink)}
.tbl th.sortable .sarrow{margin-inline-start:5px; font-size:10px; line-height:1}
.tbl th.sortable.on .sarrow{color:var(--teal)}

/* ---- Staff-only change queue ---------------------------------------------- */
.staff-only{border-color:var(--accent-strong, var(--line))}
.chg-forms{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:0}
.chg-forms>.chg-form:first-child{border-inline-end:1px solid var(--line)}
@media (max-width:900px){
  .chg-forms{grid-template-columns:1fr}
  .chg-forms>.chg-form:first-child{border-inline-end:0; border-bottom:1px solid var(--line)}
}
.chg-form{padding:16px 18px}
.chg-t{font-size:13px; font-weight:700; color:var(--ink); margin-bottom:10px}
.chg-grid{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:10px; margin-bottom:12px}
.chg-grid>label{display:flex; flex-direction:column; gap:4px; font-size:11.5px; font-weight:600; color:var(--muted)}
.chg-grid>label:first-child{grid-column:1 / -1}
.chg-grid .opt{font-weight:500; color:var(--muted-2); text-transform:none; letter-spacing:0}
.inp{
  width:100%; box-sizing:border-box; font:inherit; font-size:12.5px; color:var(--ink);
  background:var(--card); border:1px solid var(--line); border-radius:8px; padding:8px 10px;
}
.inp:focus{outline:none; border-color:var(--teal); box-shadow:var(--ring)}

/* ---- Engine filter (Citation Center) ---- */
.filterbar{display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:12px}
.filterbar .fb-lbl{font-size:11.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted-2); margin-right:2px}
.efilt{display:inline-flex; align-items:center; gap:6px; font:inherit; font-size:13px; font-weight:600;
  color:var(--muted); background:var(--surface); border:1px solid var(--line); border-radius:999px;
  padding:6px 13px; cursor:pointer; transition:border-color .12s, color .12s, background .12s}
.efilt:hover{border-color:var(--teal); color:var(--ink)}
.efilt .pilln{background:var(--line-2); color:var(--muted); font-size:11.5px; padding:1px 7px; border-radius:999px}
.efilt.on{background:var(--teal-tint); border-color:var(--teal); color:var(--teal-600)}
.efilt.on .pilln{background:var(--teal); color:#fff}
.efilt:focus-visible{outline:none; box-shadow:0 0 0 3px var(--teal-tint)}

/* ---- Suggested prompts (Prompt Explorer) ---- */
.idea{display:flex; gap:14px; align-items:flex-start; border:1px solid var(--line);
  border-radius:12px; padding:13px 15px; background:var(--surface)}
.idea-main{flex:1; min-width:0}
.idea-q{font-weight:600; font-size:14.5px; color:var(--ink); line-height:1.4}
.idea-ev{margin-top:9px; padding:9px 12px; border-left:3px solid var(--teal);
  background:var(--teal-tint); border-radius:0 8px 8px 0; font-size:13px;
  color:var(--ink); line-height:1.55}
.idea-act{display:flex; gap:7px; flex:none; padding-top:2px}
@media(max-width:700px){ .idea{flex-direction:column} .idea-act{width:100%} }

/* ---- Toggleable chart legend (AI Visibility history) ---- */
.lgi{cursor:pointer; user-select:none; transition:opacity .12s}
.lgi:not(.on){opacity:.38}
.lgi:not(.on) b, .lgi:not(.on) .mini{color:var(--muted-2) !important}
.lgi:focus-visible{outline:none; box-shadow:0 0 0 3px var(--teal-tint); border-radius:6px}

/* ---- Visibility projection (AI Visibility) ---- */
.axis-x .fut{color:var(--muted-2); opacity:.6; font-style:italic}
.fc-box{margin-top:14px; padding:12px 14px; border:1px dashed var(--line);
  border-radius:11px; background:var(--surface-2)}
.fc-h{display:flex; align-items:center; gap:9px; flex-wrap:wrap; font-size:13px; margin-bottom:9px}
.fc-rows{display:flex; flex-direction:column; gap:6px}
.fc-row{display:flex; align-items:center; gap:9px; flex-wrap:wrap; font-size:13px}
.fc-row .fc-n{font-weight:600; color:var(--ink); min-width:82px}
.fc-off [data-forecast]{display:none}

/* ---- Grow (client-facing suggestions) ---- */
.grow-list{display:flex; flex-direction:column; gap:14px}
.grow-card{border-left:3px solid var(--teal)}
.gc-h{display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap}
.gc-t{font-family:'Unbounded',sans-serif; font-size:17px; font-weight:600;
  margin:0 0 7px; color:var(--ink); display:inline-block; margin-right:9px}
.gc-pitch{margin-top:10px; font-size:14px; line-height:1.6; color:var(--ink); max-width:74ch}
.gc-ev{margin-top:14px; padding:12px 14px; border-radius:10px; background:var(--surface-2);
  border:1px solid var(--line)}
.gc-ev-h{font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted-2); margin-bottom:9px}
.gc-ev-row{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; padding:4px 0; font-size:13px}
.gc-ev-l{color:var(--muted); min-width:200px; flex:1}
.gc-ev-v{color:var(--ink); font-weight:600}
.gc-foot{margin-top:15px; padding-top:13px; border-top:1px solid var(--line-2);
  display:flex; gap:10px; align-items:center; flex-wrap:wrap}
.gc-form{display:flex; gap:9px; align-items:center; flex:1; flex-wrap:wrap}
.gc-form .input{flex:1; min-width:220px; padding:8px 11px; border:1px solid var(--line);
  border-radius:9px; background:var(--surface); font-size:13px; color:var(--ink); font-family:inherit}
.gc-form .input:focus{outline:none; border-color:var(--teal); box-shadow:0 0 0 3px var(--teal-tint)}
@media(max-width:640px){ .gc-ev-l{min-width:0} }

/* ---- Dashboard pointer to Grow ---- */
.nudge{display:flex; align-items:center; gap:12px; padding:12px 16px; margin-bottom:14px;
  border-left:3px solid var(--teal); background:var(--teal-tint); border-color:var(--teal);
  flex-wrap:wrap; font-size:13.5px}
.nudge-i{color:var(--teal-600); display:flex; flex:none}
.nudge-t{flex:1; min-width:220px; color:var(--ink); line-height:1.5}
.nudge-t b{font-weight:600}
.nudge-t .rr-meta{display:block; margin-top:2px}
.nudge-a{display:flex; gap:7px; flex:none}
@media(max-width:640px){ .nudge-a{width:100%} }

/* ---- Account alerts panel (staff) ------------------------------------------
   Deliberately plain: a monitoring list earns its keep by being scanned in two
   seconds every morning, and anything that draws the eye to one row does so at
   the cost of the others. */
.alert-list{display:flex; flex-direction:column; margin-top:10px}
.alert-row{
  display:flex; align-items:flex-start; gap:10px; justify-content:space-between;
  padding:9px 0; border-bottom:1px solid var(--line);
}
.alert-row:last-child{border-bottom:0}
.alert-name{font-size:12.5px; color:var(--ink); font-weight:600; line-height:1.35}
.alert-sub{font-size:11px; font-weight:500; color:var(--muted); margin-top:2px; line-height:1.4}
.alert-row .chip{flex:none}

/* ---- Traffic overview panels (SEO Performance) ---- */
.ov-strip{display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:1px; background:var(--line);
  border:1px solid var(--line); border-radius:11px; overflow:hidden}
.ov{background:var(--surface); padding:11px 13px}
.ovl{font-size:11.5px; color:var(--muted); margin-bottom:5px}
.ovv{font-size:19px; font-weight:600; color:var(--ink); display:flex; align-items:center; gap:6px; flex-wrap:wrap}
.ov.pos-b{background:var(--pos-tint)}
.donut-pair{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:20px}
.donut-box{min-width:0}
.donut-key{display:flex; flex-direction:column; gap:5px; font-size:12px; color:var(--muted); min-width:0}
.donut-key span{display:flex; align-items:center; gap:7px}
.donut-key b{color:var(--ink); margin-left:auto}
@media(max-width:900px){ .ov-strip{grid-template-columns:repeat(3,minmax(0,1fr))} .donut-pair{grid-template-columns:1fr} }
@media(max-width:560px){ .ov-strip{grid-template-columns:repeat(2,minmax(0,1fr))} }

/* ---- Client assignment on the Team page ---- */
.cl-chips{display:flex; gap:4px; flex-wrap:wrap; max-width:250px}
.cl-pick{display:flex; flex-direction:column; gap:2px; max-height:230px; overflow-y:auto;
  border:1px solid var(--line); border-radius:9px; padding:6px}
.cl-opt{display:flex; align-items:center; gap:8px; padding:5px 7px; border-radius:7px;
  font-size:13px; color:var(--ink); cursor:pointer}
.cl-opt:hover{background:var(--surface-2)}
.cl-opt span{flex:1; min-width:0}
.cl-opt.is-am{cursor:default; opacity:.9}
.cl-opt input[type=checkbox]{flex:none}

/* ---- Position bands, 2x2 in the narrow column ---- */
.pos-grid{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1px; background:var(--line);
  border:1px solid var(--line); border-radius:10px; overflow:hidden}
.pos-grid .pos-b{background:var(--pos-tint); padding:10px 12px}
.pos-grid .pl{font-size:11.5px; color:var(--muted); margin-bottom:3px}
.pos-grid .pv{font-size:20px; font-weight:600; color:var(--ink); line-height:1.1}

/* ---- Donuts and source list in the narrow column ---- */
.side-donut{display:flex; gap:14px; align-items:center; flex-wrap:wrap}
.side-donut .donut-key{flex:1; min-width:130px; font-size:11.5px; gap:4px}
.src-list{display:flex; flex-direction:column}
.src-row{display:flex; align-items:center; gap:9px; padding:7px 0; font-size:13px;
  border-bottom:1px solid var(--line-2)}
.src-row:last-child{border-bottom:0}
.src-row .src-n{flex:1; min-width:0; color:var(--ink); overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap}
.src-row b{color:var(--ink)}

/* ============================================================
   GUIDED TOUR
   The veil is four panels rather than one, so the highlighted
   element stays clickable — somebody being shown a control
   should be able to try it.
   ============================================================ */
.tour-root{position:fixed; inset:0; z-index:9000; pointer-events:none; display:none}
.tour-root.on{display:block}
/* Four panels around the highlight rather than one sheet with a shadow: the
   dark area swallows stray clicks, and the thing being explained is genuinely
   clickable, so somebody being shown a control can try it. */
.tour-veil{position:absolute; background:rgba(11,20,38,.55); pointer-events:auto;
  transition:top .18s ease, left .18s ease, width .18s ease, height .18s ease}
.tour-hole{position:absolute; border-radius:12px; pointer-events:none;
  box-shadow:0 0 0 2px var(--teal), 0 0 0 7px rgba(118,22,244,.20);
  transition:top .18s ease, left .18s ease, width .18s ease, height .18s ease}
.tour-pop{position:absolute; pointer-events:auto; background:var(--surface);
  border:1px solid var(--line); border-radius:14px; padding:16px 17px 13px;
  box-shadow:0 18px 50px rgba(11,20,38,.28); max-width:400px}
/* The chapter on the left, the count on the right: a thirty-step tour reads as
   a set of short ones rather than one long one. */
.tour-step{display:flex; align-items:baseline; justify-content:space-between; gap:12px;
  font-size:10.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--teal-600); margin:0 22px 7px 0}
.tour-count{color:var(--muted-2); font-weight:600; flex:none}
.tour-title{font-family:'Unbounded',sans-serif; font-size:16px; font-weight:600;
  margin:0 0 7px; color:var(--ink)}
.tour-body{margin:0; font-size:13.5px; line-height:1.62; color:var(--muted)}
/* The "so what do I do here" detail, kept out of the paragraph so the paragraph
   stays one readable thought. */
.tour-pts{margin:10px 0 0; padding:0; list-style:none; display:flex; flex-direction:column; gap:6px}
.tour-pts li{position:relative; padding-left:15px; font-size:12.5px; line-height:1.55; color:var(--muted-2)}
.tour-pts li::before{content:''; position:absolute; left:2px; top:7px; width:5px; height:5px;
  border-radius:50%; background:var(--teal)}
.tour-prog{height:3px; border-radius:3px; background:var(--line-2); margin:14px 0 0; overflow:hidden}
.tour-prog span{display:block; height:100%; width:0; border-radius:3px; background:var(--teal);
  transition:width .22s ease}
.tour-act{display:flex; align-items:center; gap:8px; margin-top:11px}
/* "Start over" is deliberately quiet: it is the rare choice, and it throws away
   the place you just spent twenty minutes reaching. */
.tour-restart{border-color:transparent; color:var(--muted-2); box-shadow:none}
.tour-restart:hover{color:var(--ink); border-color:var(--line)}
.tour-hint{margin-top:9px; font-size:10.5px; color:var(--muted-2); text-align:right; letter-spacing:.02em}
.tour-x{width:24px; height:24px; padding:0; line-height:1;
  border:0; border-radius:7px; background:transparent; color:var(--muted-2); font-size:19px;
  cursor:pointer; display:flex; align-items:center; justify-content:center}
.tour-x:hover{background:var(--surface-2); color:var(--ink)}
/* A small pointer at the card's edge, so the link between the card and the
   highlighted thing survives a crowded page. */
.tour-pop::after{content:''; position:absolute; width:10px; height:10px; background:var(--surface);
  border:1px solid var(--line); transform:rotate(45deg)}
.tour-pop[data-side="bottom"]::after{top:-6px; left:26px; border-right:0; border-bottom:0}
.tour-pop[data-side="top"]::after{bottom:-6px; left:26px; border-left:0; border-top:0}
.tour-pop[data-side="right"]::after{left:-6px; top:26px; border-right:0; border-top:0}
.tour-pop[data-side="left"]::after{right:-6px; top:26px; border-left:0; border-bottom:0}
.tour-pop:not([data-side])::after,.tour-pop[data-side=""]::after{display:none}
@media(max-width:640px){ .tour-pop{max-width:calc(100vw - 24px)} .tour-hint{display:none} }

/* ---- COACH MODE (the staff tour) --------------------------------------
   No veil at all. The staff tour teaches how to set a client up, which means
   typing into the fields it points at — an overlay that greys the page and
   swallows clicks makes the lesson impossible to follow. So the page stays
   completely live: the step is a card docked out of the way, and the thing
   being described just gets a ring. */
.tour-coach{pointer-events:none}          /* the page underneath stays clickable */
.tour-coach .tour-pop,.tour-coach .tour-pill{pointer-events:auto}
.tour-coach .tour-hole{box-shadow:0 0 0 2px var(--teal), 0 0 0 6px rgba(118,22,244,.16)}
/* Docked, and it stays docked. A card that jumps around between steps is a card
   you have to find again every time. */
.tour-coach .tour-pop{position:fixed; right:20px; bottom:20px; top:auto; left:auto;
  width:370px; max-width:calc(100vw - 32px); max-height:calc(100vh - 40px); overflow:auto}
.tour-coach .tour-pop::after{display:none}
.tour-coach.min .tour-pop{display:none}
/* Minimised: still obviously running, one click from resuming. */
.tour-pill{position:fixed; right:20px; bottom:20px; z-index:9001; pointer-events:auto;
  padding:9px 14px; border-radius:100px; border:1px solid var(--line); cursor:pointer;
  background:var(--fill-strong); color:var(--on-fill-strong); font-size:12px; font-weight:600; font-family:inherit;
  box-shadow:0 10px 26px rgba(11,20,38,.24)}
.tour-pill:hover{filter:brightness(1.08)}
.tour-tools{position:absolute; top:8px; right:9px; display:flex; gap:2px}
.tour-min{width:24px; height:24px; padding:0; line-height:1; border:0; border-radius:7px;
  background:transparent; color:var(--muted-2); font-size:17px; cursor:pointer;
  display:flex; align-items:center; justify-content:center}
.tour-min:hover{background:var(--surface-2); color:var(--ink)}
/* The one instruction for this step, when it has one. */
.tour-do{margin-top:11px; padding:9px 11px; border-radius:9px; background:var(--teal-tint);
  border:1px solid rgba(118,22,244,.28); font-size:12.5px; line-height:1.55; color:var(--ink); font-weight:600}
@media(max-width:640px){
  .tour-coach .tour-pop{right:10px; left:10px; bottom:10px; width:auto}
  .tour-pill{right:10px; bottom:10px}
}


/* `hidden` is only a default in the UA stylesheet, so any display rule of ours
   beats it. Module-conditional panels rely on it, so it has to actually win. */
[hidden]{display:none !important}

/* The note under a settings field: what the value is and what goes wrong if it
   is not. Quiet enough to skip when you already know. */
.fnote{margin-top:6px; font-size:11.5px; line-height:1.6; color:var(--muted)}
.fnote .code{font-size:11px}

/* Sources under a Brand Health answer — the pages the engine cited for it.
   Quiet by design: evidence you can check, not a second headline competing
   with the sentence it supports. */
.fsrc{margin-top:9px; padding-top:8px; border-top:1px solid var(--line-2)}
.fsrc-h{font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--muted-2); margin-bottom:5px}
.fsrc-h .rr-meta{font-weight:500; letter-spacing:0; text-transform:none}
.fsrc-l{margin:0; padding-left:18px; display:flex; flex-direction:column; gap:3px}
.fsrc-l li{font-size:11.5px; line-height:1.5; color:var(--muted-2)}
.fsrc-l a{color:var(--teal-600); text-decoration:none; word-break:break-word}
.fsrc-l a:hover{text-decoration:underline}
.fsrc-l .rr-meta{margin-left:6px}
.fsrc-ask{display:inline-block; margin-top:7px; font-size:11.5px; font-weight:600;
  color:var(--teal-600); text-decoration:none}
.fsrc-ask:hover{text-decoration:underline}
/* An unsourced answer, after we asked twice for the sources. Amber rather than
   red: the claim is not necessarily wrong, it is unevidenced, and those are
   different things. It sits left-barred so the eye reads it as attached to the
   answer above it rather than as a note about the page. */
.fsrc-none{font-size:11.5px; line-height:1.55; color:var(--muted-2);
  border-left:2px solid var(--warn); padding:2px 0 2px 8px}

/* ============================================================
   GRANT GOOGLE ACCESS
   The banner above Connections. Every field in that card points
   at a Google property, and none of them do anything until
   somebody on the client's side lets us read it — so the step
   that blocks all the others goes first, and turns quiet once
   both properties are actually resolving.
   ============================================================ */
.gaccess{border-color:rgba(232,161,58,.45); background:var(--warn-tint); margin-bottom:16px}
.gaccess.done{border-color:rgba(76,122,36,.35); background:var(--green-tint)}
.ga-head{display:flex; gap:12px; align-items:flex-start}
.ga-ico{flex:none; width:32px; height:32px; border-radius:9px; display:flex; align-items:center;
  justify-content:center; background:var(--surface); color:#B5791E; box-shadow:var(--shadow)}
.gaccess.done .ga-ico{color:#4c7a24}
/* #4c7a24 is 5.1:1 on white and 3.4:1 on the dark card — still over the 3:1 a
   non-text graphic needs, but only just, and this is the tick that tells someone
   their Google connection is alive. --green is the same brand hue already tuned
   per theme, so it follows the page instead of being re-picked by hand. */
:root[data-theme="dark"] .gaccess.done .ga-ico{color:var(--green)}
.ga-ico svg{width:17px; height:17px}
.ga-t{font-size:14px; font-weight:600; color:var(--ink)}
.ga-s{font-size:12.5px; line-height:1.6; color:var(--muted); margin-top:3px}
/* The address, given room and a copy button: transcribing it by hand is where
   this goes wrong, and Google accepts a wrong address without complaint. */
.ga-addr{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:14px 0 4px;
  padding:10px 12px; background:var(--surface); border:1px solid var(--line); border-radius:10px}
.ga-addr code{flex:1; min-width:200px; font-size:12.5px; color:var(--ink); word-break:break-all; user-select:all}
.ga-steps{display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:12px; margin-top:14px}
@media (max-width:820px){ .ga-steps{grid-template-columns:1fr} }
.ga-step{display:flex; gap:11px; padding:13px 14px; background:var(--surface);
  border:1px solid var(--line); border-radius:var(--r)}
.ga-step.ok{border-color:rgba(76,122,36,.35)}
.ga-n{flex:none; width:22px; height:22px; border-radius:50%; background:var(--fill-strong); color:var(--on-fill-strong);
  font-size:11.5px; font-weight:700; display:flex; align-items:center; justify-content:center}
.ga-step.ok .ga-n{background:#4c7a24}
.ga-st{font-size:13px; font-weight:600; color:var(--ink); margin-bottom:4px}
.ga-sb{font-size:12px; line-height:1.6; color:var(--muted)}
.ga-step .btn{margin-top:9px}
.ga-foot{margin-top:12px}

/* Field label with the explanation behind an info icon.
   Printed under every input, that guidance turned a form of eight fields into
   three screens of grey text — and text nobody reads explains nothing. It is
   still one keystroke or hover away, and the form is scannable for the nine
   visits out of ten where you already know what you came to change. */
.flbl{display:flex; align-items:center; gap:6px; margin-bottom:6px}
.flbl>label{font-size:11.5px; font-weight:600; color:var(--muted); display:block}
/* Same row used for a block heading, where the icon shouldn't inherit the
   heading's size or weight. */
.section-title.flbl{margin-bottom:0}
.section-title.flbl .ihelp,.flbl .rr-lbl+.ihelp{color:var(--muted-2)}
.ihelp{position:relative; display:inline-flex; align-items:center; color:var(--muted-2);
  cursor:help; border-radius:50%; outline:none}
.ihelp>svg{width:13px; height:13px; display:block}
.ihelp:hover,.ihelp:focus-visible{color:var(--teal-600)}
.ihelp:focus-visible{box-shadow:var(--ring)}
.ihelp-pop{
  position:absolute; left:-8px; top:calc(100% + 7px); z-index:70; width:290px; max-width:70vw;
  background:var(--surface); border:1px solid var(--line); border-radius:11px;
  box-shadow:var(--shadow-lg); padding:11px 12px;
  font-size:11.5px; line-height:1.6; font-weight:400; color:var(--muted); text-transform:none;
  letter-spacing:0; white-space:normal;
  /* `display:none`, NOT `visibility:hidden`.
     A visibility-hidden box still generates a box and still counts toward the
     document's scrollable overflow. This one is 290px wide and anchored to the
     LEFT of its icon, so every ⓘ silently reserved 290px of page to its right.
     On the dashboard the rightmost KPI label put ~150px of that past the page
     edge — the document gained horizontal scroll, and because the sidebar is
     position:sticky it slid out from under the content and showed as a gap.
     Nobody was hovering anything; the tooltip did it while hidden.
     The fade is gone with it. A tooltip that appears instantly is a fair trade
     for a layout that cannot be pushed sideways by a hidden element. */
  display:none;
}
.ihelp:hover .ihelp-pop,.ihelp:focus .ihelp-pop,.ihelp:focus-within .ihelp-pop{display:block}
.ihelp-pop b:first-child{display:block; color:var(--ink); font-size:12px; font-weight:600; margin-bottom:5px}
.ihelp-pop .code{font-size:10.5px}
/* And while it IS shown it must not hang off the right edge either — that would
   reintroduce the same scrollbar on hover. Overflow to the LEFT of the origin
   does not extend scrollWidth in LTR, so right-anchoring is the safe direction.

   `.ihelp-flip` is set by js/tooltip.js, which measures where the icon actually
   is. The list below it is the no-JS fallback and the reason this rule needed
   rewriting: it named the containers a tooltip might sit in — .kpis, .strip,
   .breakdown — and when the dashboard's KPI cards moved into `.bento` the list
   was not updated, so the rightmost card's popup ran off the screen. Naming
   every place a thing can appear is a list that goes stale the first time
   anything moves; measuring does not. */
.ihelp.ihelp-flip .ihelp-pop{left:auto; right:-8px}

.grid.g-2 > div:nth-child(even) .ihelp-pop,
.kpis   > *:nth-last-child(-n+2) .ihelp-pop,
.ov-strip > *:nth-last-child(-n+2) .ihelp-pop,
.strip  > *:nth-last-child(-n+2) .ihelp-pop,
.bento  > *:nth-last-child(-n+2) .ihelp-pop,
.breakdown > *:nth-last-child(-n+2) .ihelp-pop{left:auto; right:-8px}
@media (max-width:760px){
  .grid.g-2 > div:nth-child(even) .ihelp-pop,
  .kpis > *:nth-last-child(-n+2) .ihelp-pop,
  .ov-strip > *:nth-last-child(-n+2) .ihelp-pop,
  .strip > *:nth-last-child(-n+2) .ihelp-pop,
  .bento > *:nth-last-child(-n+2) .ihelp-pop,
  .breakdown > *:nth-last-child(-n+2) .ihelp-pop{left:-8px; right:auto}
  /* The measured flip still wins on a narrow screen, because on a phone the
     popup is 70vw and the icon's position decides it, not the grid. */
  .ihelp.ihelp-flip .ihelp-pop{left:auto; right:-8px}
}

/* "copy prompt" — a utility beside a question heading, not a call to action.
   Muted until hover so the eye lands on the engine's answer, which is the thing
   worth reading. It is a real <button>, so it is reachable by keyboard; the
   focus ring is not dimmed with it, because a control you cannot see while
   tabbing to it is not quiet, it is missing. */
.copy-prompt {
  border: 0; background: none; cursor: pointer;
  /* A 26px box around a 14px glyph. The icon is quiet; the thing you have to
     hit with a thumb is not, and shrinking the target to match the ink is how
     quiet controls become unusable ones. */
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; margin: -4px 0; padding: 0;
  flex: none; vertical-align: middle;
  color: var(--muted-2); opacity: .45;
  transition: opacity .12s ease, color .12s ease;
}
.copy-prompt:hover { opacity: 1; color: var(--teal-600); text-decoration: underline; }
.copy-prompt:focus-visible { opacity: 1; color: var(--teal-600); outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 3px; }


/* ============================================================================
   THE TWO SHAPES FROM voctos.com
   Measured element by element in a browser — see previews/brand-persona.md.
   ========================================================================= */

/* ---------- 1 · FOLDER TILE ----------
   A switcher, and only a switcher. The top-right corner is SQUARE while the
   other three are 10px, and a 48×10 bar rides 9px above it in the same colour.
   That pairing is the whole effect; round the corner and it is a plain card.
   The tab means "there are others behind this one", so using it for a static
   figure is a promise the page does not keep. */
.vtiles{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));column-gap:24px;row-gap:24px;padding-top:15px}

/* The tile is label-led. The first build put a 46px outline drawing in the
   middle of each one, which made six mediocre line illustrations the loudest
   thing on the page and left the actual destination — the words — small and
   underneath. A person scanning this row is looking for "Paid Media", not for a
   picture of a slider. So the icon shrinks into a plate in the corner, the
   label moves down-left and grows, and the arrow keeps the opposite corner. */
.vtile{position:relative;background:var(--vt);color:var(--vt-fg,#fff);
  border-radius:12px 0 12px 12px;padding:16px 18px 18px;min-height:126px;
  box-shadow:var(--tile-shadow);display:flex;flex-direction:column;
  align-items:flex-start;justify-content:space-between;
  transition:transform .18s cubic-bezier(.2,.6,.2,1), box-shadow .18s ease}
.vtile:hover{transform:translateY(-3px);box-shadow:0 18px 28px -8px rgba(13,23,45,.28)}
/* The tab is the piece that sticks out above the box, and it is where the arrow
   goes — a folder's tab is what you reach for. It was an 11px sliver with the
   arrow floating separately inside the tile, so the tile read as "a box with a
   decoration on top" rather than as one shape. The tab is now tall enough to
   hold the arrow, and the arrow lives in it. */
.vtile .vt-tab{position:absolute; right:0; top:-14px; width:38px; height:15px;
  background:var(--vt); border-radius:5px 12px 0 0;
  display:flex; align-items:center; justify-content:center; padding-bottom:1px}
.vtile .vt-arw{width:10px; height:10px; opacity:.8; transition:opacity .16s ease, transform .16s ease}
.vtile:hover .vt-arw{opacity:1; transform:translate(1px,-1px)}
/* The white tile's tab needs its own edge, since the tile's ring is an inset
   shadow and stops at the tile. */
.vt-white .vt-tab{box-shadow:inset 0 1px 0 rgba(13,23,45,.08), inset -1px 0 0 rgba(13,23,45,.08), inset 1px 0 0 rgba(13,23,45,.08)}

/* the plate, not the picture */
.vtile .vt-ic{width:34px;height:34px;border-radius:9px;flex:none;
  display:flex;align-items:center;justify-content:center;
  background:var(--vt-pl,rgba(255,255,255,.18))}
.vtile .vt-ic svg{width:19px;height:19px;display:block}

.vtile .vt-lb{font-family:'Unbounded',sans-serif;font-weight:400;font-size:15.5px;
  line-height:1.22;letter-spacing:-.018em;text-align:left}
.vtile .vt-lb .vt-w{display:block;font-family:'Inter',sans-serif;font-size:11.5px;
  font-weight:500;letter-spacing:0;opacity:.8;margin-top:5px}

.vt-blue{--vt:#2563EB} .vt-navy{--vt:#0F172A} .vt-slate{--vt:#64748B}
.vt-violet{--vt:#7616F4}
.vt-yellow{--vt:#FDE047;--vt-fg:#0D172D;--vt-pl:rgba(13,23,45,.13)}
.vt-white{--vt:#FFFFFF;--vt-fg:#0D172D;--vt-pl:rgba(13,23,45,.07);
  box-shadow:var(--tile-shadow),inset 0 0 0 1px rgba(13,23,45,.08)}


/* ============================================================
   THE BENTO LAYOUT
   ============================================================

   Cells of deliberately different sizes, on a 12-column grid.

   WHY THIS AND NOT EQUAL ROWS. Size is the cheapest hierarchy available and
   equal rows throw it away. The dashboard used to open with eleven figures of
   identical weight and no answer to "what am I looking at first" — the reader
   had to work out the priority the page already knew. Here the most important
   thing is physically the biggest, and the order you read it in is the order it
   matters in.

   It composes with the four shapes rather than replacing them: a `.vdark` or a
   `.card` drops into a cell unchanged, and the cell only decides how wide it is.
*/
.bento{display:grid; grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:24px; row-gap:16px}
.bento > *{min-width:0}
/* ALL twelve spans exist. `.b1` and `.b2` were missing, and a cell asking for a
   class that does not exist does not fail — it silently takes the grid's default
   of one column. Six boxes asking for `b2` each rendered one column wide and the
   row stopped at half the page, which looked like a layout choice rather than a
   typo. Declare the whole set so no span can be "almost right".

   Scoped to `.bento >` on purpose: `b1`–`b5` are also the login page's decorative
   bubbles (`class="bub b1"`), which are absolutely positioned and are not grid
   items. Unscoped, these rules landed on them too. A span class only means
   anything inside the grid, so that is where it is allowed to apply. */
.bento > .b1{grid-column:span 1} .bento > .b2{grid-column:span 2} .bento > .b3{grid-column:span 3}
.bento > .b4{grid-column:span 4} .bento > .b5{grid-column:span 5} .bento > .b6{grid-column:span 6}
.bento > .b7{grid-column:span 7} .bento > .b8{grid-column:span 8} .bento > .b9{grid-column:span 9}
.bento > .b10{grid-column:span 10} .bento > .b11{grid-column:span 11} .bento > .b12{grid-column:span 12}
/* Cells stretch to their row, so anything that wants to sit at the bottom needs
   a column context. Without this a card with a footer action grew a hole in its
   middle instead of ending with one. */
.bento > .card, .bento > .vdark{display:flex; flex-direction:column}

/* ---------- THE CLAIM ----------
   The page opens with a sentence, not a label.

   The site's own voice is claim-led — "Your Customers Ask AI First. Be the
   Answer." A portal that opens with "AI Visibility Score / 67" is a
   spreadsheet with a logo on it. Same number; it now says something.

   Unbounded 400. The site's headings are LIGHT at large sizes, and bold large
   type is the fastest way to stop looking like this brand. */
/* THE PAGE HEADLINE — the size it always was.
 *
 * What changed here is the CONTENT, not the type: it used to be a sentence built
 * from the data ("You are the answer in 60% of AI conversations") and it is now
 * the page's name. That was the whole request. I also shrank it to 20px to match
 * Semrush's ProductHead spec, which was never asked for — their header is small
 * because their layout is built around it, and lifting one measurement out of
 * someone else's system does not carry the reasons that made it right there. */
.claim{font-family:'Unbounded',sans-serif; font-size:38px; font-weight:400;
  letter-spacing:-.028em; line-height:1.16; margin:10px 0 0}
.claim-sub{color:var(--muted); font-size:15px; margin:14px 0 0}

/* THE SENTENCE THAT USED TO BE THE HEADLINE.
   Every page opened with its own figure set in 38px Unbounded — "You are the
   answer in 60% of AI conversations" — so the largest words on the page changed
   with the data and the page never announced what it was. The heading is now the
   page's name; the figure moved down here, where a number belongs. Still prose,
   deliberately: it is a standing fact, not an alert, and a box around every one
   of them would leave forty-one pages of permanent notices. */
/* The Info row: 14-15px, PRIMARY text, directly under the title. This is where
   the figure went when it stopped being the headline. */
.claim-lede{font-size:17px; line-height:1.5; color:var(--ink); margin:13px 0 0; font-weight:400}
/* The lime stroke from the website, spent ONCE and now spent on the figure
   itself rather than on a word in a 38px sentence. Same rule as before: used
   twice on a page it is decoration, used once it is emphasis. */
.claim-lede em{font-style:normal; font-weight:600; position:relative; white-space:nowrap; color:var(--ink)}
.claim-lede em::after{content:''; position:absolute; left:0; right:0; bottom:-.12em;
  height:.14em; border-radius:99px;
  background:linear-gradient(90deg,#7B9412,#D4EE69 44%,#7B9412)}
/* Their guidance for text that supports a heading: --text-secondary. */
.claim-lede + .claim-sub{margin-top:8px}

/* THE PAGE HEADER RUNS THE FULL WIDTH OF ITS ROW.
   These four carried a character-count ceiling (21ch, 64ch, 70ch, 72ch) — the
   usual typographic measure, which keeps a line short enough for the eye to
   find the start of the next one. On this layout it was costing more than it
   bought: the content column is around 1500px, so a 70ch lede wrapped at
   roughly half of it and left the header looking like a narrow column pasted
   onto a wide page, with the fold landing mid-sentence. The cap is gone and the
   grid governs the width instead. Worth knowing if a page ever grows a genuinely
   long sub-line: the measure limit was doing a real job, and the place to put it
   back is on that page, not on all forty-one. */

/* THE UPDATE BOX under a page headline — see partials/claim-alert.ejs.
   Only used where the sub-line reports a CHANGE. The colour comes from the
   caller because the box frames a number that already has a direction, and a
   frame that disagrees with its contents is worse than no frame. */
.claim-alert{display:flex; align-items:flex-start; gap:11px; margin:15px 0 0;
  padding:12px 15px 12px 13px; border:1px solid var(--line); border-left-width:3px;
  border-radius:11px; background:var(--surface); color:var(--ink);
  font-size:14.5px; line-height:1.5; box-shadow:var(--shadow-sm, 0 1px 2px rgba(13,23,45,.05))}
.claim-alert .ca-i{flex:none; margin-top:1px}
.claim-alert.pos {border-left-color:var(--pos);  background:var(--pos-tint)}
.claim-alert.pos  .ca-i{color:var(--pos)}
.claim-alert.neg {border-left-color:var(--neg);  background:var(--neg-tint)}
.claim-alert.neg  .ca-i{color:var(--neg)}
.claim-alert.warn{border-left-color:var(--warn); background:var(--warn-tint)}
.claim-alert.warn .ca-i{color:#B5791E}
.claim-alert.info{border-left-color:var(--teal); background:var(--teal-tint)}
.claim-alert.info .ca-i{color:var(--teal-600)}
/* The dark panels invert the tint, and #B5791E on a dark amber wash is unreadable. */
:is(.vnotch,.vdark) .claim-alert.warn .ca-i{color:var(--warn)}

/* ---------- the hero's orbit ----------
   voctos.com's own illustration language: flat ellipse strokes in lime and
   violet, no fill, no glow. Drawn in the markup rather than linked so it
   inherits the panel and needs no asset request. It is decoration, so it is
   inert to the pointer and invisible to a screen reader. */
/* `.vdark .vd-orbit`, not `.vd-orbit`.
 *
 * The panel sets `.vdark > *{position:relative}` to lift its content above the
 * dotted texture. That is one class, and so is this — equal specificity, so
 * source order decided it, and since the orbit is declared first the panel's
 * rule won and turned it back into a relatively-positioned element. It dropped
 * into normal flow, took 400px of height at the top of the panel, and pushed
 * the score underneath it: the hero rendered as a giant drawing with the number
 * hidden below the fold.
 *
 * Qualifying with the parent makes this (0,2,0) and settles it wherever either
 * rule sits in the file. Exactly the trap that cost an afternoon on --sb-w. */
.vdark .vd-orbit{position:absolute; right:-70px; top:50%; transform:translateY(-50%);
  width:400px; height:400px; pointer-events:none}
@media(max-width:1080px){.vdark .vd-orbit{opacity:.34; right:-150px}}

/* ---------- the score ring ----------
   A number out of 100 gets a shape that is also out of 100. A bare figure makes
   the reader supply the scale from memory. */
.vring{width:132px; height:132px; flex:none}
.vring-t{font-family:'Unbounded',sans-serif; font-size:31px; font-weight:400}
.vsig{display:flex; flex-direction:column; gap:11px; flex:1; min-width:0}
.vsig-r{display:grid; grid-template-columns:minmax(0,1fr) 44px; align-items:center;
  gap:8px; font-size:12.5px}
.vsig-r .n{color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.vsig-r .v{font-family:'Unbounded',sans-serif; font-size:13px; text-align:right}
.vsig-r .b{height:5px; border-radius:99px; background:var(--surface-2);
  overflow:hidden; grid-column:1/-1}
.vsig-r .b i{display:block; height:100%; border-radius:99px; background:var(--teal)}

/* ---------- a cell's own sparkline ----------
   Pushed to the bottom of its cell so a row of these lines up regardless of how
   much text sits above each one. */
.vspark{width:100%; height:44px; display:block; margin-top:auto; padding-top:12px}

/* ---------- THE ACTION BAR ----------
   The row of things a client can DO, and the one thing that is coming, together
   at the top.

   WHY THEY BELONG IN THE SAME STRIP. Everything below this line answers "how am
   I doing"; this line is the only part of the page that answers "what can I do"
   and "what is next". Splitting them — the buttons at the top and the meeting
   four scrolls down among the readings — meant the two questions a client
   actually arrives with were the two furthest apart on the page.

   Every item is conditional. A bar that always shows five buttons regardless of
   whether there is anything to approve or pay is a menu, and people stop reading
   menus. When there is nothing to do and nothing booked, this does not render. */
/* ONE ROW. The meeting used to drop to a second line, which made the bar look
   like two unrelated strips and pushed the page's first real figure further down.
   It wrapped because both this and `.qbar-acts` were `flex-wrap:wrap`, so the
   buttons kept their full max-content width and shoved the meeting off the end.
   Nothing here wraps now; the labels give way instead (see `.qact span`), which
   is the right thing to lose first — a truncated word is still a findable
   button, a button on the next line is a different layout. */
.qbar{display:flex; align-items:center; gap:12px; flex-wrap:nowrap;
  margin:22px 0 0; padding:14px 18px; background:var(--surface);
  border-radius:var(--r-lg)}
.qbar-acts{display:flex; align-items:center; gap:9px; flex-wrap:nowrap;
  flex:0 1 auto; min-width:0}
.qact{display:inline-flex; align-items:center; gap:8px; padding:10px 15px;
  border-radius:10px; border:1px solid var(--line); background:var(--surface);
  font-size:13.5px; font-weight:500; color:var(--ink); white-space:nowrap;
  /* A shadow, so a button reads as a raised thing rather than an outlined area
     of the same card it sits on. On a dark ground a drop shadow cannot be seen
     at all, so --btn-shadow carries a hairline of light there instead — same
     job, the only technique that works on that background. */
  box-shadow:var(--btn-shadow);
  min-width:0;
  transition:border-color .14s ease, background .14s ease, box-shadow .14s ease}
.qact:hover{border-color:var(--ink); box-shadow:var(--btn-shadow-hover)}
/* The part that gives way when the row runs out of room. `min-width:0` is the
   half people forget: without it a flex item will not shrink below its content
   and the ellipsis never appears. */
.qact span{overflow:hidden; text-overflow:ellipsis; min-width:0}
.qact svg{color:var(--muted-2); flex:none}
.qact:hover svg{color:var(--ink)}
/* The count is the reason the button is there, so it is part of the label
   rather than a badge stuck on the corner. */
.qact b{font-weight:700; color:var(--teal-600)}
/* `qact-lead`, NOT `pri`. `.pri` already exists in this stylesheet as the
   priority badge — 10.5px, uppercase, pill — so a button carrying it came out
   as a tiny shouting label. A class name is part of the vocabulary; reusing one
   that already means something else is how two unrelated components end up
   fighting over the same three letters. */
.qact.qact-lead{background:var(--fill-strong); border-color:var(--fill-strong); color:var(--on-fill-strong)}
/* The icon rides the same pair. A fixed white-at-70% was fine while the fill was
   always navy; against the inverted near-white pill it disappears entirely.
   `currentColor` at 72% follows whichever way the pair points. */
.qact.qact-lead svg{color:color-mix(in srgb, var(--on-fill-strong) 72%, transparent)}
/* `--ink` is the TEXT colour, and in dark mode it is near-white — the same value
   the fill already has, so the hover did nothing there and inverted the button in
   light mode by coincidence rather than by decision. The hover belongs to the
   same pair as the fill. */
.qact.qact-lead:hover{background:var(--fill-strong-hover); border-color:var(--fill-strong-hover)}

/* The meeting sits at the far end: it is the only item here that is not a
   button, and the separation keeps "what's booked" from reading as a sixth
   thing to click. */
/* `flex:none` on the meeting, so the buttons are what compress. The meeting is
   a date, a title and two controls — none of which survives being squeezed the
   way a button label does. */
.qmeet{display:flex; align-items:center; gap:13px; margin-left:auto; flex:none; min-width:0; max-width:46%;
  position:relative; border-radius:12px; padding:4px 6px; margin-right:-6px}
.qmeet:hover{background:var(--surface-2)}
.qmeet-b{display:block; min-width:0; color:inherit; text-decoration:none}
/* The actions, revealed on hover or keyboard focus. NOT display:none until then
   — a hidden box that still takes part in layout is what made .ihelp-pop widen
   this app's pages; see the note on that rule. This one is display:none and is
   only laid out once it is shown. */
/* Opens UPWARD. Downward it landed on top of the notched panel below the bar —
   over its bite and its headline — so revealing two buttons obscured the one
   thing on the page asking to be clicked. Above the bar there is only the
   header, which is text. */
.qmeet-acts{display:none; position:absolute; right:0; bottom:calc(100% + 6px); z-index:12;
  gap:7px; padding:8px; background:var(--surface); border:1px solid var(--line);
  border-radius:12px; box-shadow:var(--shadow-lg, 0 12px 28px -10px rgba(13,23,45,.25))}
.qmeet:hover .qmeet-acts, .qmeet:focus-within .qmeet-acts{display:flex}
.qmeet .btn{box-shadow:var(--btn-shadow); flex:none}
.qmeet .btn:hover{box-shadow:var(--btn-shadow-hover)}
/* Below this the row genuinely cannot hold both, and an ellipsis on every label
   is worse than a second line. Wrapping comes back rather than the labels
   shrinking to nothing. */
@media(max-width:1180px){
  .qbar{flex-wrap:wrap}
  .qbar-acts{flex-wrap:wrap}
  .qmeet{max-width:100%}
}
.qmeet-d{width:46px; height:46px; border-radius:12px; background:var(--teal-tint);
  color:var(--teal-600); display:flex; flex-direction:column; align-items:center;
  justify-content:center; flex:none; line-height:1.05}
.qmeet-d b{font-family:'Unbounded',sans-serif; font-size:17px; font-weight:400}
.qmeet-d span{font-size:9.5px; font-weight:600; text-transform:uppercase; letter-spacing:.06em}
.qmeet-t{font-size:13.5px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.qmeet-m{font-size:11.5px; color:var(--muted-2)}
@media(max-width:900px){.qmeet{margin-left:0; width:100%; padding-top:12px;
  border-top:1px solid var(--line-2)}}

/* ---------- the shelf, as separate boxes ----------
   Six counts that used to be cells inside one card, divided by a 6%-opacity
   hairline. That line is enough to separate two words and not enough to separate
   two numbers, so the row read as a single slab. As separate cards the grid's own
   gap does the separating and no divider is needed at all — which is how the rest
   of this layout already works.

   Quieter than the readings above them on purpose: smaller figure, less padding.
   Same size for all six would make the page a wall again. */
/* Roomier than the first attempt, and quieter. These are the least important
   readings on the page, so they should be the calmest thing on it — cramped
   boxes with saturated fills were doing the opposite and pulling the eye down
   past the numbers that matter. */
.shelf-c{padding:18px 20px}
/* WRAPS, never truncates. Two columns of the grid is ~140px, and at that width
   "Keywords · Top 10" ellipsised to "Keyw…" — six boxes all reading the same
   four letters. Two short lines say what the box is; an ellipsis says nothing.
   A fixed min-height keeps the six figures on one baseline whether their label
   took one line or two. */
/* Same rule as .bd .bl: label left, ⓘ hard right. `align-items:flex-start` and
   not `center`, because these labels can wrap to two lines and a centred icon
   would then float in the middle of a two-line block instead of sitting with
   the first line. */
.shelf-c .cl{font-size:11.5px; color:var(--muted); font-weight:500;
  line-height:1.35; min-height:2.7em;
  display:flex; align-items:flex-start; justify-content:space-between; gap:8px}
.shelf-c .cl .ihelp{flex:none; margin-left:auto; margin-top:1px}
.shelf-c .cv{font-family:'Unbounded',sans-serif; font-size:25px; font-weight:400;
  letter-spacing:-.02em; margin-top:6px; display:flex; align-items:baseline; gap:6px}
/* "·1 due" is a qualifier on the number beside it, so it must not wrap onto its
   own line — split across two rows it reads as a second reading. */
.shelf-c .cv .mini{font-size:11px; font-weight:700; white-space:nowrap}

/* THE TINTS. Derived from the brand, cycled across the row.
 *
 * Six white boxes in a line are a fence. The tints break the run and give the
 * eye somewhere to land, and that is ALL they do — the colour does not encode
 * good, bad, up or down.
 *
 * Which is exactly why none of them is this page's green or red. Those two mean
 * something here, and a decorative green box beside a green "▲ 12" would teach
 * the reader that the colour is a verdict. These are pale enough that ink stays
 * at 14:1 on every one of them, and far enough from --pos/--neg to be read as
 * paper rather than judgement. */
/* Third setting, and the reasoning for the two before it is worth keeping.
   The first was too saturated and pulled the eye down past the numbers that
   matter; the second was so pale it read as six white boxes again. This sits
   between them AND adds a matching edge a shade deeper — the border is what
   actually defines a box at low saturation, so the fill can stay quiet and the
   box still be a box. Small text clears AA on every one (worst is 5.02). */
.shelf-c.sc-violet{background:#F1EBFD; box-shadow:inset 0 0 0 1px #E4D9FA}
.shelf-c.sc-lime  {background:#EFF6DF; box-shadow:inset 0 0 0 1px #DFECC6}
.shelf-c.sc-navy  {background:#E9ECF4; box-shadow:inset 0 0 0 1px #D8DEEB}
.shelf-c.sc-sky   {background:#E6F0FA; box-shadow:inset 0 0 0 1px #D1E3F4}
.shelf-c.sc-sand  {background:#FAEFE0; box-shadow:inset 0 0 0 1px #F0E0C9}
.shelf-c.sc-rose  {background:#FBE9EF; box-shadow:inset 0 0 0 1px #F4D6E0}
/* In dark mode a pale tint is a bright slab. The same six, mixed into the
   surface instead of over white, so they stay a hint rather than a light box. */
/* Retuned against the card surface these actually sit on.
   The previous set was mixed against #121F27 — the surface the duplicate dark
   block was declaring. On the real one they measured about 1.05:1 off the card,
   which is not a tint, it is six boxes you cannot see. These sit at 1.40:1 with
   a hairline a shade lighter again, so the box is a box at a glance, and the
   label still clears AA on every one (worst 4.91:1). */
:root[data-theme="dark"] .shelf-c.sc-violet{background:#393144; box-shadow:inset 0 0 0 1px #544965}
:root[data-theme="dark"] .shelf-c.sc-lime  {background:#303720; box-shadow:inset 0 0 0 1px #4F5A35}
:root[data-theme="dark"] .shelf-c.sc-navy  {background:#2D3449; box-shadow:inset 0 0 0 1px #424C6C}
:root[data-theme="dark"] .shelf-c.sc-sky   {background:#1E3748; box-shadow:inset 0 0 0 1px #2E5570}
:root[data-theme="dark"] .shelf-c.sc-sand  {background:#3D3324; box-shadow:inset 0 0 0 1px #605039}
:root[data-theme="dark"] .shelf-c.sc-rose  {background:#403037; box-shadow:inset 0 0 0 1px #604852}

/* THE ⓘ, AT THE END OF THE ROW — NOT STUCK TO THE TITLE.
   Sitting immediately after the label it reads as part of the wording, as
   though the heading were "SEO Health ⓘ". At the far end it reads as what it
   is: a control belonging to the card, available if you want it. */
.rr-end{display:inline-flex; align-items:center; gap:7px; margin-left:auto; flex:none}

/* ---------- a reading we do not have ----------
   An em-dash set in Unbounded at 40–96px is a thick solid bar. Sitting where a
   figure would be, it does not read as "nothing" — it reads as a redaction, as
   though a number existed and had been covered over. Small and light is what
   an absence actually looks like. */
.noval{font-size:.42em; color:var(--muted-2); letter-spacing:0; vertical-align:.28em}
:is(.vnotch,.vdark) .noval{color:rgba(255,255,255,.45)}

/* ---------- NOT CONNECTED ----------
   Dashed and transparent: visibly a different KIND of thing from a card, not a
   card with a smaller number in it.

   This is the one that matters commercially. "Organic Traffic 0" rendered in the
   same box as "AI Visibility 67" reads to a client as the work not working,
   when the truth is that nobody has connected Analytics. A dash in a dashed
   frame cannot be misread as a measurement. */
/* THE "NOT MEASURED" NOTICE.
 *
 * This used to be transparent with a dashed hairline — deliberately quiet, on
 * the theory that an absence should not shout. The theory was wrong in practice:
 * a client scrolling past a row of confident figures does not stop for grey text
 * on the page colour, so the one panel explaining WHY three numbers are empty was
 * the one panel nobody read. A blank reading that goes unexplained gets read as a
 * zero, which is the exact misunderstanding this block exists to prevent.
 *
 * So it is a solid amber-tinted card with a rule down its leading edge. Amber and
 * not red on purpose: nothing is broken and nobody is at fault, something is just
 * not connected yet. It is the same weight the site gives an invoice that is due
 * — worth acting on, not worth worrying about. */
/* It is a WHITE CARD, not an amber wash.
   The wash was the first attempt: --warn-tint over the page measured 1.08:1 in
   light mode — an amber at 13% over a grey paper is the same luminance as the
   paper, so the "louder" version was still invisible, just differently. Every
   other block on this site separates by being a surface on the paper (1.31:1),
   and that is the mechanism that works here too. The amber then does the one job
   it is good at: marking the edge and the icon, where it sits against the card
   rather than against the page. */
.voff{background:var(--surface); border:0; border-radius:var(--r-lg);
  box-shadow:inset 4px 0 0 var(--voff-rule), var(--card-shadow);
  padding:20px 24px 20px 28px; display:flex; gap:26px; align-items:center; flex-wrap:wrap}
/* The icon is what makes it read as a notice before a word of it is read. */
.voff::before{content:''; flex:none; width:30px; height:30px; border-radius:9px;
  background:var(--voff-rule);
  -webkit-mask:no-repeat center/17px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8 5.2v3.4M8 11v.1' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='8' r='6.4' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E");
  mask:no-repeat center/17px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8 5.2v3.4M8 11v.1' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='8' r='6.4' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E")}
.voff-h{font-size:14px; color:var(--ink); max-width:34ch; line-height:1.55; font-weight:500}
.voff-h b{color:var(--ink); font-weight:700}
.voff-i{display:flex; flex-direction:column; gap:5px; min-width:0}
.voff-i .n{font-size:12.5px; color:var(--muted); font-weight:600}
/* The call to action is a button, not a coloured word. This is the one thing on
   the notice somebody is meant to DO, and an underlined link in a paragraph of
   explanation is the easiest thing on a page to skim past. */
/* The button sits on the card, so it needs a fill that is NOT the card. --paper
   is the page colour, which reads as an inset control — the same 1.31:1 the card
   uses to separate from the page, borrowed in reverse. */
.voff-i a{display:inline-flex; align-items:center; gap:6px; align-self:flex-start;
  padding:7px 13px; border-radius:9px; background:var(--paper); color:var(--ink);
  font-size:12.5px; font-weight:600; box-shadow:var(--btn-shadow);
  transition:box-shadow .14s ease, transform .14s ease}
.voff-i a:hover{box-shadow:var(--btn-shadow-hover); transform:translateY(-1px); text-decoration:none}

@media(max-width:1080px){
  .b3,.b4,.b5,.b6,.b7,.b8,.b9{grid-column:span 12}
  .claim{font-size:29px}
}

/* ---------- 4 · DARK PANEL ----------
   The page's ONE headline number. Navy gradient, a dotted corner, and a figure
   big enough that it is read before anything else on the screen.
 *
 * WHY THIS SHAPE HAD TO EXIST SEPARATELY.
 *
 * Three of the four shapes were built; this one was not, so its job fell to the
 * notched panel — and twenty pages ended up opening with a notch. The notch
 * means "here is the one thing to do next". A shape that appears on every page
 * as the header does not mean that any more; it just means "page". The system's
 * own rule says two on a screen and neither is the next thing, and twenty pages
 * with one each is the same failure spread thin.
 *
 * So the split is: a hero that STATES something is a dark panel, a hero that
 * ASKS something keeps its notch. Most heroes state.
 *
 * It shares the notch's entire child-inversion set — every rule below that
 * lightens a chip, a meter, a help icon or a nested card reads
 * `:is(.vnotch,.vdark)`. That was the point of doing it that way rather than
 * copying sixty rules: a contrast fix made for one shape is made for both, and
 * they cannot drift.
 */
/* NO overflow:hidden ON THE PANEL ITSELF.
 *
 * It was here to keep two DECORATIONS inside the rounded corners — the dot
 * texture below and the orbit SVG on the dashboard, both hung outside the box on
 * negative offsets. It also clipped everything else, and the ⓘ popovers inside
 * this panel open downward: on Brand Health the "Share of voice" explanation was
 * sliced in half by the bottom edge.
 *
 * That is the fourth time a container in this file has eaten a tooltip — after
 * .strip, .ov-strip and .kpi. The pattern each time: overflow:hidden added for a
 * decoration, paid for by the content. So the decorations now clip themselves
 * and the panel clips nothing. scripts/check-overflow.js checks this properly
 * now, against the rendered DOM rather than by looking for the classes I
 * happened to think of. */
.vdark{position:relative; color:#fff;
  background:linear-gradient(160deg,#091122,#182748);
  border-radius:var(--r-lg); padding:32px}
/* Holds the decorations and clips only them. Sits under the content, ignores
   the pointer, and is the only thing in here with a clip. */
.vd-deco{position:absolute; inset:0; overflow:hidden; border-radius:inherit; pointer-events:none; z-index:0}
/* The texture. Decorative, so it is masked to fade out rather than stopping at
   a hard edge, and it sits under everything — a dotted grid crossing a figure
   is the sort of thing that looks fine at desk distance and awful on a screen
   share. */
/* Was right:-30px / top:-30px, which only worked because the panel clipped it.
   Pulled inside; the radial mask already fades it out toward that corner, so it
   reads the same without needing anything to cut it off. */
.vdark::before{content:''; position:absolute; right:0; top:0;
  width:210px; height:210px; opacity:.14; pointer-events:none;
  background-image:radial-gradient(#fff 1.4px, transparent 1.4px);
  background-size:13px 13px;
  -webkit-mask:radial-gradient(circle at 100% 0, #000 40%, transparent 72%);
          mask:radial-gradient(circle at 100% 0, #000 40%, transparent 72%)}
.vdark > *{position:relative}
.vd-k{font-family:'Unbounded',sans-serif; font-size:11px; font-weight:500;
  letter-spacing:.14em; text-transform:uppercase; color:var(--lime)}
.vd-row{display:flex; align-items:flex-end; gap:16px; margin:14px 0 10px; flex-wrap:wrap}
/* Tabular figures, because this number is compared against itself run to run and
   a proportional 1 makes 111 narrower than 100. */
.vd-num{font-family:'Unbounded',sans-serif; font-size:58px; font-weight:400;
  line-height:1; letter-spacing:-.03em; font-variant-numeric:tabular-nums}
.vd-num small{font-size:20px; font-weight:400; color:rgba(255,255,255,.55); margin-left:2px}
.vd-sub{color:rgba(255,255,255,.76); font-size:14px; line-height:1.6; max-width:56ch}
.vd-sub b{color:#fff}
/* Buttons dropped in here were designed for a white card, so they arrive as a
   dark outline on a dark panel and effectively disappear. Inverted once, here,
   rather than per page. */
.vd-act{display:flex; gap:10px; margin-top:20px; flex-wrap:wrap; align-items:center}
.vd-act .btn{background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.24); color:#fff}
.vd-act .btn:hover{background:rgba(255,255,255,.22); border-color:rgba(255,255,255,.4)}
.vd-act .btn svg{color:currentColor}
/* The delta pill. Lime on the navy is 5.94:1; the red needs its own lighter
   value or it lands at 3.03:1, which is the failure nobody notices because red
   still looks like red. */
/* The change pill on a line of its OWN, between the figure and the sentence —
   as opposed to sitting inside `.vd-row` beside the number, which is the other
   way this panel uses it.
   It was an inline `margin-top:16px` with nothing underneath, so it had 16px of
   air above and zero below and read as the first line of the paragraph rather
   than as a footnote to the number it describes. The space belongs on both
   sides, and slightly more above than below, because the pill is closer in
   meaning to the figure than to the prose. */
.vd-drow{margin:18px 0 14px}
.vd-d{font-size:12px; font-weight:600; padding:5px 11px; border-radius:100px;
  background:rgba(161,203,58,.20); color:#D4EE69; white-space:nowrap}
.vd-d.neg{background:rgba(252,165,165,.16); color:#FCA5A5}
.vd-d.flat{background:rgba(255,255,255,.12); color:rgba(255,255,255,.78)}
/* The one panel on the page that is the page. Bigger and lighter than the
   standard dark panel — at 96px Unbounded 300 the figure stops being a KPI and
   becomes the thing the screen is about. Reserved for a dashboard hero; a second
   one anywhere would make both of them ordinary. */
.vdark.vd-hero .vd-num{font-size:96px; font-weight:300; letter-spacing:-.045em}
.vdark.vd-hero .vd-num small{font-size:26px}
.vdark.vd-hero{min-height:330px}

/* ---------- THE DARK PANEL, WITH A BITE ----------
   The site's own hero shape: a rectangle with the top-right corner taken out
   and the action living in the cut.

   WHY THE TWO SHAPES MERGE HERE RATHER THAN STAYING APART. The system separates
   them because a notch means "do this next" and a dark panel means "here is the
   number" — and it is right that a page cannot have two of either. But a page
   whose headline number ALSO has one page-level action does not need two panels
   to say so. Splitting them put the button in a strip of its own above the
   panel, floating, attached to nothing.

   So: the panel keeps the number, and the bite carries the one action. The
   one-per-page rule is unchanged — this counts as the page's notch.

   Every child rule the notch already owns is written `:is(.vnotch,.vdark)`, so
   the cut and its fillet work here with no new markup rules. All this class adds
   is the squared corner and the room above the content for the cut to sit in. */
.vdark.vd-cut{--nw:174px; --nh:46px; --ir:26px;
  border-radius:var(--r-lg) 0 var(--r-lg) var(--r-lg);
  padding-top:calc(var(--nh) + 14px)}
/* The cut is a hole punched to the PAGE colour, so on a panel sitting on any
   other ground it would show the wrong colour through. Every panel on this app
   sits on --paper, which is what the notch already assumes. */
/* A narrower bite, for a panel that shares its row.
   The cut is a fixed 174px, so on a full-width panel it is a corner taken off
   and on a two-thirds one it starts reading as a tab stuck on top. Same shape,
   less of it — enough that two or three words still fit. */
.vdark.vd-cut.vd-cut-sm{--nw:150px}
@media(max-width:760px){.vdark.vd-cut{--nw:150px}}
@media(max-width:760px){.vdark{padding:24px} .vd-num{font-size:46px}
  .vdark.vd-hero .vd-num{font-size:62px}}

.vnotch{--nw:174px; --nh:46px; --ir:26px;
  position:relative; background:var(--vn,#2563EB); color:#fff;
  border-radius:var(--r-lg) 0 var(--r-lg) var(--r-lg);
  padding:calc(var(--nh) + 14px) 32px 32px}
/* The bite is an <a> on client pages and a <button> on the ones with an action.
   Only `border-bottom-left-radius` was set, so the <button> kept the browser's
   own border and drew a grey box around itself — the cut stopped reading as a
   piece taken out of the panel and started reading as a chip floating over it.
   The reset below is what makes the two elements identical. */
:is(.vnotch,.vdark) .vn-cut{position:absolute; top:0; right:0; width:var(--nw); height:var(--nh);
  background:var(--paper); border:0; border-bottom-left-radius:var(--ir);
  margin:0; padding:0; appearance:none; -webkit-appearance:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center; gap:8px;
  font-family:'Unbounded',sans-serif; font-size:13.5px; font-weight:400;
  line-height:1; color:var(--ink); text-decoration:none; text-align:center}
:is(.vnotch,.vdark) .vn-cut:focus-visible{outline:none; box-shadow:inset 0 0 0 2px var(--teal)}
:is(.vnotch,.vdark) .vn-cut:hover{color:var(--teal)}
:is(.vnotch,.vdark) .vn-fillet{position:absolute; top:0; right:var(--nw); width:var(--ir); height:var(--ir);
  background:var(--paper);
  -webkit-mask:radial-gradient(circle var(--ir) at 0 var(--ir),#0000 99%,#000 100%);
          mask:radial-gradient(circle var(--ir) at 0 var(--ir),#0000 99%,#000 100%)}
:is(.vnotch,.vdark) h2,:is(.vnotch,.vdark) .vn-h{font-family:'Unbounded',sans-serif; font-size:23px; font-weight:400;
  margin:0 0 16px; line-height:1.28; color:#fff; max-width:24ch}
/* DARKEN, never lighten. The site tints this bar with white, which on its own
   blue panel is 3.51:1 — lightening a mid-tone surface moves it toward white
   text. -16% black is 6.77:1 on the blue and 18.5:1 on the navy, so one value
   serves both. */
:is(.vnotch,.vdark) .vn-bar{background:rgba(0,0,0,.16); border-radius:14px; padding:14px 20px; font-size:14.5px}
:is(.vnotch,.vdark) .vn-meta{color:rgba(255,255,255,.74); font-size:13px}
.vn-navy{--vn:#0F172A}

/* ---- content living INSIDE the notched panel ----------------------------
   The panel is dark, so anything dropped into it that was designed for a white
   card arrives unreadable. The two status chips are the sharp case: --pos is
   3.28:1 on the navy and --neg is 3.03:1, both below AA, and both are exactly
   the sort of thing nobody notices because green still looks like green. They
   get lighter variants here (10.25:1 and 9.41:1) rather than a lower bar. */
:is(.vnotch,.vdark) .eyebrow{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .rr-meta,:is(.vnotch,.vdark) .sub{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .rr-meta b{color:#fff}
:is(.vnotch,.vdark) .score-num{color:#fff; -webkit-text-fill-color:#fff; background:none}
:is(.vnotch,.vdark) .score-num small{-webkit-text-fill-color:rgba(255,255,255,.66); color:rgba(255,255,255,.66)}
:is(.vnotch,.vdark) .chip{border-color:transparent; background:rgba(255,255,255,.14); color:#fff}
:is(.vnotch,.vdark) .chip.pos{color:#4ADE80}
:is(.vnotch,.vdark) .chip.neg{color:#FCA5A5}

/* the six signal boxes: a plate, not a bordered card inside a panel */
:is(.vnotch,.vdark) .bd{background:rgba(255,255,255,.10); border:0; border-radius:14px; padding:14px 15px}
:is(.vnotch,.vdark) .bd .bl{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .bd .bv{color:#fff}
:is(.vnotch,.vdark) .bd .bm{background:rgba(255,255,255,.18)}
:is(.vnotch,.vdark) .bd .bm>span{background:var(--green)}          /* lime, 5.94:1 on the plate */

/* the help icon has to invert too, or it is a dark circle on a dark panel */
/* The base rule fills this circle with --surface-2, a near-white. Setting only
   the colour and the border left a solid white blob on the panel with a
   barely-visible letter inside it — the icon disappeared into its own
   background. The fill has to be overridden too. */
:is(.vnotch,.vdark) .info>summary{background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.30); color:#fff}
:is(.vnotch,.vdark) .info>summary:hover{background:rgba(255,255,255,.26); border-color:rgba(255,255,255,.6); color:#fff}
:is(.vnotch,.vdark) .info[open]>summary{background:#fff; color:var(--navy); border-color:#fff}
:is(.vnotch,.vdark) .ihelp{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .ihelp:hover,:is(.vnotch,.vdark) .ihelp:focus-visible{color:#fff}
/* …but the popover itself floats over the page, so it stays light */
:is(.vnotch,.vdark) .info-pop,:is(.vnotch,.vdark) .ihelp-pop{color:var(--ink)}
:is(.vnotch,.vdark) .info-pop b,:is(.vnotch,.vdark) .ihelp-pop b{color:var(--ink)}

/* WHICH COLOUR THE PANEL IS, AND WHY IT IS NOT A PREFERENCE.
   Blue (#2563EB) is the site's own notch colour and it is fine for what the
   site uses it for — a line of copy and one button. It cannot carry data:
   measured on blue, 72%-white body text is 3.44:1, the inner plate is 4.32:1,
   lime is 1.89:1 and both status chips fall under 3:1. On navy the same set
   reads 9.6, 13.56, 5.94 and 9.4+. So:
     .vnotch          — blue, copy and a CTA only
     .vnotch.vn-navy  — navy, whenever numbers live inside it
   Every panel below that holds a reading is navy for that reason. */

/* the overview strip, when it is the thing inside the panel */
:is(.vnotch,.vdark) .ov-strip{background:none; border:0; gap:14px; border-radius:0; overflow:visible}
:is(.vnotch,.vdark) .ov{background:rgba(255,255,255,.10); border-radius:14px; padding:14px 15px}
:is(.vnotch,.vdark) .ovl{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .ovv{color:#fff}
:is(.vnotch,.vdark) .muted{color:rgba(255,255,255,.62)}
:is(.vnotch,.vdark) .hint,:is(.vnotch,.vdark) .rr-meta{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) a.strong,:is(.vnotch,.vdark) .strong{color:#fff}
:is(.vnotch,.vdark) .chip.flat{background:rgba(255,255,255,.12); border-color:transparent; color:#fff}
:is(.vnotch,.vdark) .vn-warn{color:var(--warn)}   /* #E8A13A is 8.15:1 on the navy — no lighter variant needed */

/* KPI cards, and the controls that live inside them, when the panel holds them.
   The PPC pages put a <select> and a locked state in the same row as the
   figures, so the panel has to dress those too or the row half-inverts. */
/* THE KPI STRIP IS A WHITE PANEL, NOT A ROW OF CARDS.
   `.kpis` itself carries the white background, the border and the shadow, and
   the `.kpi` children are transparent cells divided by hairlines. So inverting
   only `.kpi` was not enough: the container kept painting a white slab across
   the dark panel, and the inverted children put white text on it. Invoices
   rendered as an empty white box — nothing missing, everything the same colour
   as its background.
   The container gives up its own surface here and goes back to being a grid;
   the cells become plates, which is what the rule below already did. */
:is(.vnotch,.vdark) .kpis{display:grid; gap:14px;
  background:none; border:0; box-shadow:none; border-radius:0}
:is(.vnotch,.vdark) .kpi{background:rgba(255,255,255,.10); border:0; border-radius:14px; padding:16px 17px; box-shadow:none}
:is(.vnotch,.vdark) .kpi .lbl{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .kpi .val{color:#fff}
:is(.vnotch,.vdark) .kpi .val small{color:rgba(255,255,255,.66)}
:is(.vnotch,.vdark) .kpi .foot,:is(.vnotch,.vdark) .lockwhy{color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .lockwhy a{color:#fff; text-decoration:underline}
:is(.vnotch,.vdark) .kpi.locked .val{color:rgba(255,255,255,.5)}
:is(.vnotch,.vdark) .meter{background:rgba(255,255,255,.18)}
:is(.vnotch,.vdark) .meter>span{background:var(--green)}
:is(.vnotch,.vdark) .kpi.locked .meter>span{background:rgba(255,255,255,.3)}
:is(.vnotch,.vdark) .metricsel{background-color:rgba(255,255,255,.14); border-color:rgba(255,255,255,.22); color:#fff}
:is(.vnotch,.vdark) .metricsel:hover{background-color:rgba(255,255,255,.2); border-color:rgba(255,255,255,.35)}
/* The dropdown list itself is drawn by the OS on its own surface — it does not
   inherit the panel, so the options need an explicit light pair or they come out
   white-on-white on some platforms. */
:is(.vnotch,.vdark) .metricsel option{background:var(--surface); color:var(--ink)}
:is(.vnotch,.vdark) .periodonly{border-color:rgba(255,255,255,.3); color:rgba(255,255,255,.72)}
:is(.vnotch,.vdark) .divider{background:rgba(255,255,255,.14)}
/* Belt and braces: anything that still arrives carrying .card — from a partial,
   or from a page converted later — must not paint itself white inside the panel. */
:is(.vnotch,.vdark) .card{background:rgba(255,255,255,.10); border:0; box-shadow:none; color:inherit}

/* THE TWO THINGS THAT STILL CAME OUT DARK-ON-DARK.
   Both were already wrong inside the notched panel before the dark panel
   existed; converting the pages is what made them visible. They are the same
   failure — a token tuned for a white card, used on navy — and the same fix.

   .tag carries its own tinted background AND its own dark text, so on the panel
   it drew a pale lozenge with text that is nearly the same value as the tint.
   The tints are re-cut against the panel and the text goes light.

   .modelled is a caption at --muted-2, which is 2.1:1 here. It is used for
   exactly the sentences that qualify a number — "modelled, not measured" — so
   it being the hardest thing on the panel to read is the wrong way round. */
:is(.vnotch,.vdark) .tag{background:rgba(255,255,255,.14); color:#fff}
:is(.vnotch,.vdark) .tag.paid{background:rgba(74,222,128,.16); color:#4ADE80}
:is(.vnotch,.vdark) .tag.overdue{background:rgba(252,165,165,.16); color:#FCA5A5}
:is(.vnotch,.vdark) .tag.pending{background:rgba(232,161,58,.20); color:#F5C46A}
:is(.vnotch,.vdark) .tag.open{background:rgba(255,255,255,.16); color:#fff}
:is(.vnotch,.vdark) .modelled{color:rgba(255,255,255,.72); border-top-color:rgba(255,255,255,.16)}
:is(.vnotch,.vdark) .modelled b{color:#fff}

/* WHY THE PAGE HAD A HORIZONTAL SCROLLBAR — and why that showed as a gap.
   `1fr` is `minmax(auto,1fr)`, and `auto` floors a grid track at min-content. One
   long label — "Mentions, last 30 days", "Cost per conversion" — widened its
   column past the container, the grid overflowed, the document gained horizontal
   scroll, and because .sidebar is position:sticky it slid out from under the
   content and left a strip of page showing beside it. Raising the gaps and cell
   padding for the new look is what pushed it over.

   Every track is minmax(0,1fr) now. These children need the same floor, because
   a flex/grid item's default min-width is also auto. */
.kpis > *, .ov-strip > *, .strip > *, .breakdown > *, .vtiles > *,
.grid > *, .score-hero > *{min-width:0}
.ovv, .kpi .val, .strip .cv, .bd .bv{overflow-wrap:anywhere}

/* ============================================================
   "WHAT AI KNOWS ABOUT YOU" — one row per question
   Six questions x three engines, each answer printed in full with its sources,
   made a page you scroll for a minute to learn there is nothing wrong. The
   answers are still all here; they are just not all open at once.

   WHAT STAYS VISIBLE WHEN CLOSED is the point. A collapsed row still names every
   engine and colours it by verdict, so "one of these is wrong" is readable
   without opening anything — and a group containing a wrong answer, or engines
   that contradict each other, opens itself. Collapsing a finding out of sight
   would be worse than the long page.
   ============================================================ */
/* A flush list, not six tinted blocks. Each row was a rounded card on a tint
   with its own margin, which is a lot of furniture around one line of text —
   the shape said "six separate things" when the point is one list you scan.
   Rows are separated by a hairline and the whole list sits in the card. */
.qgrp{border-top:1px solid var(--line-2)}
.qgrp:first-of-type{border-top:0}
.qgrp[open]{background:var(--surface-2); border-radius:12px; border-top-color:transparent}
.qgrp[open] + .qgrp{border-top-color:transparent}
.qgrp > summary{list-style:none; cursor:pointer; display:flex; align-items:center;
  gap:9px; flex-wrap:wrap; padding:11px 12px; border-radius:12px; user-select:none}
.qgrp > summary::-webkit-details-marker{display:none}
.qgrp > summary:hover{background:var(--surface-2)}
.qgrp[open] > summary:hover{background:transparent}
.qgrp > summary .q-l{font-weight:600; font-size:13.5px}
.qgrp > summary .q-chev{margin-left:auto; color:var(--muted-2); transition:transform .16s ease; flex:none}
.qgrp[open] > summary .q-chev{transform:rotate(180deg)}
.qgrp[open] > summary{border-bottom:1px solid var(--line-2); border-radius:12px 12px 0 0}
/* SAME COLOUR ON SAME COLOUR WAS THE PROBLEM, NOT THE GAP.
   `.qgrp[open]` paints itself --surface-2 and each answer is a .bubble, which is
   also --surface-2. There has been a 9px gap between them all along; it just had
   nothing to separate, so three answers read as one slab with hairlines through
   it. Lifting each answer onto the surface makes the gap visible, and the extra
   few pixels then actually do something. */
.q-body{padding:12px 14px 14px; display:flex; flex-direction:column; gap:14px}
/* The inline border-left carries the verdict colour and is an inline style, so
   it wins on that edge — this only supplies the other three. */
.qgrp[open] .q-body > .bubble{background:var(--surface); border:1px solid var(--line)}
/* how far the checking has got, in the quietest thing on the row */
.q-meta{font-size:11px; color:var(--muted-2)}

/* the engine roll-call in the closed row: name + verdict, no numbers to decode */
.q-eng{display:inline-flex; gap:6px; flex-wrap:wrap}
.q-dot{display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:500;
  padding:1px 7px; border-radius:100px; background:var(--surface-2); color:var(--muted-2)}
.qgrp[open] .q-dot{background:var(--surface)}
.q-dot::before{content:""; width:6px; height:6px; border-radius:50%; background:var(--muted-2); flex:none}
.q-dot.ok{color:var(--pos)}      .q-dot.ok::before{background:var(--pos)}
.q-dot.bad{color:var(--neg)}     .q-dot.bad::before{background:var(--neg)}

/* the answer itself: four lines, then the rest on request. The summary IS the
   text, so opening it does not reprint the paragraph underneath. */
.ans > summary{list-style:none; cursor:pointer}
.ans > summary::-webkit-details-marker{display:none}
.ans .txt{white-space:pre-wrap; display:-webkit-box; -webkit-line-clamp:4; line-clamp:4;
  -webkit-box-orient:vertical; overflow:hidden}
.ans[open] .txt{display:block; overflow:visible}
.ans > summary::after{content:"Read the whole answer"; display:inline-block; margin-top:6px;
  font-size:11.5px; font-weight:600; color:var(--teal-600)}
.ans[open] > summary::after{content:"Show less"}
.ans > summary:hover::after{text-decoration:underline}

/* sources behind a count, because seven links is taller than the answer */
.fsrc-d > summary{list-style:none; cursor:pointer; font-size:11.5px; font-weight:600;
  color:var(--muted); display:inline-flex; align-items:center; gap:6px}
.fsrc-d > summary::-webkit-details-marker{display:none}
.fsrc-d > summary:hover{color:var(--ink)}
@media(max-width:1240px){.vtiles{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:760px){.vtiles{grid-template-columns:repeat(2,minmax(0,1fr))} .vnotch{--nw:150px}}

/* ============================================================
   THE VISIBILITY CHART'S HOVER READOUT
   A crosshair, an enlarged point, and a small card carrying the
   date, the score and the move since the run before it.

   The readout is HTML rather than SVG text because it needs a
   background, a corner radius and two type sizes — each of which
   is a fight inside an SVG and free outside one. It is positioned
   in percentages of the wrapper, so it follows the chart at every
   width without the script knowing anything about layout.
   ============================================================ */
.vchart-wrap{position:relative}
.vchart{display:block; cursor:crosshair; outline:none}
/* A visible focus ring, because this is a tabbable element and the arrow keys
   do something. Removing the outline without replacing it is how a keyboard
   user loses their place. */
.vchart:focus-visible{outline:2px solid var(--teal-600); outline-offset:3px; border-radius:var(--r)}

/* Inert until the script moves them. Opacity rather than `display`, so the
   first hover does not cost a layout. */
.vc-cross{stroke:var(--teal-600); stroke-width:1; stroke-dasharray:3 3; opacity:0;
  transition:opacity .12s ease; pointer-events:none}
.vc-dot{fill:var(--surface); stroke:var(--teal-600); stroke-width:2.5; opacity:0;
  transition:opacity .12s ease; pointer-events:none}
.vchart.is-hovering .vc-cross{opacity:.5}
.vchart.is-hovering .vc-dot{opacity:1}

.vchart-tip{position:absolute; z-index:3; pointer-events:none;
  background:var(--surface); border-radius:var(--r);
  box-shadow:var(--card-shadow); padding:9px 12px; min-width:104px;
  white-space:nowrap}
.vchart-tip[hidden]{display:none}
.vchart-tip-k{font-size:11px; color:var(--muted); font-weight:600; letter-spacing:.01em}
.vchart-tip-v{font-family:'Unbounded',sans-serif; font-size:20px; font-weight:400;
  color:var(--ink); line-height:1.15; margin-top:2px; font-variant-numeric:tabular-nums}
.vchart-tip-d{display:inline-block; margin-top:4px; font-size:11.5px; font-weight:600;
  color:var(--muted-2)}
.vchart-tip-d.pos{color:var(--pos)}
.vchart-tip-d.neg{color:var(--neg)}

/* On a dark ground the card shadow does nothing, so the readout needs an edge to
   be a separate thing from the chart behind it. */
:root[data-theme="dark"] .vchart-tip{box-shadow:0 0 0 1px rgba(255,255,255,.12), 0 10px 28px rgba(0,0,0,.55)}

/* A column heading that has to name its own baseline. "Visibility" alone left
   the ▲/▼/= chips in that column unexplained, and the Visibility history legend
   further down the same page measures from a DIFFERENT starting point — so the
   two contradict each other on screen while both being correct. Lighter and
   uncapitalised so it reads as a qualifier on the heading, not a second heading. */
.tbl th .th-note{font-weight:500; letter-spacing:0; text-transform:none;
  color:var(--muted-2); margin-left:5px}
/* Same job in the chart legend: the baseline travels with the number. */
.legend .lg-since{font-weight:500; opacity:.72}

/* SPARKLINES under the figures in the dark panel — see partials/sparkline.ejs.
 *
 * BLUE, and set as a variable rather than a literal so the partial can name one
 * colour and each context decide what it is. The first pass drew these in
 * --teal, which on this navy ground is a dark purple sitting on dark blue: the
 * line was technically there and effectively invisible. --blue is the token
 * already in the palette, lightened here because the panel is dark.
 *
 * Full width, fixed height, so six of them line up whatever the figures above
 * them are. Opacity is high enough to read and low enough that the row of
 * numbers is still what you see first — a sparkline that competes with its own
 * figure has the priority backwards. */
.spark-wrap{position:relative; margin-top:9px}
.ov .spark{display:block; width:100%; height:34px; overflow:visible;
  --spark:var(--blue); opacity:.9}
.ov .spark:focus-visible{outline:2px solid var(--blue); outline-offset:3px; border-radius:4px}
/* On the navy panel: a lighter blue, because --blue at #2563EB against #0D172D
   is 1.9:1 — present in the markup, absent to the eye. */
.vdark .ov .spark{--spark:#6BA6FF; opacity:.95}
/* The end dot marks "now". On a line with no axis it is the only thing saying
   which end is the latest reading. */
.ov .spark .spark-end{opacity:.95}
/* The hover layer, shared with the big chart (public/js/chart.js). Scaled down:
   the readout sits in a 34px-tall box, so the big chart's padding would cover
   the line it is describing. */
.spark-wrap .vchart-tip{font-size:12px; padding:7px 9px; min-width:0; white-space:nowrap}
.ov .spark .vc-cross{stroke:var(--spark); stroke-width:1; opacity:0; stroke-dasharray:2 2}
.ov .spark .vc-dot{fill:var(--spark); stroke:var(--surface); stroke-width:1.5; opacity:0}
.ov .spark.is-hovering .vc-cross, .ov .spark.is-hovering .vc-dot{opacity:.85}
.vdark .ov .spark .vc-dot{stroke:#0D172D}

/* CORE WEB VITALS — the card is split in two: a titled band, then the readings.
 *
 * The band is FULL BLEED. `.card-p` puts 22px of padding on this card, so the
 * band cancels it with a matching negative margin and pays it back as its own
 * padding — the tint then runs edge to edge and the card reads as two parts
 * rather than as a grey label floating inside one. The top corners match
 * --r-card, or the tint would square off the corners the card has rounded.
 *
 * The tint is `sc-sky`, already in the palette for the shelf cards, and it is
 * the same blue the sparklines now use. Deliberately NOT a verdict colour: this
 * card carries good and bad readings alike, and the verdict is already stated
 * twice — the score chip on the right and a Good/Poor chip on every row. A
 * green or red band would be a third opinion, and it would be the loudest.
 *
 * Grey was the first attempt and read as chrome — a toolbar above the numbers
 * rather than the head of a card. */
.card-h.cwv-h{align-items:center; gap:9px;
  margin:-22px -22px 18px; padding:13px 22px;
  background:linear-gradient(100deg,#0D172D 0%,#183A63 52%,#2A70B4 100%);
  color:#fff;
  border-radius:var(--r-card) var(--r-card) 0 0}
/* White, because the band is now dark at one end. Every stop above was checked
   against #FFFFFF before it shipped: 17.8:1 at the navy end down to 5.15:1 at
   the lightest, so the title clears AA across the whole sweep rather than only
   where it happens to start. */
.card-h.cwv-h h3{margin:0; color:#fff}
/* --navy is INVERTED in dark mode (#0D172D becomes #C3CADB), so these stops are
   literals. `var(--navy)` here would have turned the band pale in dark mode and
   left white text on it — the exact bug this file has been bitten by before. */
:root[data-theme="dark"] .card-h.cwv-h{
  background:linear-gradient(100deg,#0A1524 0%,#14314F 52%,#1E5382 100%)}

/* The source badge: an icon for the tool, the word for the KIND of evidence.
   `lab` and `field` are not decoration — a lab figure is one simulated phone,
   a field figure is real visitors, and the two are read completely differently.
   The tool name moved into the title, the distinction stayed on screen.
   On the tinted band it takes the card's own surface so it still reads as a
   chip rather than dissolving into the tint behind it. */
.src-badge{display:inline-flex; align-items:center; gap:5px; cursor:help;
  padding:2px 8px 2px 6px; border-radius:99px;
  /* DARKENED, not lightened. A white veil reads well at the navy end of the
     band and washes out at the blue end — 3.79:1 for white text at the last
     stop, under AA for a label this size. Tinting toward the navy instead holds
     8.04:1 at the worst point on the sweep, and it works whichever way the card
     is sized, which a fixed colour picked for one width would not. */
  background:rgba(13,23,45,.34); border:1px solid rgba(255,255,255,.28);
  font-size:11.5px; letter-spacing:.01em}
/* THE COLOUR NEEDS THIS SELECTOR, NOT THE ONE ABOVE.
   `.card-h .hint` (0,2,0) sets --muted-2 and this badge carries `hint`, so a
   plain `.src-badge` rule (0,1,0) lost the cascade no matter where it sat in the
   file. The label went grey, and `fill="currentColor"` handed that grey straight
   to the icon — which is why the lighthouse read as a smudge on a dark pill
   however boldly it was drawn.

   Scoped to `.cwv-h` for (0,3,0) rather than `.card-h .src-badge`, which ties
   `.card-h .hint` at (0,2,0) and only wins because it happens to sit lower in
   the file. A tie decided by source order is a rule waiting to break the next
   time anything is moved — and moving things is exactly what happened here. */
.card-h.cwv-h .src-badge{color:#fff}
.src-badge svg{flex:none; opacity:.85}

/* CHIPS AND ICON ON THE GRADIENT BAND.
 *
 * `.chip` tints are alpha — --neg-tint is rgba(229,72,77,.10) — which is a pale
 * pink on a white card and very nearly nothing on a dark blue one. The score
 * chip's pill disappeared into the band and left dark red text on blue at
 * 1.1:1 at the light end: present in the markup, invisible on screen. The same
 * assumption that made the sparklines vanish in --teal.
 *
 * So on this band a chip gets a SOLID white pill and keeps its verdict colour
 * as text — that reads at every stop of the gradient rather than at whichever
 * one it happens to sit over, and the verdict survives, which matters more here
 * than matching the chip style used on white cards.
 *
 * Amber is darkened: #B5791E is 3.67:1 on white, under AA. #8F5F15 is 5.5:1.
 * Green and red already clear it (5.44 and 5.89), so they are left alone. */
.cwv-h .chip{background:#fff}
.cwv-h .chip.pos{color:var(--pos)}
.cwv-h .chip.neg{color:var(--neg)}
.cwv-h .chip.warn{color:#8F5F15}
:root[data-theme="dark"] .cwv-h .chip{background:rgba(255,255,255,.94)}
:root[data-theme="dark"] .cwv-h .chip.pos{color:#0E7A37}
:root[data-theme="dark"] .cwv-h .chip.neg{color:#C0272C}

/* The icon was held at .85 to sit quietly on a white card. On the band it needs
   its full weight — it is the only thing carrying "where did this come from". */
.cwv-h .src-badge svg{opacity:1}
