/* wiz-dash skin — MSG-13 Part 2.
   =============================================================
   ONE hand-written stylesheet. No framework, no Tailwind utilities, no
   Basecoat. Light and dark, both hand-written, both measured on the rendered
   page by scripts/render-audit.py before this file was allowed to ship.

   WHY THE FRAMEWORK IS STILL GONE
   -------------------------------
   The page used to depend on two stylesheets both arriving AND both being
   understood. When ours did not arrive, Basecoat's `.tabs` turned the client
   tab strip into a vertical column and an <svg> with no width rule fell back to
   the CSS default of 300x150. It degraded into nonsense instead of into
   plainness. One file cannot do that: if it fails to load you get unstyled
   HTML, which is ugly and completely readable.

   CUSTOM PROPERTIES ARE BACK; oklch() AND color-mix() ARE NOT.
   Part 1 dropped custom properties along with everything else. That was one
   step too far: `var()` has been safe in every browser since 2017 and was never
   the risk. The risk was the VALUE - an unsupported `oklch()` or `color-mix()`
   makes a declaration vanish silently while everything around it still applies,
   which is precisely the half-styled page nobody can diagnose from a
   screenshot. So the palette below is custom properties holding plain HEX, and
   a theme is a swap of that palette and nothing else.

   ROBUST TO A FORCED-DARK EXTENSION.
   Marko runs Chrome 152, so browser support was never the cause; a Dark
   Reader-type extension is one of the two remaining suspects. The defence is
   to declare a colour scheme and ship a real dark theme: an extension that is
   told the site handles dark itself has something to defer to. Hence
   `color-scheme` on the root in both themes and the matching <meta> in
   base.html. It cannot be guaranteed - an extension can always override - but
   a site with no declared scheme and no dark theme guarantees the opposite.
*/

/* ------------------------------------------------------------- typefaces
   VENDORED, SERVED BY THIS APP. Declared here rather than linked, because this
   file is deliberately the only stylesheet the dashboard loads - see the note
   at the top about what happened when it needed two. The same .woff2 files the
   client-facing forms use. No CDN; 4p proves it both ways. */
/* Vendored by m25/vendor_fonts.py. Served by this app, never a CDN:
   the onboarding form is client-facing and the CSP allows self only.
   Subsetting and unicode-range come from Google's own css2 response, so a
   client whose brand name is not ASCII still renders. */

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-0.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-2.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-0.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-2.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-0.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-2.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 800;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-0.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 800;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 800;
  font-stretch: 100%;
  font-display: swap;
  src: url(/static/fonts/Archivo-2.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* vietnamese */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/PublicSans-2.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/PublicSans-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/PublicSans-0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/PublicSans-2.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/PublicSans-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/PublicSans-0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/PublicSans-2.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/PublicSans-1.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/PublicSans-0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/* ---------------------------------------------------------------- palette */
:root {
  color-scheme: light;

  /* ---- THE SHELL FOLLOWS THE THEME (Marko, 22.9.2026 - M39 item 6) -----
     THIS REVERSES A SIGNED DECISION AND SAYS SO. From 11.9.2026 the spec
     read "dark navigation shell, light working canvas", and the sidebar was
     near-black in both themes on purpose. Marko, switching to light: "the
     sidebar stays black" - the shell now follows the theme like everything
     else, and DASHBOARD-VISUAL-SPEC.md is updated rather than contradicted.

     The shell keeps its OWN tokens, which was the good half of the original
     decision: --surface means "dimmed row / empty state / panel" in
     twenty-one places and must not be dragged around by the navigation.

     M27d LESSONS 256 is why the render audit was repointed rather than
     deleted - it had been taught to REQUIRE a dark sidebar beside light
     content, so leaving it would have blocked this instruction. */
  /* DEEPER THAN THE CANVAS, on purpose. The first value here was
     #f4f6fb - exactly --canvas - which would have made the rail the
     same colour as the page. A rail that matches the page is not a
     rail, and the render audit has failed that since M12. */
  /* THE RAIL FOLLOWS THE THEME. Marko's ruling of 22.9, which MSG-56 Batch 1
     asks to reverse; MSG-57 says his rulings win. A light rail in the light
     theme. The Kit's one dark card does not need this to be dark - see
     .card.hero, which redefines these five on itself. */
  --shell:         #e3e8f2;
  --shell-ink:     #0a0e1a;
  --shell-muted:   #5b6577;
  --shell-line:    #dde2ec;
  --shell-active:  #e6ecf7;
  --canvas:        #f4f6fb;
  --accent:        #2575fc;
  --accent-soft:   #edf3ff;
  /* A BLUE STRONG ENOUGH TO CARRY WHITE TEXT. --accent is a
     surface and a marker colour; at #2575fc it is 4.18:1 under
     white, so anything that paints white ON it needs this one
     instead. Dark keeps --accent's own value: it already
     passes at 5.77:1 and a fix must not move what works. */
  --accent-strong: #1a5fd0;   /* white TEXT on it: 5.85:1 */
  --accent-press:  #14499f;   /* the solid button, pressed */
  --accent-ink:    #ffffff;
  /* Text on the brand gradient, which is dark in
     both themes. The ONLY white in this file that is not a
     token, was here - now it is one. */
  --on-brand:      #ffffff;
  /* A LINK ON THE SHELL. The hero and the sidebar paint themselves
     with --shell, so a link on them needs a colour chosen for THAT
     surface - not the page's --link, and never a literal. The pale
     blue that used to be hard-coded here was picked when the shell
     was dark in both themes; when the shell went light it became
     1.50:1. */
  --shell-link:    #10459e;
  /* THE MARK'S OWN SURFACE. A gradient sets background-image, so
     background-color stays transparent and nothing - not the eye
     at a glance, not the render audit - can say what the text
     sits on. This is that surface, solid and measurable. */
  --brand-solid:   #6a11cb;   /* 5.74:1 on the light rail */
  --radius-card:   13px;
  /* FOUR NAMES THE STYLESHEET USED AND NEVER DEFINED (Batch 1).
     An unresolved var() drops its whole declaration without a word, so
     ol.steps had no border, the monthly tables had none either, and two
     captions were inheriting their colour instead of being muted. Aliases,
     because each points at a token that is already themed - so these do not
     need repeating in :root.dark. */
  --line:   var(--border);
  --input:  var(--border);
  --muted:  var(--text-muted);
  --radius: 8px;          /* inner elements: inputs, pills, KPI tiles */

  /* ADDENDUM B. The two curves the whole app uses. Animate transform and
     opacity only; the fold height is the one documented exception. */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  /* Chart series, fixed order, never cycled (spec). Colour follows the
     entity, not its rank. */
  --series-1: #2575fc;
  --series-2: #0d9488;
  --series-3: #6a11cb;
  --series-4: #d97706;
  --series-5: #db2777;
  /* ADDENDUM A1. #c3cad8 was 1.55:1 on white, so every month that was not
     the current one all but disappeared. #848fa3 is 3.26:1 - the 3:1 a
     non-text graphic needs. */
  --series-muted: #848fa3;
  --row-line:  #f1f4fa;   /* rows inside a card, lighter than the card's edge */
  --grid-line: #edf0f7;   /* chart grid */

  --bg:            #f4f6fb;
  --surface:       #f4f6fb;   /* sidebar, empty states, dimmed rows, panels */
  --card:          #ffffff;
  --text:          #0a0e1a;
  --text-muted:    #626c81;   /* F8 (MSG-58): was #667085, which measured
                                 4.36:1 at 12px on the raised surfaces
                                 (#edf0f7, #eef0f5) - under the floor.
                                 #626c81 is the first step that clears
                                 4.5 on all five: worst 4.62:1. */
  --border:        #e2e7f2;
  --border-strong: #c3cad8;
  --hover:         #edf3ff;
  --active:        #e1ebff;
  --link:          #1a5fd0;
  --code-bg:       #edf0f7;
  --row-hover:     #f8fafe;
  /* The third layer: hover rows, inputs, popovers, the inside of a
     segmented control. Named for what it IS, not for one place it is used. */
  --raised:        #f8fafe;
  --primary:       #0b0f1a;
  --primary-text:  #ffffff;
  --primary-hover: #1c2436;
  --focus:         #2575fc;

  /* Verdict colours. Identical in both themes on purpose: each is dark enough
     to carry white text at better than 7:1, and a verdict that changes colour
     with the theme is a verdict two people describe differently. */
  --good:    #146c43;
  --warn:    #8a6100;
  --bad:     #b42318;
  --neutral: #5c5c5c;
  --on-chip: #ffffff;
  /* F8 (MSG-58). DELTA TEXT, not a chip background. --good and --bad are
     the colours a chip is PAINTED with and white sits on them; using them
     as text colours is what left the dark deltas at 2.60:1 and 2.84:1.
     Light: the chip colours already measure 5.76 and 5.65 as text on every
     surface they appear on, so they are reused here deliberately. */
  --delta-good: #146c43;
  --delta-bad:  #b42318;

  --stale-row: #fdf6e3;
  --never-row: #fdeceb;
}

:root.dark {
  color-scheme: dark;

  /* In dark the shell is a SHADE APART from the canvas, not the same colour -
     a navigation rail that matches the page is not a rail. The series are the
     spec's separate dark steps, not an inversion of the light ones. */
  /* LIGHTER THAN THE PAGE, not darker. Against MSG-56's #0e1117 page a
     near-black rail measures 1.04:1 - the audit's words were "a rail that
     matches the page is not a rail" - and at that end of the scale no
     darker colour can reach the 1.12 it wants. This is 1.16:1. */
  --shell:         #1c212b;
  --shell-ink:     #f2f3f5;
  --shell-muted:   #98a2b3;
  --shell-line:    #1b202b;
  --shell-active:  #182034;
  --canvas:        #1e2128;
  --accent:        #4a8bf7;
  --accent-soft:   #1b2333;
  --accent-strong: #4a8bf7;
  --accent-press:  #6ea5f9;   /* lighter, because the ink on it is dark */
  --accent-ink:    #0b0f1a;
  --on-brand:      #ffffff;
  --shell-link:    #9dc0ff;
  /* Brighter than light's #6a11cb: against the near-black shell
     that one is 2.38:1 and the tile disappears. */
  /* BETWEEN TWO BOUNDS. The tile has to clear 3:1 against the rail (it is a
     graphic) and the white mark on it has to clear 4.5:1 (it is text), and
     the rail coming up to #1c212b narrowed the gap between them:

         white on it needs    luminance <= 0.183
         on the rail it needs luminance >= 0.140

     #7c3aed is 0.134 - fine under white, 2.83:1 on the rail.
     #9067f0 is 0.219 - fine on the rail, 3.90:1 under white.
     #8450f0 is 0.169 - inside both, with room on each side. */
  --brand-solid:   #8450f0;
  --radius-card:   13px;

  --series-1: #4a8bf7;
  --series-2: #0d9488;
  --series-3: #9067f0;
  --series-4: #bc7d0a;
  --series-5: #e5488f;
  --series-muted: #667085;   /* ADDENDUM A1: 3.50:1 on #161a22 */
  --row-line:  #1f242e;
  --grid-line: #1f242e;

  /* Batch 1. Three distinguishable layers, so a card is a card because of
     its FILL and not because of a hairline round it. */
  --bg:            #0e1117;   /* page */
  --surface:       #161a22;   /* cards, panels, empty states */
  --card:          #161a22;
  --raised:        #1d222c;   /* hover rows, inputs, popovers */
  --text:          #eef1f6;
  --text-muted:    #a3acbb;   /* 7.3:1 on the card */
  --border:        #262c38;   /* only where a fill cannot do the job */
  --border-strong: #39414f;
  --hover:         #1d222c;
  --active:        #232936;
  --link:          #8ab4ff;
  --code-bg:       #262b33;
  --row-hover:     #22262e;
  --primary:       #f2f3f5;
  --primary-text:  #14161a;
  --primary-hover: #ffffff;
  --focus:         #8ab4ff;

  --good:    #1a7f4f;
  --warn:    #9a6d00;
  --bad:     #c62f22;
  --neutral: #6b7280;
  --on-chip: #ffffff;
  /* F8 (MSG-58). Lightened until they clear the floor as TEXT on the page
     (#1d222c) and on a stat tile (#262b33): 4.53:1 and 4.52:1, from 2.84
     and 2.60. Red still means down-is-bad - ADDENDUM A.5 is untouched, the
     colours are just readable now. */
  --delta-good: #3ea373;
  --delta-bad:  #f65f52;

  --stale-row: #2c2718;
  --never-row: #2e1b19;
}

/* ---------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }
html { background: var(--bg); }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* Public Sans for everything a person reads as words. The system stack
     stays behind it: if a .woff2 fails to arrive the page is plain, not
     broken. */
  font-family: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 14px;
  line-height: 1.5;
}

/* DISPLAY TYPE: Archivo, 700/800, tightened. Page titles, card titles and
   every number that is the point of its card (spec, TYPE). */
/* Batch 1 TYPE SCALE. h3 is deliberately NOT in this list any more: the
   scale puts sub-sections in Public Sans at 14px/700, and only the page
   title, the section title and a number that is the point of its card are
   display type. */
h1, h2, .kpi-value, .hero .value, .card.hero .value {
  font-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, Arial, sans-serif;
  font-weight: 800;
}
h1 { font-size: 28px; letter-spacing: -0.02em; }
h2 { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }

/* NUMBERS LINE UP WHEREVER THEY STACK (spec, NUMBER RULES). Mechanical: the
   figures are unchanged, the columns simply stop wobbling. */
table, .num, .delta, code, .axis-tick {
  font-variant-numeric: tabular-nums;
}
/* ADDENDUM A2: NOT on the big standalone numbers. 25 of 25 table numbers
   keep tabular-nums; a 30px KPI value and a 48px hero value do not. */
.kpi-value, .hero .value, .card.hero .value, .stat-value {
  font-variant-numeric: proportional-nums;
}
h1, h2, h3, p, ul, ol, figure, table { margin: 0; }
ul { padding: 0; }
img, svg { vertical-align: middle; }
/* ICONS ARE CONSTRAINED HERE, ONCE. An <svg> with a viewBox and no width is
   300x150 by default, and that default is what produced the giant sun and moon
   when our stylesheet was missing. */
svg { width: 20px; height: 20px; flex: none; }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  background: var(--code-bg);
  color: var(--text);
  border-radius: 3px;
  padding: 1px 4px;
  overflow-wrap: break-word;
}
/* Batch 1. These are the declarations that actually won before - the block
   above was overridden four lines later, which is why the page title was
   22px and not the 28px the Kit asks for. One place now. */
h1 { margin-bottom: 4px; }
h2 { margin-bottom: 8px; }
h3 { font-size: 14px; font-weight: 700; color: var(--text); margin: 24px 0 8px; }
p, li, td, th, dd, dt, h1, h2, h3, summary, div { overflow-wrap: break-word; }

/* ------------------------------------------------------------- sidebar */
.sidebar {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: 240px;
  /* THE DARK NAVIGATION SHELL (spec, SHELL). Its own tokens - --surface is
     twenty-one dimmed rows and empty states and stays light. */
  background: var(--shell);
  color: var(--shell-ink);
  border-right: 1px solid var(--shell-line);
  overflow-y: auto;
  z-index: 40;
}

.sidebar > nav { display: flex; flex-direction: column; height: 100%; }
.sidebar header { display: block; }
.sidebar .brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 12px;
  border-bottom: 1px solid var(--shell-line);
  font-weight: 600;
  font-size: 15px;
  white-space: nowrap;
  color: var(--shell-ink);
}
/* C1. The mark is Marko's logo now, not a two-letter badge. No background:
   the white square behind it is exactly what the derivation took off, and
   painting one back would put it there again. The radius stays, so the tile's
   own rounded corners are not squared off by a stray box on any browser. */
.sidebar .brand .mark {
  display: block;
  width: 26px; height: 26px;
  flex: none;
  border-radius: 5px;
  object-fit: contain;
}
.sidebar .brand .sub { color: var(--shell-muted); font-weight: 400; font-size: 12px; }

/* The collapse arrow stays visible in BOTH states - hiding it in rail mode left
   nothing to click and read as a dead button. */
.sidebar .railtoggle {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px; height: 26px;
  flex: none;
  padding: 0;
  border: 1px solid var(--shell-line);
  border-radius: 5px;
  background: var(--shell-active);
  color: var(--shell-muted);
  cursor: pointer;
}
.sidebar .railtoggle:hover { background: var(--shell-active); color: var(--shell-ink); }
.sidebar .railtoggle svg { width: 14px; height: 14px; }

.sidebar section { padding: 10px 8px; flex: 1 1 auto; }
.sidebar h3 {
  margin: 0 0 6px;
  padding: 0 8px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--shell-muted);
  white-space: nowrap;
}
.sidebar ul { list-style: none; margin: 0; padding: 0; }
.sidebar li { margin: 0; }
.sidebar li > a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 6px;
  /* THE SHELL S OWN FOREGROUND, not the page s. This named var(--text) and
     was overridden by a later --shell-ink rule, so it never showed - but a
     token chosen for a different surface, sitting behind an override, is the
     exact shape that produced the 1.50:1 hero link. */
  color: var(--shell-ink);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
}
.sidebar li > a:hover { background: var(--shell-active); text-decoration: none; }
.sidebar li > a[aria-current="page"] { background: var(--shell-active); font-weight: 600; }
.sidebar .ico { width: 18px; height: 18px; flex: none; color: var(--shell-muted); }
.sidebar > nav > footer {
  display: block;
  /* M39 FIX 3: the shell's own line and muted ink. This rule kept the
     page's --text-muted, which is 4.37:1 on the light shell - the six
     failing nodes, one per page. */
  border-top: 1px solid var(--shell-line);
  padding: 10px 12px;
  color: var(--shell-muted);
  font-size: 11.5px;
  white-space: nowrap;
  overflow: hidden;
}
.sidebar > nav > footer span { display: block; }

/* The rail: icons only. The labels stay in the HTML - hiding is a display
   decision, not a content one, and the sidebar check reads them. */
:root.rail .sidebar { width: 56px; }
:root.rail .shell { margin-left: 56px; }
:root.rail .sidebar .lbl,
/* C1. Was `> span:not(.mark)`, written when the mark WAS a span. The mark is
   an <img> now, so that selector stopped covering the word next to it and the
   collapsed rail would have kept "wizdash" in a 64px column. */
:root.rail .sidebar .brand > *:not(.mark):not(.railtoggle),
:root.rail .sidebar h3,
:root.rail .sidebar > nav > footer { display: none; }
:root.rail .sidebar .brand { flex-direction: column; gap: 6px; padding: 12px 6px; }
:root.rail .sidebar .railtoggle { margin: 0; transform: rotate(180deg); }
:root.rail .sidebar li > a { justify-content: center; padding: 8px 0; }
:root.rail .sidebar section { padding: 10px 4px; }

/* ---- THE DARK NAVIGATION SHELL (M27d, DASHBOARD-VISUAL-SPEC) ----------
   AFTER the rules above, on purpose. These have the same specificity as the
   sidebar rules they replace, so order is what decides - placed before them,
   the originals won and the rail rendered dark with dark-on-dark text. The
   rendered audit measured the word "wiz" at 1.01:1.

   The spans are named because the colour lives on them, not on their parent:
   `.lbl` and `.sub` each carry their own rule further up. */
.sidebar { color: var(--shell-ink); }
.sidebar a, .sidebar li > a, .sidebar .lbl { color: var(--shell-ink); }
.sidebar h3, .sidebar .sub, .sidebar .brand .sub { color: var(--shell-muted); }
.sidebar .ico { color: var(--shell-muted); }
.sidebar .brand, .sidebar .brand span { color: var(--shell-ink); }
.sidebar li > a:hover { background: var(--shell-active); color: var(--shell-ink); }
.sidebar li > a:hover .ico, .sidebar li > a:hover .lbl { color: var(--shell-ink); }
.sidebar li > a[aria-current="page"] {
  background: var(--shell-active);
  color: var(--shell-ink);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}
.sidebar li > a[aria-current="page"] .lbl { color: var(--shell-ink); }
.sidebar li > a[aria-current="page"] .ico { color: var(--shell-link); }
/* THE BRAND MARK (spec: gradient). M39 FIX 5 makes it SOLID.
 *
 * A gradient has no single contrast, and this one failed white at its blue
 * end in light (4.18:1) and along its whole ramp in dark (3.90 -> 3.32:1).
 * The audit could see none of that, because a gradient leaves
 * background-color transparent and it measured the shell behind instead.
 * Solid is readable in both themes and can be checked. Say the word and the
 * gradient comes back with stops that pass. Never inside a chart. */
/* C1. The dark-shell override was painting a brand-coloured square behind the
   badge. The logo needs no plate in either theme - it is the same image on
   both, which is the point of taking the corners out. */
.sidebar .railtoggle { color: var(--shell-muted); border-color: var(--shell-line); }
.sidebar .railtoggle:hover { background: var(--shell-active); color: var(--shell-ink); }
.sidebar > nav > footer,
.sidebar > nav > footer span { color: var(--shell-muted); border-top-color: var(--shell-line); }

/* --------------------------------------------------------- shell + top */
.shell { margin-left: 240px; min-height: 100vh; display: flex; flex-direction: column; }
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 0 20px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.topbar .where { font-size: 13px; color: var(--text-muted); }
.topbar .where strong { color: var(--text); font-weight: 600; }
.topbar .right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.topbar .who { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
.topbar .who b { color: var(--text); font-weight: 600; }

.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.iconbtn:hover { background: var(--hover); }
.iconbtn svg { width: 16px; height: 16px; }
.iconbtn .txt { white-space: nowrap; }
/* `labelled` is what the templates write on a button that carries a word as
   well as a glyph. It had no rule at all until the class-coverage check asked. */
.iconbtn.labelled, .labelled { padding: 0 12px; }
/* One button, two words: the toggle says what it will DO, not what is on. An
   unlabelled icon was reported as "no theme toggle exists" while it was on
   screen, working, at 20:1 contrast. */
.iconbtn .t-light, .iconbtn .i-sun { display: none; }
:root.dark .iconbtn .t-light, :root.dark .iconbtn .i-sun { display: inline; }
:root.dark .iconbtn .t-dark, :root.dark .iconbtn .i-moon { display: none; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.btn:hover { background: var(--hover); text-decoration: none; }


/* ------------------------------------------------- interaction (Batch 1)
   MEASURED BEFORE: 0 transitions on any interactive element, and
   :focus-visible declared only for input, select and textarea - so a person
   tabbing through a page of links and buttons saw nothing at all.

   ADDENDUM B: transform and opacity only, 150ms for colour, 160ms for the
   press. Hover changes are inside @media (hover: hover) so a touch device
   does not keep a hover state after a tap. */
a, button, summary, [role="button"], select, .btn, .iconbtn, .seg > * {
  /* MSG-59 #2. `color` IS NOT IN THIS LIST ANY MORE, and that is the fix for
     the half-repainted theme.

     Every themed colour here comes from a custom property - a link's colour
     is var(--link) - and a transition over a var()-derived colour does not
     land on the new value when the variable changes: the element serves the
     OLD colour indefinitely. Measured on 12.10 after switching to dark:

         --link on the element    #8ab4ff          (the new value, correct)
         computed color           rgb(26,95,208)   (the old one, 7.5s later)
         the same element cloned  rgb(138,180,255) (correct - no transition)
         .dark removed, re-added  rgb(124,170,249) <- BETWEEN the two, which
                                  is what proved a transition was running

     Enumerating every rule in every sheet that matched that link found
     exactly one declaring a colour, so nothing was overriding it. The
     transition itself was holding the old value.

     The fade is kept on :hover below, where a colour change is a pointer
     moving over one control rather than the whole document repainting. */
  transition: background-color .15s var(--ease-out),
              border-color .15s var(--ease-out),
              transform .16s var(--ease-out);
}
/* THE COLOUR FADE, on the way in. On :hover rather than on the base state, so
   a theme switch - which hovers nothing - resolves colours immediately. The
   cost is that leaving a hover snaps the colour back instead of fading it,
   because a transition must be declared on the base state to run both ways:
   150ms on the way out, against every colour on the page being right the
   moment the theme changes. */
a:hover, button:hover, summary:hover, [role="button"]:hover, select:hover,
.btn:hover, .iconbtn:hover, .seg > *:hover {
  transition: color .15s var(--ease-out),
              background-color .15s var(--ease-out),
              border-color .15s var(--ease-out),
              transform .16s var(--ease-out);
}

/* A THEME SWITCH IS NOT AN ANIMATION. (MSG-59 #2.)

   `color` above resolves through var(--link), and --link is not a registered
   custom property. When the theme class flips, the custom property changes -
   which is not an interpolatable value change - and the transition this rule
   starts on `color` never reaches the new endpoint. Measured on 12.10: the
   same <a> served rgb(138,180,255) seven and a half seconds after switching
   to light, while :root AND the element itself both resolved --link to
   #1a5fd0; a brand new <a> inserted beside it computed the correct
   rgb(26,95,208). Exactly one theme behind, in both directions.

   So for the single frame in which the class flips, nothing transitions.
   Hover fades are untouched - this class exists only for the duration of the
   switch (app.js adds it, forces a style flush, flips `dark`, flushes again
   and removes it).

   !important and the universal selector are both deliberate: this has to beat
   every transition in the sheet, including ones added later, and a theme
   switch is the one moment when suppressing all of them is correct. */
:root.theme-switching,
:root.theme-switching *,
:root.theme-switching *::before,
:root.theme-switching *::after {
  transition: none !important;
  animation-duration: 0s !important;
  animation-delay: 0s !important;
}
button:active, .btn:active, [role="button"]:active, .iconbtn:active,
.seg > button:active {
  transform: scale(.97);
}
/* EVERY focusable thing, not only the form fields. */
a:focus-visible, button:focus-visible, summary:focus-visible,
[role="button"]:focus-visible, [tabindex]:focus-visible,
details:focus-visible, .seg > button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}
@media (hover: hover) and (pointer: fine) {
  tbody tr.linked:hover, tbody tr:has(a):hover { background: var(--raised); }
}

/* B4 / MSG-57. THE STICKY HEADER WAS EATING THE ANCHOR.
   The top bar is sticky at 52px, so a browser that scrolls an element to the
   top of the viewport puts it UNDER the bar. scroll-margin-top on everything
   that can be the target of a #fragment - not only on alerts, which was the
   one place it had been applied. 84px = the 52px bar, the border, and enough
   room that the row has a line of context above it. */
[id] { scroll-margin-top: 84px; }
html { scroll-padding-top: 84px; }

/* ADDENDUM B. Gentler, not zero: the fades stay at 150ms, every transform
   and every height change goes. */
@media (prefers-reduced-motion: reduce) {
  a, button, summary, [role="button"], select, .btn, .iconbtn, .seg > * {
    transition: background-color .15s linear, color .15s linear;
  }
  button:active, .btn:active, [role="button"]:active, .iconbtn:active,
  .seg > button:active { transform: none; }
}

/* --------------------------------------------- buttons, one hierarchy
   Primary: solid accent, at most one per view, for the main action.
   Secondary: surface fill and a border - the default.
   Quiet: text only, for "edit threshold", "change status", "Remove". */
/* --accent-strong, not --accent: white text on #2575fc is 4.18:1 and text
   needs 4.5. --accent stays the colour of links, borders and chart bars,
   where 3:1 is the bar and it clears it comfortably. */
.btn[data-variant="primary"], .btn.primary {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  color: var(--accent-ink);
}
.btn[data-variant="primary"]:hover, .btn.primary:hover {
  background: var(--accent-press);
  border-color: var(--accent-press);
}
.btn[data-variant="quiet"], .btn.quiet {
  background: transparent;
  border-color: transparent;
  color: var(--link);
  padding: 0 6px;
}
.btn[data-variant="quiet"]:hover, .btn.quiet:hover {
  background: var(--raised);
  text-decoration: underline;
}

/* A SEGMENTED CONTROL, not four loose buttons. Used for the alert statuses
   in Batch 4; declared here with the rest of the button hierarchy. */
.seg {
  display: inline-flex;
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 2px;
  gap: 2px;
}
.seg > button, .seg > a {
  appearance: none;
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 4px 10px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  /* MSG-59 #12: 222 of 239 interactive elements on /alerts were under 24px
     tall, and these status segments are nearly all of them. An explicit
     min-height rather than trusting padding plus a line box to add up. */
  min-height: 28px;
  display: inline-flex;
  align-items: center;
}
.seg > button:hover, .seg > a:hover { color: var(--text); text-decoration: none; }
.seg > button[aria-pressed="true"], .seg > a[aria-current="true"] {
  background: var(--card);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
}

main {
  width: 100%;
  max-width: 1560px;
  /* Batch 1: anchored to the rail, not centred in the shell. */
  margin: 0 auto 0 0;
  padding: 24px 32px 0;
}
footer.foot {
  width: 100%;
  max-width: 1560px;
  margin: 0 auto 0 0;
  padding: 24px 32px;
  color: var(--text-muted);
  font-size: 11.5px;
}

/* ----------------------------------------------------------- headings */
.page-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.page-head h1 { margin: 0; }
.whoteam {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 18px;
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--text);
}
.whoteam b {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  margin-right: 5px;
}
.sectionhead {
  font-size: 16px;
  margin: 24px 0 4px;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--text);
}
/* #14: SECONDARY TEXT IS 12.5px, not 13px - MSG-56's figure. This is the
   only thing this pass makes smaller, and it is the one the spec names;
   everything carrying content went UP to 14px. */
.muted { color: var(--text-muted); font-size: 12.5px; margin: 4px 0; }
.lede { margin: 2px 0 0; }
.sp-xs { margin-top: 6px; }
.sp-sm { margin-top: 8px; }
.sp-md { margin-top: 10px; }
.sp-lg { margin-top: 14px; }
.sp-xl { margin-top: 18px; }

/* ------------------------------------------- the client tab strip (1.5) */
.tabs {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: flex-start;
  gap: 2px;
  margin: 14px 0 4px;
  border-bottom: 1px solid var(--border);
}
.tabs a {
  padding: 7px 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.tabs a:hover { color: var(--text); text-decoration: none; }
.tabs a.on { color: var(--text); font-weight: 600; border-bottom-color: var(--text); }
.tabs .soon { margin-left: auto; font-size: 12px; color: var(--text-muted); }

/* ------------------------------------------------ the shell (Batch 1)
   B19. MARKO, 4.10: "kad stisnem na logo da me odvede na home". The mark and
   the wordmark are one link; the chevron beside them is not, because
   collapsing the rail and leaving the page are different intentions. */
.brandlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: inherit;
  text-decoration: none;
  border-radius: 6px;
}
.brandlink:hover { text-decoration: none; color: var(--shell-ink); }
.brandlink:hover .mark { opacity: .85; }

/* ONE COLLAPSE CONTROL ON THE DESKTOP. The rail chevron and this button wrote
   the same preference. The button keeps its word - a check asserts it carries
   one, and an unlabelled icon was once reported as not existing while it was
   on screen - and appears where the rail itself is not: under 640px. */
.narrowonly { display: none; }
@media (max-width: 640px) {
  .narrowonly { display: inline-flex; }
}

/* ENGINE INTERNALS. A view name, a table name, a grain, a milestone id: real
   information, and useful to exactly one reader. Rendered only for an admin
   and only when "Show engine details" is on, so the default is off and
   nothing is deleted - the text stays in the DOM, which is what the prose
   checks read. */
/* The internals line in the SIDEBAR FOOTER. Its own class, because it is a
   different thing from the in-page debug spans: MSG-56 puts it here for an
   admin, so it is always visible to that reader and not behind the toggle. */
.railnote {
  font-size: 11.5px;
  color: var(--shell-muted);
  font-variant-numeric: tabular-nums;
}

/* The in-page internals: a view name, a grain, "M52 part B". Hidden unless
   an admin turns on "Show engine details" in the top bar. Default off, and
   the text stays in the DOM - which is what the prose checks read. */
.debug {
  font-size: 11.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
:root:not(.showdebug) .debug { display: none; }

/* --------------------------------------------- the page header (Batch 2)
   ONE HEADER FOR EVERY PAGE. Measured before: every page started at a
   different height, Showcase had no h1 at all, and content began at a
   different x depending on which page you were on. */
.pagehead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}
.pagehead-text { min-width: 0; }
.pagehead h1 { margin: 0; }
/* ONE LINE, ~110 characters. What the page is - not how it works, which is
   what the disclosure below is for. */
.pagehead-sub {
  margin: 6px 0 0;
  font-size: 12.5px;
  color: var(--text-muted);
  max-width: 76ch;
}
/* The window and the freshness: "1-30 Sep 2026 - last pull 29 Sep 02:23". */
.pagehead-meta {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.pagehead-actions { display: flex; align-items: center; gap: 8px; }

/* MSG-63. A row of controls INSIDE a card - the two ClickUp buttons on the
   Calendar tab. Not .pagehead-actions, which is the row in a page heading:
   same shape, different place, and a class named for the wrong place is how
   somebody later changes one and breaks the other. Wraps, because two
   buttons with long labels do not fit a narrow card. */
.ctlrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* ------------------------------------------ the disclosure (Batch 2)
   WHERE THE EXPLANATION WENT. Not away - down, closed, and still in the DOM.
   Every page opened with a paragraph about how its own numbers are worked
   out; all of it true, needed once, and in the way every time somebody
   opened the page to read a number. */
details.explain {
  margin: 12px 0 0;
  font-size: 12.5px;
  color: var(--text-muted);
}
details.explain > summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--link);
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
}
details.explain > summary::-webkit-details-marker { display: none; }
details.explain > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform .2s var(--ease-out);
}
details.explain[open] > summary::before { transform: rotate(90deg); }
details.explain > summary:hover { text-decoration: underline; }
.explain-body {
  margin-top: 8px;
  padding-left: 11px;
  border-left: 2px solid var(--border);
  max-width: 84ch;
}
.explain-body p { margin: 0 0 8px; }
.explain-body p:last-child { margin-bottom: 0; }

/* ----------------------------------------- the breadcrumb (Batch 2)
   On every page now. `.where` already had a rule; these are the parts the
   old six-path `if` never needed. */
.where { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* NO OPACITY. --text-muted is already a reduction, and .6 on top of it
   composited to 2.14:1 in light and 3.21:1 in dark. Two reductions
   multiply, and the audit measures the result rather than the declaration. */
.crumbsep { color: var(--text-muted); }
.enginetoggle { margin: 0; }

/* ================================================ Batch 3: the client page */

/* ---------------------------------------------- the two rows of tabs
   EIGHTEEN TABS IN ONE ROW IS A ROW NOBODY READS. The primary row is the
   group; the secondary row is that group's tabs. Rendered from
   clienttabs.py, so the grouping is a thing a check can ask about. */
.tabgroups {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  margin: 0 0 2px;
  border-bottom: 1px solid var(--border);
}
.tabgroup {
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text-muted);
  border-radius: 8px 8px 0 0;
  white-space: nowrap;
}
.tabgroup:hover { color: var(--text); background: var(--raised); text-decoration: none; }
.tabgroup.on {
  color: var(--text);
  background: var(--card);
  box-shadow: inset 0 -2px 0 var(--accent);
}
/* EVERY TAB IS STILL IN THE DOM. Half a dozen checks grep this strip for a
   data-verify attribute or an href, and MSG-56 rule 4 keeps every element
   verify.sh reads. The other groups' tabs are here and reachable by clicking
   their group; this list is what a check and the crawler read. */
.tabs-all { display: none; }

/* ------------------------------------------------- the stat tiles (A4)
   Label in SENTENCE case - "Open rate", not "OPEN RATE". That is this rule
   only: table headers stay uppercase, which is the other half of A4. */
.statrow {
  display: grid;
  /* 128px, not 150: four tiles at 150 plus gaps is a 600px minimum, which is
     what pushed the chart column to 60px. They wrap to two rows sooner now,
     which is the right trade - a tile reads fine on two rows and a 60px
     chart does not read at all. */
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 12px;
  margin: 12px 0;
}
.statrow.small { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); }
.stat {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  min-width: 0;
}
.stat-label {
  display: block;
  font-size: 12.5px;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.stat-value {
  display: block;
  font-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: 30px;
  font-weight: 800;
  line-height: 1.1;
  /* A2: proportional figures on a big standalone number. */
  font-variant-numeric: proportional-nums;
}
.stat.tiny .stat-value { font-size: 20px; }
/* A DASH IS NOT A NUMBER. Muted, so a missing reading does not read as a
   measurement - and never 0, which is a reading. */
.stat-absent { color: var(--text-muted); font-weight: 700; }

/* A5: the colour follows "is up good", not the arrow. The template decides
   which class; this only paints it. */
.delta {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  margin-top: 6px;
  font-size: 12.5px;
  font-weight: 600;
}
.delta-arrow { font-size: 11.5px; line-height: 1; }   /* the floor applies to a glyph too */
.delta.good { color: var(--delta-good); }
.delta.bad  { color: var(--delta-bad); }
.delta.flat { color: var(--text-muted); font-weight: 400; }

/* ------------------------------------------------------- the charts */
.splitgrid {
  display: grid;
  /* A REAL FLOOR ON THE CHART COLUMN (A6). It was minmax(0, 1.1fr), and the
     tiles beside it have a 600px minimum of their own - so the chart column
     gave up everything it had and rendered 60px wide, which turned a 46-unit
     column slot into a 5px hit area. 360px is enough for twelve slots at the
     24px A6 asks for. */
  grid-template-columns: minmax(0, 1fr) minmax(360px, 1.1fr);
  gap: 20px;
  align-items: start;
}
/* Stack BEFORE either side is squeezed, not after. */
@media (max-width: 1180px) {
  .splitgrid { grid-template-columns: minmax(0, 1fr); }
}
.chart { margin: 0; min-width: 0; }
.chart-cap {
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.chart-unit { text-transform: none; letter-spacing: 0; font-weight: 400; }
/* height: auto IS NOT OPTIONAL HERE. `svg { width: 20px; height: 20px }`
   near the top of this file constrains every icon, and overriding only the
   width left this at 20px tall - so preserveAspectRatio scaled the whole
   chart to 13% and nobody could see it was wrong, only that it was small. */
.chart-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
/* A8: SOLID hairlines, never dashed. */
.chart-grid { stroke: var(--grid-line); stroke-width: 1; }
.chart-base { stroke: var(--border); stroke-width: 1; }
/* A1: every month muted, the current one in the accent. */
.chart-bar { fill: var(--series-muted); transition: fill .15s var(--ease-out); }
.chart-col.is-current .chart-bar { fill: var(--series-1); }
.chart-hit { fill: transparent; }
.chart-col { cursor: default; }
.chart-col:hover .chart-bar, .chart-col:focus-visible .chart-bar { fill: var(--accent-strong); }
.chart-col:focus { outline: none; }
.chart-col:focus-visible .chart-hit {
  fill: var(--hover);
  outline: 2px solid var(--focus);
}
/* A GAP IS DRAWN AS A GAP, not as a bar of height zero. */
.chart-gapmark { stroke: var(--border-strong); stroke-width: 2; stroke-dasharray: 2 2; }
.chart-val {
  fill: var(--text);
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.chart-tick {
  fill: var(--text-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.chart-none {
  margin: 0;
  padding: 16px;
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: 8px;
  font-size: 12.5px;
  color: var(--text-muted);
}
.chart-tip {
  margin-top: 6px;
  font-size: 12.5px;
  color: var(--text);
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  display: inline-block;
}
.chart-tip[hidden] { display: none; }
details.chart-table { margin-top: 10px; font-size: 12.5px; }
details.chart-table > summary {
  cursor: pointer;
  color: var(--link);
  font-weight: 600;
  list-style: none;
}
details.chart-table > summary::-webkit-details-marker { display: none; }
.chart-table-body { margin-top: 8px; overflow-x: auto; }

/* ------------------------------------------- must fix, as compact rows
   A SEVERITY STRIPE, NOT A COLUMN. The six-column table said the same thing
   in more space, and the thing a person needs first is "which rule, how far
   off, how many". */
.rulerows { list-style: none; margin: 8px 0 0; padding: 0; }
.rulerow {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 10px 12px 10px 14px;
  background: var(--card);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: 8px;
  margin-bottom: 6px;
  font-size: 13px;
}
.rulerow.sev-critical, .rulerow.sev-high { border-left-color: var(--bad); }
.rulerow.sev-medium { border-left-color: var(--warn); }
.rulerow.sev-low { border-left-color: var(--neutral); }
.rulerow-name { font-weight: 700; }
.rulerow-what { color: var(--text-muted); }
.rulerow-now { font-variant-numeric: tabular-nums; }
.rulerow-arrow { color: var(--text-muted); font-size: 11.5px; }
.rulerow-target { font-variant-numeric: tabular-nums; }
.rulerow-open { color: var(--text-muted); margin-left: auto; white-space: nowrap; }
details.needswork > summary { font-weight: 700; }

/* ------------------------------------------------ top flows, ranked
   RELATIVE TO THE LEADER, not to an axis: the question is "which of ours
   earns most", and an axis answers a different one. */
.ranked { list-style: none; margin: 8px 0 0; padding: 0; counter-reset: rank; }
.rankrow {
  display: grid;
  grid-template-columns: minmax(8rem, 18rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 5px 0;
  font-size: 13px;
}
.rank-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rank-bar {
  display: block;
  height: 10px;
  background: var(--surface);
  border-radius: 5px;
  overflow: hidden;
}
.rank-bar > span {
  display: block;
  height: 100%;
  background: var(--series-1);
  border-radius: 5px;
}
.rank-val { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ------------------------------------------------- H6: the first run */
.firstrun-prog { margin: 8px 0; font-size: 13px; }
.bar {
  display: block;
  height: 8px;
  background: var(--surface);
  border-radius: 4px;
  overflow: hidden;
  max-width: 28rem;
}
.bar > span { display: block; height: 100%; background: var(--accent); }

/* ------------------------------- the hero's four numbers (ADDENDUM A3)
   The hero figure is at least 48px and there is exactly one per view, so
   these four are KPI-sized rather than hero-sized: four 48px numbers would
   be four heroes. */
.card.hero .hero-n {
  font-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: 30px;
  font-weight: 800;
  line-height: 1.1;
  font-variant-numeric: proportional-nums;
}

/* ================================================= Batch 4: alerts + today */

/* ---------------------------------------------------- one row per alert
   It was a block with two lines of facts, four loose buttons and a
   <details> holding four people pickers - about 8 KB per alert and 171
   buttons on one screen. */
.alertrow {
  display: grid;
  /* MSG-59 #12. FIXED COLUMNS, because every row is its own grid.

     `display: grid` is on the ROW, so each row sized its own `auto` columns
     to its own content: a row carrying the optional "Team notified" pill made
     that column wide for that row alone and pushed everything after it
     sideways. The review read it as the pill "pushing the value column" - the
     cause is that the columns were content-sized per row, so no two rows
     agreed on where a column starts.

     Five of the eight are fixed now. `what` keeps the 1fr so it absorbs the
     slack, and `client` keeps its range because a client name is the one
     thing worth giving room to. */
  grid-template-columns:
    4.5rem                    /* severity */
    minmax(6rem, 10rem)       /* client */
    /* MSG-72: A FLOOR, measured. This was minmax(0, 1fr) and the
       statuses column beside it is `auto`, so at 1440 the grid gave the
       control its 336px of max-content and this column 0 - and a 167px
       name in a 0px box prints over the two columns after it. The
       control wraps between buttons now, so it can give the space
       back. */
    minmax(6rem, 1fr)         /* what */
    7.5rem                    /* now vs target */
    5.5rem                    /* since */
    6.5rem                    /* owner and told - fixed, so an absent pill
                                 leaves the same gap as a present one */
    auto                      /* the statuses */
    5rem;                     /* details */
  align-items: center;
  gap: 4px 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--row-line);
  /* #14: was 13px. MSG-56 sets row text at 14px. */
  font-size: 14px;
}
.alertrow:last-child { border-bottom: 0; }
@media (hover: hover) and (pointer: fine) {
  .alertrow:hover { background: var(--raised); }
}
/* F8 (MSG-58). WAS `opacity: .72`, AND THAT CAUSED ~40 OF THE 52 FAILURES.
   Opacity composites every text node under it, so the white pill text read
   as beige on its own background (3.20:1), the links as washed blue
   (3.22:1), the timestamps and status words as washed grey. None of those
   colours is written anywhere - the audit folds the ancestor's opacity in,
   which is the whole point of it.
   Line 1736 of this file already says "Quieter by SURFACE, never by opacity
   - opacity fades text towards the background and takes the contrast with
   it." Written for another element, violated here. Same mistake as
   .crumbsep in MSG-56.
   The tint is chosen against the muted grey that sits on it: 5.6:1 light,
   5.8:1 dark. A triaged row still reads as quieter; nothing on it is
   harder to read. */
.alertrow.dim { background: #f9f9f9; }
/* THE DARK TINT USES :root.dark, WHICH IS HOW THIS STYLESHEET SELECTS DARK.
   Two wrong attempts before this one, both caught by the audit rather than
   by reading:
     1. a bare `:root:not([data-theme="light"])` outside any media query,
        which matches in the LIGHT default too - so light rows went #232832
        and 16 new failures appeared;
     2. `[data-theme="dark"]`, which never matches here at all: base.html
        writes `<html class="dark">`, a CLASS, so the dark rows stayed
        #f9f9f9 and the audit reported light text on a near-white row.
   The theme is a class on :root. `grep ":root.dark"` is what the rest of
   this file does, and it is what this does now. */
:root.dark .alertrow.dim { background: #232832; }
.sevpill { white-space: nowrap; }
.arow-client { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* H8: one line and the full name in the title attribute. */
.arow-what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* MSG-72: BETWEEN WORDS, NEVER INSIDE ONE (R34). This was
   `white-space: nowrap` with nothing clipping it, so a target phrase
   longer than the 7.5rem column - "vs >= 75% of the baseline" - printed
   across the date column beside it. Measured at 1440 and 1600: the same
   120px column, the same overflow. The figure itself is one word and
   cannot break. */
.arow-fig { font-variant-numeric: tabular-nums; white-space: normal;
            overflow-wrap: normal; word-break: normal; }
/* MSG-72: between words, never inside one (R34). "17 Sep 2026, 12:00"
   is ~105px of text in an 88px column, and nowrap printed the time
   under the pill beside it. */
.arow-since { white-space: normal; overflow-wrap: normal;
              word-break: normal; font-size: 12.5px; }
.arow-who { display: flex; gap: 4px; flex-wrap: wrap; }
/* MSG-72: a pill in this column keeps to the column. "Team notified -
   not confirmed" is three lines in 6.5rem, and a capsule radius on
   three lines is an ellipse painted over the control beside it. */
.arow-who .pill { max-width: 100%; border-radius: 8px; }
.arow-seg { margin: 0; }
.arow-more { white-space: nowrap; font-weight: 600; }

/* The row becomes two lines rather than eight squeezed columns. A row that
   has to be read sideways is not a row.

   1359, NOT 1100, SINCE MSG-68 A1. Measured 20px at a time with a group
   open: the eight-column row needs 1331px and fits from 1340 up, so the
   whole band 1101..1339 scrolled the page sideways the moment somebody
   expanded a group - which nobody could do before A1, because no group on
   this screen was ever open and no row ever contributed its width.

       1120px  document 1292 / viewport 1120   over by 172
       1280px  document 1331 / viewport 1280   over by  51
       1320px  document 1331 / viewport 1320   over by  11
       1340px  document 1340 / viewport 1340   ok

   The number is the MEASUREMENT plus margin, not the last failing sample -
   the same argument the 700px query below makes about 680. The segmented
   control is deliberately untouched: making it wrap at every width fixed
   this and folded five buttons at 1440, which took a row from 54px to 81px
   and broke MSG-56's "a row is ONE row". */
@media (max-width: 1359px) {
  .alertrow {
    grid-template-columns: auto minmax(0, 1fr) auto;
    row-gap: 6px;
  }
  .arow-seg, .arow-who { grid-column: 1 / -1; }
}

/* ------------------------------------------------- the filter bar */
.morefilters { margin: 0; font-size: 12.5px; }
.morefilters > summary { cursor: pointer; font-weight: 600; color: var(--link); }
.morefilters-body {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
/* THE APPLY BUTTON IS HIDDEN ONLY WHEN THE SCRIPT IS THERE TO REPLACE IT.
   :root.js is set by app.js, so a page whose script never arrived keeps a
   button that works - the filters are a GET form and they always have. */
:root.js .applybtn { display: none; }

/* ------------------------------------------- the drawer (ADDENDUM B 1) */
.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(8, 11, 18, .45);
  opacity: 0;
  transition: opacity .2s var(--ease-out);
  z-index: 60;
  /* IT DIMS, IT DOES NOT BLOCK. A modal backdrop would stop somebody
     clicking the next row, and ADDENDUM B item 1 is explicitly about opening
     the next alert while one is open. Closing on a click outside is done by
     a document listener in app.js instead. */
  pointer-events: none;
}
.drawer-backdrop.open { opacity: 1; }
.drawer-backdrop[hidden] { display: none; }
.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(34rem, 100%);
  background: var(--card);
  border-left: 1px solid var(--border);
  box-shadow: -8px 0 24px rgba(8, 11, 18, .18);
  z-index: 61;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  /* ENTER 240ms on --ease-drawer. The exit is 180ms and is set on the
     element without .open, below. */
  transition: transform .24s var(--ease-drawer);
}
.drawer.open { transform: translateX(0); }
.drawer:not(.open) { transition: transform .18s var(--ease-out); }
.drawer[hidden] { display: none; }
/* THE DRAWER OVERLAYS. THE LIST KEEPS ITS WIDTH. (MSG-59 #1, which reverses
   MSG-58 Part C's "the drawer narrows the list" - MSG-60 Part A accepts the
   reversal by name.)

   What was here: `:root.drawer-open main { padding-right: min(35rem, 60%) }`
   plus two rules reflowing .alertrow at that width, and a transition on
   padding-right. Three things wrong with it, all of them observed on the live
   page on 6.10:

     - the list became ~600px, so every row stacked onto four lines and each
       severity pill stretched across its cell as a ~190px solid bar;
     - padding-right is a LAYOUT property, so animating it reflowed all 25
       rows on every frame of a 240ms transition;
     - and if the class ever outlived the drawer - which is what the review
       reported - the page stayed broken until a reload, because nothing on
       screen explained why the list was narrow.

   The drawer is `position: fixed` and slides on `transform` already, so it
   needs no room made for it. Opening the next row while one is open still
   works: the backdrop only dims and does not swallow clicks (app.js), so a
   click on a row behind it reaches that row. ADDENDUM B item 1 is satisfied
   by the content swap, not by moving the list.

   `drawer-open` itself stays - the drawer's affordances key off it - and is
   still removed on every close path. It no longer carries any layout. */
@media (prefers-reduced-motion: no-preference) {
  main, footer.foot { transition: none; }
}

/* A PILL NEVER STRETCHES. It was the squeeze that made this visible - a pill
   in a wide grid cell filling it as a solid bar - but a pill is as wide as
   its word wherever it sits, so this belongs on the pill and not on the
   layout that exposed it. */
.pill, .chip, .sevpill, .statepill, .agepill {
  justify-self: start;
  align-self: center;
  width: max-content;
  max-width: 100%;
}

.drawer-bar {
  display: flex;
  justify-content: flex-end;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}
.drawer-close {
  appearance: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 4px 10px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
}
.drawer-body { padding: 16px 18px 24px; overflow-y: auto; }
.drawerhead { margin-top: 0; }

/* GENTLER, NOT ZERO. The drawer fades in place and nothing slides. */
@media (prefers-reduced-motion: reduce) {
  .drawer {
    transform: none;
    opacity: 0;
    transition: opacity .15s linear;
  }
  .drawer.open { opacity: 1; }
  .drawer:not(.open) { transition: opacity .15s linear; }
}

/* ----------------------------------------------- the alert's own page */
.alertdetail { max-width: 46rem; }
.alertdetail .paneldetail {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 16px;
  margin: 0 0 16px;
  font-size: 13px;
}
.alertdetail .paneldetail dt {
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.alertdetail .paneldetail dd { margin: 0; }

/* H1: the audit trail's paging. Newest / Newer / page N of M / Older /
   Oldest - because a trail shown a window at a time has to let you move the
   window, which is what R9 asks of it. */
.paging {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 16px 0;
  font-size: 12.5px;
}

/* B17's rows, and anywhere else a list is sentences rather than bullets. */
ul.plain { list-style: none; margin: 8px 0 0; padding: 0; }
ul.plain > li { padding: 4px 0; font-size: 13px; }

/* -------------------------------------------------- Today's three tiles */
.todaytiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 12px;
  margin: 0 0 24px;
}
.todaytile {
  display: block;
  background: var(--card);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 14px 16px;
  color: inherit;
}
.todaytile:hover { background: var(--raised); text-decoration: none; }
.todaytile .tt-n {
  display: block;
  font-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: 30px;
  font-weight: 800;
  line-height: 1.1;
  font-variant-numeric: proportional-nums;
}
.todaytile .tt-l { display: block; font-size: 12.5px; color: var(--text-muted); }
.todaytile.none { border-left-color: var(--border-strong); }
.todaytile.none .tt-n { color: var(--text-muted); }

/* ---------------------------------- B9: the block select's question
   It had no label at all - its only words were "no block chosen", which says
   what it is not. It records which block of THIS email the animation goes
   in, and the designer reads that off the brief. */
.blocklabel {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-muted);
}

/* ------------------------------- B16: the definition, on the row
   A definition is a sentence of conditions - "Placed Order >=2 over all time
   / AND / Revenue >=382.28 / ..." - so it reads as code rather than prose,
   and it wraps rather than being cut: a definition you can only see half of
   is a definition you have to click for, which is what B16 is about. */
.segdef {
  margin-top: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
  overflow-wrap: anywhere;
  max-width: 44rem;
}
.segdef-gap {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--warn);
}

/* ------------------------------------------- B7: a swatch is a colour
   Section 6.3's stored text has a SWATCH column full of HTML the MODEL
   wrote - <span style="color:#000000">a block character</span> - and the
   template escapes it, which is right: rendering model markup would be an
   injection path onto the page. So the colour is recognised, the hex is
   validated, and the chip is drawn by us. This is that chip. */
.swatchchip {
  display: inline-block;
  width: 1.1em;
  height: 1.1em;
  vertical-align: -0.2em;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
}

/* ---------------------------------------- the client header (Batch 3)
   The name and its meta line on the left; the freshness pill and the Ask
   button on the right. Wraps to two rows under 700px rather than squeezing
   the name. */
.clienthead { align-items: flex-start; margin-bottom: 12px; }
.clienthead .pagehead-actions { flex-wrap: wrap; justify-content: flex-end; }

/* H8. A LONG NAME WRAPS HERE. "Peter 8 Brands - Hike-Care DE" and a
   60-character name both have to be readable, and a heading is the one place
   with room for them - overflow-wrap, not ellipsis, because the h1 is where
   you find out what the whole name is. The tables are where a name gets one
   line and a title attribute (td.nameco, Batch 1). */
.clientname {
  overflow-wrap: break-word;
  max-width: 44ch;
}

/* ------------------------------- B3: who is on this client, and a way in
   The picker was a <select multiple> three quarters of the way down the
   panel. It still is; this block is what sends you to it, so replacing a
   copywriter starts from the list of who is on the team. */
.teamblock {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
}
.teamblock h3 { margin-top: 0; }
.teamfacts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 16px;
  margin: 0 0 12px;
  font-size: 13px;
}
.teamfacts dt {
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  align-self: baseline;
}
.teamfacts dd { margin: 0; }

/* ----------------------------------- B2: what the last save actually stored
   Read out of the row after the write, not out of the request - which is the
   difference between "we sent it" and "it is there". */
.savednote {
  margin: 0 0 16px;
  padding: 10px 14px;
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  font-size: 13px;
  color: var(--text);
}

/* ------------------------------------------------- the Ask button */
.askbtn { white-space: nowrap; }

/* -------------------------------------------------------------- cards */
.card {
  display: block;
  background: var(--card);
  border: 1px solid var(--border);
  /* 13px and a 1px line, no heavy drop shadow (spec, SHELL). */
  border-radius: var(--radius-card);
  /* Batch 1 SPACING: card padding 20px, 16px between cards. */
  padding: 20px;
  margin: 16px 0;
  /* MSG-61 Part E (Marko, 12.10.2026): NO overflow HERE.
     `overflow-x: auto` on a card made every card on every page a scroll
     container - CSS computes the other axis to auto with it - so
     el.scrollIntoView() inside a card scrolled the CARD by zero and the
     document never moved. A table that needs horizontal room gets
     .table-container, which is the wrapper below. */
  color: var(--text);
}
/* 32px between page sections, 16px between cards. A section is a run of
   cards under one heading. */
.pagesection { margin-top: 32px; }
.pagesection > h2:first-child { margin-bottom: 12px; }

/* THE ONE DARK CARD PER SCREEN (spec): the hero metric. A modifier on the
   card that is already there - nothing is moved to make room for it. */
.card.hero {
  /* THE ONE DARK CARD PER SCREEN, IN BOTH THEMES. The five properties are
     redefined HERE rather than globally, so every rule already written
     against .card.hero - its KPI tiles, its tables, its links, the ones the
     render audit measured the day the card went dark and its children did
     not - keeps working unchanged, and the rail is left alone. */
  --shell:        #0b0f1a;
  --shell-ink:    #eef1f6;   /* 17.4:1 on the card */
  --shell-muted:  #a3acbb;   /*  8.6:1 on the card */
  --shell-line:   #1b202b;
  --shell-active: #182034;
  --shell-link:   #9dc0ff;   /*  9.0:1 on the card */
  background: var(--shell);
  border-color: var(--shell-line);
  color: var(--shell-ink);
}
.card.hero h2, .card.hero h3 { color: var(--shell-ink); }
/* MSG-59 #7's disclosure, inside the one dark card. `details.explain >
   summary` takes var(--link) - #1a5fd0 in the light theme - and .card.hero
   redefines --shell to #0b0f1a on itself, which put the summary at 3.27:1
   against AA's 4.5. The hero already carries a link token measured at 9.0:1
   for exactly this, and the body takes the hero's muted (8.6:1): a closed
   <details> gives the render audit nothing to measure, so the body is fixed
   on the same reasoning rather than only the line that was visible. */
.card.hero details.explain,
.card.hero .explain-body { color: var(--shell-muted); }
/* MSG-77-FIX B5 (Q47). The subtitle inside the dark hero. It took the
   light theme's --text-muted (#626c81) on #0b0f1a = 3.63:1, which is
   the "grey-on-black text hard to read" of the walkthrough's Showcase
   row. The hero's own muted is 8.6:1 on the same card. */
.card.hero .pagehead-sub,
.card.hero .pagehead-meta,
.card.hero .pagenote,
.card.hero p.muted,
.card.hero .muted { color: var(--shell-muted); }
.card.hero details.explain > summary { color: var(--shell-link); }

/* A Klaviyo id printed beside an unresolved name on the overview's rank rows.
   It carries .muted for its colour; this is the half .muted cannot say - an
   id is one token and must not break mid-string. */
.rank-id { white-space: nowrap; font-size: 12px; }
.card.hero .muted, .card.hero .lede { color: var(--shell-muted); }
.card.hero a { color: var(--shell-link); }
.card.hero .value {
  display: block;
  /* ADDENDUM A3: at least 48px, one per view. Was 25px. */
  font-size: 48px;
  line-height: 1.05;
  font-weight: 800;
  color: var(--shell-ink);
}
.card.hero .unit { font-size: 0.6em; font-weight: 700; color: var(--shell-muted); }

/* EVERYTHING INSIDE THE HERO IS ON THE HERO. The card went dark and its
   children kept their light-theme treatment: the KPI tiles stayed white with
   the hero's pale ink on them (1.18:1 - invisible), and the tables kept a
   muted grey meant for white (3.57:1). The rendered audit measured all three
   the moment the page was added to its list. */
.card.hero .kpi {
  background: transparent;
  border-color: var(--shell-line);
  color: var(--shell-ink);
}
/* The label is a <label>, not a .kpi-label - the markup predates the class. */
.card.hero .kpi .muted,
.card.hero .kpi label,
.card.hero .kpi-label { color: var(--shell-muted); }
.card.hero .kpi b, .card.hero .kpi strong { color: var(--shell-ink); }
.card.hero th { color: var(--shell-muted); border-bottom-color: var(--shell-line); }
.card.hero td { color: var(--shell-ink); border-bottom-color: var(--shell-line); }
.card.hero tbody tr:hover { background: var(--shell-active); }
.card.hero code { background: var(--shell-active); color: var(--shell-ink); }
.card.hero .pill { border-color: var(--shell-line); }

/* THE KPI CARD: label / value / delta+period (spec). */
.kpi-label {
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.kpi-value { font-size: 25px; font-weight: 800; display: block; }
.kpi-value .unit { font-size: 0.6em; font-weight: 700; color: var(--text-muted); }
.delta {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  background: #eef0f5;
  color: var(--text);
}
.delta.up   { background: #e8f6ee; color: #12653f; }
.delta.down { background: #fdecec; color: #a01b12; }
:root.dark .delta      { background: #262b33; }
:root.dark .delta.up   { background: #17301f; color: #8ce0b0; }
:root.dark .delta.down { background: #3a1d1a; color: #ffb3aa; }
/* Quieter by SURFACE, never by opacity - opacity fades text towards the
   background and once took muted text to 2.25:1. */
.card.handled { background: var(--surface); }
.card.tier-benchmark { border-style: dashed; }

/* ================= THE FOUR REQUIRED STATES (spec) =====================
   loading  skeleton blocks shaped like the content that follows
   empty    dashed card: why it is empty, what happens next, one action
   error    what broke, what it means for the numbers, what to do, last sync
   partial  the data, with a visible "stale since <time>" note

   `.empty` already existed and is restyled in place. The other three are
   components for the surfaces that render them - no markup is added here. */

.skeleton {
  display: block;
  height: 12px;
  margin: 8px 0;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--surface) 25%,
                              var(--hover) 37%, var(--surface) 63%);
  background-size: 400% 100%;
  animation: skeleton 1.4s ease infinite;
}
.skeleton.title { height: 18px; width: 40%; }
.skeleton.value { height: 25px; width: 28%; }
.skeleton.row   { height: 10px; }
@keyframes skeleton { 0% { background-position: 100% 50%; }
                      100% { background-position: 0 50%; } }
/* A person who has asked not to see motion gets a plain block, not a pulse. */
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}

.state-error {
  display: block;
  border: 1px solid var(--bad);
  border-left-width: 3px;
  border-radius: var(--radius-card);
  background: var(--card);
  padding: 12px 14px;
  margin: 12px 0;
}
.state-error h3 { color: var(--bad); font-size: 13.5px; }
.state-error .what { margin: 4px 0; }
.state-error .lastgood { color: var(--text-muted); font-size: 12px; }

.state-partial {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--warn);
  border-radius: 999px;
  background: var(--stale-row);
  color: var(--text);
  padding: 2px 10px;
  font-size: 12px;
  font-weight: 600;
  width: fit-content;
  margin: 8px 0;
}
.state-partial::before {
  content: "";
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--warn); flex: none;
}

.empty {
  display: block;
  text-align: left;
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-card);
  padding: 14px 16px;
  margin-top: 8px;
  color: var(--text);
}
.empty p { margin: 2px 0; }
.problems {
  background: var(--card);
  border: 1px solid var(--bad);
  border-left: 3px solid var(--bad);
  border-radius: 6px;
  padding: 12px 16px;
  margin: 12px 0;
}
.problems ul { margin: 6px 0 0 18px; }
.err {
  background: var(--card);
  border: 1px solid var(--bad);
  border-left: 3px solid var(--bad);
  color: var(--text);
  padding: 8px 12px;
  border-radius: 5px;
}
.since {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 13px;
  font-size: 13px;
  margin-top: 12px;
}

/* ------------------------------------------------------------- tables */
/* ------------------------------------------------- MSG-62: a link block
   The three groups on Setup > Data & people that each hold one fact about
   where a client lives outside HQ: its ClickUp folder, its Figma folder and
   what finance has. A rule rather than a bare <div> because the groups need
   separating from each other and from the forms under them - and because a
   class used in a template and defined nowhere is what the class-coverage
   check exists to catch. It caught this one. */
.linkblock {
  margin: 0 0 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.linkblock:last-of-type { border-bottom: 0; padding-bottom: 0; }
.linkblock > p:first-child { margin-top: 0; }

/* ------------------------------------------- MSG-61: two small utilities
   Both are used by controls this round added and both were caught by the
   class-coverage check, which reads every class in every template against
   this sheet. */

/* A form that has to sit on one line inside a table cell: U8's "Mark
   refreshed" and the House rules pencil. `inline-flex` so the control and
   its inputs stay on the baseline; `gap` because margins between form
   controls collapse unpredictably. */
.inlineform {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  /* MSG-77: never wider than what it sits in. It was briefly a second
     `.inlineform` rule at the bottom of this sheet; one fact, one
     place. */
  max-width: 100%;
  margin: 0;
}

/* A LABEL THAT IS READ BUT NOT DRAWN. The House rules pencil's select and
   its reason box each have one: a control with no label is unusable with a
   screen reader, and a visible label in a table cell is noise. The clip
   rectangle is the long-standing form of this - `display: none` would take
   it out of the accessibility tree, which is the opposite of the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------- MSG-61 U6: the showcase
   A FILTER HIDES; IT DOES NOT DROP. `display: none` keeps the element in the
   DOM and takes it out of the layout, so its offsetParent is null - which is
   what "still in the DOM, no longer visible" means to the check that reads
   it. visibility:hidden would have left the gaps; height:0 would have left
   it hit-testable. */
.sc-off { display: none; }

/* "Collapse all images". The class goes on <body>, not on each card, so one
   toggle folds every picture and nothing has to be walked. The cards keep
   their subject, their verdict and their links - it becomes a list. */
body.sc-noimg .scpic { display: none; }
.table-container { width: 100%; overflow-x: auto; }
/* MSG-61 Part E, THE OTHER HALF: where the containment went.
   .card lost `overflow-x: auto` (Marko, 12.10.2026) and that rule had been
   hiding four things. Each rule below is aimed at an element MEASURED
   pushing the page at 360px afterwards, found by walking every box and
   keeping only those with no scrollable ancestor.

   MY FIRST VERSION OF THIS WAS `.card > table.prose`, and it reached none of
   them - the widest table on Model costs sits directly in a .pagesection and
   the Segments table sits in a div inside a card. The comment under the
   700px query below says exactly this about `.card > table`, and I wrote the
   same selector anyway before measuring. */

/* 1. A TABLE, WHEREVER IT SITS. `width: 100%` keeps it filling its box at
   every width, so nothing looks narrower than it did; `display: block` is
   what gives a table a scrollbox at all - overflow on a `display: table`
   element is ignored. */
/* a bare <table> is a table too. The KPIs channel table carries no class
   at all and is 969px wide with a nowrap <th>, so it pushed the page at
   1100px as well as at 360px - this is not a phone problem and the rule is
   not in a width query. */
table.prose,
table.tight,
.card table,
main > table {
  display: block;
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
}
/* ...except where a wrapper already scrolls it. Two scrollboxes around one
   table is two scrollbars for one movement. */
.table-container table,
.chart-table-body table { overflow-x: visible; }

/* 2. A PICTURE FITS ITS BOX. The 35 showcase images are 600px wide and the
   card used to scroll them. The width/height attributes stay on the img -
   they are what stops the page jumping as each one loads - and `height:
   auto` keeps the aspect ratio while max-width does the fitting. */
.card img,
.scpic img { max-width: 100%; height: auto; }

/* 3. SO DOES A FORM CONTROL. The Data tab's select measured 411px inside a
   360px page. */
.card select, .card input, .card textarea,
.entry select, .entry input, .entry textarea { max-width: 100%; }

/* 4. A LONG NAME WRAPS RATHER THAN PUSHING. .arow-seg on Alerts carries a
   segment name as one unbroken string: the form was 141px wide with a 385px
   scrollWidth. `overflow-wrap: anywhere` breaks it; `min-width: 0` is what
   lets a flex item shrink below its content at all. */
.arow-seg,
.arow-seg .seg { min-width: 0; overflow-wrap: anywhere; }
/* MSG-72 B: THE CONTROL WRAPS BETWEEN BUTTONS WHEN IT MUST.
   Measured on /alerts at 1440px on a row that had been triaged: five
   buttons are 381px of content in a 336px column, so Undo was painted
   33px under the Details link and elementFromPoint at its centre
   returned that link - the button existed, was visible, and could not
   be pressed. Four buttons fit, so an untriaged row looks exactly as
   it did; a triaged one is ~30px taller, which MSG-58 weighed against
   a tidy 54px row before anybody had tried to press the fifth. */
.arow-seg .seg { flex-wrap: wrap; row-gap: 4px; }
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 8px;
  font-size: 12px;
  table-layout: auto;
}
th, td {
  text-align: left;
  /* #14: MSG-56 sets table text at 14px. It was inheriting 14px from body in
     most places and 13px wherever a container set 13px, which is how the
     KPIs tab ended up with 12px as its most common size. Stated here. */
  font-size: 14px;
  padding: 8px 10px;
  /* THE ROW DIVIDER IS LIGHTER THAN THE CARD EDGE (spec, TABLES): rows read as
     rows inside a card instead of as a grid drawn over it. */
  border-bottom: 1px solid var(--row-line);
  vertical-align: top;
  overflow-wrap: break-word;
}
th {
  color: var(--text-muted);
  font-weight: 600;
  /* Batch 1: 11.5px is the floor for the whole app. This was 10.5px. */
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
  /* The header keeps the stronger line - it is the edge of the head, not a
     divider between two rows. */
  border-bottom: 1px solid var(--border);
}
tbody tr:hover { background: var(--row-hover); }
tbody tr:last-child td { border-bottom: 0; }
table.tight td, table.tight th { padding: 5px 8px; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Batch 1 / H8. "Varnoc/k" and "churne/d" broke because the column was
   narrower than the word, not because of the wrap mode. One line, and the
   whole name in the title attribute when it is too long to show. */
td.nameco, th.nameco, td.statusco, th.statusco, td.dateco, th.dateco {
  white-space: nowrap;
}
td.nameco {
  max-width: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nowrap { white-space: nowrap; }
table:not(.prose) td:nth-child(n+3), table:not(.prose) th:nth-child(n+3) {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
table.prose td { text-align: left; }
tr.stale td { background: var(--stale-row); }
tr.never td { background: var(--never-row); }
tr.dim td { background: var(--surface); }
tr.ctx td { padding-top: 0; font-size: 12px; color: var(--text); }
tr.ctx td.muted, tr.ctx td.muted .muted { color: var(--text); }
td.rev, .rev { min-width: 220px; }

/* A STATUS PILL CARRIES A DOT (spec: status colours always ship with an icon
   or a label, never colour alone). The dot is drawn, so it survives a
   greyscale print and a colour-blind reader the same way. */
.pill.good::before, .pill.warn::before, .pill.bad::before,
.pill.neutral::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* -------------------------------------------------------------- pills */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  max-width: 100%;
}
.pill.good { background: var(--good); color: var(--on-chip); border-color: transparent; }
.pill.warn { background: var(--warn); color: var(--on-chip); border-color: transparent; }
.pill.bad  { background: var(--bad);  color: var(--on-chip); border-color: transparent; }
.pill.chan { background: var(--surface); color: var(--text-muted); border-color: var(--border); }
/* The colour never travels alone - the template prints the word beside it. */
.pill.health-green      { background: var(--good);    color: var(--on-chip); border-color: transparent; }
.pill.health-yellow     { background: var(--warn);    color: var(--on-chip); border-color: transparent; }
.pill.health-red        { background: var(--bad);     color: var(--on-chip); border-color: transparent; }
.pill.health-unrateable { background: var(--neutral); color: var(--on-chip); border-color: transparent; }

/* THE DOT TAKES THE SAME COLOURS. They were defined only as .pill.health-*,
   so the dot on the clients list - which is not a pill - rendered with no
   colour at all, and class-coverage caught it by name.

   The same tokens, deliberately: the dot on the list and the pill on the
   health page must be the same colour for the same verdict, and two
   definitions of one verdict is one verdict and one future disagreement.

   The comment above still holds - the colour never travels alone. Here the
   word is in a visually-hidden span instead of visible text, which keeps
   the same promise for a reader who cannot see the colour at all. */
.healthdot.health-green      { background: var(--good); color: var(--on-chip); }
.healthdot.health-yellow     { background: var(--warn); color: var(--on-chip); }
.healthdot.health-red        { background: var(--bad); color: var(--on-chip); }
.healthdot.health-unrateable { background: var(--neutral); color: var(--on-chip); }
td.badges { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }

/* ----------------------------------------------------------- KPI tiles */
.kpis { display: flex; flex-wrap: wrap; gap: 10px; margin: 12px 0; }
.kpi {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 14px;
  min-width: 160px;
  max-width: 100%;
}
.kpi.wide { min-width: 320px; }
.kpi label {
  display: block;
  color: var(--text-muted);
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.kpi b {
  display: block;
  margin-top: 3px;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.4px;
  font-variant-numeric: tabular-nums;
  /* MSG-59 #3. WAS `overflow-wrap: break-word`, which is what split
     "$2,494.81" into "$2,494.8" and a lone "1" on the next line: a number
     offers no break opportunity, so break-word breaks it mid-digit. The value
     is compact now (fmt.compact_money) with the exact amount in the title, so
     there is nothing long left to wrap - and nowrap means a narrower column
     cannot bring the break back. */
  white-space: nowrap;
}
.kpi .muted { font-size: 11.5px; }

/* ------------------------------------------------------------- alerts */
/* MSG-68 A1. IT WRAPS. Measured at 414px with a group open: hiding the
   twelve .alert-head divs took the document from 461 back to 414, and
   nothing else on the page moved it. The item that will not shrink is
   `someone is on all of these` - a pill, so nowrap, 165px - and a flex row
   with no wrap takes its min-content from it. Unreachable until A1 opened a
   group, because a rule card renders only inside one. */
.alert-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
.alert-head > .pill { max-width: 100%; }
.alert-body { flex: 1 1 auto; min-width: 0; }
.alert-body h2 { margin: 0; }

details.triage {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card);
  margin: 12px 0;
  padding: 0 14px;
}
details.triage > summary,
details.benchmark > summary,
details.itempanel > summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
  font-weight: 600;
  font-size: 13px;
  color: var(--text);
}
details.triage[open] > summary { border-bottom: 1px solid var(--border); }
/* MSG-68 A1. THE UNTRIAGED GROUP IS A GROUP LIKE THE OTHERS AND STILL THE
   HEADING OF THE PAGE. Making every triage group a <details> - so that a
   fold a person makes can be remembered - would otherwise have dropped
   "New" from the 16px section head it was to the 13px summary the collapsed
   groups use: the most important group on the screen rendered as the
   quietest heading on it. Same type, same 2px rule, as .sectionhead. */
.untriagedhead { font-size: 16px; font-weight: 700; color: var(--text); }
details.triage.untriaged > summary,
details.triage.untriaged[open] > summary {
  font-size: 16px;
  padding: 12px 0 8px;
  border-bottom: 2px solid var(--text);
}
/* ---- MSG-72 B: a state group inside one alert block (MSG-69 B2) ----
   The same fold, one level in. `.card` is the block; a group in it is a
   surface rather than a second card, and the open group's heading steps
   from 16px to 14px so it does not compete with the problem's own <h2>
   while still being the heaviest thing inside the block. */
.card > details.triage {
  background: var(--surface);
  margin: 8px 0;
  padding: 0 12px;
}
.card > details.triage .untriagedhead { font-size: 14px; }
.card > details.triage.untriaged > summary,
.card > details.triage.untriaged[open] > summary {
  font-size: 14px;
  padding: 10px 0 8px;
  border-bottom-width: 1px;
}
/* The group's own table scrolls inside the group, not inside the card:
   .table-container needs a floor of nothing, or the fold's padding
   becomes the table's minimum (the Data tab lesson, MSG-69). */
.card > details.triage .table-container { min-width: 0; max-width: 100%; }
details.benchmark {
  border: 1px dashed var(--border-strong);
  border-radius: 6px;
  margin: 8px 0;
  padding: 0 12px;
  background: var(--surface);
}
details.benchmark > summary { font-weight: 500; color: var(--text-muted); }
details.itempanel { margin-top: 8px; }
details.itempanel > summary { font-weight: 500; color: var(--text); }
.paneldetail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 2px 16px;
  margin: 8px 0;
  padding: 10px 12px;
  background: var(--surface);
  border-radius: 5px;
  color: var(--text);
}
.paneldetail b {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  margin-right: 5px;
}

/* -------------------------------------------------------------- forms */
.filters, .entry { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin: 10px 0; }
label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
/* input:not([type]) IS H4 (MSG-56 ADDENDUM). Six fields on
   /onboarding/new and two on bfcm_form.html declare no type, so they matched
   none of the selectors below and rendered as system defaults next to the
   type="url" field, which matched. An attribute selector matches the
   attribute, not the default. A typeless input is always a text input -
   every other kind has to declare itself - so this cannot catch a control
   that wants different styling. */
input:not([type]),
input[type=text], input[type=email], input[type=password], input[type=date],
input[type=month], input[type=time], input[type=search], input[type=tel],
input[type=number], input[type=url], select, textarea {
  display: block;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  padding: 6px 9px;
  font: inherit;
  font-size: 13px;
  max-width: 100%;
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--focus);
}
button, input[type=submit] {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 5px;
  border: 1px solid var(--border-strong);
  background: var(--bg);
  color: var(--text);
  cursor: pointer;
}
button:hover, input[type=submit]:hover { background: var(--hover); }
.entry button[type=submit], form.entry button, .filters button[type=submit],
form.stackform button[type=submit] {
  background: var(--primary);
  color: var(--primary-text);
  border-color: var(--primary);
}
.entry button[type=submit]:hover, form.entry button:hover,
.filters button[type=submit]:hover,
form.stackform button[type=submit]:hover { background: var(--primary-hover); color: var(--primary-text); }
form.inline { display: flex; gap: 4px; flex-wrap: wrap; }
form.inline button { font-size: 11.5px; padding: 3px 8px; font-weight: 500; }
input:disabled, select:disabled, button:disabled, textarea:disabled {
  /* M39 FIX 3b: .55 composites to 4.03:1 over the light shell. Disabled
     text is formally exempt from AA, but it still has to be read. */
  opacity: 0.65;
  cursor: not-allowed;
}

/* --------------------------------------------------- stored report text */
pre.msg {
  white-space: pre-wrap;
  overflow-wrap: break-word;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px 14px;
  font: inherit;
  color: var(--text);
  margin: 8px 0 0;
  max-width: 100%;
}

/* -------------------------------------------------------------- login */
.loginwrap {
  max-width: 340px;
  margin: 80px auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 28px;
}
.loginwrap h1 { font-size: 24px; }
.loginwrap h1 span { color: var(--text-muted); }
.loginwrap label { display: block; margin: 14px 0 0; }
.loginwrap input { width: 100%; margin-top: 5px; padding: 8px 10px; font-size: 14px; }
.loginwrap button {
  width: 100%;
  margin-top: 18px;
  padding: 9px;
  background: var(--primary);
  color: var(--primary-text);
  border-color: var(--primary);
}
.loginwrap button:hover { background: var(--primary-hover); color: var(--primary-text); }


/* --------------------------------------------------------------------------
   M14 C1 - the alert item.
   Marko: "nabacano, tesko se cita". Each item reads in three lines and nothing
   competes with them: what it is, where it stands, what to do. Everything you
   open rather than scan is behind the details toggle underneath.
   -------------------------------------------------------------------------- */
.alertitem {
  border-top: 1px solid var(--border);
  padding: 12px 0 10px;
}
.alertitem:first-of-type { border-top: 0; }
.alertitem.dim { background: var(--surface); border-radius: 6px; padding-left: 10px; padding-right: 10px; }

.ai-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}
.ai-head .ai-what { font-weight: 600; }

/* Line 2 is the one that has to be scannable, so the labels are small caps and
   the figures are not. */
.ai-fig {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 6px 0 0;
  font-size: 13px;
}
.ai-fig b {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  margin-right: 5px;
}
.ai-do { margin: 8px 0 6px; font-size: 13px; }
.ai-do b {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  margin-right: 5px;
}
.ai-do ol { margin: 4px 0 0 20px; padding: 0; }
.ai-do li { margin: 2px 0; }
.alertitem form.inline { margin-top: 8px; }

/* C3: the engine's own diagnostic, one click behind the plain sentence.
   MSG-68: this comment had no terminator, so it ran on and swallowed
   the whole 'M14 C1 - the alert item' block below as far as that
   block's own terminator. No CSS was lost - a CSS comment does not
   nest, so the next terminator closed it - but the comments in this
   sheet are load-bearing documentation, and a missing terminator is
   how the next person's edit disappears. */


/* --------------------------------------------------------------------------
   M14 C1 - the alert item.
   Marko's word was "nabacano" - thrown together. Each item now reads in three
   lines and nothing competes with them: what it is, where it stands, what to
   do. Everything you open rather than scan sits behind the details toggle.
   -------------------------------------------------------------------------- */
.alertitem {
  border-top: 1px solid var(--border);
  padding: 12px 0 10px;
}
.alertitem:first-of-type { border-top: 0; }
.alertitem.dim {
  background: var(--surface);
  border-radius: 6px;
  padding-left: 10px;
  padding-right: 10px;
}

.ai-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}
.ai-head .ai-what { font-weight: 600; }

/* Line 2 has to be scannable, so the LABELS shrink and the figures do not. */
.ai-fig {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 6px 0 0;
  font-size: 13px;
}
.ai-fig b, .ai-do b {
  color: var(--text-muted);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  margin-right: 5px;
}
.ai-do { margin: 8px 0 6px; font-size: 13px; }
.ai-do ol { margin: 4px 0 0 20px; padding: 0; }
.ai-do li { margin: 2px 0; }
.alertitem form.inline { margin-top: 8px; }

/* C3: the engine's own diagnostic, one click behind the plain sentence. */
details.whytoggle { display: inline; }
details.whytoggle > summary {
  display: inline;
  cursor: pointer;
  font-size: 11.5px;
  color: var(--link);
  margin-left: 6px;
}
details.whytoggle[open] > summary { display: block; margin: 4px 0 2px; }

/* M14 A3 - the threshold pencil.
   It reuses details.whytoggle so it collapses the same way every other "more
   detail" control on this app does, and adds only what a FORM inside a summary
   needs: it must not sit on one line with the summary, and its fields must not
   inherit the 11px the summary uses. Restated, never inherited - the palette is
   plain hex on :root and the dark theme swaps the tokens, so a rule written
   only for one theme is a rule that disappears in the other. */
details.threshedit[open] { display: block; margin-top: 6px; }
.threshedit > form.entry {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  margin: 6px 0 4px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px;
}
.threshedit input[type="number"] { width: 8em; }
.threshedit input[type="text"] { min-width: 16em; }
.threshedit > p { margin: 2px 0 0; font-size: 12px; }

/* M15 A6 - the correlation banner. Loud enough to change what you do next,
   quiet enough not to look like a fifth alert. A left border rather than a
   filled block: a filled warning colour here would compete with the severity
   pill two lines above it, and they are not the same claim. */
.correlated {
  margin: 6px 0 10px;
  padding: 8px 10px;
  border-left: 3px solid var(--warn);
  background: var(--surface);
  font-size: 13px;
}

/* M15 A1 - an anchored item must not land under the sticky header. The value
   is the header height plus a line, so the item Marko just pressed is visible
   with its heading, not flush against the top edge. */
.alertitem, tr[id^="alert-"] { scroll-margin-top: 84px; }

/* M15 A1 - and it is highlighted for a moment, so a page that reloaded to the
   same place does not look like a page that did not reload at all. */
.alertitem:target, tr[id^="alert-"]:target { background: var(--hover); }

/* M16 1a - an admin-only card. A left edge rather than a wash: the card is
   normal content that one reader can see and the others cannot, not a warning
   about the content itself. */
.card.guru { border-left: 3px solid var(--bad); }

/* M16 1b - the inline source form, same shape as the threshold pencil. */
details.srcedit[open] { display: block; margin-top: 6px; }
.srcedit > form.entry {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px;
  margin: 6px 0 4px; padding: 8px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 6px; font-size: 13px;
}
.srcedit > p { margin: 2px 0 0; font-size: 12px; }

/* M16 2b - the dismissed list. Collapsed, quiet, and never gone. */
details.dismissedlist { display: block; }
details.dismissedlist > summary { font-size: 13px; color: var(--text-muted); cursor: pointer; }
details.dismissedlist[open] > summary { margin-bottom: 6px; color: var(--text); }

/* ------------------------------------------------------------------- M18
   A LONG form, stacked.

   `.entry` is a flex row - correct for a filter bar, wrong for a form with
   twenty fields and headings, where it flows the labels horizontally and drags
   the <h3> group headings in among the inputs.

   `.publicform` is the onboarding form's own page frame. That page does NOT
   extend base.html - an anonymous visitor must not be served the sidebar, the
   client list or the signed-in identity - so it brings its own. */
form.stackform { display: block; max-width: 46rem; }
form.stackform label { display: block; margin: 0.7rem 0; font-weight: 500; }
form.stackform label input,
form.stackform label select,
form.stackform label textarea {
  display: block; width: 100%; margin-top: 0.25rem; font-weight: 400;
}
form.stackform h3 { margin: 1.5rem 0 0.4rem; }
form.stackform .actions { display: flex; gap: 0.6rem; margin-top: 1.4rem; }

body.publicform { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
body.publicform main { display: block; }
ol.steps { list-style: none; padding: 0; display: flex; flex-wrap: wrap;
           gap: 0.5rem; margin: 1.2rem 0; }
ol.steps li { border: 1px solid var(--input); border-radius: var(--radius);
              padding: 0.35rem 0.7rem; font-size: 0.85rem; }
ol.steps li.current { border-color: var(--primary); font-weight: 600; }
ol.steps .num { font-weight: 700; margin-right: 0.3rem; }

/* ---------------------------------------------------------------- M21
   A BLOCK TO READ, NOT TO ANSWER.

   Marko's onboarding items 21 (the Klaviyo/Shopify discount note) and 40
   (partner access) are things a client reads. Rendered with no rule at all
   they ran straight into the questions on either side and read as if they
   were part of the previous answer.

   Tokens only - it follows the theme, like everything else here. */
.note {
  background: var(--surface);
  border-left: 3px solid var(--border-strong);
  border-radius: 6px;
  padding: 0.75rem 1rem;
  margin: 0 0 1rem;
}
.note > :first-child { margin-top: 0; }
.note > :last-child  { margin-bottom: 0; }

/* A STANDALONE WARNING LINE. `.warn` existed only as `.pill.warn`, so a
   paragraph carrying the class got nothing at all - which is how the strategy
   draft's "this draft is not finished" line came out looking like ordinary
   body text, next to the thing it was warning about. */
p.warn, div.warn {
  background: var(--surface);
  border-left: 3px solid var(--warn);
  border-radius: 6px;
  padding: 0.6rem 0.9rem;
  margin: 0 0 1rem;
}

/* M22 0.4. An answer given earlier in the same form: shown, not asked again. */
.mirrored {
  display: inline-block;
  padding: 0.35rem 0.6rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-right: 0.5rem;
}
.changelink { font-size: 0.9em; }

/* M22 0.7d. Competitor pop-ups, shown rather than listed. */
.shots { display: flex; flex-wrap: wrap; gap: 1rem; }
.shots figure { margin: 0; max-width: 22rem; }
.shots img { width: 100%; height: auto; border: 1px solid var(--border); border-radius: 6px; display: block; }
.shots figcaption { font-size: 0.9em; margin-top: 0.35rem; }

/* ================================================================= M22 half B
   THE BRAND HUB. A guideline that looks like a text dump is the one nobody
   opens twice, which is the whole finding the build spec starts from. */
.brandhead { margin-bottom: 1rem; }
.brandbody { display: flex; gap: 1.5rem; align-items: flex-start; }
.brandindex {
  position: sticky; top: 1rem; flex: 0 0 15rem; max-height: 80vh;
  overflow-y: auto; font-size: 0.92em;
  border-right: 1px solid var(--border); padding-right: 0.75rem;
}
.brandindex .ix-part { font-weight: 600; margin: 0.75rem 0 0.25rem; }
.brandindex a { display: block; padding: 0.15rem 0; }
.brandsections { flex: 1 1 auto; min-width: 0; }
.partline { margin: 1.5rem 0 0.5rem; }
.sec h3 { margin-top: 0; }
.donewhen { border-top: 1px dashed var(--border); padding-top: 0.5rem;
            margin-bottom: 0; font-size: 0.9em; }

/* COLOUR, SHOWN AS COLOUR. The complaint that started this rewrite was hex
   codes where the colour itself should be. */
.swatches { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1rem; }
.swatches figure { margin: 0; width: 9rem; }
.swatches .sw { height: 4.5rem; border-radius: 6px; border: 1px solid var(--border); }
.swatches figcaption { font-size: 0.85em; margin-top: 0.35rem; }
.combos { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }
.combo { display: flex; justify-content: space-between; align-items: center;
         gap: 1rem; padding: 0.5rem 0.75rem; border-radius: 6px;
         border: 1px solid var(--border); }
.specimen { border-top: 1px solid var(--border); padding: 0.75rem 0; }

/* 2.3 is quotes. They read as quotes. */
.pullquotes p { border-left: 3px solid var(--border-strong);
                padding-left: 0.75rem; margin-left: 0; }

/* PRINT: the same page, cleanly, one Part per page. The One-Pager is rendered
   from this view rather than from a second template nobody would keep true. */
@media print {
  .brandindex, .tabs, nav, form, .iconbtn, footer { display: none !important; }
  .brandbody { display: block; }
  .partline { page-break-before: always; }
  .partline:first-of-type { page-break-before: avoid; }
  .sec { page-break-inside: avoid; border: none; box-shadow: none; }
  a[href]:after { content: ""; }
}


/* ---- M23 stage 1: the catalogue -------------------------------------
   Tokens only, both themes for free. The class-coverage check asks for a rule
   per compound, so every class the template uses is styled here even where the
   default would have done. */
.cat-grid { display: grid; gap: .75rem; margin: .5rem 0 1rem;
            grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.cat-item { border: 1px solid var(--border); border-radius: 6px;
            padding: .5rem; background: var(--surface); min-width: 0; }
.cat-item img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
                border-radius: 5px; display: block;
                background: var(--surface); }
.cat-noimg { width: 100%; aspect-ratio: 1 / 1; display: flex;
             align-items: center; justify-content: center;
             border-radius: 5px;
             background: var(--surface); color: var(--text-muted);
             font-size: .75rem; }
.cat-name { margin-top: .4rem; font-size: .85rem; line-height: 1.3;
            overflow-wrap: anywhere; }
.cat-name a { color: var(--text); text-decoration: none; }
.cat-name a:hover { text-decoration: underline; }
.cat-meta { font-size: .78rem; color: var(--text-muted); margin-top: .2rem; }
/* Whole reviews, wrapped. Never clipped - see the template comment. */
.cat-review { max-width: 46rem; white-space: pre-wrap; overflow-wrap: anywhere; }

/* ---- M26 stage 1: the clients-list window selector ------------------
   One window drives total store revenue, flows and campaigns together. */
.winbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
          margin: 0 0 12px; font-size: .9rem; }
.winopt { padding: 5px 11px; border: 1px solid var(--border);
          border-radius: 999px; text-decoration: none; color: var(--text-muted);
          background: var(--card); }
.winopt:hover { border-color: var(--border-strong); color: var(--text); }
.winopt.on { border-color: var(--focus); color: var(--text);
             background: var(--hover); font-weight: 600; }

/* ---- M26b ruling 4: the engine's own stored windows, demoted --------
   Kept in full, below the selected window, closed by default. */
.engine-windows { margin-top: 10px; border-top: 1px solid var(--border);
                  padding-top: 8px; }
.engine-windows > summary { cursor: pointer; color: var(--text-muted);
                            font-size: .85rem; list-style: revert; }
.engine-windows > summary:hover { color: var(--text); }
.engine-windows[open] > summary { margin-bottom: 4px; }

/* M37 item 2 - long lists show their first ten.
 *
 * Marko: "ne trebam vidjeti svih 60, stavi neki collapse button."
 *
 * THE ROWS PAST THE TENTH ARE STILL IN THE PAGE. They are hidden by CSS, not
 * left out of the markup, so a proof script reading the HTML sees all sixty
 * whether the list is open or shut - the fold is for the person, and nothing
 * is hidden from a check. Adding `.unfolded` to the wrapper shows them.
 *
 * No second stylesheet: this is the one skin, as M13 requires. */
.folded .fold-more { display: none; }
.folded.unfolded .fold-more { display: table-row; }
.folded.unfolded .fold-open { display: none; }
.fold-open { margin-top: 6px; }

/* M39 item 1 - a one-click control that has been pressed.
 *
 * Marko: "Every time I click a button it goes up or down the page."
 * The verdict now posts from app.js and the row updates where it stands, so
 * the ONLY thing that tells him it worked is this state plus the "who, when"
 * line beside it. It has to be unmistakable in both themes. */
button.chosen,
.btn.chosen {
  /* M39 FIX 3: --accent under white is 4.18:1. This state is the only
     thing that tells Marko the click landed, so it has to be readable,
     not merely visible. */
  background: var(--accent-strong);
  color: var(--accent-ink);
  border-color: var(--accent-strong);
  font-weight: 600;
}
[data-said] { font-size: 11.5px; margin-bottom: 2px; }

/* M39 item 2 - every section on a client page folds.
 *
 * Marko: "Everything needs a collapse button."
 *
 * THE HEADING IS THE CONTROL. A folded section keeps its h2 - and therefore
 * its count - and hides everything after it. The rows are still in the
 * markup: this is display, not a query, so nothing is hidden from a check. */
[data-foldable] > h2 { cursor: pointer; user-select: none; }
/* BATCH 3 ITEM 5: THE TOGGLE IS A REAL BUTTON. It was a click handler on the
   heading - no keyboard, no role, nothing for a screen reader to announce,
   and no way to read from the DOM whether a section was open. It looks
   exactly like the heading it replaced, because it IS the heading's text. */
.foldbtn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.foldbtn:hover { color: var(--link); }
[data-foldable] > h2::after {
  content: " ▾";
  font-size: 11.5px;
  color: var(--text-muted);
}
/* U5: h3 folds as well as h2 - a brand section's heading is an h3. The
   marker goes on whichever heading the section has. */
[data-foldable].folded-section > h2::after,
[data-foldable].folded-section > h3::after { content: " ▸"; }
[data-foldable] > h2, [data-foldable] > h3 { cursor: pointer; }
[data-foldable].folded-section > *:not(h2):not(h3) { display: none; }

/* M39 item 1 - the rows where a verdict becomes a learning soonest.
 * A MARK, NOT A FILTER: the rest of the list is still on the page. */
tr.rate-first > td:first-child::before {
  content: "rate first";
  display: inline-block;
  margin-right: 6px;
  padding: 1px 6px;
  border-radius: 9px;
  font-size: 11.5px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  border: 1px solid var(--accent);
}

/* M41 B3. A LAYOUT THUMBNAIL in the library, so a design verdict is given on
   the thing that was actually sent rather than on a subject line and a
   memory. Tokens only - the border follows the theme like everything else,
   and the image is never allowed to set the row height. */
.thumb {
  display: block;
  max-width: 120px;
  height: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--card);
  margin-bottom: 6px;
}


/* ------------------------------------------------------- narrow widths */
/* M44-FIX-2. THE INTERNAL SHELL BELOW 640px.
 *
 * `.shell` carried an unconditional 240px margin for the sidebar, so at
 * 360px the content column was 120px and the topbar's own children ran past
 * it - `.right` alone is 111px and `.who` is nowrap, so nothing could
 * shrink. This file had no width media query of any kind: two @media rules
 * in the whole stylesheet, for reduced motion and for print.
 *
 * It reuses the RAIL the sidebar already has, rather than inventing a second
 * narrow layout or hiding the navigation. Everything here is a layout rule -
 * no colour is set, so nothing can drift from the palette. */
@media (max-width: 640px) {
  .sidebar { width: 56px; }
  .shell   { margin-left: 56px; }

  /* The labels go, exactly as they do in rail mode, so the 56px is real.
     F7 (MSG-58): `.brand > span:not(.mark)` was the selector C1 already
     fixed for rail mode - the mark became an <img>, so a rule about a span
     next to it stopped covering the wordmark. Rail mode was corrected and
     this copy was not, so "wizdash" stayed in a 56px column and a.brandlink
     measured 84px in a 55px parent. The word lives inside .brandlink, so
     that is what this hides; the mark stays. */
  .sidebar .lbl,
  .sidebar .brandlink > span,
  .sidebar .brand > span:not(.mark),
  .sidebar h3,
  .sidebar > nav > footer { display: none; }
  .sidebar .brand { flex-direction: column; gap: 6px; padding: 12px 6px; }

  /* The topbar is allowed to become two lines instead of one long one.
     The wrap itself now starts at 700px - see the block below - because the
     overflow ran to 680 and this one only started at 640. */
  .topbar { flex-wrap: wrap; padding: 6px 12px; row-gap: 4px; }
  .topbar .right { margin-left: 0; }
  .topbar .who { white-space: normal; }

  /* Cards and tables stop insisting on a width the screen does not have. */
  .card { padding: 12px; }
  .table-container { overflow-x: auto; }
}


/* ------------------------------------------------- M52 A5. THE STATUSLINE
   A sentence that says what state this page is in. It used to be marked up
   as a .pill, which is inline-flex and therefore cannot wrap: one refusal
   made the copy page 6,357 pixels wide, with its own text running off the
   coloured strip onto the white behind it.

   It wraps, it keeps the line breaks the refusal reason now carries (one per
   distinct cause), and it cannot be wider than what contains it. */
.statusline {
  display: block;
  max-width: 100%;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  white-space: pre-line;      /* one line per reason, as stored */
  overflow-wrap: anywhere;    /* and a single long token still wraps */
  word-break: break-word;
}
.statusline.warn { border-color: var(--warn-border, var(--border-strong)); }
.statusline.good { border-color: var(--good-border, var(--border-strong)); }

/* AND NO PILL CAN STRETCH A PAGE AGAIN. A pill is a short label; when one is
   somehow long it becomes two lines, never a horizontal scrollbar on the
   whole document. */
.pill {
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
}


/* ------------------------------------------- M52 A10. THE TWO DEFINITIONS
   House on the left, Klaviyo on the right, the same four parts in every row.
   It was a run of siblings whose presence depended on the verdict, so no two
   rows lined up and a pill shared a baseline with a wrapping sentence. */
.segcmp { padding-block: 14px; }
.segcmp + .segcmp { border-top: 1px solid var(--border); }

.segcmp-head {
  display: flex;
  flex-wrap: wrap;          /* the pill drops below the name, never past it */
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
}
.segcmp-head h4 { margin: 0; }

.segcmp-why { margin: 0 0 10px; max-width: 70ch; }

.segcmp-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}
/* One column on a narrow screen. Two columns that do not fit are worse than
   one that does - the point of this block is reading them against each
   other, and a horizontal scrollbar destroys that. */
@media (max-width: 760px) {
  .segcmp-cols { grid-template-columns: 1fr; gap: 10px; }
}

.segcmp-col > h5 {
  margin: 0 0 2px;
  font-size: 0.85em;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}
.segcmp-col > h5 + p,
.segcmp-col > h5 + ul { margin-top: 0; margin-bottom: 10px; }
.segcmp-col p { overflow-wrap: anywhere; }

/* THE CONDITIONS THAT DIFFER are the reason anybody opens this block. */
.segcmp-diff {
  background: var(--warn-soft, rgba(180, 120, 0, 0.10));
  border-left: 3px solid var(--warn-border, #b47800);
  padding: 2px 6px;
  border-radius: 3px;
}


/* --------------------------------------- M52 A-FIX. TABLES SCROLL, PAGES DO NOT
   The width sweep found three pages whose tables ran past their card below
   900px: the copy page, the calendar month page and the Segments tab. A wide
   table is allowed to be wide - six columns of prose do not fit on a phone
   and shrinking them to fit makes them unreadable. What it may not do is
   drag the document sideways, because that is what took the copy page to
   6,357 pixels and made everything else on it unreachable.

   So below 900px the table becomes its own scrolling box. Its rows still lay
   out as a table inside it; only the table scrolls. */
@media (max-width: 900px) {
  /* `.card > table` missed the Segments tab, whose table sits inside a
     div inside the card - a child selector that has to know the markup is
     the same trap as a hardcoded field name. */
  table.prose,
  .card table,
  .entry table,
  .segcmp table {
    display: block;
    overflow-x: auto;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
  }
}

/* At every width, a long word in a cell wraps rather than setting the
   column's minimum width. A single unbroken token - a ULID, a url, a
   campaign id - is what usually makes a table wider than its card. */
table.prose td,
table.prose th,
.card table td,
.card table th {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* And a card never becomes the thing that scrolls the page. */
.card { max-width: 100%; }


/* ------------------------------------- M52 B2. A TALL FIELD IN A FLAT ROW
   `.entry` aligns its fields by the bottom, which is right while they are
   all one line tall. The team-members picker is five rows deep, so it held
   its bottom on the line, grew upward, and carried its label with it -
   "kocka ide gore, tekst dole".

   Narrowed with :has() to the rows that actually contain a tall control, so
   every single-line filter bar on the rest of the dashboard is untouched. A
   browser without :has() keeps the old behaviour rather than breaking. */
.entry:has(select[multiple]),
.filters:has(select[multiple]) {
  align-items: flex-start;
}

/* And the label reads top-down: its words, then its control, then its note -
   so a five-row box and a one-line input start on the same line. */
.entry:has(select[multiple]) > label,
.filters:has(select[multiple]) > label {
  display: flex;
  flex-direction: column;
  gap: 4px;
}


/* ------------------------------------------- C10. WHAT THIS PAGE IS FOR
   Marko asked "što je ovo?" on nine screens. One sentence under the heading,
   in the muted voice, wide enough to read and narrow enough not to be mistaken
   for the page's content: 68ch is about a dozen words a line.

   It sits tight under the heading it belongs to (the heading's own margin is
   what separates them) and holds the page off. */
.pagenote {
  margin: -2px 0 18px;
  max-width: 68ch;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.55;
}
.card > .pagenote:first-child { margin-top: 0; }


/* ------------------------------------------------ C8. A MONTH IS ONE CLICK
   The summary is the month; the table under it is unchanged. The marker keeps
   the browser's own triangle, so the control looks like what it is on every
   platform rather than a word somebody has to learn. */
.monthgroup { margin: 0 0 10px; }
.monthgroup > summary {
  cursor: pointer;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  font-size: 14px;
  display: flex; align-items: baseline; gap: 10px;
}
.monthgroup > summary:hover { background: var(--hover, var(--surface)); }
.monthgroup[open] > summary { border-radius: 8px 8px 0 0; }
.monthgroup > summary > .muted { font-size: 12px; }
.monthgroup > .table-container { border: 1px solid var(--line); border-top: 0;
  border-radius: 0 0 8px 8px; }


/* C12. The step row: three links with air between them, not a run-on line. */
.monthstep { display: flex; gap: 18px; flex-wrap: wrap; margin: -4px 0 14px; }


/* --------------------------------------------- C3. THE CONTENTS OF A PAGE
   One line per item and the columns line up down the list, so it reads as a
   list and not as a paragraph of pills. The rule name takes the space it
   needs; the figure and the person keep their own columns.

   `grid` rather than a table because these are list items, not rows of data -
   a screen reader should hear a list of things to look at. */
.toc ol.tocitems { list-style: none; margin: 8px 0 0; padding: 0; }
.tocitems > li {
  display: grid;
  grid-template-columns: 78px minmax(9rem, 1fr) minmax(0, 1.2fr) auto auto;
  gap: 4px 12px;
  align-items: baseline;
  padding: 7px 0;
  border-top: 1px solid var(--line);
  font-size: 13px;
}
.tocitems > li:first-child { border-top: 0; }
.tocitems .tocwhat { overflow-wrap: anywhere; }
.tocitems .tocfig { font-variant-numeric: tabular-nums; white-space: nowrap; }
.tocitems .tocwho { white-space: nowrap; }
@media (max-width: 760px) {
  .tocitems > li { grid-template-columns: 78px 1fr; }
  .tocitems .tocfig, .tocitems .tocwho { grid-column: 2; }
}


/* ------------------------------------------- C2. A CLIENT FOLDS ON TODAY
   Lighter than the month group on Reports: this sits INSIDE a card that
   already has a border, so a second box around it would be a box in a box. */
.clientgroup { margin: 6px 0; }
.clientgroup > summary {
  cursor: pointer;
  padding: 6px 2px;
  display: flex; align-items: baseline; gap: 9px;
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
}
.clientgroup > summary > .muted { font-size: 12px; }
.clientgroup[open] > summary { margin-bottom: 4px; }


/* ------------------------------------------ C6. A FLOW, AND ITS EMAILS
   The summary carries the flow's own totals, so the table underneath is
   something you open when you want the steps - not something you have to
   open to find out whether the flow earned anything. */
.flowgroup { margin: 0 0 8px; border: 1px solid var(--line); border-radius: 8px; }
.flowgroup > summary {
  cursor: pointer;
  padding: 9px 11px;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px;
  font-size: 13.5px;
}
.flowgroup[open] > summary { border-bottom: 1px solid var(--line); }
.flowgroup .flowname { font-size: 14px; }
.flowgroup .flowsum {
  margin-left: auto;
  color: var(--text-muted);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
.flowgroup > table { margin: 0; }
@media (max-width: 760px) { .flowgroup .flowsum { margin-left: 0; width: 100%; } }


/* C7. The month comparison: a control, closed until somebody wants it. */
.comparemonths { margin: 6px 0 16px; }
.comparemonths > summary { cursor: pointer; font-size: 13px; padding: 4px 0; }
.comparemonths td, .comparemonths th { font-variant-numeric: tabular-nums; }
.comparemonths thead th { text-align: right; }
.comparemonths thead th:first-child { text-align: left; }
.comparemonths td:not(:first-child) { text-align: right; }


/* C9. One gap per line, the destination at the end of it and set apart, so a
   long sentence does not read as a link to somewhere in its middle. */
.gaplist > li { margin: 0 0 7px; line-height: 1.5; }
.gaplist > li > a { white-space: nowrap; margin-left: 6px; font-size: 12.5px; }


/* C11. One tier per line under the select it explains. */
.tierlines { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; }
.tierlines > li { margin: 0 0 4px; color: var(--text-muted); line-height: 1.45; }
.tierlines > li > strong { color: var(--text); }


/* ------------------------------------------------- C13. A CALLOUT WRAPS
   Screenshot_14: the segment warning is one long sentence in a tinted box and
   it runs out of the box. Same fault as A5's refusal banner and the same fix,
   applied to the CLASS rather than to that one element, so the next long
   sentence somebody puts in a callout wraps as well.

   `overflow-wrap: anywhere` and not `break-word`: a segment name with no
   spaces in it - which is most of them - is exactly what has to be allowed to
   break, and break-word will not break inside a long unbroken token when the
   box is narrow enough. */
p.warn, .warn {
  overflow-wrap: anywhere;
  word-break: normal;
  white-space: normal;
  max-width: 100%;
}


/* ==================================================== D1. THE MONTH, READABLE
   A campaign is a card, not a row of ten cells with three stacked lines in
   most of them. The face of the card carries what the send IS; the disclosure
   carries what somebody does with it. */

.monthstrip {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 0 0 16px;
}
.monthstrip > div {
  flex: 1 1 8rem;
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 10px 12px;
  background: var(--surface);
}
.monthstrip b {
  display: block; font-size: 21px; line-height: 1.15;
  font-variant-numeric: tabular-nums;
}
.monthstrip span { font-size: 11.5px; color: var(--text-muted); }

.campcard {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 14px;
  margin: 0 0 10px;
}
.campcard > header {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
  margin-bottom: 8px;
}
.campcard .campdate {
  font-variant-numeric: tabular-nums;
  font-size: 12.5px; color: var(--text-muted);
  min-width: 7.5rem;
}
.campcard .campname { font-weight: 700; font-size: 15px; }
.campcard .campdue { margin-left: auto; font-size: 12px; }

/* One fact per line: label in a fixed column, value beside it. A definition
   list because that is what these are - a label and its value - and a screen
   reader should hear the pairing. */
.campfacts {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 4px 12px;
  margin: 0;
  font-size: 13px;
}
.campfacts > dt { color: var(--text-muted); font-size: 11.5px;
                  text-transform: uppercase; letter-spacing: .03em;
                  padding-top: 2px; }
.campfacts > dd { margin: 0; overflow-wrap: anywhere; }

.campmore { margin-top: 10px; }
.campmore > summary {
  cursor: pointer; font-size: 12.5px; color: var(--text-muted);
  padding: 4px 0;
}
.campmore[open] > summary { margin-bottom: 6px; }

@media (max-width: 620px) {
  .campfacts { grid-template-columns: 1fr; gap: 1px 0; }
  .campfacts > dt { padding-top: 7px; }
  .campcard .campdue { margin-left: 0; width: 100%; }
}


/* D5. What a section taught us, above the rows it was taught by. */
.sectionlearn {
  border-left: 3px solid var(--primary, var(--line));
  padding: 8px 0 8px 12px;
  margin: 8px 0 14px;
  font-size: 13px;
}
.sectionlearn > b { display: block; margin-bottom: 4px; }
.evidencelist > summary { cursor: pointer; font-size: 12px; color: var(--text-muted); }
.evidencelist ul { margin: 4px 0 0; }
.toc > ul { columns: 2; column-gap: 28px; }
@media (max-width: 620px) { .toc > ul { columns: 1; } }


/* ================================================ D4. SPACING ON THE BRAND TAB
   "Sve preblizu — razmaci." Thirty-nine section cards stacked with almost
   nothing between them, and the specimens, swatches and combos inside them
   touching their own edges. Nothing moves; everything gets room. */
.brandsections .sec { margin: 0 0 18px; padding: 16px 18px; }
.brandsections .sec > h3 { margin: 0 0 10px; line-height: 1.35; }
.brandsections .sec .prose { margin-top: 10px; line-height: 1.6; }
.brandsections .sec .donewhen { margin-top: 14px; padding-top: 10px;
                                border-top: 1px solid var(--line); }
.specimen { margin: 0 0 16px; padding: 12px 14px;
            border: 1px solid var(--line); border-radius: 9px; }
.specimen .usefor { margin: 0 0 8px; font-size: 13px; }
.combos { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
          gap: 10px; margin-top: 10px; }
.combo { padding: 12px 14px; border-radius: 9px; display: grid; gap: 6px; }
.shots figure { margin: 0 12px 12px 0; }


/* D7. What each decision in the competitor queue does - one per line, with
   the name of the control in bold at the start of it. */
.controlsnote { margin: 8px 0 14px; font-size: 13px; line-height: 1.55; }
.controlsnote > li { margin: 0 0 5px; }

/* D2 / D9. A field in a flex row that should take the space left over: the
   "what to change" box next to two narrow selects. Without it the text box is
   as wide as its placeholder and the row has a gap at the end. */
.entry > label.grow { flex: 1 1 18rem; min-width: 0; }
.entry > label.grow > input { width: 100%; }


/* E1. One conversation per block, and a follow-up sits under what it follows. */
.askthread { margin: 0 0 14px; }
.askthread + .askthread { border-top: 1px solid var(--line); padding-top: 10px; }
tr.followup > td:first-child { padding-left: 18px; }

/* ---- MSG-56 Batch 5: the rail, in four groups --------------------------
   Work · Library · Team · Admin. The header sits OUTSIDE each <ul>, which is
   also what keeps 4p's count honest: it reads the rail with a sed range on
   <ul data-nav="sections">, and a header inside the list would be counted as
   a thirteenth entry.

   The colour is --shell-muted, which .sidebar .brand .sub already uses on
   this same rail and which therefore already clears the contrast floor in
   both themes. NOT a fixed grey (the rail follows the theme, 22.9) and NOT
   the foreground at reduced opacity - an opacity composites against whatever
   is behind it, which is how .crumbsep ended up at 2.14:1 earlier this
   round.

   .navgroup carries its own rule. It had only .navgroup + .navgroup,
   .navgroup h3 and .navgroup ul, which style the second group, the heading
   and the list - and nothing on the element carrying the class. */
.navgroup { margin-bottom: 14px; }
.navgroup:last-child { margin-bottom: 0; }
.navgroup h3 {
  margin: 0 0 4px 10px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--shell-muted);
}
.navgroup ul { margin: 0; padding: 0; list-style: none; }

/* ---- MSG-56 Batch 5: an audit change, as a chip ------------------------
   The CHANGE column used to print the whole recorded detail as JSON, so for
   a row carrying a before and an after the one field that changed was
   invisible in a wall of text.

   inline-block, not a block: several chips sit on one line of the row and
   wrap together, and a row with four changed fields should grow by a line
   rather than by four. */
.chip {
  display: inline-block;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
  margin: 1px 3px 1px 0;
  font-size: 12px;
  line-height: 1.6;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}
.chip strong { font-weight: 600; }

/* ---- MSG-56 Batch 5: the clients list's first column -------------------
   The dot, the name and the status pill are one column. They used to be
   three, with Health five columns further along, so reading "is this client
   all right" meant crossing the table.

   THE DOT REUSES THE EXISTING health-* COLOURS rather than defining a second
   set, which could disagree with the pill on the health page itself. */
.clientcell { white-space: normal; }
.healthdot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: baseline;
  border: 1px solid var(--border);
  /* F2 (MSG-58). THE VERDICT IS THE ELEMENT'S TEXT NOW, so it has to be
     pushed out of the 10px box rather than wrapped in a hidden span: the
     readers take the element's immediate text, and a nested span read as
     empty.
     text-indent + overflow, NOT display:none or visibility:hidden - those
     take it out of the accessibility tree too, and the word is what a
     reader who cannot tell the colours apart is given. */
  /* The TEXT COLOUR is on the four health rules below, not here: 4q
     objected that `color: var(--on-chip)` in this rule declares no
     background, and it was right - on-chip is a promise about a surface
     and this rule does not set one. .pill.health-* pairs them in the
     same rule; so does the dot now. */
  text-indent: -9999px;
  overflow: hidden;
  white-space: nowrap;
  /* F8 (MSG-58). THE WORD IS OFF-SCREEN AND STILL MEASURED. The render audit
     measures every text node with a box of at least 1x1, and this box is
     10x10 - so it read the inherited link blue on the dot's red and called
     it 1.12:1, correctly.
     --on-chip is the token .pill.health-* already uses against these same
     four backgrounds, and those pairs are among the 58 the token check
     proves in both themes. Reusing it rather than picking a colour, and
     rather than teaching the audit to ignore text-indent - which is also
     how a real failure would hide. */
}
/* unrateable keeps the colour the health page already gives it. */

/* The verdict in words, for a screen reader and for anybody who cannot tell
   the colours apart. NOT display:none - that removes it from the
   accessibility tree too, which would leave the dot announcing nothing at
   all. This is the standard visually-hidden pattern. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- The sparkline. Shape only: the number beside it is the value, so
   there is no axis and no scale. currentColor, so it inherits the colour of
   the text it sits with and therefore reads in both themes without a token
   of its own. */
.spark {
  display: inline-block;
  margin-left: 6px;
  vertical-align: middle;
  overflow: visible;
}
.spark path {
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.75;
}

/* ---- The audit's raw JSON, kept under the chips. Closed, monospace, and
   allowed to wrap: a recorded detail can be long and a cell that scrolls
   sideways hides the end of it. */
.rawjson { display: inline-block; margin-left: 4px; }
.rawjson summary { font-size: 11.5px; cursor: pointer; }
.rawjson code {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--text-muted);
}

/* The two trend lines at the top of the revenue cell. flex with a gap, so
   the two sit side by side and wrap together on a narrow screen rather than
   pushing the column wider - the render audit asserts no horizontal
   scrollbar at 1440px, and this column is the widest one. */
.sparkrow {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin-bottom: 6px;
  font-size: 12px;
}

/* ---- F7 (MSG-58): the top bar wraps before it overflows -----------------
   Measured: it ran past its container at 9 of 28 sampled widths, 360-680px,
   with div.right 334px in a 304px parent. The wrap existed but only below
   640, so the band 640-680 kept one long line that did not fit.

   700, not 680: the sweep samples every 40px, so a breakpoint set on the
   last failing sample is a breakpoint that still fails at 681. */
@media (max-width: 700px) {
  /* ---------------------------------------- MSG-61 Part E, at phone widths
     `.card { overflow-x: auto }` used to scroll the card sideways instead of
     the page, which hid four things at 360 and 414px. Marko removed it on
     12.10.2026, so each one is now told to fit. Measured, not guessed: every
     rule here is aimed at an element found pushing the document with no
     scrollable ancestor above it.

     IN THIS QUERY AND NOT IN THE BASE RULES. The pages are clean from 560px
     up; changing how Alerts or Data lay out on a desktop to fix what only a
     phone sees would be a worse trade than the one it fixes. */

  /* The triage control is a segmented row - five buttons. `flex-wrap: wrap`
     moved to the base rules in MSG-68 A1 (at 1280px with a group open the
     Undo button's right edge was at 1331, so it is not only a phone that
     pays). These two are the PHONE lever and stay here: they let the track
     be squeezed below its min-content, which at 1440 folded the control and
     took a row from under 60px to 81px. */
  .seg { flex-wrap: wrap; max-width: 100%; }
  .arow-seg, .arow-seg .seg { min-width: 0; max-width: 100%; }

  /* A flex or grid item will not shrink below its own content without this,
     which is why `.entry select { max-width: 100% }` was already in the
     sheet and the Data tab's select was still 411px wide inside a 360px
     page: the 100% was 100% of a label that was itself too wide. */
  .entry, form.entry, .entry > label, .filters, .filters > label {
    min-width: 0;
    max-width: 100%;
  }
  /* AT ANY DEPTH, NOT AT THE DEPTH I GUESSED. The Alerts rule dropdown sits
     three levels down - LABEL < .morefilters-body < DETAILS.morefilters <
     FORM.filters - and was 311px wide at left=254, so its right edge was
     565 on a 360px page. A select is as wide as its longest option unless
     it is told otherwise, and "Flow revenue has dropped" is a long one. */
  .filters label, .filters select, .filters input,
  .entry label, .morefilters-body, .morefilters-body * {
    min-width: 0;
    max-width: 100%;
  }

  /* THE CARD'S CONTENTS FIT THE CARD. Measured: a .card on Alerts and on
     KPIs is 240px wide with 331px of content in it, and since .card stopped
     scrolling that content paints outside and pushes the page. Its tables
     are already their own scrollboxes; this is everything else.

     NOT `.card { overflow-x: auto }` at this width either. That is the rule
     Marko removed, and the reason does not change on a phone: a card that
     scrolls is a card that eats scrollIntoView. */
  /* A KPI tile has a 320px floor and main is 304px wide here. min-width
     beats max-width when they disagree, which is why the `max-width: 100%`
     on .kpi never helped. One tile per line on a phone. */
  .kpi.wide { min-width: 100%; }

  .card { overflow-wrap: anywhere; }
  .card > *, .card > * > * { max-width: 100%; min-width: 0; }

  /* Brand's body is a flex ROW - the contents list beside the sections -
     and at 360px the list takes the width while the sections start at
     left=352 and end at 390, past a 360px edge. It stacks on a phone, which
     is what every other two-column layout in this sheet does at this width.
     The overflow-wrap is for the pasted text inside it. */
  .brandbody {
    min-width: 0;
    overflow-wrap: anywhere;
    flex-direction: column;
  }
  .brandbody > * { max-width: 100%; min-width: 0; }

  /* And the last five pixels. MAIN measured 365 against a 360 page with
     nothing deeper pushing it, which is the shape of a box whose padding is
     outside its width. */
  .shell, main { min-width: 0; max-width: 100%; }
  main > * { max-width: 100%; }

  .topbar { flex-wrap: wrap; row-gap: 4px; }
  .topbar .right { margin-left: 0; }
  .topbar .who { white-space: normal; }
  /* min-width:0 lets a flex child shrink below its content width, which is
     what actually stops a long cluster pushing past the bar. */
  .topbar > * { min-width: 0; }
  .topbar .right { flex-wrap: wrap; }
}

/* ---- WHO IS ON THIS CLIENT (MSG-59 #6/#10, MSG-60 U7) -------------------
   Initials, with the full name and the role in the title. A row of eight
   people cannot widen a table and the names stay one hover away. */
.peoplechips {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.pchip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 26px;
  padding: 0 6px;
  border-radius: 13px;
  border: 1px solid var(--border);
  /* THE TEXT COLOUR IS DECLARED WITH THE BACKGROUND, not inherited from
     whatever the chip happens to sit on (4q). A chip whose background is set
     in one rule and whose colour is set in another is the bug that passed in
     light and failed in dark. */
  background: var(--surface);
  color: var(--text);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0;
  cursor: default;
  /* It is initials: never let it stretch or wrap. */
  white-space: nowrap;
  flex: 0 0 auto;
}
/* The word in front of a chip group on the client header. Small, muted and
   never wrapping away from the chip it labels. */
.pchip-role {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  margin-right: 1px;
}
.pchip.more {
  background: transparent;
  color: var(--text-muted);
  font-weight: 600;
}
/* The way back to where people are edited - quiet, because it is a signpost
   and not the point of the row. */
.pchip-edit {
  font-size: 12px;
  margin-left: 2px;
  white-space: nowrap;
}
/* On the Clients list the chips sit in a table cell, where the cell is wide
   and the chips must not pretend to fill it. */
td .peoplechips { justify-content: flex-start; }

/* ---- #19 A FORM THAT ASKS QUESTIONS STACKS ------------------------------
   `.entry` is `display: flex; align-items: flex-end` (line ~2169), which is
   right for a filter bar and wrong for a form: it puts every label on the
   same row as its field, aligned to the field's baseline. That is exactly
   what the review saw on the Swipes form - "labels sit at the baseline of the
   fields and the fields run inline in one row".

   Scoped to a form that has opted in, so the filter bars that rely on
   `.entry` being a row are untouched. The onboarding form's `.stackform`
   shape is the one being matched. */
form.stackform .entry,
.dropform .entry {
  display: block;
  margin: 0.7rem 0;
}
form.stackform .entry > label,
.dropform .entry > label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 500;
}
form.stackform .entry > input,
form.stackform .entry > textarea,
form.stackform .entry > select,
.dropform .entry > input,
.dropform .entry > textarea,
.dropform .entry > select { width: 100%; max-width: 46rem; }

/* ---- U1 THE CLIENTS HEADER STOPPED TOUCHING ITSELF ----------------------
   Screenshot 11: "Start onboarding" and the window selector sat against each
   other with no gap. They are two separate blocks, so the fix is room between
   them and room INSIDE the window bar, which wraps rather than collides at
   narrow widths. */
.onboardbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 14px;
}
.winbar {
  display: flex;
  /* MSG-69 B4 (R7). flex-END, not center. A date input and a button have
     different heights, and centring them lines up neither the tops nor
     the bottoms: the button's text floated 8px above the inputs' text at
     every width wide enough to hold all three, which is exactly what
     "not on one baseline" looks like. Measured by live-m69-b4.py, which
     reads getBoundingClientRect in a real browser at twelve widths -
     this is not visible in the stylesheet. */
  align-items: flex-end;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  /* A selector that wraps onto two lines needs the rows to breathe, or the
     second row touches the table below it. */
  row-gap: 6px;
}

/* MSG-69 B4, measured twice. ONE HEIGHT AND ONE BOX MODEL, which is what
   actually puts the three on one baseline.

   `align-items: center` put the button's text 8px ABOVE the inputs';
   switching the container to flex-end moved it 4px BELOW them. Same
   defect, different sign - because the two controls have different
   heights and different box models, so aligning their BOXES never lines
   up their TEXT. 36px is what the date input already measures with the
   house padding, so nothing shrinks and nothing jumps.

   The 360px width is arithmetic, not alignment: two 123px date inputs
   and a 113px button are 359px of controls, and they cannot share a
   line at 360px however they are aligned. The sweep prints the line
   count at every width and the handoff says so, rather than a check
   quietly testing something weaker than the instruction. */
/* The label hugs its input. The flex child is the LABEL, and a label
   whose line box holds the word "From" is taller than the 36px input
   inside it - so bottom-aligning the labels with the button left the
   INPUTS 4px above their own label's bottom. Measured, three times. */
.winbar label {
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* AND THIS IS THE 4px. The label's computed style was `margin: 4px 0`,
     and flex-end aligns MARGIN boxes - so the label's border box, and
     the input inside it, sat 4px above the button's. The bar's own
     row-gap already does what that margin was for. */
  margin-top: 0;
  margin-bottom: 0;
}
.winbar input[type="date"],
.winbar .btn,
.winbar button {
  box-sizing: border-box;
  height: 36px;
}

/* ---- #20 THE AUDIT TABLE READS -----------------------------------------
   WHEN wrapped onto three lines and ACTION broke mid-word
   ("client_admi n_changed"). The date goes on one line with the time under
   it, and an action is a chip that cannot break. */
.aud-when { white-space: nowrap; }
.aud-when .aud-date { display: block; }
.aud-when .aud-time { display: block; font-size: 11.5px; color: var(--text-muted); }
.aud-action {
  display: inline-block;
  white-space: nowrap;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* ---- U3 THE ROW YOU JUST CHANGED ----------------------------------------
   The viewport lands on it correctly; this is what says so. Measured on
   12.10: pressing a status moved the row from 2,321 px to 6,142 px down a
   7,488 px document, landing at top 168 of 900 - the right row, in the right
   place, with nothing marking it.

   An outline rather than a background, so it cannot change the contrast of
   the text inside it (the lesson from the dim alert rows: opacity and
   background on a container are a contrast change to every node under it).
   It clears on the first click or keypress. */
.landed {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
  border-radius: 4px;
}
@media (prefers-reduced-motion: no-preference) {
  .landed { transition: outline-color .2s var(--ease-out); }
}

/* ---- MSG-69 B11 (R22): the onboarding run page does not break words
   Marko's screenshot: "Mark done" wrapped letter by letter in a 46px
   cell and the status read "waiting_h uman". A word with an underscore
   in it has no break opportunity a browser likes, so it broke anywhere.
   Both columns are nowrap and the button has a floor; the table scrolls
   inside its card (the house .table-container) rather than squeezing. */
button.markdone {
  white-space: nowrap;
  min-width: 7.5rem;
}
.runsteps .nowrapcell { white-space: nowrap; }
.runsteps th, .runsteps td { vertical-align: top; }
/* MSG-77 Q45: THE STEP COLUMN IS WIDE ENOUGH FOR A NAME IN TWO LINES.
   Marko's screenshot: "Create the client's brand in Wizard Tools"
   came down the page one or two words per line, because the
   who-does-it paragraph in the fourth column took everything the
   table had. A floor on the name column and a ceiling on the
   paragraph, so the two share: a table's natural widths are decided
   by its longest cell, and that cell is a paragraph. */
.runsteps td:first-child, .runsteps th:first-child { min-width: 15rem; }
.runsteps td:nth-child(4) { max-width: 34rem; }
/* MSG-75. THIS TABLE HAS NO NUMERIC COLUMN.
   `table:not(.prose) td:nth-child(n+3)` right-aligns the third
   column onward, which is right for a table of figures and wrong
   for this one: it right-aligned the status, the whole paragraph
   saying who does the step, and the button. Found in a
   screenshot of the four new steps; it has been true since the
   table was written. */
/* SAME SHAPE, SAME WEIGHT: `.runsteps td` is (0,1,1) and the rule it
   has to beat is (0,2,2), so the first attempt did nothing at all and
   the screenshot still showed right-aligned prose. */
table.runsteps td:nth-child(n+3),
table.runsteps th:nth-child(n+3) { text-align: left; }

/* ---- MSG-69 B8 (R18): a key field is masked without being a password
   `type="password"` is what makes 1Password read this form as a login
   and offer to save it, which is what Marko screenshotted. So the field
   is a text input and the masking is here. -webkit-text-security is the
   property Chromium and WebKit implement; where it is not supported the
   field still reads as a secret because of the letter-spacing and the
   monospace face.

   THIS IS NOT SECURITY. It stops a shoulder, not a reader - the value is
   in the DOM either way. What protects the key is that it is never
   echoed back into the form, never logged, never stored in a column, and
   handed to the engine as a file read once and unlinked. */
input[data-secret="1"] {
  -webkit-text-security: disc;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.08em;
}

/* ---- MSG-69 B22 (R32): the inline priority control
   A select and a button in one table cell. MSG-56's rule is that a row
   is ONE row, and without this the two wrap under each other and the
   cell becomes two lines at 360. The Set button is hidden by
   `:root.js .applybtn` where a script can submit on change, so with a
   script this is usually a select on its own. */
.prioform {
  display: inline-flex;
  gap: 0.3rem;
  align-items: center;
  white-space: nowrap;
  margin-top: 0.25rem;
}
.prioform select { min-width: 5.5rem; }

/* ---- MSG-69 B15: a select in a table cannot widen the table
   A select's intrinsic width is its longest option, and a table cell
   grows to whatever its content demands - so one long option pushed the
   finance table to 718px at a 360px viewport and the page scrolled
   sideways. The labels are short now; this is the ceiling so that the
   next long one cannot do it again. */
table.prose select,
table.tight select {
  max-width: 100%;
  min-width: 0;
}

/* ---- MSG-69 B23 / R34: a label never breaks inside a word
   The sweep (live-m69nowrap) found four: 'Put back in the queue' on 17
   lines, 'Mark refreshed' on 10, 'Remove' on 6, and the figure 118,989
   broken after its comma.

   Buttons: .btn is an inline-flex item with no white-space, so in a
   tight row it shrank below its label and the label broke character by
   character. nowrap plus no shrink keeps the label whole; a row that
   cannot fit its buttons gets wider or wraps them, and the
   sideways-scroll check in the same measurement table is what keeps
   that honest.

   The figure: 30px in a grid column that can be 110px wide does not fit
   on one line. A number that breaks stops being a number, so it never
   wraps and its size follows the viewport up to the 30px it had. */
/* MSG-70: BETWEEN WORDS, NEVER INSIDE ONE - which is what R34 asks.
   `nowrap` was a stronger claim and it cost two things: the topbar ran
   7px past the window at 720px, and "Create December 2026 in ClickUp"
   stuck 41px out of a 188px column on the calendar month page. A word
   is still unbreakable; the label may take two lines. */
button,
.btn {
  white-space: normal;
  overflow-wrap: normal;
  word-break: normal;
}
.btn {
  flex-shrink: 0;
}
.stat-value {
  white-space: nowrap;
  font-size: clamp(20px, 5.2vw, 30px);
}

/* ---- MSG-69: a table may be narrower than its content
   Measured on the Data tab: at a 360px viewport the document was 515px
   and the finance table was 594px INSIDE a .table-container that is
   width:100% and overflow-x:auto. The container should have clipped and
   scrolled; it did not, because a flex or grid item's default min-width
   is auto - its own min-content size - so the item simply grew and the
   overflow had nothing to do.

   min-width: 0 lets the container be narrower than the table, which is
   what makes its own scrolling work. max-width: 100% stops it asking
   for more room than its parent has.

   And break-word on CELLS so that one unbreakable string - a 16-digit
   ClickUp id, a long link - cannot set a whole table's minimum width.
   break-word, NOT anywhere: it breaks a word only when the word cannot
   fit on a line of its own. `anywhere` would split ordinary words and
   R34's sweep would catch it, correctly. Headers are left alone - a th
   is one of the short labels that sweep asserts on. */
.table-container {
  min-width: 0;
  max-width: 100%;
}
table.prose td,
table.tight td {
  overflow-wrap: break-word;
}

/* ---- MSG-70: a field in a .wf-foot row fits its row
   .wf-foot (wiz-forms.css) sizes the ROW; it says nothing about the
   fields in it. On the standalone /wizai page I wrote `wf-foot input {
   width: 100% }` in the page's own <style>, and then used the same
   markup on the Data tab, where that rule does not exist - so the input
   sized itself to its content: 322px of content in a 214px box, which
   is what live-m61 counts as a page scrolling sideways.

   Here rather than in the one template, because there will be more
   .wf-foot rows and the next one should not have to find this out. */
.wf-foot label {
  display: block;
  min-width: 0;
}
.wf-foot input,
.wf-foot select,
.wf-foot textarea {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}

/* ---- MSG-70: a table inside a scrolling wrapper is a table
   FOUND BY ELIMINATION: hiding each of the Data tab's seventeen tables
   in turn, exactly one released the document from 515px to 360px at a
   360px viewport - the finance block's, 594px of content in a 214px
   box, inside a wrapper that scrolls and an ancestor chain where every
   box is 360px or less.

   `table.tight` makes a table `display: block` with its own scrollbox;
   `.table-container table` then sets overflow-x: visible, because two
   scrollboxes around one table is two scrollbars for one movement. What
   that leaves is a BLOCK-level table whose internal table box sizes to
   its content and is not constrained by the block's width - so it
   contributes to the document's scroll width through a 214px wrapper.

   Inside a wrapper, a table is a table again: the wrapper is the
   scrollbox, which is what that comment always intended, and a real
   table box is constrained by it. max-content keeps the columns from
   being crushed into 214px - this table holds a question, the reason it
   is asked and an answer, and squeezed to a phone's width none of the
   three would be readable. A scrollbar is the right trade. */
.table-container > table,
.table-container > form > table {
  /* NOT max-content: with display:table restored, max-content asked
     for everything the columns wanted - 2697px - and the document went
     from 515 to 1920. The wrapper's width, and the cells wrap, which
     is what the other sixteen tables on this page already do. */
  display: table;
  width: 100%;
  max-width: 100%;
  table-layout: auto;
}

/* ---- MSG-70: break-word does not change a column's minimum width
   The finance table kept demanding 594px through two rules that looked
   right, because `overflow-wrap: break-word` breaks a word at render
   time and leaves intrinsic min-content alone - and a table's minimum
   is the sum of its columns' min-content widths. `anywhere` is the one
   that moves it.

   td only (a th is a short label R34's sweep asserts on), and only
   inside a wrapper that scrolls - those are the tables meant to fit.
   The no-wrap sweep runs after this, over fourteen pages at eight
   widths, so anything short that this breaks is caught by name. */
.table-container td {
  overflow-wrap: anywhere;
}

/* ---- MSG-70: a table obeys its width only with table-layout: fixed
   With `table-layout: auto` a table's used width is at least the sum of
   its columns' min-content widths - `width` is a suggestion it may
   ignore. That is why the finance table stayed 594px wide in a 214px
   box through five other fixes. `fixed` sizes the columns from the
   first row and the specified width, and the content wraps inside
   them.

   On the one table that needs it, not on every wrapped table: fixed
   layout changes how columns are sized everywhere it is applied, and
   the other sixteen tables on that page already fit. */
table.fitcols {
  table-layout: fixed;
  width: 100%;
}
/* The question and the answer get a third each; the reason it is asked
   takes the rest, because it is the column holding a sentence. */
table.fitcols th:nth-child(1), table.fitcols td:nth-child(1) {
  width: 30%;
}
table.fitcols th:nth-child(3), table.fitcols td:nth-child(3) {
  width: 32%;
}

/* ---- MSG-70: the topbar wraps rather than running past the window
   Measured on /calendar/varnock/2026-12 at 720px: the document was
   727px and the furthest element was the topbar's right-hand block,
   ending at 727 with the Log out button in it.

   R34's fix made every button nowrap and stop shrinking - right for a
   label that used to break into ten lines - so in this one row nothing
   gives way any more and the items' sum ran 7px past the window at
   exactly one width. The row wraps to two lines instead, which at 720px
   is ordinary; a page scrolling sideways is not.

   The name keeps nowrap - a name split over two lines reads as two
   people - but may now be truncated, so the row has something to give
   before it wraps. */
.topbar {
  flex-wrap: wrap;
  row-gap: 6px;
}
.topbar .right {
  flex-wrap: wrap;
  min-width: 0;
  row-gap: 6px;
}
.topbar .who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.topbar .where {
  min-width: 0;
}

/* ====================================================================
   MSG-77 C1 (F7) and F19: the overlay, the spinner and the pending
   button. Tokens only - every colour here is one the palette already
   defines, so dark mode needs no second rule and 4q's contrast check
   reads them like any other.
   ==================================================================== */
.pickfilter {
  display: block;
  width: 100%;
  margin: 0 0 .35rem 0;
}

/* The spinner. One element, no image, and it stops where a person has
   asked for less motion. */
.spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-right: .5em;
  vertical-align: -.125em;
  border: 2px solid var(--border);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: wizspin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}
@keyframes wizspin { to { transform: rotate(360deg); } }

button.ispending {
  opacity: .85;
  cursor: progress;
}

/* F7's overlay: the whole screen, the client's name, and one
   instruction. It is never dismissed - the run page replaces it. */
.startoverlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  padding: 1rem;
}
.startoverlay[hidden] { display: none; }
body.overlay-open { overflow: hidden; }
.startoverlay-box {
  max-width: 34rem;
  text-align: center;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.75rem 1.5rem;
}
.startoverlay-box h2 { margin: .75rem 0 .5rem 0; }
.startoverlay-box .spinner { width: 2rem; height: 2rem; border-width: 3px; }
.startbar {
  height: 6px;
  margin: 1rem 0;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}
.startbar-fill {
  height: 100%;
  width: 35%;
  border-radius: 999px;
  background: var(--text-muted);
  animation: wizbar 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .startbar-fill { animation: none; width: 100%; }
}
@keyframes wizbar {
  0%   { margin-left: -35%; }
  100% { margin-left: 100%; }
}

/* ===================================================================
   MSG-77 G3. The Figma block on Today: a <pre> full of image URLs.
   A pre keeps every line on one line and those lines are 85 characters
   of URL, so the page grew to 1637px in a 1440px window and scrolled
   sideways - which the render audit's own control caught.
   =================================================================== */
/* MSG-77 G6 joined this rule: the monthly check's prompt block and its
   HQ findings are the same shape - a <pre> whose lines are prose, not
   code - and without it /today measured 4368px in a 1440px window. */
pre.prose[data-verify="figma_block"],
pre.prose[data-verify="month_check_prompt"],
pre.prose[data-verify="month_check_hq"],
/* MSG-77-FIX-2 item 9: the two Brand-doc pages, same treatment. */
pre.prose[data-verify="branddoc_onboarding"],
pre.prose[data-verify="branddoc_kpis"] {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-width: 100%;
  font-size: 12px;
  line-height: 1.5;
  padding: 10px 12px;
  border: 1px solid var(--line, #E2E7F2);
  border-radius: 8px;
  max-height: 18rem;
  overflow-y: auto;
}

/* A BUTTON IN A TABLE CELL, found by sweeping /today for the first
   time (G6 put a row on it). "Mark refreshed" is 119px of text in the
   fourth column of a four-column table, which is 85px at phone width,
   and a cell cannot shrink a button whose words will not wrap. This is
   the design-refresh row and it predates MSG-77; it fails at all 28
   widths sampled until the words are allowed to wrap. No effect
   wherever the cell is wide enough, which is everywhere but a phone. */
td .btn, td button {
  white-space: normal;
  max-width: 100%;
}

/* ===================================================================
   MSG-77: new content with no width, found by the eight-width sweep.

   Six of ninety-six cells were red and both pages were this round's:
   the house-rules card on /rates now holds Figma URLs (660px of
   table inside a 360px phone) and the Data tab has a paste box for a
   ClickUp address (503px). A URL has no break opportunity a browser
   likes, and an <input> has no maximum width of its own.
   =================================================================== */
[data-verify="house_rules_card"] td,
[data-verify="house_rules_card"] th {
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* A gap link is prose in a narrow column: it wraps. The longest
   destination so far made the Data tab 503px wide inside a 360px
   phone, and the next long one should not be able to. */
[data-verify="brain_gap_link"],
[data-verify="brand_gap_link"] {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* The pencil's own form and the paste boxes: never wider than the card
   they sit in. `width: 100%` alone would still overflow a flex row, so
   the maximum is what does the work. */
input[name="folder_link"],
input[name="page_url"],
input[name="value"] {
  max-width: 100%;
  min-width: 0;
}
