/* ============================================================================
   Vermeiren master dashboard — shared shell.
   ONE source of truth: D:\vermeiren\shared_ui\shell.css
   Copied into each app's static/ by shared_ui/sync_ui.py. Never edit the copies.

   Used by all three apps:
     bot/dashboard_static/       approval queue + analytics   (/)
     translation_pipeline/static/ translations                (/translate/)
     campaign_pipeline/static/    launch campaigns            (/campaign/)
   ========================================================================== */

/* The 2026-08-05 direction (shared_ui/DESIGN_RULES.md): light by default, glass
   over a navy-and-teal atmosphere, one 4px spacing scale, Vermeiren navy as the
   accent and red as danger only. Token NAMES are unchanged from the old shell,
   which is what lets the 11 live pages inherit the direction by value swap:
   87% of their colour decisions already read a token. The reference for every
   value here is ui_mockups/new_shell.html. */
:root {
  /* ── the scale ── */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  --frame:    20px;  /* every floating thing keeps this off every edge */
  --rail-col: 44px;  /* the column every icon and the logo centres in */
  --rail-pad: 12px;  /* the rail's own padding, both sides */
  /* DERIVED, never typed: a typed width beside a fixed column is how every rail
     icon sat exactly 1.00px right of centre in the first pass. */
  --rail-w:   calc(var(--rail-col) + var(--rail-pad) * 2);
  --rail-open: 220px;
  --util-h:    42px;  /* the floating capsule, measured; content clears it */

  /* ── light: the default ── */
  --page:           #E9EBEF;   /* cool stone, biased toward the navy */
  --page-2:         #F3F5F8;
  --surface-1:      rgba(255,255,255,0.68);
  --surface-2:      rgba(255,255,255,0.92);
  --glass-lift:     rgba(255,255,255,0.90);
  /* The one surface that is NOT glass, and the reason is legibility rather than
     taste: a modal is text laid over text. --surface-1 is 68% opaque, so the
     campaign cards behind the picture-request dialog read straight through it
     ("Kuarz TIER 1" and "ALESIA" were both legible on the deployed page,
     2026-08-12). Glass is right for a panel that sits IN the page and wrong for
     one that covers it, so every <dialog> takes this instead. */
  --surface-solid:  #FFFFFF;
  --border:         rgba(16,26,44,0.12);
  --grid:           rgba(16,26,44,0.08);
  --ghost:          rgba(16,26,44,0.045);
  --baseline:       rgba(16,26,44,0.25);

  --text-primary:   #101724;
  --text-secondary: #434D5C;
  --muted:          #69737F;

  --brand:          #17408B;   /* Vermeiren navy */
  --brand-ink:      #FFFFFF;
  --brand-soft:     rgba(23,64,139,0.10);
  --teal:           #3B7B92;   /* the second voice; never competes with navy */

  --good:           #1D7A47;
  --warn:           #96650F;
  --critical:       #B4342B;   /* danger only. never a button that is not one */

  --series-1: #2a78d6;  --series-2: #1baf7a;  --series-3: #eda100;
  --series-4: #008300;  --series-5: #4a3aa7;  --series-6: #e34948;

  --radius:      14px;
  --radius-s:    10px;
  --radius-pill: 999px;
  --shadow:    0 1px 2px rgba(16,26,44,.06), 0 8px 26px rgba(16,26,44,.09);
  --shadow-lg: 0 18px 56px rgba(16,26,44,.16);
  --blur: blur(20px) saturate(1.35);

  --sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", sans-serif;
  --mono: ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* Everything the browser draws itself (select popups, date calendars,
     scrollbars, spinners) follows the theme instead of glaring white in dark.
     Declared HERE and in the dark block below, exactly where the tokens switch:
     shell.js always stamps data-theme on the root, so a prefers-color-scheme
     path would let native chrome disagree with the stamped theme. */
  color-scheme: light;
  /* Checkboxes and radios in the brand navy. accent-color is the design-system
     answer at the right cost: never rebuild them as custom elements. */
  accent-color: var(--brand);
  /* The select's closed-state chevron. Drawn inline rather than fetched (the
     box has no CDN); the stroke is --muted's value and lives in this token
     block on purpose, beside the colour it repeats. */
  --chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%2369737F' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
:root[data-theme="dark"] {
  --page:           #090C12;   /* near-black, biased toward the navy */
  --page-2:         #0C1017;
  --surface-1:      rgba(255,255,255,0.050);
  --surface-2:      rgba(255,255,255,0.080);
  --glass-lift:     rgba(255,255,255,0.09);
  /* Lifted off --page rather than equal to it, so a dialog still reads as being
     in front of the page once it stops being transparent. */
  --surface-solid:  #151A23;
  --border:         rgba(255,255,255,0.11);
  --grid:           rgba(255,255,255,0.07);
  --ghost:          rgba(255,255,255,0.055);
  --baseline:       rgba(255,255,255,0.28);

  --text-primary:   #E7EAEF;
  --text-secondary: #A3ABB8;
  --muted:          #7A8390;

  --brand:          #6E9BE8;   /* the same navy, lifted so it reads on black */
  --brand-ink:      #060F1E;
  --brand-soft:     rgba(110,155,232,0.15);
  --teal:           #6FA3B8;

  --good:           #6FC08D;
  --warn:           #E0A550;
  --critical:       #E4655C;

  --series-1: #3987e5;  --series-2: #199e70;  --series-3: #c98500;
  --series-4: #008300;  --series-5: #9085e9;  --series-6: #e66767;

  --shadow:    0 1px 2px rgba(0,0,0,.38), 0 8px 28px rgba(0,0,0,.34);
  --shadow-lg: 0 18px 60px rgba(0,0,0,.55);
  --blur: blur(22px) saturate(1.5);

  color-scheme: dark;
  --chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%237A8390' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; }

body {
  margin: 0; background: var(--page); color: var(--text-primary);
  font-family: var(--sans);
  font-size: 13.5px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
}

:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px;
}

/* A link wears the brand, visited included: browser blue on a navy platform
   (and purple after a click) is the same defect as the grey "Choose File"
   button. Element-level, so every class rule a page already has still wins. */
a { color: var(--brand); text-underline-offset: 2px; }

/* Glass with nothing behind it is just grey, so there is something for it to
   refract: navy low left, teal high right. Fixed, so scrolling moves the
   content across the light rather than moving the light. Negative z-index:
   it paints above the body's background and under everything in flow. */
.sky {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(58vw 52vw at 6% 90%,  color-mix(in srgb, var(--brand) 16%, transparent), transparent 62%),
    radial-gradient(52vw 46vw at 92% 2%,  color-mix(in srgb, var(--teal)  16%, transparent), transparent 60%),
    var(--page-2);
}
:root[data-theme="dark"] .sky {
  background:
    radial-gradient(58vw 52vw at 6% 90%,  color-mix(in srgb, var(--brand) 24%, transparent), transparent 62%),
    radial-gradient(52vw 46vw at 92% 2%,  color-mix(in srgb, var(--teal)  20%, transparent), transparent 60%),
    var(--page-2);
}

/* ══ THE RAIL ═════════════════════════════════════════════════════════════
   The app switcher, as a floating vertical pill rail detached from the left
   edge. Each entry carries what is waiting behind it (the live counts), which
   is the thesis: the dashboard tells you where the work is instead of you
   opening nine pages to find out.

   Class names are sb-prefixed on purpose: three live pages define their own
   `.rail`, two their own `.mark`, one its own `.lbl`, and an unprefixed shell
   chrome would restyle their elements (the `.msg.bot a` trap, shell-sized).
   shell.js adds `railed` to <body>, so a page that loads this file without
   mounting the shell (login) keeps its layout. */
body.railed { padding-left: calc(var(--frame) * 2 + var(--rail-w)); padding-top: var(--s7); }

.shellbar {
  position: fixed; left: var(--frame); top: 50%; transform: translateY(-50%);
  z-index: 40;
  width: var(--rail-w);
  padding: var(--rail-pad);
  display: flex; flex-direction: column; gap: var(--s2);
  border-radius: 34px;
  background: var(--surface-1);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  /* The hairline is an INSET RING, not a border: border-box makes a 1px border
     eat 2px of the content box, which is how every icon in the first pass sat
     exactly 1.00px right of centre. A ring paints and consumes nothing. */
  border: 0;
  box-shadow: var(--shadow-lg),
              inset 0 0 0 1px var(--border),
              inset 0 1px 0 var(--glass-lift);
  transition: width .34s cubic-bezier(.2,.8,.25,1);
  overflow: hidden;
}
.shellbar:hover, .shellbar:focus-within { width: var(--rail-open); }
.shellbar hr { border: 0; border-top: 1px solid var(--grid); margin: var(--s1) 0; width: 100%; }

/* the mark: it fills the column, so it is centred by the same geometry rather
   than by a nudge. A white plate in both themes, because the roundel's navy
   outer ring would disappear against the dark canvas. */
.sb-mark { display: flex; align-items: center; gap: var(--s3); text-decoration: none; color: inherit; }
.sb-mark .sb-glyph {
  width: var(--rail-col); height: var(--rail-col); min-width: var(--rail-col);
  border-radius: var(--radius-pill); background: #FFFFFF;
  display: grid; place-items: center;
  box-shadow: 0 3px 14px color-mix(in srgb, var(--brand) 30%, transparent);
}
.sb-mark .sb-glyph img { width: 100%; height: 100%; display: block; border-radius: var(--radius-pill); }
.sb-mark .sb-wm { white-space: nowrap; opacity: 0; transition: opacity .2s .08s; }
.shellbar:hover .sb-wm, .shellbar:focus-within .sb-wm { opacity: 1; }
.sb-mark .sb-wm b { display: block; font-size: 13px; font-weight: 640; letter-spacing: -0.01em; }
.sb-mark .sb-wm span { display: block; font-size: 10px; color: var(--muted);
                       text-transform: uppercase; letter-spacing: .13em; }

/* a tab. No horizontal padding: the icon box IS the column, so it centres. */
.sb-nav {
  position: relative;
  display: flex; align-items: center; gap: var(--s3);
  padding: 0;
  border-radius: var(--radius-pill);
  text-decoration: none; color: var(--text-secondary);
  transition: background .18s, color .18s;
}
.sb-nav:hover { background: var(--ghost); color: var(--text-primary); }
.sb-nav .sb-ico {
  position: relative;
  width: var(--rail-col); height: var(--rail-col); min-width: var(--rail-col);
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  transition: color .18s;
}
/* The pill that lights up is `inset` inside the column, so it is centred by
   construction: inset cannot drift the way a typed width did. */
.sb-nav .sb-ico::before {
  content: ""; position: absolute; inset: var(--s1);
  border-radius: var(--radius-pill);
  transition: background .18s, box-shadow .18s;
}
.sb-nav svg { position: relative; z-index: 1; }
.sb-nav .sb-lbl { white-space: nowrap; font-size: 13px; opacity: 0; transition: opacity .2s .08s; }
.shellbar:hover .sb-lbl, .shellbar:focus-within .sb-lbl { opacity: 1; }
.sb-nav.current { color: var(--text-primary); background: var(--brand-soft); }
.sb-nav.current .sb-ico { color: var(--brand-ink); }
.sb-nav.current .sb-ico::before {
  background: var(--brand);
  box-shadow: 0 2px 12px color-mix(in srgb, var(--brand) 42%, transparent);
}

/* What is waiting behind this pill. Anchored right in both states, so it never
   clips against the rail edge and the eye keeps it in one place. */
.sb-wait {
  position: absolute; right: var(--s1); top: var(--s1);
  min-width: 18px; height: 18px; padding: 0 var(--s1);
  border-radius: var(--radius-pill);
  display: grid; place-items: center;
  font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums;
  transition: right .34s cubic-bezier(.2,.8,.25,1), top .2s, transform .2s;
}
.shellbar:hover .sb-wait, .shellbar:focus-within .sb-wait {
  right: var(--s3); top: 50%; transform: translateY(-50%);
}

/* ── the floating capsule, top right: theme and who ─────────────────────── */
.shellutil {
  position: fixed; top: var(--frame); right: var(--frame); z-index: 40;
  display: flex; align-items: center; gap: var(--s1);
  padding: var(--s1); border-radius: var(--radius-pill);
  background: var(--surface-1);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 0;
  box-shadow: var(--shadow), inset 0 0 0 1px var(--border), inset 0 1px 0 var(--glass-lift);
}
.shellutil .sb-who {
  display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s2) 0 var(--s3);
  font-size: 12px; color: var(--text-secondary); white-space: nowrap;
}
.shellutil .iconbtn { border: 0; border-radius: var(--radius-pill); padding: var(--s2) var(--s3); }
/* The interface language, beside the theme toggle because it is the same kind of
   control: how this page looks to me, on this machine. It is painted only for an
   account that HAS a language, so an admin's capsule is unchanged. The code is a
   label, not a translated word, and it names where the button GOES. */
.shellutil .lg-btn { gap: var(--s1); }
.lg-code { font-size: 11px; font-weight: 700; letter-spacing: .03em; }
/* The name, which for an admin is the link into Accounts (A2, 2026-08-11). Same
   size and colour as the span it replaces, so nothing about the capsule moved;
   it just answers a click now. */
.shellutil .sb-nm { display: inline-flex; align-items: center; gap: var(--s2);
                    color: inherit; text-decoration: none; }
a.sb-nm:hover { color: var(--text-primary); }
/* Below 640 the NAME goes and the glyph stays. Hiding `.sb-nm` outright, which
   is what this rule used to do, would take the only entrance to Accounts off
   every phone. A team account has no glyph, so it renders exactly as before. */
@media (max-width: 640px) { .shellutil .sb-who .sb-nm .sb-lbl { display: none; } }

.iconbtn {
  background: none; border: 1px solid var(--border); border-radius: var(--radius-s);
  padding: 6px 9px; cursor: pointer; color: var(--text-secondary);
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1; text-decoration: none;
  font: inherit;
}
/* A page that TYPES an iconbtn's size must also zero the padding: border-box
   leaves a 26px box 8px of content for an 11px glyph and the icon sits off
   centre — the typed-width trap again. Sized means padding 0, and the flex
   centring above does the rest. */
.iconbtn:hover { background: var(--ghost); color: var(--text-primary); }

/* ── the rail below 880: it moves to the bottom edge ────────────────────── */
@media (max-width: 880px) {
  body.railed {
    padding-left: 0;
    padding-top: calc(var(--frame) + var(--util-h) + var(--s3));
    /* clears the bottom rail: its height plus the frame it floats in, plus a
       gap, so the last card is never underneath it. */
    padding-bottom: calc(68px + var(--frame) + var(--s7));
  }
  .shellbar {
    flex-direction: row; width: auto; left: var(--frame); right: var(--frame);
    top: auto; bottom: var(--frame); transform: none;
    border-radius: 28px; overflow-x: auto; gap: var(--s1);
    justify-content: space-around;
  }
  .shellbar:hover, .shellbar:focus-within { width: auto; }
  .shellbar .sb-mark, .shellbar hr { display: none; }
  .shellbar .sb-nav .sb-lbl { display: none; }
}

/* ── the phone: the rail keeps what carries work, More holds the rest ────── */
.sb-more { display: none; }
@media (max-width: 560px) {
  :root { --frame: 12px; }
  .shellbar .sb-second { display: none; }
  .shellbar.all .sb-second { display: flex; }
  .shellbar .sb-more { display: flex; }
  .shellbar.all .sb-more .sb-wait { display: none; }
}

/* ── touch: a finger needs 44px, and there is no approach to expand on ───── */
@media (hover: none) {
  button.btn, a.btn, label.btn { min-height: 44px; }
  button.btn.sm, a.btn.sm, label.btn.sm { min-height: 40px; }
  .iconbtn { min-height: 40px; min-width: 40px; justify-content: center; }
  .pill { min-height: 40px; display: inline-flex; align-items: center; }
  .more { grid-template-rows: 1fr; opacity: 1; }
  /* a finger needs more than the 13px default box (the reference sheet's .chk) */
  input[type=checkbox], input[type=radio] { width: 24px; height: 24px; }
}

/* ── page head: title, subtitle, in-app tabs ────────────────────────────── */

.pagehead {
  display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap;
  padding: 18px clamp(16px, 3vw, 28px) 0;
  /* Full width by default (2026-07: dashboard pages are 50/50 panes and want the
     whole viewport). Long-form reading pages opt back down via body.reading. */
  max-width: var(--wrap, 100%); margin: 0 auto; width: 100%;
}
.pagehead .titles { min-width: 0; }
.pagehead h1 { font-size: 21px; margin: 0; font-weight: 680; letter-spacing: -0.02em; }
.pagehead .sub { color: var(--muted); font-size: 12.5px; margin-top: 2px; }

.tabs { display: flex; gap: 3px; margin-left: auto; }
.tabs button, .segbar button {
  background: none; border: 1px solid transparent; border-radius: var(--radius-s);
  padding: 7px 15px; color: var(--text-secondary); cursor: pointer; font: inherit;
}
.tabs button:hover, .segbar button:hover { background: var(--ghost); color: var(--text-primary); }
.tabs button.active, .segbar button.active {
  color: var(--text-primary); font-weight: 650;
  border-color: var(--border); background: var(--surface-1);
}

main {
  max-width: var(--wrap, 100%); margin: 0 auto;
  padding: 18px clamp(16px, 3vw, 28px) 72px; width: 100%;
}
/* ── how wide a page is ──────────────────────────────────────────────────────
   Decided by what KIND of page it is, not by which week it was built. That is
   the whole rule; every page picks exactly one of these three and says so on
   its own <body>, statically:

     (no class)     the whole viewport. A queue, a board, a list of cards, or a
                    two-pane tool where the second pane earns its half: the
                    approval queue, tasks, campaigns, translations, team home.
     .reading       one column of document prose, centred. A deliverable or a
                    translation being read and corrected.
     .conversation  one column of chat, centred, sized so the column is the
                    conversation rather than a frame around it.

   Two things about this that cost real defects:

   Put the class on <body>, never on <main>. The page header reads --wrap too,
   and the two must agree or the title sits wider than the content it labels.

   Declare it in the HTML, never apply it from JavaScript after mount. The
   translation share page used to add "reading" in Shell.mount().then(), so it
   painted at full width and then jumped to 1120px once /api/me came back, on
   the one page a country reviewer opens from a chat message. */
body.reading      { --wrap: 1120px; }
/* 880px is derived, not chosen: a chat bubble is capped at 82ch, which measures
   619px, and the column is sized so a bubble fills about four fifths of it. Any
   wider and left-versus-right stops meaning anything because every bubble is
   short of both edges; the products page was 1120px and left a 413px dead band
   down the right of every answer. */
body.conversation { --wrap: 880px; }

.segbar { display: flex; gap: 3px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.segbar .lbl { color: var(--muted); font-size: 12px; margin-right: 6px; }

/* ── surfaces ───────────────────────────────────────────────────────────── */
/* One kind of card (DESIGN_RULES: "there is no second kind"): --surface-1 plus
   the blur, a hairline, and the inset top highlight that reads as a lit edge.
   .panel is the canonical name going forward; .card keeps working because ten
   pages use it, and both are the same glass. */

.card, .panel {
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--s4); margin-bottom: 14px;
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--shadow), inset 0 1px 0 var(--glass-lift);
}
.panel { padding: var(--s5); margin-bottom: 0; }
.panel > h3 { margin: 0 0 var(--s1); font-size: 15px; font-weight: 620; letter-spacing: -0.015em; }

/* One quiet-text class. Absorbs the page-local .hint / .dmuted / .hmuted
   copies as each page migrates (phase 3/4): a page stops defining its own the
   day it is rebuilt, and until then its local rule wins on its own page. */
.muted { color: var(--muted); font-size: 12.5px; }

/* == THE HIGHLIGHT ========================================================
   One treatment for every tinted thing: a wash of its own colour with the type
   at full strength, and NO border and no rule down the side — a hairline drawn
   on top of a blur cancels the blur. `--tint` is the only thing that changes
   between a warning, a danger, a status and a count. */
.tint {
  --tint: var(--warn);
  color: var(--tint);
  background: color-mix(in srgb, var(--tint) 15%, transparent);
  backdrop-filter: blur(16px) saturate(1.7);
  -webkit-backdrop-filter: blur(16px) saturate(1.7);
  border: 0; box-shadow: none;
  border-radius: var(--radius-pill);
}
.tint.crit  { --tint: var(--critical); }
.tint.good  { --tint: var(--good); }
.tint.brand { --tint: var(--brand); }
/* a small thing needs a heavier wash to still read as a marker at 10px */
.tint.sm { background: color-mix(in srgb, var(--tint) 22%, transparent); }

/* == EXPAND ON APPROACH ===================================================
   A small box holds the essentials; everything a person only wants once they
   are interested rides in `.more`, which opens on hover AND on focus-within so
   a keyboard reaches it. Exactly ONE child (.mi), or the implicit auto row
   stops it ever collapsing. The gap above the revealed content is a MARGIN on
   the first revealed line, never padding on the box: with border-box a padding
   survives a zero height. */
.more {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .26s cubic-bezier(.2,.8,.25,1), opacity .2s;
}
.more > .mi {
  overflow: hidden; min-height: 0;
  display: grid; gap: var(--s2); align-content: start;
}
.more > .mi > :first-child { margin-top: var(--s4); }
.card:hover .more, .card:focus-within .more,
.panel:hover .more, .panel:focus-within .more { grid-template-rows: 1fr; opacity: 1; }
/* Sentence case, not capitals. Three of these titles are DATA, not chrome: a
   deliverable's own title on the campaign share page, a language name on the
   translation share page, and a string from the i18n table, which capitals in
   Czech and Latvian mistreat. A title that says something ("Demand map: what
   people asked about, and whether a distributor is on the lookup list") is a
   sentence, and a sentence in capitals is slower to read than one that is not.
   13px rather than 12px because lowercase at the same nominal size looks
   smaller: it keeps the title's weight on the page unchanged. */
.card > h2 {
  font-size: 13px; font-weight: 700; margin: 0 0 12px; color: var(--muted);
}

.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 158px), 1fr)); gap: 10px; margin-bottom: 20px; }
.tile {
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 13px 15px;
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  box-shadow: var(--shadow), inset 0 1px 0 var(--glass-lift);
}
.tile .v { font-size: 27px; font-weight: 680; letter-spacing: -0.025em; font-variant-numeric: tabular-nums; }
.tile .v small { font-size: 14px; font-weight: 500; color: var(--text-secondary); }
.tile .k { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.tile.alert { border-color: color-mix(in srgb, var(--critical) 45%, var(--border)); }
.tile.alert .v { color: var(--critical); }

/* ── controls ───────────────────────────────────────────────────────────── */

/* Every button is a pill and its shape says what it does (DESIGN_RULES):
   filled navy is the one action you most likely want, at most one per group;
   outlined is everything else; outlined red destroys or refuses. */
/* `a.btn` too: a download or an open-the-PDF is an action, and three pages
   already write <a class="btn sm"> expecting the pill. Until 2026-08-05 only
   <button> got it, so those anchors quietly rendered as bare links.
   `label.btn` for the same reason: a file input rides inside a label so the
   person sees a pill, and until the native-controls pass the campaign's
   packshot label quietly rendered as bare text. */
button.btn, a.btn, label.btn {
  font: inherit; font-weight: 550; border-radius: var(--radius-pill); padding: 8px 16px;
  cursor: pointer; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-primary);
  display: inline-flex; align-items: center; gap: 6px;
  transition: border-color .16s, background .16s;
  text-decoration: none;
}
button.btn:hover:not(:disabled), a.btn:hover, label.btn:hover { border-color: var(--brand); }
button.btn:disabled { opacity: .45; cursor: default; }
button.btn.primary {
  background: var(--brand); color: var(--brand-ink); border-color: transparent; font-weight: 650;
  box-shadow: 0 2px 14px color-mix(in srgb, var(--brand) 30%, transparent);
}
button.btn.primary:hover:not(:disabled) { filter: brightness(1.08); background: var(--brand); }
button.btn.good { border-color: var(--good); color: var(--good); font-weight: 650; }
button.btn.danger {
  color: var(--critical); background: none;
  border-color: color-mix(in srgb, var(--critical) 42%, transparent);
}
button.btn.danger:hover:not(:disabled) { border-color: var(--critical); }
button.btn.sm, a.btn.sm, label.btn.sm { padding: 5px 11px; font-size: 13px; font-weight: 500; }

/* `input:not([type])` too: an input written without a type IS text in the DOM,
   but `input[type=text]` never matches it, so it rendered as the browser's
   square default. Four shipped that way (the leads row's six fields, the
   content search, two approval note fields) and the catch-all is what stops
   the fifth. */
input:not([type]),
input[type=text], input[type=password], input[type=date], input[type=search],
input[type=email], input[type=number], input[type=datetime-local],
select, textarea {
  font: inherit; padding: 8px 11px; border-radius: var(--radius-s);
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-primary);
  width: 100%;
}
textarea { resize: vertical; line-height: 1.55; }

/* The select's CLOSED state is ours: our chevron in the token colour, room for
   it, and no stock arrow. The OPEN popup is the operating system's and cannot
   be styled; color-scheme (token blocks above) is what keeps it from flashing
   light-on-dark, and that is the whole treatment. */
select {
  appearance: none; -webkit-appearance: none;
  background-image: var(--chev);
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 12px;
  padding-right: 32px;
}
select[multiple] { background-image: none; padding-right: 11px; }

/* The OPEN popup, where the browser lets us in. Chromium's customizable
   select (appearance: base-select) puts the popup into the page, where the
   tokens reach it: rounded surface, padded options, the selection as the
   brand wash. Pure CSS behind @supports — Firefox and anything older keeps
   the native popup and never sees this. Adopted 2026-08-05 on the user's
   direct feedback (the catalogue picker's popup was a wall of square blue),
   which reverses the native-controls pickup's "do not adopt" line: that line
   guarded against experimental MARKUP, and this block carries none. */
@supports (appearance: base-select) {
  select:not([multiple]) { appearance: base-select; }
  select::picker(select) {
    appearance: base-select;
    background: var(--page-2); color: var(--text-primary);
    border: 1px solid var(--border); border-radius: var(--radius-s);
    box-shadow: var(--shadow-lg);
    padding: var(--s1);
    margin-top: var(--s1);
  }
  select option {
    padding: 6px 10px; border-radius: var(--radius-s);
    background: none;
  }
  select option:hover, select option:focus-visible { background: var(--ghost); }
  /* the highlight rule: a wash of the brand, full-strength type, no border */
  select option:checked { background: var(--brand-soft); font-weight: 600; }
  select optgroup { font-weight: 650; color: var(--muted); }
  /* our chevron stays the one arrow; the UA would draw a second one */
  select::picker-icon { display: none; }
}

/* ── file inputs ──────────────────────────────────────────────────────────
   The browser's own "Choose file" chrome never appears: the input rides
   visually hidden inside a <label class="btn">, so the person sees a pill.
   Never display:none — the harness sets files on it, a screen reader needs
   it focusable, and the label stays its accessible name. The chosen filename
   is echoed in a .fileecho span beside the label (a hidden input also hides
   "which file did I pick"); shell.js keeps it current. */
label.btn { position: relative; }
label.btn input[type=file] {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  border: 0; clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}
label.btn:focus-within { outline: 2px solid var(--brand); outline-offset: 2px; }
.fileecho { color: var(--muted); font-size: 12.5px; }
.fileecho:empty { display: none; }

/* One shared disclosure marker: the campaign page alone uses <details> nine
   times, each with the browser's stock triangle in the text colour. */
summary { cursor: pointer; }
summary::marker { color: var(--muted); }

/* ── .saymore: the one control for text that is not on the screen ────────────
   2026-08-11. The person this platform is for drove it and said: "there's a lot
   of text on the create, on the content board, all of it. So that's maybe why I
   cannot understand it." Measured that day, Create went from 287 visible words at
   the door to 699 as it was ANSWERED, and the Calendar explained seven colours in
   one 118-word paragraph.

   Almost every one of those sentences was added to fix a defect somebody hit, so
   the fix is not shorter writing, which two sessions had already tried. It is that
   a sentence somebody needs ONCE is one press away rather than on the screen for
   ever. A sentence behind this is not a deleted sentence.

   ONE control, and it is here rather than on the pages so that it stays one: this
   platform's own history is that two copies of anything drift. `<details>` and not
   a dialog, because it costs no JavaScript, it survives the innerHTML repaints
   these pages run on, it is keyboard-reachable for nothing, and a closed one counts
   as its summary in `measure_screens.py`, which is how the change is measured at
   all.

   The summary carries the FACT and the body carries the detail. A summary that has
   to be opened to be understood has moved the problem rather than solved it, and on
   a refusal it would be worse than that: silence and refusal must never render
   identically, so what is behind this is always the reason and never the fact that
   there is one. */
details.saymore { margin: var(--s3) 0 0; }
details.saymore:first-child { margin-top: 0; }
details.saymore > summary {
  font-size: 12.5px; font-weight: 600; color: var(--muted);
  padding: var(--s1) 0; line-height: 1.5;
}
details.saymore > summary:hover { color: var(--text-secondary); }
details.saymore > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
details.saymore[open] > summary { margin-bottom: var(--s2); }
details.saymore > :not(summary) { font-size: 13px; color: var(--text-secondary); }
details.saymore p { margin: 0 0 var(--s2); max-width: 84ch; }
details.saymore p:last-child { margin-bottom: 0; }
details.saymore ul { margin: 0; padding-left: var(--s5); }
details.saymore li { margin: 0 0 var(--s1); }

/* Scrollbars inside the working surfaces follow the tokens; the viewport
   scrollbar is left to color-scheme. Thin where a panel scrolls. */
main, dialog { scrollbar-color: var(--border) transparent; }
.overflow, .glossbody, .cmpbody, .prose .tablewrap { scrollbar-width: thin; }
/* This class used to uppercase its text, which made a form look like a tax
   return and produced a real defect: it rendered a product name in capitals
   inside a "type this name exactly to confirm" prompt, so the name on screen
   was a different name from the one on the campaign. Sentence case now. 12px
   rather than 11px for the same reason as .card > h2 above. */
label.fld {
  font-size: 12px; color: var(--muted);
  font-weight: 600; display: block; margin: 14px 0 6px;
}
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.row > div { flex: 1; min-width: 200px; }

.pills { display: flex; flex-wrap: wrap; gap: 7px; }
.pill {
  border: 1px solid var(--border); border-radius: 999px; padding: 5px 13px;
  cursor: pointer; user-select: none; font-size: 13px; background: var(--surface-2);
}
.pill:hover { border-color: var(--brand); }
.pill.on { background: var(--brand); color: var(--brand-ink); border-color: var(--brand); font-weight: 600; }

/* ── the product picker ──────────────────────────────────────────────────────
   Shell.productPicker's skin, moved here from the campaign page and the content
   page in phase A (2026-08-07). Both had their own copy of these classes; they
   had already drifted in padding and in which absence they warned about, and two
   page-local blocks defining the same class names is exactly what the no-shadow
   rule exists to stop. One block, one component.                             */

.presults {
  border: 1px solid var(--border); border-radius: var(--radius-s);
  max-height: 300px; overflow: auto; margin-top: var(--s2);
}
.prow {
  display: flex; gap: var(--s3); align-items: baseline; width: 100%; text-align: left;
  padding: var(--s2) var(--s3); border: 0; border-bottom: 1px solid var(--grid);
  background: none; font: inherit; color: inherit; cursor: pointer;
}
.prow:last-child { border-bottom: 0; }
.prow:hover, .prow:focus-visible { background: var(--ghost); }
.prow .pk-name { font-weight: 600; flex: 1; }
.prow .pk-fact { font-size: 12px; color: var(--muted); }
/* An absence is a warning, not a count: it changes what the person may do next. */
.pk-nospec, .pk-nophoto { color: var(--warn); font-size: 12px; font-weight: 650; }
.picked {
  border: 1px solid var(--good); border-radius: var(--radius-s);
  padding: var(--s1) var(--s4); margin-bottom: var(--s3);
  background: color-mix(in srgb, var(--good) 8%, transparent);
}
.picked .prd {
  display: flex; gap: var(--s3); align-items: baseline;
  padding: var(--s2) 0; border-bottom: 1px solid var(--grid);
}
.picked .prd:last-child { border-bottom: 0; }
.picked .prd b { flex: 1; }

/* ── bits ───────────────────────────────────────────────────────────────── */

.badge {
  font-size: 11px; font-weight: 650; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--border); display: inline-flex; align-items: center; gap: 5px;
}
.badge .dot { width: 8px; height: 8px; border-radius: 50%; }
.chip {
  font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--ghost); color: var(--text-secondary);
  display: inline-flex; align-items: center; gap: 4px;
}
.status-pill { font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--border); white-space: nowrap; }
.empty { color: var(--muted); text-align: center; padding: 34px 0; }
.notice { font-size: 12.5px; padding: 9px 13px; border-radius: var(--radius-s); margin-bottom: 12px; }
.notice.err { background: color-mix(in srgb, var(--critical) 12%, transparent); color: var(--critical); }
.notice.ok  { background: color-mix(in srgb, var(--good) 12%, transparent); color: var(--good); }
.notice.warn{ background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; color: var(--muted); font-weight: 600; font-size: 11.5px; padding: 7px 8px; border-bottom: 1px solid var(--grid); text-transform: uppercase; letter-spacing: .03em; }
td { padding: 8px; border-bottom: 1px solid var(--grid); vertical-align: top; }
tr:last-child td { border-bottom: none; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.trunc { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.overflow { overflow-x: auto; }

.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; }
.legend .item { display: inline-flex; align-items: center; gap: 6px; }
.legend .sw { width: 10px; height: 10px; border-radius: 3px; }
.tooltip {
  position: fixed; pointer-events: none; z-index: 40; display: none;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-s);
  padding: 8px 10px; font-size: 12px; box-shadow: var(--shadow-lg);
}
.tooltip .t { font-weight: 650; margin-bottom: 3px; }
.tooltip .row { display: flex; align-items: center; gap: 6px; color: var(--text-secondary); }
.tooltip .sw { width: 8px; height: 8px; border-radius: 2px; }

.spinner {
  width: 13px; height: 13px; border: 2px solid var(--border);
  border-top-color: var(--brand); border-radius: 50%; display: inline-block;
  animation: spin .7s linear infinite; vertical-align: -2px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── working: a press that is running, and a picture that is arriving ────────
   Reported 2026-08-12: "you click the buttons and the buttons are static, so you
   do not understand if it is generating." 26 of the campaign app's 32 network
   presses said nothing at all, 29 of 36 across all 14 live pages, and three of
   the six that did say something said it in a span BESIDE the button, which is
   the one place that does not answer the question: the thing that looks broken
   is the button.

   Styled off ATTRIBUTES and not new classes. A class here enters the SHARED
   vocabulary and can shadow a page -- `.pn` for the picker collided with the
   tasks page's own `.pn` ("3 open") -- and `aria-busy` is the fact itself rather
   than a name for it, so assistive technology gets it without a second
   mechanism. The three names that ARE new are namespaced `wk-`, like `pk-`,
   `sb-` and `cslt-` before them, and all three were swept to 0 hits across the
   tree immediately before they were written. */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"] > .spinner { margin-right: var(--s2); }

.wk-line { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
           font-size: 13px; color: var(--text-secondary); margin: var(--s3) 0 var(--s2); }
.wk-line > .spinner { align-self: center; }
.wk-line b { font-weight: 650; color: var(--text-primary); }
.wk-bar { height: 3px; border-radius: var(--radius-pill); background: var(--ghost);
          overflow: hidden; margin: 0 0 var(--s3); }
.wk-bar > i { display: block; height: 100%; width: 34%; border-radius: inherit;
              background: var(--brand);
              animation: wk-slide 1.4s cubic-bezier(.2, .8, .25, 1) infinite; }
@keyframes wk-slide { from { transform: translateX(-105%); } to { transform: translateX(305%); } }
.wk-log { font: 12px/1.55 var(--mono); white-space: pre-wrap; color: var(--text-secondary);
          max-height: 220px; overflow-y: auto; margin-top: var(--s2); }
.wk-stale { color: var(--warn); }

/* A picture that has not arrived says so WHERE it will be, so the layout does
   not jump when it does. The skeleton is a BACKGROUND on the <img> and never a
   wrapper: a wrapper changes the box model under `.centry img`, `.cphotos img`
   and `.sh-sheet img`, and `.msg.bot a` already records what happens when a
   page's own two-class rule outranks a helpful one-class one. */
img[data-load] { background-color: var(--ghost); }
img[data-load="pending"] {
  background-image: linear-gradient(100deg, var(--ghost) 28%, var(--surface-2) 50%, var(--ghost) 72%);
  background-size: 220% 100%; animation: wk-sheen 1.3s linear infinite;
}
img[data-load="failed"] { background-color: color-mix(in srgb, var(--warn) 12%, transparent); }
@keyframes wk-sheen { from { background-position: 180% 0; } to { background-position: -80% 0; } }

/* The design rules: three durations, one curve, nothing moves that a person did
   not touch, and everything is off here. THIS BLOCK DID NOT EXIST UNTIL TODAY,
   so the .spinner above had been spinning through it since the shell was
   written, and the rule was unimplemented rather than merely unenforced.

   A frozen spinner reads as nothing at all, which is why every busy state
   carries WORDS and the label is not optional: under this block the words ARE
   the state. The bar becomes a full quiet rule rather than a stopped sliver,
   because a 34%-wide bar that never moves reads as "34% done". */
@media (prefers-reduced-motion: reduce) {
  .spinner, .wk-bar > i, img[data-load="pending"] { animation: none; }
  .spinner { border-top-color: var(--border); }
  .wk-bar > i { width: 100%; opacity: .4; }
  img[data-load="pending"] { background-image: none; }
}

.hide { display: none !important; }

/* ── prose: rendered campaign copy and translations ─────────────────────── */
/* These deliverables are documents. They should read like documents, not like a
   terminal dump of the markdown they happen to be stored as.
   The CONTAINER fills the card (DESIGN_RULES.md: "No max-width on a page
   region"); the cap goes on the text elements only, at 90ch to match the rest
   of the platform. Same fix already shipped once on each app's own share page
   on 2026-07-15 (translation_pipeline/static/index.html, the old
   campaign_pipeline/static/index.html's .blk .doc) -- this is that fix reaching
   the shared class both apps' /share pages still render through, where a fixed
   74ch column measured at 578px sat inside an 882px card and left 287px of
   empty card on the right. Measured 2026-08-13. */

.prose { font-family: var(--sans); font-size: 14.5px; line-height: 1.65; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose h1, .prose h2, .prose h3 {
  font-weight: 680; line-height: 1.3; letter-spacing: -0.01em;
  margin: 22px 0 8px; color: var(--text-primary); max-width: 90ch;
}
.prose h1 { font-size: 19px; }
.prose h2 { font-size: 16px; }
.prose h3 {
  font-size: 13px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); font-weight: 700;
}
.prose p { margin: 0 0 11px; max-width: 90ch; }
.prose ul, .prose ol { margin: 0 0 11px; padding-left: 22px; max-width: 90ch; }
.prose li { margin: 3px 0; }
.prose strong { font-weight: 650; }
.prose hr { border: 0; border-top: 1px solid var(--grid); margin: 20px 0; }
.prose code { font-family: var(--mono); font-size: .9em; background: var(--ghost); padding: 1px 5px; border-radius: 4px; }
.prose blockquote {
  margin: 0 0 11px; padding: 6px 13px; border-left: 3px solid var(--baseline);
  background: var(--ghost); border-radius: 0 var(--radius-s) var(--radius-s) 0; color: var(--text-secondary);
  max-width: 90ch;
}
.prose table { max-width: 100%; margin: 0 0 14px; }
.prose .tablewrap { overflow-x: auto; max-width: 100%; }
/* A social post is pasted verbatim into Meta, so its line breaks are content. */
.prose .verbatim {
  white-space: pre-wrap; font-family: var(--sans);
  background: var(--ghost); border-radius: var(--radius-s); padding: 10px 13px; margin: 0 0 11px;
}

/* ── the glossary ────────────────────────────────────────────────────────────
   Some of the vocabulary here has to be learned: a tier, an occasion, what
   "unknown" does to a market, what a [CHECK] marker is asking of you. Each used
   to be re-explained in a paragraph beside the field it applied to, on a screen
   where somebody is trying to get work done rather than read. The definitions
   are in shell.js, in one table; this is what they look like.

   Three presentations, all routing through the one delegated listener:
     .glossall   one quiet entry point under the page title ("Words used here")
     .glossbtn   a `?` badge beside a field label, for one term
     .glossterm  the word itself, underlined, for use inside a sentence          */

.glossall {
  background: none; border: 0; padding: 4px 0 0; margin: 0; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--text-secondary);
  text-decoration: underline dotted; text-underline-offset: 3px;
}
.glossall:hover { color: var(--brand); }

/* A label's own `?`. Sized in em so it follows whatever label it sits beside,
   and never so large that it competes with the label itself. */
.glossbtn {
  background: none; border: 1px solid var(--border); border-radius: 50%;
  width: 1.35em; height: 1.35em; padding: 0; margin-left: 5px; cursor: pointer;
  font: inherit; font-size: .92em; font-weight: 700; line-height: 1;
  color: var(--muted); vertical-align: -.12em;
}
.glossbtn:hover { color: var(--brand); border-color: var(--brand); }

.glossterm {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  color: inherit; text-decoration: underline dotted; text-underline-offset: 2px;
  text-decoration-color: var(--baseline);
}
.glossterm:hover { color: var(--brand); text-decoration-color: var(--brand); }

/* showModal() brings Esc, the focus trap and focus restore with it, so the
   dialog is the element rather than a div pretending to be one. */
.glossdlg {
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-solid); color: var(--text-primary);
  box-shadow: var(--shadow-lg);
  width: min(640px, calc(100vw - 28px)); max-width: none;
  max-height: min(80vh, 720px); overflow: hidden;
  flex-direction: column;                 /* only applies once [open] displays it */
}
.glossdlg[open] { display: flex; }
.glossdlg::backdrop { background: rgba(11,11,11,.42); }
:root[data-theme="dark"] .glossdlg::backdrop { background: rgba(0,0,0,.62); }

.glosshead {
  display: flex; align-items: center; gap: 12px; flex: 0 0 auto;
  padding: 13px 16px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.glosshead h2 { margin: 0; font-size: 15px; font-weight: 680; }
.glosshead .iconbtn { margin-left: auto; }

.glossbody { padding: 4px 16px 16px; overflow-y: auto; flex: 1 1 auto; }
.gterm { padding: 14px 0; border-bottom: 1px solid var(--grid); scroll-margin-top: 4px; }
.gterm:last-child { border-bottom: 0; padding-bottom: 4px; }
.gterm h3 { margin: 0 0 5px; font-size: 14px; font-weight: 700; }
/* The term somebody asked for, when they came in through a `?` rather than the
   whole list: marked, not isolated, because the neighbouring definitions are
   usually the other half of the answer. */
.gterm.on {
  border-left: 3px solid var(--brand); padding-left: 11px;
  background: color-mix(in srgb, var(--brand) 6%, transparent);
  border-radius: 0 var(--radius-s) var(--radius-s) 0;
}
.gterm .def { font-size: 13.5px; }
.gterm .def p:last-child { margin-bottom: 0; }

/* The markers are what a human must act on. Same colours everywhere they appear. */
.mk-check { background: color-mix(in srgb, var(--warn) 26%, transparent); border-bottom: 1px dotted var(--warn); padding: 0 3px; border-radius: 3px; }
.mk-local { background: color-mix(in srgb, var(--series-2) 22%, transparent); padding: 0 3px; border-radius: 3px; }
.mk-quote { background: color-mix(in srgb, var(--critical) 20%, transparent); padding: 0 3px; border-radius: 3px; }
/* Highlighted is not findable: in a 36-paragraph document an amber wash three
   screens down is found by scrolling. This is what the jump lands on. */
.mk-on { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* ── source beside translation ───────────────────────────────────────────────
   The reading page stays a document at 1120px, which is the right measure for
   one column of prose and the wrong one for two. So the comparison is a dialog
   over the whole window, opened deliberately, rather than a second layout the
   reader did not ask for. Same [open] discipline as .glossdlg: no `display` in
   the base rule, or a <dialog> shows on every page that loads this file. */
.cmpdlg {
  padding: 0; border: 0; background: var(--surface-solid); color: var(--text-primary);
  width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh;
  flex-direction: column;                 /* only applies once [open] displays it */
}
.cmpdlg[open] { display: flex; }
.cmpdlg::backdrop { background: rgba(11,11,11,.42); }
:root[data-theme="dark"] .cmpdlg::backdrop { background: rgba(0,0,0,.62); }

.cmphead {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto; flex-wrap: wrap;
  padding: 11px 18px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.cmphead h2 { margin: 0; font-size: 15px; font-weight: 680; }
.cmphead .sp { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.cmpbody { overflow-y: auto; flex: 1 1 auto; padding: 0 18px 44px; }

/* minmax(0, 1fr), not 1fr: a bare 1fr floors at min-content, and one long URL in
   a translation then pushes the whole dialog sideways. */
.cmpgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 26px; }
.cmpcol {
  /* Sticky over scrolling prose, so it is the same legibility question as the
     dialog itself: a translucent header shows the paragraphs sliding under it. */
  position: sticky; top: 0; z-index: 1; background: var(--surface-solid);
  padding: 10px 0 8px; font-size: 12px; font-weight: 650; letter-spacing: .02em;
  color: var(--text-secondary); border-bottom: 1px solid var(--border);
}
.cmpcell {
  padding: 11px 0; border-bottom: 1px solid var(--grid); min-width: 0;
  font-size: 14.5px; line-height: 1.65;
}
.cmpcell > :first-child { margin-top: 0; }
.cmpcell > :last-child { margin-bottom: 0; }
.cmpcell.src { color: var(--text-secondary); }
/* A row where one paragraph answered two, or answered none. Marked on both cells
   so the reader sees it whichever side they are reading. */
.cmpcell.odd { background: color-mix(in srgb, var(--warn) 7%, transparent); }
.cmpcell .why { display: block; font-size: 11.5px; color: var(--warn); margin-top: 5px; }
.cmpcell .nothing { color: var(--muted); font-style: italic; font-size: 13px; }

/* Below this a two-column comparison is two half-columns of nothing. Stack them:
   the grid order is already source, translation, source, translation. */
@media (max-width: 760px) {
  .cmpgrid { grid-template-columns: minmax(0, 1fr); }
  /* The two column headings stop heading anything once the columns are stacked, so
     each paragraph carries its own language instead. */
  .cmpcol { display: none; }
  .cmpcell { padding: 8px 0; }
  .cmpcell::before {
    content: attr(data-lang); display: block; margin-bottom: 3px;
    font-size: 11px; font-weight: 650; letter-spacing: .02em; color: var(--muted);
  }
  .cmpcell.src { border-bottom: 0; padding-bottom: 4px; }
}

/* ── the consult box ─────────────────────────────────────────────────────────
   The drawer's first form (phase C, 2026-08-07): the product bot's own
   conversation, over whatever page you are on, opened from the rail or from a
   button on Home. Built in shell.js, never page-local, because phase D upgrades
   this same component rather than adding a second one.

   EVERY class here is prefixed `cslt-`, and that is not a style preference. A
   class added to this file enters the shared vocabulary and can shadow a page
   that had the name first: `.msg` is page-local on three migrated pages,
   `.thread`, `.turn`, `.cbox` and `.log` on others, and a collision fails the
   innocent page's shadow rule rather than this one. Swept programmatically
   against every page before it was written; zero collisions.

   Same [open] discipline as .glossdlg and .cmpdlg: no `display` in the base
   rule, or a <dialog> shows on every page that loads this file.               */
.cslt-dlg {
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-solid); color: var(--text-primary);
  box-shadow: var(--shadow-lg);
  width: min(620px, calc(100vw - 28px)); max-width: none;
  height: min(74vh, 680px); max-height: calc(100vh - 32px);
  flex-direction: column;                 /* only applies once [open] displays it */
}
.cslt-dlg[open] { display: flex; }
.cslt-dlg::backdrop { background: rgba(11,11,11,.42); }
:root[data-theme="dark"] .cslt-dlg::backdrop { background: rgba(0,0,0,.62); }

.cslt-head {
  display: flex; align-items: flex-start; gap: var(--s3); flex: 0 0 auto;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.cslt-titles { min-width: 0; }
/* The header is where the box says whose conversation this is. It is not
   decoration: the same thread is open on the Products page, and a person who
   thinks this is a scratch pad would be surprised by what the bot remembers. */
.cslt-sub { color: var(--muted); font-size: 12px; margin-top: 2px; }
.cslt-head .cslt-x { margin-left: auto; flex: none; }

.cslt-fail { flex: 0 0 auto; padding: 0 var(--s4); }
.cslt-fail:empty { display: none; }
.cslt-fail .notice { margin-top: var(--s3); }

.cslt-thread {
  flex: 1 1 auto; overflow-y: auto; padding: var(--s4);
  display: flex; flex-direction: column; gap: var(--s3);
}
.cslt-turn { max-width: 82%; font-size: 13.5px; line-height: 1.6; }
.cslt-turn > :first-child { margin-top: 0; }
.cslt-turn > :last-child { margin-bottom: 0; }
.cslt-me {
  align-self: flex-end; background: var(--brand); color: var(--brand-ink);
  padding: var(--s2) var(--s3); border-radius: var(--radius-s);
}
.cslt-bot {
  align-self: flex-start; background: var(--surface-2);
  padding: var(--s2) var(--s3); border-radius: var(--radius-s);
}
.cslt-pending { color: var(--muted); }
.cslt-note { align-self: center; color: var(--muted); font-size: 12.5px; text-align: center; }

.cslt-form {
  flex: 0 0 auto; display: flex; gap: var(--s2); align-items: flex-end;
  padding: var(--s3) var(--s4); border-top: 1px solid var(--border);
  background: var(--surface-2);
}
.cslt-in { flex: 1 1 auto; min-width: 0; resize: vertical; }
.cslt-send { flex: none; }

/* ── the two segments (phase D) ──────────────────────────────────────────────
   Two NAMED CONVERSATIONS in one mount, never one blended chat: Products is the
   product bot, with its own per-user memory, sticky country and repetition
   ledger, and Platform is the agent, where every turn is independent. A router
   deciding per turn which brain owns a sentence would have to route follow-ups,
   and a follow-up carries its meaning in the OTHER brain's state, so the router
   breaks whichever thread it interrupts. The segments make the boundary a fact
   the reader can see instead of a guess a router makes.                       */
.cslt-seg {
  display: flex; gap: 2px; flex: none; margin-top: var(--s2);
  background: var(--surface-2); border-radius: var(--radius-s); padding: 2px;
}
.cslt-segbtn {
  border: 0; background: none; font: inherit; cursor: pointer;
  color: var(--muted); padding: var(--s1) var(--s3);
  border-radius: calc(var(--radius-s) - 2px);
}
.cslt-segbtn[aria-selected="true"] {
  background: var(--surface-1); color: var(--text-primary); font-weight: 600;
}

/* A result and a choice are BUTTONS, because the drawer never builds a URL and
   the page decides what opening one means. They are appended with appendChild,
   so a listener on the dialog is what makes them work. */
.cslt-list { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s2); }
.cslt-hit {
  display: block; width: 100%; text-align: left; cursor: pointer; font: inherit;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: var(--radius-s); padding: var(--s2) var(--s3);
}
.cslt-hit:hover { background: var(--surface-2); }
.cslt-rank { color: var(--muted); font-variant-numeric: tabular-nums; }
.cslt-why, .cslt-quote { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
.cslt-quote { border-left: 2px solid var(--border); padding-left: var(--s2); }
.cslt-can { margin: var(--s2) 0 0; padding-left: var(--s4); font-size: 12.5px; }

/* Two classes deep, deliberately. `.msg.bot a` on the products page is two
   classes and would outrank a one-class rule, and the failure mode is a drawer
   that looks almost right: the wrong link colour, a nowrap, and a download
   arrow it never asked for. This drawer is on that page. */
.cslt-dlg .cslt-turn a { color: var(--brand); text-decoration: underline; white-space: normal; }
.cslt-dlg .cslt-turn a::after { content: none; }

/* The rail's own entry for it. It is not a page, so it carries no count and is
   not one of the four the phone keeps: it is the door to the drawer. */
.cslt-trigger {
  background: none; border: 0; width: 100%; cursor: pointer;
  font: inherit; text-align: left;
}

@media (max-width: 560px) {
  .cslt-dlg { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }
  .cslt-turn { max-width: 92%; }
}

/* ── the picture, full size ───────────────────────────────────────────────────
   Every content image on the platform opens here, because on this platform an
   image is EVIDENCE and a thumbnail cannot be checked. The rule that made this
   necessary is already written down: zoom the wordmark, THEN zoom the line under
   it. A 130px card cannot answer either question, and the reviewer whose job is
   to catch an invented badge was being asked to catch it at 130px.

   Dark chrome regardless of theme: this is a viewer, the picture is the only
   thing that should carry colour, and a white surround shifts how a photograph's
   own white balance reads. Same [open] discipline as every other dialog here. */
.lb-dlg {
  padding: 0; border: 0; background: transparent;
  width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh;
  overflow: hidden; flex-direction: column; color: #FFFFFF;
}
.lb-dlg[open] { display: flex; }
.lb-dlg::backdrop { background: rgba(6,8,12,.92); }

.lb-bar {
  display: flex; align-items: center; gap: var(--s3); flex: 0 0 auto;
  padding: var(--s3) var(--s4); background: rgba(0,0,0,.55);
  border-bottom: 1px solid rgba(255,255,255,.10);
}
.lb-cap {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 12.5px; color: rgba(255,255,255,.82);
}
.lb-sp { margin-left: auto; display: flex; align-items: center; gap: var(--s2); flex: none; }
/* The zoom readout is also the reset control, which is why it is a button and
   not a label: at 240% the thing you want is one press back to fitted. */
.lb-pct {
  font: 12px/1 var(--mono); font-variant-numeric: tabular-nums;
  color: rgba(255,255,255,.82); background: rgba(255,255,255,.10);
  border: 0; border-radius: var(--radius-pill); padding: var(--s2) var(--s3);
  cursor: pointer; min-width: 62px; text-align: center;
}
.lb-pct:hover { background: rgba(255,255,255,.18); }
.lb-dlg .iconbtn { color: #FFFFFF; }
.lb-dlg .iconbtn:hover { background: rgba(255,255,255,.16); }

/* The stage clips; the image is moved by transform inside it. Nothing here
   scrolls, because a scrollbar plus a drag-to-pan is two answers to one
   question and they fight on a trackpad. */
.lb-stage {
  flex: 1 1 auto; position: relative; overflow: hidden;
  display: grid; place-items: center; touch-action: none;
}
.lb-img {
  position: absolute; top: 0; left: 0; transform-origin: 0 0;
  max-width: none; max-height: none;
  user-select: none; -webkit-user-drag: none;
}
.lb-stage.lb-fit  { cursor: zoom-in; }
.lb-stage.lb-can  { cursor: grab; }
.lb-stage.lb-drag { cursor: grabbing; }
.lb-hint {
  position: absolute; left: 50%; bottom: var(--s4); transform: translateX(-50%);
  font-size: 12px; color: rgba(255,255,255,.72); background: rgba(0,0,0,.55);
  border-radius: var(--radius-pill); padding: var(--s2) var(--s3);
  pointer-events: none;
}
/* Never silent: a picture that will not load has to say so, exactly as the
   thumbnails it was opened from do. */
.lb-fail { color: rgba(255,255,255,.82); font-size: 13px; padding: var(--s5); text-align: center; }

/* What tells a reader an image is worth pressing. Applied by the page, so a
   logo or an icon never gets it. */
img[data-lb] { cursor: zoom-in; }

@media (max-width: 560px) {
  .lb-hint { display: none; }       /* no wheel and no hover to explain */
  .lb-cap  { display: none; }
}

/* ── asking before something irreversible ────────────────────────────────────
   Until 2026-08-12 twenty-five presses across four pages asked with the
   BROWSER's own confirm() and prompt(): a grey sheet dropping from the top of
   the tab, carrying the origin URL, an OK and a Cancel, in the browser's
   chrome and the browser's language. It reads as an error or a security
   warning, and it sat on exactly the presses that most need a person calm and
   reading — the four that spend credits, the release of a picture to a
   country, the withdrawal of an approval, the send of a message to a real
   customer. Somebody startled by the box asking the question is not reading
   the question.

   None of what replaces it is decoration. confirm() cannot say what a press
   costs, cannot colour a destruction red, cannot name its own button
   ("Generate", never "OK"), cannot speak the country reviewer's language, and
   cannot be driven by the browser suites, which is why not one of these
   presses had ever been driven. It also blocks the whole tab while it is up,
   so the page behind it is frozen rather than merely covered.

   EVERY class here is prefixed `cf-`, swept against every page's class_vocab
   before it was written (zero collisions), for the reason the `.pn` collision
   cost: a name added to this file enters the shared vocabulary and can shadow
   a page that had the name first. Same [open] discipline as .glossdlg,
   .cmpdlg and .cslt-dlg: no `display` in the base rule, or a <dialog> shows on
   every page that loads this file.                                          */
.cf-dlg {
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-solid); color: var(--text-primary);
  box-shadow: var(--shadow-lg);
  width: min(500px, calc(100vw - 28px)); max-width: none;
  max-height: calc(100vh - 32px);
  flex-direction: column;                 /* only applies once [open] displays it */
}
.cf-dlg[open] { display: flex; }
.cf-dlg::backdrop { background: rgba(11,11,11,.42); }
:root[data-theme="dark"] .cf-dlg::backdrop { background: rgba(0,0,0,.62); }

.cf-head {
  display: flex; align-items: flex-start; gap: var(--s3); flex: 0 0 auto;
  padding: var(--s5) var(--s5) 0;
}
.cf-head h2 { margin: 0; font-size: 16px; font-weight: 680; line-height: 1.35; }

/* The one thing that says which KIND of question this is before a word of it
   is read: red destroys, refuses or spends, navy is everything else. An icon
   AND a colour, never a colour on its own. */
.cf-mark {
  flex: none; width: 30px; height: 30px; border-radius: var(--radius-pill);
  display: grid; place-items: center;
  color: var(--brand); background: color-mix(in srgb, var(--brand) 14%, transparent);
}
.cf-dlg.cf-danger .cf-mark {
  color: var(--critical); background: color-mix(in srgb, var(--critical) 14%, transparent);
}

.cf-body {
  padding: var(--s3) var(--s5) 0; overflow-y: auto; flex: 1 1 auto;
  font-size: 14px; line-height: 1.6; color: var(--text-secondary);
}
.cf-body p { margin: 0 0 var(--s2); }
.cf-body p:last-child { margin-bottom: 0; }

/* What the press costs, or what about it cannot be taken back: the single fact
   a person is being asked to weigh, so it is never left as the third sentence
   of a paragraph. Tinted, which per the design rules is coloured glass with no
   border and no rule down the side. */
.cf-cost {
  --tint: var(--warn);
  margin: var(--s3) var(--s5) 0; padding: var(--s2) var(--s3);
  border-radius: var(--radius-s); font-size: 13px; font-weight: 600;
  display: flex; align-items: center; gap: var(--s2);
  color: var(--tint); background: color-mix(in srgb, var(--tint) 15%, transparent);
  border: 0; box-shadow: none;
}
.cf-dlg.cf-danger .cf-cost { --tint: var(--critical); }

.cf-fields { padding: var(--s4) var(--s5) 0; display: grid; gap: var(--s4); }
.cf-f { display: grid; gap: var(--s1); min-width: 0; }
.cf-f > label { font-size: 12.5px; font-weight: 650; color: var(--text-primary); }
.cf-f .cf-hint { font-size: 12px; color: var(--muted); line-height: 1.5; }
.cf-f input, .cf-f select, .cf-f textarea { width: 100%; }
.cf-f textarea { min-height: 78px; }
/* Never silent, and never a dialog that simply refuses to close: an empty
   required field says which one and why, next to itself. */
.cf-bad { font-size: 12px; color: var(--critical); font-weight: 650; }

.cf-foot {
  display: flex; justify-content: flex-end; gap: var(--s2); flex-wrap: wrap;
  padding: var(--s5); flex: 0 0 auto;
}
/* At 320px a right-aligned pair wraps into a column and lands the destructive
   press at the BOTTOM, which is exactly where a thumb rests. Reversed, so the
   button that acts is on top and Cancel is under the thumb. */
@media (max-width: 460px) {
  .cf-foot { flex-direction: column-reverse; }
  .cf-foot .btn { width: 100%; justify-content: center; }
}
