@charset "UTF-8";
/* ═══════════════════════════════════════════════════════════════════════
   sales.css — AiMY Sales v2, product chrome only
   ───────────────────────────────────────────────────────────────────────
   Layout and shell. NO COMPONENTS: every component on this surface comes
   from aimy-ds.css, which is extracted from the design system and must not
   be edited here. If something is missing, it is a gap in the system and
   belongs in ../GAPS.md, not in a local re-implementation.

   The ecosystem chrome — topnav, sidebar metrics, canvas stacking, the
   notification panel, the theme toggle — is shared with AiMY QA and AiMY
   Knowledge v2 and is carried across deliberately so the three agents read
   as one product. Where a value differs from Knowledge's, it is because
   Knowledge's is off the spacing scale: its own css-audit reports thirteen
   violations in this exact region, all introduced after the last audit run.
   They are on the scale here.

   Three sections Knowledge carries are NOT here, and their absence is the
   point: BUTTON HEIGHTS, COPY BUTTON and AIMY TOAST were local patches for
   design-system gaps 1.10-1.12, and the system closed all three upstream.
   Sales extracts from a commit that has them, so it forks nothing.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   FORM-CONTROL RESET

   The design system's base has no reset for form controls, and most of its
   specimens are <div>s, so the omissions never surface on the documentation
   page. On the correct interactive elements they do: <button> does not
   inherit font-family, and .tab declares no background at all — so on a
   <button> the UA's buttonface shows through as a light chip on a dark
   shell, which reads as the navbar rendering in light mode.

   Nothing here sets a colour, size or spacing. It is a reset, not a
   restyle. Element-selector specificity (0,0,1), so any component class
   still wins.
═══════════════════════════════════════════════ */
input, select, textarea { font-family: inherit; }

/* ═══════════════════════════════════════════════
   THE TYPE SYSTEM

   Leadership's words were "things feel detached and not in its optimal
   place". That is not taste, and it is not spacing. It is measurable, and
   the measurement is this:

       var(--fs-2xs)  10px   47 declarations
       var(--fs-xs)   11px   50 declarations
       var(--fs-sm)   12px   26 declarations
       everything else       14 declarations

   NINETY-SEVEN OF ONE HUNDRED AND THIRTY-SEVEN font sizes on this surface
   were 10px or 11px. The product was not densely set; it was set entirely
   in micro-label, with no body text anywhere for a label to be smaller
   *than*. Nothing was ranked, so nothing anchored, so everything floated —
   which is exactly what "detached" describes.

   The root cause is one line that does not exist. `aimy-ds.css`'s `body`
   rule declares font-family, background, colour, display, flex-direction
   and min-height, and NO FONT-SIZE. So the document root was the browser's
   16px and every component invented its own reduction from it, one at a
   time, with nothing to calibrate against. Butterick's first maxim of page
   layout is "body text first — its four properties determine everything".
   It was never set, so the other three could not be right.

   ── The scale ──

   Four steps inside 3px (11 · 12 · 13 · 14) is not a scale, it is a
   cluster: the eye cannot read a 1px difference as a level, so four
   "different" sizes all read as one. Re-spaced so every step is visible:

       12 · 14 · 16 · 18 · 20 · 24 · 32

   Three text levels and one label style, per Butterick's max-3-headings
   rule: body 16, group heading 18, page title 20 — and 14 for the meta
   line under a name. 12 is the MINIMAL-COMPONENT size — a tag, a work
   state, a timestamp — and it is never a sentence.

   EVERY STEP IS EVEN, and that is the rule that makes the floor hold. "Under
   14 is too small" catches eleven and lets thirteen through; "even, and
   never below twelve" catches both, and a sweep can apply it without a
   human ruling on each site. The first pass corrected the shell and this
   file's `--ty-*` roles and left THIS block alone, so a corrected shell was
   overridden back to 13px by the product on top of it. A product override
   is the last word on a token, so it is the first place a floor has to
   reach.

   THE DUPLICATES ARE DELIBERATE. `--fs-md` resolves to the same value as
   `--fs-base`, and `--fs-xl` to the same as `--fs-lg`. A component that
   reaches for a step which no longer exists lands on the nearest real one
   instead of re-opening the cluster. Deleting the tokens would have been
   the other option and is worse: the library reads them, and the library
   is extracted rather than authored here.

   Overridden in this file rather than in `aimy-ds.css` for the reason the
   `--sidebar-width` override gives a few sections down — the extracted
   stylesheet stays a faithful copy of the library, and every divergence
   stays visible. All of it is filed in ../GAPS.md.
═══════════════════════════════════════════════ */
/* ══ FIVE STEPS, AND THIS TIME THEY ARE USED ═══════════════════════════════

   THE SCALE WAS TWELVE STEPS AND THE SURFACE USED TWO. Counted: 232
   `font-size` declarations in this file, of which **200 — 86% — resolve to
   12px or 13px**, one pixel apart. `--fs-2xs` alone is 95 of them.
   `--fs-3xl`, `--fs-4xl` and `--fs-5xl` were declared and never used once.

   That is the whole of "there is no hierarchy". A reader cannot see a
   difference of one pixel, so every label, every meta line, every card title
   and every block heading arrived at the same volume, and the eye had
   nowhere to land. No amount of spacing fixes a page whose type is one size.

   So: five steps a reader can tell apart, each with a named job, and the top
   of the scale finally in use. AiMY QA sets its anchor figures at 32px
   against 11px body — a ratio of three — which is what makes its cards
   legible at eleven pixels. This is that, one step gentler.

     12  minimal   a tag, a work state, a timestamp — recognised, not read
     14  meta      the supporting line under a name; the smallest sentence
     16  body      prose, insight sentences, anything you read
     18  heading   a block heading inside a surface
     20  title     the name of the thing you have open
     32  anchor    the ONE number a surface is about

   The gaps are 2 / 2 / 2 / 2 / 12 — even, all the way up to the anchor,
   which is a different kind of object rather than another step. Butterick's
   "smallest visible increment" applies within a role; between roles the step
   has to read as a different kind of thing, and two pixels at these sizes
   does, where one never did.

   CARD BODY CARRIES WEIGHT 600 AT BODY SIZE — QA's lesson, and the one
   thing that stops a card of small text reading as grey mush: weight carries
   hierarchy and size carries role. It used to buy that by dropping a step to
   13px, which is the trade the floor no longer allows. */
/* macOS renders text heavier than the design intends, and nothing in this
   product or the design system had ever turned that off. Once, on the root,
   never per component. A no-op on Windows and Linux, correct on a Mac. */
html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

:root {
  --fs-2xs:  12px;   /* minimal components — a mark, not a sentence */
  --fs-xs:   14px;   /* the smallest sentence — meta, the line under a name */
  --fs-sm:   16px;   /* → body */
  --fs-base: 16px;   /* BODY */
  --fs-md:   16px;   /* → body */
  --fs-lg:   18px;   /* block heading */
  --fs-xl:   20px;   /* the name of the thing you have open */
  --fs-2xl:  24px;   /* → title */
  --fs-3xl:  32px;   /* the one number a surface is about */
  --fs-4xl:  32px;   /* → anchor. One anchor size, not two. */

  /* ── Leading, and it was inverted ──

     The library's `--lh-base: 1.55` sits above Butterick's 1.2–1.45 range,
     while its SMALLEST text carries its TIGHTEST leading (`.finding-sub`,
     10px, at 1.4). That is backwards in both directions at once: small text
     needs proportionally more leading, not less, because the eye has less
     glyph to track along.

     THE SECOND CORRECTION IS THE GROUND. Butterick's range is print, which
     is dark type on white. This product is the inverse, and light type on a
     dark ground blooms — the glyphs spread slightly into the space around
     them, so lines that measure the same sit visually closer. The standing
     advice for that is to pay it back on three axes: a little more leading,
     a little more tracking, and a step more weight where the face needs it.

     Only leading is available here. Tracking on lowercase body is wrong at
     reading sizes whatever the ground, and the weight axis is already at
     its floor — body is 400 and 400 is the minimum below 18px. So body
     takes 1.5, the bottom of the 1.5–1.6 band rather than 1.45, which was
     the TOP of a range computed for the opposite polarity. The meta step
     keeps its lead over body, because the first correction above still
     holds: smaller text needs proportionally more. */
  --lh-tight:   1.25;  /* headings and the anchor numbers */
  --lh-base:    1.5;   /* body — the 1.5–1.6 band, dark ground paid for */
  --lh-relaxed: 1.6;   /* the meta and label sizes, which need more still */

  /* Tracking. -0.02em at 13px is -0.26px — imperceptible, and negative
     tracking on small lowercase is wrong even when it can be seen. It is
     applied above 24px only, where it is a real optical correction.

     `--ls-wide` goes the other way and was under-set: caps need 5–12% and
     the library ships 4%, which is uppercase with the fix left out. Raised
     at the token so every all-caps run that already reads it is corrected
     at once, rather than patching each site. */
  --ls-wide: 0.06em;
}

/* ══ ROLES, NOT RUNGS ══════════════════════════════════════════════════════

   The scale above is correct and the audit agrees: 307 sizes, all on it. It
   also reports the thing the scale cannot fix — "73% on the two smallest
   steps, 7 at the 32px anchor". A scale is not a hierarchy. Every site that
   reaches for `--fs-xs` gets 13px whether it is a name, a timestamp or a
   count, because the token says how BIG, never what FOR.

   AiMY Knowledge solved this two products ago (`knowledge.css:286-306`) and
   Sales never took the port. A role token names the job, so the decision at
   the call site is "what is this" rather than "how big is this" — and the
   answer to "what is this" is the one a reader can actually see.

   Two steps here are new: 18, the sentence under a kicker, and 60, the one
   figure a page is about. The other six are NAMES FOR SIZES THAT ALREADY
   EXIST, which is why this port moves nothing that is already shipped.

   `--fs-5xl` stays unadopted on purpose — `css-audit.js:370-374` argues that
   folding in a library step the product never chose lets the audit call a
   size legal that nobody picked. `--ty-masthead` lands on the same 46px and
   that is not an accident: the library was right about where the top of the
   scale is, and wrong only in shipping it to a product that had no role for
   it. This declares the role, so the size is chosen rather than inherited.

   ── THE NAME IS THE TOP OF THE SCALE ──

   The first cut of this put the lead figure at 60 and dropped the name to
   20, on the reasoning that you already know which campaign you opened. That
   is true and it is not the whole rule: a masthead is not there to inform
   you, it is there to tell you WHERE YOU ARE, and a page whose largest
   element is a statistic reads as a chart somebody has misplaced a title on.
   A newspaper front page is the reference this whole pass is working from,
   and on one of those the masthead is the largest thing on the page, every
   time, above a headline that is merely the largest thing in the story.

   So the name leads and nothing on the surface may outrank it. The figure
   the page is about comes second, clearly and by a visible step. */
:root {
  --ty-masthead: 46px;  /* NEW — the name of the campaign. THE CEILING.     */
  --ty-lead:     34px;  /* NEW — the ONE figure a page is about             */
  --ty-display:  24px;  /* NEW — a figure a section is about                */
  --ty-title:    20px;  /* = --fs-xl — a figure inside a row, a thing named  */
  --ty-deck:     18px;  /* NEW — the sentence under a kicker. The headline. */
  /* WAS 16px, WHICH IS THE BODY STEP. Its own comment says `= --fs-lg` and
     --fs-lg is 18px; the scale above says "18 heading" in as many words. The
     comment and the doctrine agreed with each other and the value agreed
     with neither, so ten block headings were set at exactly the size of the
     prose underneath them, carrying the whole distinction on weight. A
     heading may share a size with its body only if something else keeps
     them apart, and here nothing else did: same family, same ink, same
     step. It takes the step the scale always said it had. */
  --ty-heading:  18px;  /* = --fs-lg  — a block heading inside a surface    */
  --ty-body:     16px;  /* = --fs-base — prose, anything you read           */
  --ty-meta:     14px;  /* = --fs-xs  — a label, the line under a name      */
  --ty-micro:    12px;  /* = --fs-2xs — minimal components. Caps and tracked, never a sentence */
}

/* ══ INK ═══════════════════════════════════════════════════════════════════

   Same argument, applied to colour. `--d50` through `--d600` are rungs on a
   ramp; they say how light, never what for. So prose landed on `--d100` in
   one place and `--d200` in another, labels on `--d400` or `--d500` by feel,
   and the surface came out uniformly grey — which is the second half of "no
   hierarchy", and the half that spacing and size cannot reach.

   Five ranks, each with a job. Knowledge's rule holds here: a rank differs
   from its neighbour in at least two of {step, weight, ink}. One axis of
   difference is what a flat page looks like from the inside.

   Every role resolves THROUGH a `--d*` called rather than to a hex, so the
   light-mode inversion at `aimy-ds.css:200-261` carries them for free. There
   is no light fork below, and there must not be one. */
:root {
  --ink-primary:   var(--d50);   /* identity — figures, headlines, a name   */
  --ink-secondary: var(--d200);  /* substance — prose, the sentence itself  */
  --ink-quiet:     var(--d300);  /* attribution — who, when, where          */
  --ink-faint:     var(--d400);  /* ambient — labels, captions, counts      */
  --ink-rule:      var(--d600);  /* NON-TEXT — hairlines and dots. Never type. */
}

/* ── One face, and weight does the separating ──

   Poppins sets everything here now, titles included, so this token exists
   only to keep the ~20 rules that name it working. It resolves to the same
   face as `--font-sans`, and there is no pairing left.

   The note that stood at this line argued Poppins OUT of the display slot,
   because a 13px Poppins heading beside 13px Urbanist body was
   indistinguishable — the pairing separated nothing, which is the second
   reason headings did not anchor the groups beneath them. Its conclusion
   outlives its own premise: it is WEIGHT and size, not skeleton, that
   separate a heading from the body under it. With one face that is now the
   only mechanism there is, so it has to be spent deliberately — a title
   that is merely a little larger will not anchor anything.

   Butterick: "max 2 fonts, each gets a consistent role." One is fine. The
   cost is recorded at `--font-sans` in aimy-ds.css: Poppins cannot set a
   tabular figure, so every rule in this file and bdr.css that asks for one
   gets nothing — and a role a face cannot perform is still not a role. */
:root { --font-display: "Poppins", system-ui, sans-serif; }

/* ══ THE LIBRARY'S WEIGHTS NEVER RODE THE LADDER DOWN ══════════════

   aimy-ds.css sets font-weight as a bare number 193 times. When the ladder
   was re-cut for Poppins every one of those sat outside it, so the design
   system went on rendering at weights chosen for Urbanist — which in
   Poppins are a fifth to a quarter heavier. Nour found it on a pair of
   consent buttons: both at 700, both shouting, nothing ranking them.

   34 of the 193 actually render in this product. The other 159 are library
   components Sales never mounts, and changing those would be unverifiable
   edits to code nobody sees. The live ones move one step down, exactly as
   the tokens did, so they land on the colour they always meant:

     800 -> --fw-extrabold (700)      600 -> --fw-semibold (500)
     700 -> --fw-bold      (600)      500 -> --fw-medium   (400)

   Overridden here rather than edited there, which is the rule this file
   sets out a few sections up: the extracted stylesheet stays a faithful
   copy of the library and every divergence stays visible. Load order makes
   it work — aimy-ds.css, then this. */
/* Two floors at once. It set 9px, where the scale says twelve and even and
   never below; and #fff on --err-strong measures 3.15:1, which fails AA for
   anything under 18.66px. The note in aimy-ds.css shows this was already
   argued once — it moved off --err to --err-strong and got from 2.5 to
   3.15, which is better and still short.

   Keeping the hot red and inverting the ink is what clears it, and it is
   not a new idea here: .b-missed-n already puts --body-bg on a vivid
   ground for exactly this reason. Theme-aware in both directions — near-
   black on #ff5268 in dark, near-white on #b42318 in light. */
/* ══ THE BADGE GROUND, SO THE NUMBER CAN BE BRIGHT ═════════════════════
   Both count badges took --err for the ground and --body-bg for the ink, a
   pair that flips with the theme: near-white on deep red in light, near-
   black on hot pink in dark. It passes — 6.65:1 — and in dark mode it puts
   a near-black number on a pink pill, which is not what a count should
   look like.

   White cannot simply be swapped in. On --err at #ff7282 it measures
   2.64:1, and aimy-ds.css already records that exact number beside
   .float-badge. The ground has to come down for the ink to come up, and it
   cannot come down far, or the pill stops being visible against the page.
   Two bounds, and there is a window between them:

     white ink on the ground  >= 4.5      needs luminance <= 0.183
     ground against the page  >= 3.0      needs luminance >= 0.122

   #d92d20 sits inside it at 0.167 — white at 4.83:1, pill against the dark
   page at 3.79:1. It is also exactly what --err already is in the light
   theme, so one value serves both and nothing forks. */
:root { --badge-ground: #d92d20; }

/* .overlay-badge sets 11px at --d400 in the library and keeps both here.
   It is canvas chrome — it renders in the assistant, not on this product’s
   surface, and in fact never renders in Sales at all — so the 12px floor
   and the --d200 ink floor are not this file’s to apply to it. */

/* TWELVE IS THE TAG SIZE, NOT JUST THE METADATA SIZE. The scale calls 12 the
   minimal step and describes it as a tag, a work state, a timestamp. The tags
   themselves were split: .tag took 12 and .filter-chip and .chip took 14, so a
   row of filter pills sat a step above the state pill inside the card it
   filters. They are the same kind of object and they take the same step.

   .overlay-sugg-chip is deliberately NOT here. It carries a whole question
   somebody is meant to read and press — a control with sentence-length text,
   not a tag — so it keeps the meta step. The counts inside these chips set no
   size of their own and come down with them. */
/* .filter-chip renders in two places: six on the desk inside .s-block, and
   four in the float bar. The note below — "the canvas and the float bar keep
   the library’s type" — is about the second kind and is right: that surface
   is the same component in all three agents, and re-scaling it here makes
   the assistant look different depending on which product you opened it
   from. So the local step is scoped to the product’s own surface, and the
   canvas copy is left alone. */
.s-block .filter-chip, .chip { font-size: var(--fs-2xs); }

/* THE CANVAS KEEPS THE LIBRARY’S TYPE, so no size and no weight here — the
   note further down this file is right that re-scaling the float bar makes
   the assistant look different depending on which agent opened it, and 9px
   at 800 is what the library ships.

   The GROUND and the ink do stay ours. #fff on --err-strong measures
   3.15:1 and fails AA at any size; white on --badge-ground is 4.83:1 with
   the pill still reading 3.79:1 against the page. That is a contrast fix
   rather than a scale one, and reverting it would put back a failure. */
.float-badge {
  background: var(--badge-ground);
  color: #ffffff;
}

.aimy-toast-title, .aimy-toast-undo, .avatar, .btn, .cite, .entry-action,
.evidence-pill .val, .msg-avatar, .tag, .tc-gov strong,
.tc-title, .tc-type, .type-label, .work-state {
  font-weight: var(--fw-bold);
}

.ai-thinking-label, .bcard-conclusion, .chip, .filter-chip, .link,
.overlay-sugg-chip, .reasoning summary, .step-label, .stepper input,
.tab, .tree summary {
  font-weight: var(--fw-semibold);
}

.aimy-float-input, .evidence-pill, .msg-bubble, .overlay-input {
  font-weight: var(--fw-medium);
}

/* AND THE PAIR THAT STARTED IT. Primary and secondary were both 700, so the
   only thing ranking them was the blue, and at Poppins 700 they arrived as
   two shouts side by side. The ghost drops a step under the brand, which
   puts weight alongside the ground and the ink already separating them —
   three axes for two buttons, where there was effectively one. */
.btn-ghost { font-weight: var(--fw-semibold); }

/* ── Body text first ──
   The four properties that calibrate everything else, in one place, on the
   element that should always have carried them.

   Kerning on, always: there is no case for turning it off, and the library
   does not set it. Ligatures where the face has them. */
body {
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  /* The properties, not the raw tags they map to: these keep working when a
     non-variable fallback renders, and font-feature-settings is a single
     property that a later declaration replaces wholesale rather than adds
     to. Reserve the raw form for axes and features with no property. */
  font-kerning: normal;
  font-variant-ligatures: common-ligatures;
  text-rendering: optimizeLegibility;
}

/* ── Leading follows size, so it is declared where size is ── */
h1, h2, h3, .s-rec-name, .s-sheet-name, .s-block-h {
  line-height: var(--lh-tight);
}

/* ── The library's off-scale sizes, mapped back onto the scale ──

   The library does not set every size through its own tokens: `.btn-sm` is
   10.5px, `.input` 12.5px, `.field-label` 12px, hard-coded. Overriding the
   tokens alone would have left the most-clicked control on the surface —
   `.btn-sm`, rendered 37 times by `sales.js` — at 10.5px inside a 15px
   product.

   `.btn-sm` was also 22px tall (5px padding + 12px line-height), under the
   24×24 minimum of WCAG 2.2 SC 2.5.8. The `min-height` further down this
   file was already propping it to 24; with the type corrected it clears the
   floor on its own ink rather than on an invisible box. */
.btn-sm { font-size: var(--fs-xs); line-height: 16px; }
/* ══ THE WORD SITS IN THE MIDDLE OF ITS OWN PILL ══════════════════════════
   The library ships `padding: 3px 9px 3px 7px` — 2px less on the left,
   which was the room the dot needed. The dot is gone and the asymmetry
   stayed, so the word sat 2px left of centre: measured 8px of space to its
   left against 10px to its right. Symmetric padding, and both pills now
   measure exactly 9px either side.

   The tracking goes with the capitals that asked for it. It was a
   correction FOR a caps run — the letters of a word set in capitals sit
   too close because the shapes are all the same height — and on sentence
   case it is not a correction, it is gaps.

   An integer `line-height` for the same reason the padding is symmetric:
   1.45 × 12px is 17.4, and half of an odd remainder cannot be split
   evenly above and below the glyphs. */
.work-state {
  font-size: var(--fs-2xs);
  padding: 4px 8px; line-height: 16px;
}
/* ══ THE EIGHTH STATE ══════════════════════════════════════════════════════

   The library ships six and names them "six values, required on every
   surfaced item". This product needs eight: `reading` — AiMY having compared
   a thing against the book — and `ongoing`, work that has not finished and
   is not going to. Neither is a library concern, so neither goes in there.

   `reading` has never had a rule at all and draws as a transparent chip in
   whatever colour it inherits. That is a real hole; it is filed rather than
   fixed here, because fixing it means deciding what a comparison looks like
   and that is not this change.

   AiMY blue, because the work is AiMY's and `.ws-drafted` already settled
   that colour for it. What separates the two is the dot: drafted hollows it
   out because nothing has happened yet, and this one leaves it solid,
   because something is happening right now. */
.ws-ongoing {
  background: rgba(0, 102, 255, 0.1); color: var(--ai-text);
  border-color: rgba(0, 102, 255, 0.24);
}
.s-meta-st { line-height: 16px; }
/* ══ ONE BUTTON FAMILY ═══════════════════════════════════════

   FOUR HEIGHTS IN ONE ROW, MEASURED ON THE CAMPAIGN PAGE: 37 / 28 / 27 / 22.
   The cause is one line. `.btn` pins `line-height: 14px` and says why —
   "values chosen to reproduce exactly what this page rendered before,
   measured" — while `.entry-action` and `.chip` set no line-height at all
   and inherit the 1.45 from `body`. sales.css then raises all three to 13px,
   so the two without a pinned line box grew and the one with it did not.

   Three heights, declared, and the reason each exists:
     32  a primary — the recommended move
     28  a secondary — available, not recommended
     24  a text action — part of a sentence

   Primary and secondary differ by COLOUR, not size. AiMY QA uses identical
   padding and weight for both and lets a teal tint carry the primary; that
   is what stops a card of buttons reading as a form. */
.entry-action, .chip { line-height: 16px; }
.entry-action { min-height: 32px; padding: 0 16px; }

/* ══ THREE TIERS, AND THERE WERE ELEVEN ════════════════════════════════════

   Counted across the product: eleven distinct control treatments for 152
   rendered controls, and FIVE of them are a tinted pill that means "this is
   actionable" — brand blue (`.btn-brand`, `.em-direct`), accent magenta
   (`.em-prompt`), AI blue (`.em-investigate`), warn amber (`.em-review`) and
   teal (`.s-insight-lnk.primary`, a colour used nowhere else on the surface).

   Five colours all saying "press me" is the same as none saying it. Worse,
   the commonest treatment by a distance is `.btn-ghost` at 41 uses against
   10 filled — so the DEFAULT is the outline, which means nothing reads as
   default and everything reads as almost-important.

   Three tiers, and the reader has to learn three things:

     PRIMARY    filled brand      the recommended move. ONE per surface.
     SECONDARY  neutral outline   available, not recommended.
     TERTIARY   text              part of a sentence.

   ── THE ENTRY MODES KEEP THEIR MEANING AND LOSE THEIR COLOURS ──

   Doctrine §3 requires every action to be classified `direct` /
   `investigate` / `prompt` / `review`, and §11 requires the library's
   component. Both are honoured: the classes stay on the element, `wiring-audit`
   still reads them, and the classification is still declared at design time.
   What changes is that four modes stop being four LOOKS and map onto the
   three tiers by what they cost the reader:

     em-direct       does the thing            → primary
     em-review       shows you a proposal      → secondary
     em-prompt       opens a conversation      → tertiary
     em-investigate  opens a conversation      → tertiary

   A control that opens a conversation is not a peer of one that changes the
   book, and colouring them alike was the surface saying they were. */

/* ── Tier 1 · primary ── */
.entry-action.em-direct,
.s-insight-lnk.primary {
  background: var(--brand); color: #fff; border: 1px solid var(--brand);
  border-radius: var(--r-md);
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-bold);
  min-height: 32px; padding: 0 16px;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  cursor: pointer; text-decoration: none;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}
.entry-action.em-direct:hover,
.s-insight-lnk.primary:hover {
  background: #4d7fff; border-color: #4d7fff; color: #fff;
}

/* ── Tier 2 · secondary ── */
.entry-action.em-review, .s-insight-lnk {
  background: rgba(255, 255, 255, 0.05); color: var(--ink-muted);
  border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--r-md);
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-bold);
  min-height: 28px; padding: 0 12px;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  cursor: pointer; text-decoration: none;
  transition: background-color var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.entry-action.em-review:hover, .s-insight-lnk:hover {
  background: rgba(255, 255, 255, 0.09); color: var(--d50);
  border-color: rgba(255, 255, 255, 0.16);
}
/* `:not(.primary)` is load-bearing, not tidiness. `:root[data-theme="light"]
   .s-insight-lnk` scores (0,3,0) against `.s-insight-lnk.primary`'s (0,2,0),
   so without it the light theme's secondary rule outranks the primary and the
   one filled control on the surface quietly became an outline — the whole
   hierarchy gone, in one theme only. */
:root[data-theme="light"] .entry-action.em-review,
:root[data-theme="light"] .s-insight-lnk:not(.primary) {
  background: rgba(16, 24, 40, 0.05); border-color: rgba(16, 24, 40, 0.12);
}
:root[data-theme="light"] .entry-action.em-review:hover,
:root[data-theme="light"] .s-insight-lnk:not(.primary):hover {
  background: rgba(16, 24, 40, 0.09); border-color: rgba(16, 24, 40, 0.18);
}

/* ── Tier 3 · tertiary ── */
.entry-action.em-prompt, .entry-action.em-investigate {
  background: none; border: 0; padding: 0; min-height: 24px;
  color: var(--accent-text); font-weight: var(--fw-semibold);
  font-size: var(--fs-xs); font-family: inherit; cursor: pointer;
}
.entry-action.em-prompt:hover, .entry-action.em-investigate:hover {
  color: var(--accent); text-decoration: underline;
  text-decoration-color: rgba(var(--accent-rgb), 0.4); text-underline-offset: 3px;
}
.input, .ds-textarea, .field-label, .checkbox-label, .radio-label,
.toggle-label { font-size: var(--fs-xs); }
/* The chat overlay's suggestion chips come out of the library at 12px,
   which is the one value the five-step scale skips — it sits between the
   11px caption and the 13px meta and reads as neither, which is precisely
   the "everything is one pixel apart" problem this pass exists to end. They
   are control labels, so they take the meta step. Recorded as gap 39. */
.overlay-sugg-chip { font-size: var(--fs-xs); }
.field-help, .field-hint, .field-error { font-size: var(--fs-xs); line-height: var(--lh-relaxed); }

/* THE REST OF THEM, AND THE LIST IS NOT EYEBALLED.

   The first cut of this block was written from one screen and missed
   thirteen more classes that only render on the record and campaign pages.
   Measuring one surface finds the classes that surface draws, which is a
   guarantee of missing the others.

   So the list is derived instead: every rule in `aimy-ds.css` that sets a
   font-size under 12px, intersected with the classes `sales.js` and
   `index.html` actually render. That is a question about two files rather
   than about a screenshot, and it cannot go stale quietly — re-run it after
   any `ds-extract` and the answer is complete again.

   Two judgement calls inside it:

   · `.avatar-sm` is 9px initials in a 24px circle, which reads as a graphic
     rather than as text, so the reading rules do not obviously apply. They
     do here — those initials are the only thing saying who owns a record in
     a card's meta line, and nine pixels of two capitals is not
     identification. 12-in-24 is the ratio the library's own `.avatar-md`
     already uses at 11-in-32, so this follows the library.
   · `table.dtable thead th` is in the derived list and is deliberately NOT
     here. There are no tables on this surface any more. */
.avatar-sm, .cal-dow, .trust-state, .tc-type, .tc-gov,
.conf-badge, .conf-badge .conf-val, .cite-action, .agent-step-meta,
.aiu-note, .ss-clear { font-size: var(--fs-2xs); }

/* .chip was here and is not any more: it renders only on this product’s own
   surface (nine on the money page, none in the canvas), so it takes the tag
   step with every other pill rather than the library’s 14. */
.aimy-toast-undo, .tl-time, .ss-scope,
.dz-consequence, .entry-action, .aiu-body, .tc-summary,
.tl-body, .agent-step-label,
.ds-field .field-help { font-size: var(--fs-xs); }

/* THE CANVAS AND THE FLOAT BAR KEEP THE LIBRARY'S TYPE.

   `.overlay-badge`, `.aimy-float-hint`, `.float-badge`, `.filter-chip` and
   the dropdown's own text were pulled onto the local scale with everything
   else. They should not have been: the canvas is the shared AiMY surface,
   the same component in all three agents, and re-scaling it here makes the
   assistant look subtly different depending on which product you opened it
   from. The scale is for THIS PRODUCT'S SURFACE — the cards, the blocks, the
   record — not for the chrome around it. */

/* Figures that sit in a column must be comparable down that column.
   Proportional digits make 1,000 narrower than 8,888 and the eye has to
   re-find the decimal on every row. */
.s-result-count,
.ntf-count, .ss-num {
  font-variant-numeric: tabular-nums;
}

/* All-caps runs are letterspaced. Caps are homogeneous rectangles rather
   than varied contours, so they read slower; the tracking is what makes a
   short one legible at all. Anything under 5% is uppercase without the fix. */
.s-rec-kind, .s-sheet-kind, .s-camp-state, .proto-h,
.overlay-context-label {
  letter-spacing: 0.08em;
}

/* Prose caps at a readable measure. 45–90 characters is the single biggest
   readability factor, and every one of these inherits a 1160px column. */
.s-sheet-brief, .s-block-sub, .s-none,
.aiu-body, .empty-state-desc, .msg-bubble {
  max-width: 65ch;
}

/* <strong> pinned to a weight we actually load. The UA gives it
   `font-weight: bolder`, which is RELATIVE: inside a 600 parent it computes
   to 900 — a face the webfont request never included. */
strong, b { font-weight: var(--fw-semibold); }

/* Bare <code> falls back to the UA's generic monospace. The library styles
   .copy-field code and .ds-code but never the element itself.

   KEPT AS THE ONE MONO RULE IN THE PRODUCT, and it renders nowhere: there is
   no `<code>` in `sales.js`. Mono for actual code is correct typography, so
   the rule stays right if code ever appears; it just resolves to the UA's
   generic face now that the webfont is not requested. */
code { font-family: var(--font-mono); }

/* ── The library's confidence badge, in our typeface ──
   `.conf-badge .conf-val` is set in mono by `aimy-ds.css`, so `medium` sat in
   a second face directly beside the phone number above. Overridden here
   rather than in the library, which QA and Knowledge share. */
.conf-badge .conf-val { font-family: var(--font-sans); }

button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;
}

/* ═══════════════════════════════════════════════
   POINTER TARGETS — WCAG 2.2 SC 2.5.8 (24×24 minimum)

   The hit area is extended without changing how anything looks: growing the
   visible control would break the density these surfaces depend on, and the
   success criterion is about the target, not the ink.
═══════════════════════════════════════════════ */
/* `.s-brief-cta` (22px) and `.s-icp` (23px) join the group. Both are bare
   text controls whose ink is the whole control, so growing them visibly
   would put a chip where a word belongs — which is what the extended hit
   area exists to avoid. Measured on the rendered page; the stylesheet gives
   no height to read. */
.audit-revert, .filter-chip, .s-icp,
.s-inline-btn, .s-camp-out { position: relative; }
.audit-revert::after, .filter-chip::after,
.s-icp::after,
.s-inline-btn::after, .s-camp-out::after {
  content: ""; position: absolute;
  left: 0; right: 0; top: 50%; height: 24px;
  transform: translateY(-50%);
}
.btn-sm, .bcard-ack-btn, .ss-clear, .agg-more, .td-action, .cite-action,
.mem-foot .btn, .aimy-toast-undo, .v2-dropdown-btn { min-height: 24px; }

/* The text inputs draw ~20px inside a 54px bar, so most of the pill was dead
   to the pointer — you could click the thing that looks like a text field
   and have nothing happen. min-height only: align-self:stretch made a
   one-line field 36px tall against a 20px line-height, which reads as a
   textarea waiting for a second line. */
/* ══ AND A FLEX ITEM WILL NOT SHRINK BELOW ITS OWN MIN-CONTENT ══════
   `flex: 1` sets the BASIS to zero and says nothing about the floor. The
   floor is `min-width: auto`, which on a flex item means min-content — and
   an <input> with no `size` has a min-content width of twenty characters,
   173px in this face. The bar is a row of icon, field, mic and send inside
   312px on a 360px phone, so the field held 173 of it, the row could not
   fit, and the two buttons were pushed out through the right-hand end of
   the pill. Measured: send's right edge 10px past the padding box, the bar
   overflowing by 3.

   THE REST STATE ONLY, WHICH IS WHY IT SURVIVED. Focus scrolls the field
   into view on a phone and the keyboard reflows the fixed bar, so the
   overflow moves out of frame the moment anybody touches it — it is broken
   exactly while nobody is looking at it closely, and whole on the one pass
   anybody makes to check.

   The library's own answer, used at five other `flex: 1` call sites in
   aimy-ds.css and missing at these two. Here rather than there because
   these two lines already patch this pair for this product, and this sheet
   loads after it. */
.aimy-float-input, .overlay-input { min-height: 24px; min-width: 0; }
.aimy-float-bar, .overlay-input-bar { cursor: text; }
/* …and everything else in the bar goes back to an ordinary arrow, so the
   chrome around the field does not offer a text caret.

   `:not(button)` because this reset was too broad and swallowed the controls:
   the icon that opens the canvas and both send buttons sat inside the bar and
   were told they were inert. Nineteen of the twenty-two kinds of button on
   this surface showed a pointer; these three did not, and the unread badge
   drawn inside the icon inherited `default` from it — which is how it
   surfaced. Excluded here rather than overridden below, because a rule that
   says the wrong thing and a second rule that argues with it is worse than
   one rule that is right. */
.aimy-float-bar > *:not(.aimy-float-input):not(button),
.overlay-input-bar > *:not(.overlay-input):not(button) { cursor: default; }

/* A staged prompt is AiMY-composed and waiting on you, so the bar says so
   rather than looking like text you typed and abandoned. */
.overlay-input-bar.is-staged {
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}

/* ═══════════════════════════════════════════════
   PRESS FEEDBACK

   Nothing in this product acknowledged being pressed. Every control changed
   state only after its work completed, which on a surface where actions
   carry consequence reads as the click not landing — so people press again.
   That matters more here than in Knowledge: the commonest action on this
   surface writes a touchpoint, and a rep who presses twice logs twice.

   90ms is deliberately below the motion scale's smallest step (--t-fast,
   150ms). Press-release is the one interaction where 150ms is perceptible
   as lag: the finger is already up before the surface answers.
═══════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   THE MOTION SCALE, AND THE FIVE RUNGS IT DID NOT HAVE

   The library gives four steps — press 90, fast 150, base 200, slow 300 —
   and the note above ends "the scale has no press step". It had more holes
   than that. Every value a motion audit asked for above 300ms was written
   as a literal, and five of them were written more than once: 420 in two
   places, 520 in two, 700, 90 as a stagger offset, 45 as another. A scale
   with holes is a scale that gets worked around, and a worked-around scale
   is a stylesheet where the same intent has four numbers.

   So the holes are filled, HERE rather than in aimy-ds.css, because that
   file is Knowledge's and QA's too and none of this is theirs to inherit.
   Named for what they are FOR, the way --ty-masthead and --ty-deck are, and
   not for how fast they are: "quick" and "fast" as neighbours on one ramp is
   a scale nobody can recall the order of.

   They are the transitions.dev motion scale, whose 150ms `quick` --t-fast
   already IS, to the millisecond. So these are the rest of one scale rather
   than a second one laid beside it.

   THE CURVE IS THE ONE PLACE THE TWO DISAGREE, and the disagreement stands.
   That scale's `smooth-out` is cubic-bezier(0.22, 1, 0.36, 1); the library
   declares exactly that and bdr.css §22 overrides it to (0.23, 1, 0.32, 1),
   with its reason — a press seen two hundred times a day settles and gets
   out of the way. Four hundredths on the third control point is not a thing
   anybody can see, and overturning an argued decision to win a diff is how a
   scale stops being believed. Named here so the next reader finds it as a
   choice rather than as drift. Equivalences:

     --t-stagger  40ms  = duration-stagger   per-item offset
     --t-beat     80ms  = duration-micro     a path drawing, an intent delay
     --t-open    250ms  = duration-fast      a menu opening, a tab bar sliding
     --t-panel   400ms  = duration-slow      a panel arriving
     --t-emphasis 500ms = duration-very-slow the one reveal a page is about

   THE RULE THAT IS NOT A NUMBER: a stagger's TOTAL is offset × count, and it
   has to stay under about 300ms or the last item is not late by a rhythm, it
   is just late. Every stagger in this build caps its index for that reason;
   two of them had forgotten to.
═══════════════════════════════════════════════ */
:root {
  --t-press: 90ms;
  --t-stagger: 40ms;
  --t-beat: 80ms;
  --t-open: 250ms;
  --t-panel: 400ms;
  --t-emphasis: 500ms;
}

/* THE ACCENT IS THE LIBRARY'S, AND STAYS THE LIBRARY'S.

   A re-theme lived here — `--accent` and its three derived tokens moved from
   the family purple `#8b4ff4` to a magenta, on the argument that
   `design-system.md` calls `--accent` "the one token products re-theme" and
   Sales had never used the permission.

   Reverted. The permission to re-theme is not a reason to, and nobody asked
   for a different palette: the ecosystem reads as one family because the
   three products share a colour, and changing that here changed the product's
   identity as a side effect of a typography pass. Palette decisions belong to
   whoever owns the family, not to a restructure.

   `--accent`, `--accent-rgb`, `--accent-dim` and `--accent-glow` are the
   library's again. `--accent-text` below is NOT a re-theme — it is the
   contrast-corrected pair of the library's own purple, and it predates this
   work. */

/* ═══════════════════════════════════════════════
   TEXT-SAFE SIGNAL COLOURS

   `--ok`, `--warn`, `--err` and `--accent` are calibrated as FILLS — badge
   grounds, dot markers, borders. Set as small text on a card they fail
   WCAG AA: measured against the real card and rail backgrounds, the status
   word read 4.37:1 in dark and 3.82:1 in light, and the briefing's CTA
   3.77 / 4.29. That is the product's own thesis — the computed status —
   printed below the legibility floor.

   The library already made this exact distinction once, for `--ai-text`
   ("AI-blue text — darkened in light"). It has no siblings for the other
   three, so they are declared here. Filed in ../GAPS.md.

   Every value below is measured, not picked: each clears 4.5:1 against both
   the card (#141b24 / #ffffff) and the briefing rail (#171a1c / #f4f6f9),
   and keeps its hue so the signal still reads as the same signal.
═══════════════════════════════════════════════ */
:root {
  /* ══ THE SAME FAMILY, FOR A WORD INSIDE A SENTENCE ══════════════════
     A hue is not only for a pill. "Callback — they asked to be called
     back, 29 Aug." wants its first word coloured, because the word IS the
     status and the rest of the line is the explanation; the same goes for
     the ringed dot on the rung a lead is standing on. That is colour doing
     the one thing a pill cannot — living inside running text.

     These are the family's values, so a coloured word and a coloured tag
     are the same colour. That is the entire point of them, so they are
     aliases now rather than copies: the status hues moved to full
     saturation in the design system, and a copy would have left a muted
     word sitting beside a vivid pill. `--accent-text` is not a status: it
     is the ink every text action in this product wears, and an affordance
     is not a verdict, so it keeps its own brighter value. */
  /* ══ THE AiMY WASH, AND WHY LIGHT GETS LESS OF IT ═══════════════
     The tinted ground every block AiMY speaks from wears. It was written out
     at each of them with the same two alphas for both themes, and the same
     alpha is not the same weight: 10% of a deep purple over a near-white page
     is a lavender panel you read as a coloured box, where 10% of a bright one
     over near-black is a breath on the surface. The light token block already
     states the rule this follows — grounds and borders drop their alpha,
     because dark-on-light reads heavier than the reverse — and this is the
     one ground that had not.

     A token rather than copies, because it WAS copies and they would have to
     be found again next time. */
  --aimy-wash: linear-gradient(120deg, rgba(var(--accent-rgb), 0.10), rgba(0, 102, 255, 0.04));
  --aimy-wash-edge: rgba(var(--accent-rgb), 0.20);

  --ok-text:     var(--ok);
  --warn-text:   var(--warn);
  --err-text:    var(--err);
  --accent-text: #a06ff7;   /* 5.04:1 — an action, not a status */

  /* ══ AND THE TELEPHONE'S TWO VERBS ARE THE FAMILY AFTER ALL ════════
     `--answer-fill` and `--hangup-fill` were here: a green and a red
     picked by hand, pinned by contrast against a WHITE GLYPH, and argued
     for at length on the grounds that `--ok` is a mint and a telephone is
     not mint. Both are gone, and so is the argument, because the thing
     they were pinned against is gone: answer, decline and End are tinted
     circles now — the family's own hue as the glyph and the edge over a
     16% wash of itself, which is the treatment `.call-tool.is-rec` has
     always used for recording.

     That treatment needs 3:1 from the hue against the card, not from
     white against the hue, and `--ok` and `--err` clear it in both
     themes where a white glyph on them could not. So the colours are the
     family's, they theme with it, and a product no longer names two
     literals the library will never know about. */

}
:root[data-theme="light"] {
  /* ══ AND THE THREE INKS COME BACK, BECAUSE THE HUES WENT BOLD ═════
     They aliased --ok / --warn / --err, and the note that stood here said an
     override would only pin them to the old family — true while those three
     were re-derived against white upstream, and false the moment light went
     back to the bold set. The alias quietly took `--ok-text` from 7.04:1 to
     3.99 on a white card, which is the one number this token exists to hold.

     So the split is real again, and it is the split the pair was named for:
     the HUE is what a tag, a dot, a border and a tinted ground wear, and it
     is bold; the INK is what a sentence wears, and it is deep enough to be
     read. These are the three values the hues themselves carried until
     today — picked against white and re-measured inside a nested tint, since
     a tag sits on a wash of its own hue and composites darker than the card. */
  /* Just over half the dark alphas, judged against the page it sits on
     rather than derived: the wash has to read as a tint OF the surface, not
     as a card laid on one. */
  --aimy-wash: linear-gradient(120deg, rgba(var(--accent-rgb), 0.055), rgba(0, 102, 255, 0.022));
  --aimy-wash-edge: rgba(var(--accent-rgb), 0.13);

  --ok-text:     #066640;
  --warn-text:   #6b4500;
  --err-text:    #a81029;
  --accent-text: #6d3bd4;   /* 6.51 / 6.01 */
}

@media (prefers-reduced-motion: no-preference) {
  .btn, .entry-action, .filter-chip, .seg-btn, .bcard-ack-btn, .overlay-sugg-chip,
  .audit-revert, .cite-action, .td-action, .icon-btn,
  .topnav-bell, .ds-theme-toggle, .aimy-float-icon, .aimy-float-send, .overlay-send, .overlay-close,
  .agg-more, .ss-clear, .tab, .nav-item, .bcard-dismiss-reason, .s-sess-row {
    transition: transform var(--t-press) var(--ease-out),
                background-color var(--t-fast), border-color var(--t-fast), color var(--t-fast);
  }
  .btn:active, .entry-action:active, .filter-chip:active, .seg-btn:active,
  .bcard-ack-btn:active, .overlay-sugg-chip:active,
  .audit-revert:active, .cite-action:active, .td-action:active,
  .icon-btn:active, .topnav-bell:active, .ds-theme-toggle:active,
  .aimy-float-icon:active, .aimy-float-send:active, .overlay-send:active, .overlay-close:active,
  .agg-more:active, .ss-clear:active, .tab:active,
  .nav-item:active, .bcard-dismiss-reason:active, .s-sess-row:active { transform: scale(0.97); }

  /* Large surfaces move less — the same 3% reads as a lurch at card scale. */
  .type-card, .list-row {
    transition: transform var(--t-press) var(--ease-out), background-color var(--t-fast);
  }
  .type-card:active, .list-row:active { transform: scale(0.995); }
}

/* ═══════════════════════════════════════════════
   ENTRY STAGGER

   The briefing arrives as one block, which reads as a page repaint rather
   than a set of findings. A short cascade gives the eye an order to follow —
   and the order is the priority order composition already computed, so the
   motion carries the same information the ranking does.

   40ms per item, capped: past ~8 the delay would outlast the reading.
   Decorative, so it never gates interaction and vanishes under reduced
   motion.
═══════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  .s-stagger > * {
    opacity: 0;
    transform: translateY(6px);
    animation: sEnter 240ms var(--ease-out) forwards;
    animation-delay: calc(min(var(--i, 0), 8) * 40ms);
  }
  /* Single entry — a block replacing its own skeleton. No delay: the
     resolution order already supplies the cascade. */
  .s-enter {
    opacity: 0; transform: translateY(6px);
    animation: sEnter 240ms var(--ease-out) forwards;
  }
  @keyframes sEnter { to { opacity: 1; transform: translateY(0); } }
}

/* ═══════════════════════════════════════════════
   EVERY WIDTH BREAKPOINT IN THIS FILE IS A LAYOUT WIDTH — AND NOW SO IS THE
   VIEWPORT

   They used to be multiplied by 0.85. The multiplication is gone, and this is
   what it was for: assets/aimy-responsive.css scales the whole shell with
   `zoom` on <body>, at a factor assets/aimy-viewport.js publishes as
   --ui-scale. A media query does not see that. It reads the REAL viewport, in
   real px, while every rule inside it applies to a LAYOUT that is --ui-scale
   times smaller or larger — so a threshold written for the layout had to be
   converted into the viewport width that produces it.

   THE SCALE NO LONGER SHRINKS ON WIDTH, so there is nothing left to convert.
   computeScale floors the width term at 1: a viewport narrower than the 1536
   anchor renders at 1:1 and these thresholds do the narrowing, instead of the
   anchor's layout being photographed smaller — which is what the floor was
   really doing, and what cost a 1200px laptop its type size. Below 1536 real
   px, layout px and viewport px are the same number, so every threshold here
   was re-keyed to the layout value its own trailing comment already stated:
   1002.98 became 1179.98, 611.98 became 719.98, and so on.

   THE HEIGHT TERM STILL SCALES, to the same 0.85 floor, so a short window can
   still render a layout wider than its viewport and these thresholds can still
   be read against a scaled shell. That is the residual imprecision and it is
   smaller than the one it replaces: the old conversion was exact only below a
   real 1306, while this one is exact whenever the window is about 698px tall
   or more, which is every window a laptop opens.

   THE .98. Windows at 125% display scaling gives a FRACTIONAL viewport: an
   `innerWidth` reported as 1099 is really 1099.1844 CSS px, so `max-width:
   1099px` does not match and the rule misses at exactly the width it was written
   for. innerWidth rounds; the media query does not. .98 clears the fraction
   without reaching the next whole pixel. `min-width` needs no such adjustment —
   a fractional viewport is above its integer threshold, not below it.

   EACH QUERY KEEPS ITS DESIGN NUMBER IN A TRAILING COMMENT, and the two now
   agree: the number in the parentheses and the number in the comment are the
   same width. They are no longer two numbers that have to be kept in step.
═══════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   SHELL — topnav → sidebar → scrollable main
═══════════════════════════════════════════════ */
body {
  /* SUPERSEDED, AND KEPT AS THE FALLBACK. assets/aimy-responsive.css re-declares
     this as `height: 100%` against an <html> that is `100dvh` and outside the
     zoom — 100vh inside `zoom: k` is k viewports tall, which puts the float bar
     off the bottom of the screen, and on iPad Safari 100vh is the LARGE viewport
     even at k = 1. This line is what the page falls back to if that sheet does
     not load, so it stays; it is not what the page uses. */
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

/* Ambient background layer. Sits on the body bg, below every surface.
   Not decoration hung on a component — no component may depend on it. */
#appEllipse {
  position: fixed; inset: 0;
  pointer-events: none; z-index: 0; overflow: hidden;
}
:root {
  /* QA's dark body is #171a1c; the design system's token is #0f1215. The
     ellipse alphas below are calibrated against QA's, and the darker DS body
     makes the same wash read as a visible blue bloom. */
  --body-bg: #171a1c;
  --grad-ellipse-1: radial-gradient(ellipse at center,
    rgba(0, 102, 255, 0.18) 0%, rgba(97, 173, 241, 0.1) 28%, transparent 60%);
  --grad-ellipse-2: radial-gradient(ellipse at center,
    rgba(111, 223, 226, 0.1) 0%, rgba(139, 79, 244, 0.07) 38%, transparent 60%);
}
/* Light gets its own alphas rather than an opacity multiplier on the layer —
   at dark alphas these read as haze on a white body. */
:root[data-theme="light"] {
  --grad-ellipse-1: radial-gradient(ellipse at center,
    rgba(0, 102, 255, 0.07) 0%, rgba(97, 173, 241, 0.04) 28%, transparent 60%);
  --grad-ellipse-2: radial-gradient(ellipse at center,
    rgba(111, 223, 226, 0.05) 0%, rgba(139, 79, 244, 0.035) 38%, transparent 60%);
}
#appEllipse::before {
  content: ""; position: absolute;
  top: 42%; left: 50%; transform: translate(-50%, -50%);
  width: 1300px; height: 1050px;
  background: var(--grad-ellipse-1);
}
#appEllipse::after {
  content: ""; position: absolute;
  bottom: -8%; right: -4%;
  width: 750px; height: 650px;
  background: var(--grad-ellipse-2);
}

/* ═══════════════════════════════════════════════
   TOPNAV — shared ecosystem chrome

   Ported from AiMY QA and Knowledge v2 so the three agents share one
   masthead. Their local classes are used (.topnav-tabs-inner, .topnav-tab,
   .topnav-user) rather than the design system's .tabs-strip / .tab /
   .user-pill, because the library states the shared topnav is "maintained
   centrally, outside this system" — matching the siblings is the point.

   Two departures, each stated rather than smuggled:

     · Tabs stay <button>, not QA's <div>. A div is not focusable and
       announces as nothing, so QA's product tabs cannot be reached from the
       keyboard. The visual is identical.
     · V1's dialer is not here. A call changes one contact and the record of
       it belongs on that contact — and the contact is the only thing that
       knows which number to call.
═══════════════════════════════════════════════ */
.app-topnav {
  height: var(--topbar-height);
  background: transparent;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  display: flex;
  align-items: center;
  padding: 0 20px 0 24px;
  flex-shrink: 0;
  position: relative;
  z-index: 100;
}
:root[data-theme="light"] .app-topnav { border-bottom-color: var(--card-border); }

.topnav-logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; padding-right: 20px; margin-right: 4px; }
.topnav-logo-mark { display: flex; align-items: center; flex-shrink: 0; }
.topnav-logo svg { display: block; height: 32px; width: auto; }

/* ══ PINNED TO THE WINDOW, NOT CENTRED BETWEEN THE FLANKS ═════════════════
   QA lifts this strip OUT OF THE FLOW and pins it: `position: absolute;
   left: 50%; transform: translateX(-50%)` against a `position: relative`
   masthead. Every other product does the same, and it is the only way the
   pill lands on the window's centre — flex-centring puts it halfway between
   the logo and the right group, so it slides left or right by half of
   whatever the user's name and role happen to measure.

   That is exactly what was wrong here: "Nour Wael · Admin" is narrower than
   "Nour Wael · Product Design", so the same strip sat 112px further left in
   Sales than in QA, on the same screen. The spacers stay — they are what
   spreads the logo and the right group to the edges now that the tabs no
   longer occupy the middle of the flow. */
.topnav-tabs {
  position: absolute; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; justify-content: center; gap: 0;
  /* AND IT MUST NOT SWALLOW CLICKS. Lifting the strip out of the flow puts a
     475px box across the middle of the masthead; without this it is an
     invisible blocker over whatever sits beneath it. QA carries the same
     pair, and the pair is the point — the container is transparent to the
     pointer and its children take it back. */
  pointer-events: none;
}
.topnav-tabs > * { pointer-events: auto; }
.topnav-tabs-inner {
  display: flex;
  align-items: center;
  border: 0.75px solid #5b6771;
  border-radius: 24px;
  padding: 4px;
  gap: 2px;
}
/* Some entries are <button> (agents this prototype does not contain) and the
   Knowledge and QA entries are real <a>s to deployed products, so the class
   has to render identically on both. */
.topnav-tab {
  display: flex;
  align-items: center;
  gap: 4px;
  text-decoration: none;
  padding: 6px 16px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: var(--fw-semibold);
  /* `normal`, WHICH IS WHAT A DIV GETS AND A BUTTON DOES NOT. QA and
     Knowledge draw these tabs as `<div>`s inheriting nothing; this product
     draws them as `<button>`s so they can be reached from the keyboard, and
     a button here inherited this file's 1.45 body leading — 17.4px against
     QA's 16. Three pixels per tab, which made the pill container 41.4 where
     the siblings render 38, and the shared masthead a different height in
     one of the three products.

     The one departure stays a departure of ELEMENT, not of appearance. */
  line-height: normal;
  color: var(--d400);
  cursor: pointer;
  transition: background-color var(--t-fast), color var(--t-fast), border-color var(--t-fast),
              transform var(--t-press) var(--ease-out);
  border: 1px solid transparent;
  white-space: nowrap;
  background: none;
  font-family: inherit;
}
.topnav-tab:hover { background: rgba(255, 255, 255, 0.05); color: var(--d200); }
.topnav-tab.active { color: var(--accent); }
.topnav-tab svg { width: 13px; height: 14px; flex-shrink: 0; }
.topnav-tab:active { transform: scale(0.97); }
.topnav-tab:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
:root[data-theme="light"] .topnav-tabs-inner { border-color: rgba(16, 24, 40, 0.22); }
:root[data-theme="light"] .topnav-tab:hover { background: rgba(16, 24, 40, 0.05); color: var(--d100); }

/* Equal flanks so the strip centres on the WINDOW, not on the space left
   between the logo and the user pill — and they are QA's `.topnav-spacer`
   divs, not `flex: 1 1 0` on the logo and the right group. Same result, and
   the siblings' mechanism rather than a second one that happens to agree. */
.topnav-spacer { flex: 1 1 0; }
.topnav-logo, .topnav-right { flex: 0 0 auto; min-width: 0; }
.topnav-tabs { flex: 0 1 auto; min-width: 0; }

.topnav-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  flex-shrink: 0;
}
.topnav-right > * { flex-shrink: 0; }

.topnav-bell,
.rail-toggle {
  width: 34px; height: 34px; border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  display: flex; align-items: center; justify-content: center;
  color: var(--d400); cursor: pointer; position: relative;
  transition: background-color var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.topnav-bell:hover,
.rail-toggle:hover { background: rgba(255,255,255,0.08); color: var(--d200); }
.topnav-bell:focus-visible,
.rail-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.topnav-bell svg,
.rail-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; }
.bell-dot {
  position: absolute; top: 6px; right: 6px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--err); border: 1.5px solid var(--body-bg);
}
.bell-dot[hidden] { display: none; }
:root[data-theme="light"] .topnav-bell,
:root[data-theme="light"] .rail-toggle { background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10); }
:root[data-theme="light"] .topnav-bell:hover,
:root[data-theme="light"] .rail-toggle:hover { background: rgba(16,24,40,0.07); }

/* ── THE RAIL'S TOGGLE, AND IT IS THE BELL'S SKIN ──

   Not a new control shape. The bell above is already the masthead's icon
   button — same 34px box, same wash, same border, same both-theme pair — and a
   second definition of it is the thing that drifts. This adds the two
   declarations that are actually different: it is not painted above the drawer
   breakpoint, and it needs the gap the logo's own `gap: 10px` does not give it
   against the mark.

   Hidden by DISPLAY rather than by the `hidden` attribute, so the media query
   below is the only thing that decides whether it exists, and no script has to
   remember to un-hide it. */
.rail-toggle { display: none; margin-right: 2px; }

/* Present at every width, painted at none of them until the drawer exists.
   `display` rather than `hidden`, so opening is one class and the fade has
   something to transition from — toggling `hidden` in the same frame as the
   class would skip the transition and need a rAF to work around it. */
.rail-scrim { display: none; }

.topnav-user {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px 4px 4px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  cursor: pointer;
  transition: border-color var(--t-fast), background-color var(--t-fast), transform var(--t-press) var(--ease-out);
}
.topnav-user:hover { border-color: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.07); }
.topnav-user:active { transform: scale(0.97); }
.topnav-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  /* Was this gradient written out longhand — byte-identical to the token
     the library already ships, so a re-theme would have moved every other
     avatar on the surface and left this one behind. */
  background: linear-gradient(135deg, #7c3aed, #3369ff);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: var(--fw-bold);
  flex-shrink: 0;
}
/* A `<button>` centres its text; Knowledge's `<div role="button">` did not.
   The CSS here is byte-identical to Knowledge's — the element changed when
   this pill gained the Looking-as panel, and it took the UA default with it,
   so the name floated to the middle of a role line that had grown longer
   than it. Both lines share one left edge again. */
.topnav-user { text-align: left; }
.topnav-user-info { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
/* REVERTED TO THE LIBRARY'S VALUES. The topnav is shared with AiMY QA and
   AiMY Knowledge and is carried across deliberately so the three agents read
   as one product — which means its type is not this product's to re-scale.
   Bringing it onto the local scale made the same bar 1px different here than
   in the other two, which is worse than being off a scale nobody applies to
   it. Same reasoning for the avatar below. */
.topnav-user-name { font-size: 12px; font-weight: var(--fw-bold); color: var(--d50); line-height: 1; }
.topnav-user-role { font-size: 10px; color: var(--d400); line-height: 1; }
.topnav-user svg { width: 12px; height: 12px; color: var(--d400); }
:root[data-theme="light"] .topnav-user { background: rgba(16, 24, 40, 0.04); border-color: rgba(16, 24, 40, 0.10); }

/* Below this the six ecosystem tabs and a full user pill cannot both fit.
   The avatar survives; the name is the thing that can be dropped. */
@media (max-width: 1119.98px) { /* 1120 layout px */
  .topnav-user-info { display: none; }
  .topnav-user { padding: 4px; }
}

/* ── Version link ──────────────────────────────────────────────────
   A destination, not a state you toggle, so it is quieter than a product tab
   and reads as chrome. One link rather than a segmented pair: you are on V2,
   so the only thing worth offering is the way out. */
.ver-link {
  display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
  padding: 4px 12px; border-radius: var(--r-pill);
  font-family: var(--font-sans);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); line-height: var(--lh-snug);
  color: var(--ink-muted); text-decoration: none;
  border: 1px solid var(--glass-border);
  transition: background-color var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.ver-link:hover { background: rgba(255,255,255,0.08); color: var(--d100); border-color: var(--card-border); }
:root[data-theme="light"] .ver-link:hover { background: rgba(16,24,40,0.07); }
.ver-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ver-link-ico { width: 10px; height: 10px; opacity: 0.75; }
/* The flanks split what the six tabs leave, and the right one runs out first.
   V1 is the only thing in the topnav that goes somewhere else rather than
   carrying state, so it is the one that drops. */
@media (max-width: 999.98px) { /* 1000 layout px */
  .ver-link { display: none; }
}

/* ══ THE CHEVRON ON THE MARK ══════════════════════════════════
   The account pill's chevron, in its size and its ink, because it makes the
   same promise: the thing beside me opens. Quiet — no ground and no border —
   since it hangs off the brand mark and a boxed control there would read as a
   second logo. Lit while its menu is open, which `.b-menu-wrap:has()` already
   says for every other opener in the build.

   `display: none` at every width and painted only in the block above: where
   the strip is standing, a second way to the same five links is one too many. */
.prod-chev {
  display: none;
  align-items: center; justify-content: center;
  width: 24px; height: 24px; flex-shrink: 0;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--d400);
  transition: color var(--t-fast), transform var(--t-fast);
}
.prod-chev svg { width: 14px; height: 14px; }
.prod-chev:hover { color: var(--d100); }
.prod-chev:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--r-xs); }
.prod-anchor:has(.b-menu:not([hidden])) .prod-chev { color: var(--d50); transform: rotate(180deg); }
/* The menu hangs from the mark, not from the chevron's own 24px box — it is
   the AiMY mark that owns it, and a 216px card starting 150px in from the
   left edge of a 459px layout would have nowhere to go but back. */
/* Against .topnav-logo, not against the chevron's own 24px box: the menu
   belongs to the MARK, and `left: 0` on a relative logo is that edge
   exactly, where an offset from the chevron would be a number measured
   once and wrong the next time the wordmark changes size. */
.topnav-logo:has(.prod-anchor) { position: relative; }
/* .topnav-logo .prod-anchor, not .prod-anchor: `.b-menu-wrap` declares
   `position: relative` in bdr.css, which loads after this file, so at
   equal specificity the wrap wins and the menu hangs off the chevron
   again — measured, 160px in from the edge it is supposed to share. */
.topnav-logo .prod-anchor { position: static; }
.prod-anchor .b-menu { left: 0; right: auto; }

/* ══ AND THE ITEMS ARE THE PILLS ════════════════════════════════
   This list is the strip, read back, so the one you are on looks here the way
   it looks there: the AiMY glyph and the name in accent. `.topnav-tab.active`
   is that rule in one declaration and this is the same one — not the menu's
   own `is-on` ground, which is how a MENU says "this one" and would say it a
   second time, in a second language, over a mark already saying it.

   The mark keeps the strip's size rather than the menu's gap-10 icon slot,
   because it is the same mark at the same rank.

   AND NO UNDERLINES. Two of these entries are real <a>s to deployed products
   and three are not, so the underline the user agent gives an anchor drew a
   line under exactly two of five items in a list where every row is the same
   kind of thing. The strip has the same mix and has never underlined any of
   them. */
.prod-anchor .b-menu-item { text-decoration: none; gap: 8px; }
.prod-anchor .b-menu-item svg { width: 13px; height: 14px; flex-shrink: 0; }
.prod-anchor .b-menu-item.is-on,
.prod-anchor .b-menu-item.is-on:hover { background: none; color: var(--accent); }
/* The ones with nowhere to go are quiet rather than struck through: a product
   that is not built yet is not an error. */
.prod-anchor .b-menu-item[disabled]:not(.is-on) { color: var(--ink-muted); cursor: default; }
.prod-anchor .b-menu-item[disabled]:hover { background: none; }
@media (prefers-reduced-motion: reduce) { .prod-chev { transition: color var(--t-fast); } }

/* ══ THE MASTHEAD BELOW 720, AND THE STRIP IS NOT ON IT ════════════════
   assets/aimy-responsive.css §4 puts the strip back in flow at 1280 layout so
   it cannot collide with the right-hand cluster, and hands it `overflow-x:
   auto` so the flanks can take what they need. That keeps the masthead from
   OVERLAPPING; it does not keep the strip legible. Measured on the queue at a
   459px layout: the flanks took 320 of it and the strip was left an 111px
   window onto 356px of tabs — which renders as `ct  Talent`, two half-words
   in the middle of the masthead, with the tab you are actually on scrolled out
   of sight. A scroller nobody can see the ends of is not navigation.

   720 IS MEASURED, NOT CHOSEN. The flanks are 170 (logo) and 150 (the bell,
   the theme control and the pill) and the strip wants 356. Walking the width
   down: at a 776px layout the strip got 358 of the 356 it needs, at 729 still
   358, at 682 only 334 — so it stops fitting at about 727, and 720 is this
   file's existing word for the width either side of it.

   IT MOVES, IT DOES NOT GO. The products hang from the mark as a menu instead
   — the markup and the reasoning are at index.html's .prod-anchor, and the
   list is read back off this same strip so there is one of it. A row of its
   own was the first answer and cost 44px of every phone screen for five links
   that are a chevron away; this one costs 20.

   --topbar-height IS UNTOUCHED HERE, and that is the point of the change: the
   overlay, the rail drawer, its scrim and the notifications panel are all
   `position: fixed` against it (sales.css:1265, 1375, 5984, 6005), and a
   masthead that grew would have moved all four. */
@media (max-width: 719.98px) { /* 720 layout px */
  .topnav-tabs { display: none; }
  .prod-chev { display: inline-flex; }
  /* The strip was the middle of three flex regions and the two spacers exist
     to hold it there. With it gone they are two `flex: 1 1 0` boxes splitting
     the masthead between the mark and the cluster, which is the same job done
     by one of them. */
  .topnav-spacer + .topnav-spacer { display: none; }
  .topnav-logo { padding-right: 0; margin-right: 0; }
}


/* ── The notifications panel ───────────────────────────────────────
   What is still waiting on a person, derived from the same model the
   briefing rail and the Status filter read — so the three cannot disagree.

   Panel chrome follows .peek rather than inlined literals: a second dropdown
   language on one page is one too many.
═══════════════════════════════════════════════ */
.ntf-anchor { position: relative; display: flex; align-items: center; flex-shrink: 0; }

.ntf-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 940;
  width: 360px; max-width: calc(var(--vp-w) - 32px);
  max-height: min(calc(var(--vp-h) * 0.7), 520px); overflow-y: auto;
  padding: 8px; text-align: left;
  background: var(--panel-bg); border: 1px solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(12px);
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent;
}
.ntf-panel[hidden] { display: none; }
.ntf-panel::-webkit-scrollbar { width: 4px; }
.ntf-panel::-webkit-scrollbar-track { background: transparent; }
.ntf-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 2px; }
:root[data-theme="light"] .ntf-panel { scrollbar-color: rgba(16,24,40,0.12) transparent; }
:root[data-theme="light"] .ntf-panel::-webkit-scrollbar-thumb { background: rgba(16,24,40,0.12); }

.ntf-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px 12px; }
.ntf-head b {
  font-family: var(--font-display); font-size: var(--fs-sm);
  font-weight: var(--fw-semibold); color: var(--d50);
}
.ntf-count {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  padding: 2px 8px; border-radius: var(--r-pill);
  background: var(--accent-dim); color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.ntf-count[hidden] { display: none; }
.ntf-clear {
  margin-left: auto; background: none; border: 0; padding: 2px 4px; cursor: pointer;
  font-family: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); border-radius: var(--r-xs);
  transition: color var(--t-fast);
}
.ntf-clear:hover { color: var(--d100); }
.ntf-clear:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.ntf-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.ntf-row {
  display: flex; gap: 8px; padding: 12px; border-radius: var(--r-md);
  transition: background-color var(--t-fast), opacity var(--t-fast);
}
.ntf-row:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .ntf-row:hover { background: rgba(16,24,40,0.04); }
/* Read, not gone. What is outstanding stays outstanding until the corpus
   changes; the count under the bell is the thing marking clears. */
.ntf-row.is-read { opacity: 0.45; }
/* Tone on one leading dot, same as .brief-text — the rail and the bell are
   two views of one model and should not look like two components. */
.ntf-sev {
  width: 5px; height: 5px; border-radius: 50%;
  margin-top: 8px; flex-shrink: 0; background: var(--d500);
}
.ntf-sev.p1 { background: var(--err); }
.ntf-sev.p2 { background: var(--warn); }
.ntf-sev.p3 { background: var(--brand); }
.ntf-row-main { flex: 1; min-width: 0; }
.ntf-row-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.ntf-row-type { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50); }
.ntf-row-when { margin-left: auto; font-size: var(--fs-xs); color: var(--ink-muted); flex-shrink: 0; }
.ntf-row-body { margin: 0 0 8px; font-size: var(--fs-base); line-height: var(--lh-base); color: var(--ink-muted); }
.ntf-row-cta {
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-bold); cursor: pointer;
  padding: 4px 12px; border-radius: var(--r-pill);
  background: var(--accent-dim); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), 0.26);
  transition: background-color var(--t-fast), border-color var(--t-fast);
}
.ntf-row-cta:hover { background: rgba(var(--accent-rgb), 0.22); border-color: rgba(var(--accent-rgb), 0.40); }
.ntf-row-cta:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.ntf-foot { padding: 8px 4px 4px; margin-top: 8px; border-top: 1px solid var(--hairline); }
.ntf-foot-btn {
  width: 100%; padding: 8px; cursor: pointer; font-family: inherit;
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
  border-radius: var(--r-md); color: var(--ink-muted);
  background: rgba(255,255,255,0.04); border: 1px solid var(--glass-border);
  transition: background-color var(--t-fast), color var(--t-fast);
}
.ntf-foot-btn:hover { background: rgba(255,255,255,0.08); color: var(--d50); }
.ntf-foot-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
:root[data-theme="light"] .ntf-foot-btn { background: rgba(16,24,40,0.04); }
:root[data-theme="light"] .ntf-foot-btn:hover { background: rgba(16,24,40,0.07); }
/* Nothing waiting is a real state and the best one this panel can report. */
.ntf-empty { padding: 24px 12px; text-align: center; font-size: var(--fs-xs); color: var(--ink-muted); }

/* The bell sits ~90px in from the window edge, so a 360px card anchored to
   it runs off the LEFT of the screen before the viewport is narrow enough
   for max-width to catch it — the constraint is measured from the wrong
   side. Below that width the panel is pinned to the window instead, AND THAT
   RULE IS IN bdr.css, not here: bdr.css carries a second, complete definition
   of .ntf-panel at this same specificity and loads after this file, so its
   unconditional `position: absolute; right: 0` beat this file's media query
   — a media query adds no specificity. Measured at a 459px layout before the
   move: only `left: 12px` survived, which resolved against `.ntf-anchor`
   rather than the window and pushed the panel 255px off the right edge, its
   text cut mid-word. The rule lives beside the definition that wins. */

@media (prefers-reduced-motion: reduce) {
  .ntf-row, .ntf-row-cta, .ntf-foot-btn, .ntf-clear { transition: none; }
  /* The conversation column came over from AiMY Knowledge with its rules and
     without this, because Knowledge has no guard on them either. Every other
     transition in this build answers the preference; a component being
     lifted whole is not a reason for one to stop. */
  .ov-chat, .ov-chat-more, .ov-chat-mi { transition: none; }
}

/* ── Theme toggle ──────────────────────────────────────────────────
   Lifted out of the design system's documentation TOPBAR by the extractor,
   which keeps its rules; these are the product's sizing on top. */
.ds-theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--ink-muted); cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.ds-theme-toggle:hover { border-color: var(--card-border-hover); color: var(--d50); }
.ds-theme-toggle svg { width: 16px; height: 16px; display: block; }
:root[data-theme="light"] .ds-theme-toggle {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}

/* ═══════════════════════════════════════════════
   BODY — sidebar → scrollable main
═══════════════════════════════════════════════ */
.app-body { display: flex; flex: 1; min-height: 0; position: relative; z-index: 1; background: transparent; }

/* SIDEBAR METRICS — QA's, not the design system's. The DS ships
   --sidebar-width: 240px; QA's 220px rail with roomier rows reads as
   better-managed space than a wider rail holding tighter ones. Overridden
   here rather than in aimy-ds.css so the extracted stylesheet stays a
   faithful copy of the library and this divergence stays visible. */
:root { --sidebar-width: 220px; }

/* No border and no background. The sidebar sits directly on the ambient
   layer — a rule here draws a hard seam down the page and makes the rail
   read as a separate surface from the canvas it belongs to. */

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; background: transparent; }

/* The scroll container. The float bar is fixed, so it must not live inside
   this element or it scrolls away. */
.page-scroll {
  flex: 1; overflow-y: auto;
  /* Same reason as `.app-rail` — see the note there. This column is long
     enough to overflow on nearly every surface, which hides the shift
     rather than removing it: a short page that gains a block, or a node
     that opens on an already-short campaign, jumps 5px wider. */
  scrollbar-gutter: stable;
  padding: 24px 32px 96px;
}

/* THE MEASURE CAP BELONGS TO PROSE, NOT TO THE WHOLE COLUMN.

   This was `max-width: 1160px` with no auto margin, so the entire workbench
   was capped AND pinned to the left of the stage: measured at 1920, the
   column ran 32→1192 with **768px of dead space on the right**, on every
   view. It is the "everything is left aligned, what is this?" and the white
   space on the right, and it defeated `.s-rec`'s own centring — that resolved
   to 40px gutters inside the 1160 box rather than inside the room available.

   The reasoning behind the cap was sound and is kept where it applies: a line
   of 13px text should not run 120 characters. But a CARD GRID is not a line
   of text — `repeat(auto-fill, minmax(320px, 1fr))` answers a wider viewport
   with more columns, which is the whole point of it. So the column takes the
   room, and the cap moves to the things that are actually read: the record
   page and the campaign page's prose. */
.page-inner { max-width: none; }

/* ═══════════════════════════════════════════════
   CANVAS STACKING

   The design system ships .aimy-float-wrap and .aimy-overlay BOTH at
   z-index 10, so which one wins is decided by DOM order alone. That is not
   a stacking order, it is a coincidence. QA resolves it at 200 / 500;
   Knowledge adopted the same numbers and so does this, so the three agents
   stack identically. Filed in ../GAPS.md.
═══════════════════════════════════════════════ */
.aimy-float-wrap { z-index: 200; }

/* THE CANVAS IS ANCHORED TO THE VIEWPORT, NOT TO THE PAGE.

   The library ships `.aimy-overlay { position: absolute; inset: 0 }`, which
   makes it a child of the very surface it is supposed to sit above: anything
   that scrolls or grows `.app-main` carries the canvas with it — and takes
   its badge, close button, thread and input along.

   Reported four times as "the scroll bug", with a screenshot of an empty
   blurred field and nothing in it. It could not be reproduced here at the
   reporter's own viewport, URL and click path, and instrumenting the click
   that triggers it recorded **zero DOM mutations and zero scroll events**.

   So instead of hunting further for what moves the page, the canvas stops
   being movable. `fixed` at exactly the box it already occupied — under the
   top bar, right of the rail — so whatever the surface underneath does, the
   canvas cannot be carried anywhere. The offsets are the layout's own tokens
   rather than numbers, so they cannot drift from the shell they clear. */
.aimy-overlay {
  z-index: 500;
  position: fixed;
  top: var(--topbar-height); right: 0; bottom: 0;
  /* FULL WIDTH. It was offset by `--sidebar-width` to clear a briefing rail
     that no longer exists, with a `body.is-record` rule reclaiming the room
     whenever a record was open — two rules for a gap that had one cause.

     The offset was not only obsolete, it was the reason the docked rail had
     to exist: a canvas that cannot use the whole window cannot hold a
     conversation ABOUT something and show that something at the same time,
     so the thread got docked beside the work instead. Give the canvas the
     window and both problems close. */
  left: 0;
  /* ROW, AND THE LIBRARY'S COLUMN MOVES INSIDE.

     `display: flex` alone was not enough and broke the canvas outright. The
     library declares `.aimy-overlay { flex-direction: column; align-items:
     center; justify-content: flex-end; padding: 0 0 28px }` — a single
     centred stack pinned to the bottom, which is exactly right for a thread
     over an input and exactly wrong for a column beside them. Re-declaring
     only `display` left the direction at `column`, so the chat list stacked
     ON TOP of the canvas, centred, with the thread shoved to the bottom of
     the window under it.

     The whole inner contract moves to `.overlay-main` unchanged, so the
     badge, close, thread and input keep the geometry the library gives them
     — and they position against `.overlay-main` because that is what is now
     `position: relative`. */
  flex-direction: row;
  align-items: stretch;
  justify-content: flex-start;
  padding: 0;
}

/* ── The chat column and the thread beside it ──
   Fixed column, flexible thread: the list of conversations does not get
   wider on a wider screen, because a title is a title. */
.overlay-chats {
  /* Accent AS TEXT, which this build calls --accent-text and Knowledge
     calls --ink-action. The only token the lifted column asks for that is
     not already declared, bridged here rather than renamed in the rules. */
  --ink-action: var(--accent-text);
  width: 240px; flex-shrink: 0;
  display: flex; flex-direction: column; gap: 20px;
  padding: 20px 12px;
  overflow-y: auto;
  border-right: 1px solid var(--glass-border);
}
.overlay-main {
  flex: 1; min-width: 0; position: relative;
  display: flex; flex-direction: column;
  align-items: center; justify-content: flex-end;
  /* 24, not the library's 28. Carrying a value across is not a licence to
     carry an off-scale one — 28 is not on the 4px scale this product audits
     against, and a four-pixel divergence at the bottom of a full-height
     overlay is invisible next to a rule that says spacing is chosen once. */
  padding: 0 0 24px;
}

/* ══ THE CHAT COLUMN IS AiMY KNOWLEDGE'S, LIFTED WHOLE ═════════════════
   What stood here was an earlier cut of the same component — same class
   names, px where Knowledge now has rem, and none of the parts it grew
   since: the wrapper that lets a row carry a menu, the menu itself, the
   rename box, the pin, the two-line block. So this is not a port and not
   an adaptation. Every rule below is knowledge.css's, character for
   character, comments included — which is why they speak in Knowledge's
   voice about Knowledge's history. The §28 precedent, and its rule: a
   component lifted whole keeps its own bytes and says where they came
   from.

   TWO THINGS DIVERGE, and both are named here rather than left to be
   found. `.ov-chat-input::placeholder` keeps `--ink-placeholder` where
   Knowledge has `--d600`: the ink block below names --d600 NON-TEXT, and
   this build's floor for anything read is above it; --ink-placeholder is
   the one documented exception and it is what a placeholder is for.

   And `.ov-chat-n` is not here at all. Knowledge's row ends in a turn
   count; this one does not, so the rule that drew it went with the markup
   rather than being left behind for a later sweep to find and wonder about.
   bdr.js says why at the point where the row is built.

   TOKENS: everything these rules ask for is already declared in this file
   or in aimy-ds.css except `--ink-action`, which is accent-as-text —
   bridged onto `.overlay-chats` above, the way §28 bridges --qa-accent
   onto `.ntf-anchor`. */

/* ── THE COLUMN SITS ON THE DENSITY SCALE, NOT UNDER IT ──

   This list was set in raw --fs-xs/--fs-2xs: an 11px title over 10px meta, in
   a 255px column. THE DENSITY TYPE SCALE above says what is wrong with that in
   its own words -- "--fs-2xs will hand you 10px without comment, and 10px in a
   320px column is decoration wearing the clothes of information" -- and names
   --ty-micro the floor precisely so a narrow column cannot fall through it.

   The column was already half on the scale and did not agree with itself: the
   menu that hangs off a row (.ov-chat-mi) and the box that REPLACES a title
   when you rename it (.ov-chat-rename) are both --ty-meta, so the menu read
   larger than the row it belonged to and a title grew 2px the moment it became
   editable. Everything here is --ty-meta over --ty-micro now, which is one
   step up for the titles and the search box, and lands the rest on the floor.

   Hierarchy is unchanged and still carried the way the scale says -- step AND
   weight AND ink -- because every rung moved together. */

/* Starting one, then finding one. The column's only filled control, because
   starting is the verb and everything under it is the qualifier. */
.ov-chat-new { width: 100%; justify-content: center; margin-bottom: 0.5rem; }
.ov-chat-new svg { width: 0.875rem; height: 0.875rem; }
/* Find a conversation. Second in the column, because a list you scroll to
   search is a list you have already failed to search. */
.ov-chat-find { display: block; margin-bottom: 1.25rem; }
.ov-chat-input {
  width: 100%; min-height: 1.5rem; padding: 0.5rem;
  border-radius: var(--r-sm);
  border: 0.0625rem solid var(--glass-border); background: transparent;
  font-family: inherit; font-size: var(--ty-meta); color: var(--d100);
}
.ov-chat-input::placeholder { color: var(--ink-placeholder); }
.ov-chat-input:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -1px; }

.ov-chat-group { display: flex; flex-direction: column; gap: 0.125rem; }
/* 8 under the label, 20 between groups — the ratio rule, and it is what makes
   a group read as a group rather than as a list with a heading in it. */
.ov-chat-cap {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--d500); padding: 0 0.5rem; margin-bottom: 0.5rem;
}
.ov-chat {
  /* `flex-start`, so a title can run to two lines: centred would float the
     turn count against the middle of a wrapped question. */
  display: flex; align-items: flex-start; gap: 0.5rem;
  min-height: 1.5rem; padding: 0.5rem; border-radius: var(--r-sm);
  font-size: var(--ty-meta); color: var(--ink-muted);
  cursor: pointer; text-align: left; width: 100%;
  transition: background-color var(--t-fast), color var(--t-fast);
}
.ov-chat:hover { background: var(--accent-dim); color: var(--d50); }
.ov-chat:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.ov-chat.is-here { background: var(--accent-dim); color: var(--d50); font-weight: var(--fw-semibold); }

/* ══ THE ROW IS A WRAPPER NOW ══════════════════════════════════════════
   The row itself is still a <button>; the menu control is its SIBLING, not
   its child, because a button inside a button is markup browsers silently
   reparent — the control would end up outside the row it belongs to.

   `position: relative` so the menu hangs off the row without widening the
   column, the same anchoring idiom the bell and the account pill use. */
.ov-chat-row { position: relative; display: flex; align-items: stretch; }
.ov-chat-row .ov-chat { flex: 1 1 auto; min-width: 0; }

/* Quiet until wanted. Visible on hover, on keyboard focus anywhere in the
   row, and while its own menu is open — without that last one the control
   vanishes under the menu it just opened. */
.ov-chat-more {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  width: 1.5rem; margin-left: 0.125rem;
  border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink-faint); cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-fast), background-color var(--t-fast), color var(--t-fast);
}
.ov-chat-row:hover .ov-chat-more,
.ov-chat-row:focus-within .ov-chat-more,
.ov-chat-row.is-menu .ov-chat-more { opacity: 1; }
.ov-chat-more:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.ov-chat-more:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

.ov-chat-menu {
  position: absolute; top: 100%; right: 0; z-index: 20;
  min-width: 11rem; padding: 0.25rem;
  border: 0.0625rem solid var(--card-border-hover);
  border-radius: var(--r-md);
  background: var(--panel-bg);
  box-shadow: var(--shadow-lg);
}
.ov-chat-mi {
  display: flex; align-items: center; gap: 0.5rem;
  width: 100%; padding: 0.375rem 0.5rem;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer; text-align: left;
  color: var(--ink-secondary);
  font-family: inherit; font-size: var(--ty-meta);
}
.ov-chat-mi:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.ov-chat-mi:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -0.125rem; }
.ov-chat-mi.is-danger { color: var(--err); }
.ov-chat-mi.is-danger:hover { background: var(--err-bg); color: var(--err); }
/* The question above the two answers, so Delete is never the first thing
   under the pointer when the confirm opens. */
.ov-chat-mq {
  padding: 0.375rem 0.5rem 0.25rem;
  font-size: var(--ty-micro); color: var(--ink-quiet);
}

/* Renaming happens in place. A modal for a title is a ceremony, and the row
   is where the name is read. */
.ov-chat-rename {
  flex: 1 1 auto; min-width: 0;
  padding: 0.5rem;
  border: 0.0625rem solid var(--card-border-focus);
  border-radius: var(--r-sm);
  background: var(--card-bg-raised);
  color: var(--ink-primary);
  font-family: inherit; font-size: var(--ty-meta);
}
.ov-chat-rename:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* The pin reads as part of the title, not as a badge beside it. */
.ov-chat-pin { display: inline-flex; margin-right: 0.25rem; color: var(--ink-action); vertical-align: baseline; }

/* TWO LINES IN ONE ROW. The wrapper is what the turn count aligns against —
   without it `margin-left: auto` on the count would measure from the agent
   label rather than from the block, and a two-line row would push its own
   badge out of line with every one-line row above it. */
.ov-chat-lines { min-width: 0; display: flex; flex-direction: column; gap: 0.0625rem; }
/* Wraps rather than ellipsises, like everything else here that was truncating.
   A session's title IS the question that was asked, and "What does the corpus
   say about data resi…" is the half of it that does not say which question.
   The rows in this column are what you are choosing between; a list you cannot
   read is a list you cannot choose from. */
.ov-chat-name { min-width: 0; line-height: var(--lh-snug); }
/* Who answered. One rung quieter than the title and one step smaller, which is
   the whole hierarchy — it is read after the title or not at all. */
.ov-chat-agent {
  font-size: var(--ty-micro); color: var(--d500);
  line-height: var(--lh-snug); letter-spacing: var(--ls-wide);
}
.ov-chat.is-here .ov-chat-agent { color: var(--d400); }
.ov-chat-none { font-size: var(--fs-base); color: var(--d500); line-height: var(--lh-relaxed); padding: 0 0.5rem; }

/* ══ THE DRAWER'S DOOR, AND IT IS THE RAIL TOGGLE'S SKIN ════════════════
   Not a new control shape: `.rail-toggle` is already this shell's "a column
   opens over the page here", down to the icon. It sits opposite
   `.overlay-close` on the same top row of the canvas — the way in on the
   left, the way out on the right — both absolutely positioned so revealing
   either never shifts the thread.

   `display: none` at every width, painted only in the block above: where the
   column is already standing in the row, a button offering to reveal it is a
   button that does nothing. */
.ov-chats-toggle {
  display: none;
  position: absolute; top: 16px; left: 16px; z-index: 40;
  width: 34px; height: 34px; border-radius: var(--r-md);
  align-items: center; justify-content: center;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--d400); cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.ov-chats-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; }
.ov-chats-toggle:hover { background: rgba(255,255,255,0.08); color: var(--d200); }
.ov-chats-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
:root[data-theme="light"] .ov-chats-toggle { background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10); }
:root[data-theme="light"] .ov-chats-toggle:hover { background: rgba(16,24,40,0.07); }
/* ══ AND THE WAY OUT IS ABOVE WHAT IT CLOSES ═══════════════════════
   `.overlay-close` is absolute with no z-index, so it paints in the positioned
   layer at 0 and every bubble in the thread that reaches its corner paints
   over it. Hit-tested at the centre of the button: at a 1412px layout the
   thread is narrow enough that the button answers, at 894 and at 459 the
   element under the cursor is `.msg-bubble` — the control is drawn, fully
   visible, and cannot be pressed.

   A stacking number, not a design change: above 1100 layout px nothing
   reaches that corner and this moves nothing that can be seen. 10 is under
   the conversation drawer's scrim, which is right — a drawer standing over
   the canvas covers the canvas's own controls with it. */
.overlay-close { z-index: 10; }
/* AND THE BADGE, for the same reason and it was missed. `.overlay-badge` comes
   from aimy-ds.css with no `z-index`, which is `auto` — the same level the
   thread paints on, and the thread paints later. The × beside it was raised
   when this was found; the badge was not, so a bubble scrolling under the
   header still passes over it. Nothing presses a badge, so this is not a lost
   control — it is a label a message slides across, which reads as a rendering
   fault. Same 10, so the pair stay on one level and the drawer's scrim at 20
   still covers both. */
.overlay-badge { z-index: 10; }

/* Present at every width, painted at none of them until the drawer exists. */
.ov-chats-scrim { display: none; }

/* ══ BELOW 900 THE COLUMN IS A DRAWER, NOT A DELETION ══════════════════
   This was `display: none`, on the reasoning that the column and a readable
   thread cannot both fit and the thread is what you came for. The first half
   is true and the second does not follow: the column is New chat, the search,
   the thread belonging to what you have open, and every free-standing session
   — so removing it left the canvas talkable-to and not navigable, on the
   screens with the least room to remember what you had already asked.

   THE RAIL ALREADY ANSWERED THIS, at 1080 and in these words: it stops taking
   room in the row and starts opening over it. Same `is-open` class, same
   scrim, same button skin, same `visibility` rather than `display` so a shut
   drawer is out of the tab order and still has something to animate. One
   drawer mechanism in this shell, not two.

   ABSOLUTE, NOT FIXED, which is the one line that differs from the rail's.
   `.aimy-overlay` is itself fixed and, below 720, inset under the masthead
   and any minimised call — so a drawer positioned against the viewport would
   start above the surface it belongs to. Against its own overlay it cannot. */
@media (max-width: 899.98px) { /* 900 layout px */
  .ov-chats-toggle { display: inline-flex; }

  .overlay-chats {
    position: absolute; z-index: 30;
    top: 0; bottom: 0; left: 0;
    /* --vp-w, not 100dvw: inside `zoom: k` a viewport unit is k viewports
       wide. The 56px it leaves is the strip of thread behind that says the
       drawer is over something. */
    width: min(280px, calc(var(--vp-w) - 56px));
    /* THE DOOR STAYS ON TOP OF WHAT IT OPENED, so the drawer starts below it.
       The toggle is 34px at 16 from the top and sits at z-index 40 against the
       drawer's 30 — correct, because while the drawer is open that button is
       how you shut it — but the column's own 20px padding put New conversation
       straight underneath it. 58 = 16 + 34 + 8, which reads as the drawer's
       head with its control in it rather than as a gap. */
    padding-top: 58px;
    background: var(--panel-bg);
    box-shadow: var(--shadow-xl);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    transform: translateX(-100%); visibility: hidden;
  }
  .overlay-chats.is-open { transform: none; visibility: visible; }

  .ov-chats-scrim {
    display: block;
    position: absolute; z-index: 20; inset: 0;
    background: rgba(10, 14, 18, 0.5);
    opacity: 0; pointer-events: none;
  }
  .ov-chats-scrim.is-open { opacity: 1; pointer-events: auto; }
}
:root[data-theme="light"] .ov-chats-scrim { background: rgba(16, 24, 40, 0.28); }

/* The start state lives inside the motion query and the end state outside it
   — this file's own pattern, and the rail's. */
@media (prefers-reduced-motion: no-preference) {
  .overlay-chats { transition: transform 220ms var(--ease-out), visibility 220ms; }
  .overlay-chats.is-open { transition: transform 220ms var(--ease-out), visibility 0s; }
  .ov-chats-scrim { transition: opacity 220ms var(--ease-out); }
}


/* Fixed and centred on the WINDOW, not on the main column. Centring inside
   .app-main puts it half the rail's width right of the page's true centre —
   110px, and visible the moment you look for it. */
.aimy-float-wrap {
  position: fixed; bottom: 20px; top: auto;
  right: auto; transform: none;

  /* ══ IT IS CENTRED ON THE WINDOW UNTIL THAT WOULD PUT IT ON SOMETHING ══

     Window-centring is right, and it is only right while there is room for it.
     The bar is 760 wide and the rail is 320, so its left edge clears the rail by
     (L - 760) / 2 - 320: sixty-eight pixels at the 1536 anchor, sixteen at 1432,
     zero at 1400, and below that the input sits ON the rail — measured at a
     1205px layout, ninety-seven pixels of overlap, with .aimy-float-wrap's
     z-index 200 winning, so the input covered the campaign rows underneath it.
     The call panel is the same defect mirrored: `body.is-calling` offsets the
     canvas (see --call-width) and never offset this, so a live call put the
     dialer under the input at every width where both were on screen.

     Neither is new and neither is caused by the UI scale. The scale is what
     makes them REACHABLE, because a 1024 tablet now renders a 1205px layout
     instead of a 1024px one.

     ── WHY THIS IS ONE EXPRESSION AND NOT A BREAKPOINT ──

     The obvious fix — hand the bar the main column below some width — steps it
     sideways by half the rail (160px, at every width; the offset is constant)
     the moment the query flips. This clamps instead: the ideal position, held
     back off the rail on one side and off the call panel on the other. Above
     the anchor every term is slack and it resolves to exactly the centring that
     shipped before — measured at 1536, at 1920 and at 3440, the bar lands on
     (layout - 760) / 2 to the pixel, which is why the anchor diff is empty.

     THE WIDTH GOES FIRST because both bounds are stated against it. It is the
     760 the design system caps the bar at, or whatever the column has left. */
  --fb-w: min(760px, calc(var(--vp-w) - var(--rail-w) - var(--fb-reserve) - 48px));

  /* The bar is a flex row that centres its child (aimy-ds.css:1101-1104), so
     the wrap has to BE the bar's box for the arithmetic below to place the bar
     rather than the box around it. */
  width: var(--fb-w);

  left: max(
    /* never left of the rail, plus a gutter */
    calc(var(--rail-w) + 24px),
    min(
      /* the ideal: centred on the window */
      calc((var(--vp-w) - var(--fb-w)) / 2),
      /* never right of what the call panel and a gutter leave */
      calc(var(--vp-w) - var(--fb-reserve) - 24px - var(--fb-w))
    )
  );
}
/* What is parked on the right and must not be covered. Zero until a call is
   live, so the whole clamp above collapses to the centring on every ordinary
   screen. */
:root { --fb-reserve: 0px; }
/* And what is parked at the TOP, which only a phone has: the strip a call
   folds into below 720 layout px. Zero everywhere else, so the expressions
   that add it resolve to the offset they had before it existed. */
:root { --call-strip: 0px; }
body.is-calling { --fb-reserve: var(--call-width); }

/* The scope tray belongs TO the float bar — it is the bar's scope, not a
   floating pill above it. The design system's .filter-tray-inner has no
   width contract, so it shrink-wraps and its centring wrapper drops it in
   the middle of the canvas, detached from the input it modifies. */
.filter-tray-inner { width: 100%; max-width: 760px; }
/* Anchored to the bar, not to a fixed offset that assumes a bar height. */
.filter-tray { bottom: calc(100% + 6px); }

/* The thread's bottom sits flush against the input bar, so a message caught
   mid-scroll is cut by a hard horizontal edge — which next to the input
   pill's own top border reads as a stray rule across the canvas rather than
   as content continuing below. The fade is inset so the final 16px of
   padding stays solid: when you ARE at the end, nothing looks faded. */
.overlay-thread {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent 100%);
}
.overlay-thread.is-at-end {
  -webkit-mask-image: none;
          mask-image: none;
}

/* The canvas basis — "Based on …". Absolutely positioned below the badge so
   it cannot disturb the thread's flex layout. Not in the design system; QA
   and Knowledge both had to build it locally, which is the argument for the
   system owning it (../GAPS.md). */
/* ══ AND IT LEAVES ON THE FIRST SCROLL ═══════════════════════════════════
   Absolute over the thread, which is right at the top of a conversation and
   wrong once you are reading one. `visibility` rides with the opacity so a
   faded bar is not still in the tab order — delayed on the way out, flipped at
   0s on the way in. AiMY Knowledge carries the same pair for the same reason;
   its canvas also has a badge to hide, and this product removed that already.

   The close button does NOT go with it: a control you need is not chrome you
   are done with. */
.aimy-overlay .overlay-context-bar {
  transition: opacity var(--t-base) var(--ease-out), visibility 0s;
}
.aimy-overlay.is-scrolled .overlay-context-bar {
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-fast) var(--ease-out), visibility 0s linear var(--t-fast);
}
@media (prefers-reduced-motion: reduce) {
  .aimy-overlay .overlay-context-bar,
  .aimy-overlay.is-scrolled .overlay-context-bar { transition: none; }
}
.overlay-context-bar {
  position: absolute; top: 66px; left: 50%;
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 6px;
  white-space: nowrap; max-width: calc(100% - 140px);
  overflow: hidden; pointer-events: none;
  opacity: 0;
  /* `ease` is the browser's default curve and the only place in this file
     that used it. Surface motion takes --ease-out like everything else. */
  transition: opacity var(--t-base) var(--t-fast) var(--ease-out);
}
.aimy-overlay.open .overlay-context-bar { opacity: 1; }
/* .overlay-thread's top padding clears the badge alone. With the basis bar
   below it the first message lands underneath — so make room, but only when
   there is a basis to show. */
.aimy-overlay:has(.overlay-context-tag) .overlay-thread { padding-top: 104px; }
/* --d500 is #637280. This file says a few sections up that --d500 and
   --d600 are for chrome — a placeholder, a disabled control — and never for
   content, and the type floor for this product is --d200. It measured
   3.53:1 against its ground, failing AA at 12px, and it is a label somebody
   is meant to read. --ink-secondary is the floor and clears it. */
.overlay-context-label {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary); flex-shrink: 0;
}
.overlay-context-tags { display: flex; align-items: center; gap: 4px; overflow: hidden; }
.overlay-context-tag {
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--d400);
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-pill);
  padding: 2px 8px; white-space: nowrap; flex-shrink: 0;
}
:root[data-theme="light"] .overlay-context-tag {
  background: rgba(16,24,40,0.05); border-color: rgba(16,24,40,0.10);
}

/* ═══════════════════════════════════════════════
   DROP LAYER

   Covers the whole window, because asking someone to aim at a target is the
   thing drag and drop exists to avoid.
═══════════════════════════════════════════════ */
.drop-layer {
  position: fixed; inset: 0; z-index: 900;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 11, 16, 0.72);
  backdrop-filter: blur(6px);
}
.drop-layer[hidden] { display: none; }
:root[data-theme="light"] .drop-layer { background: rgba(238, 242, 246, 0.78); }
.drop-card {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 40px 48px; text-align: center;
  border: 2px dashed rgba(var(--accent-rgb), 0.45);
  border-radius: var(--r-2xl);
  background: var(--panel-bg);
  color: var(--accent);
}
.drop-title { font-size: var(--fs-md); font-weight: var(--fw-bold); color: var(--d50); }
.drop-sub { font-size: var(--fs-base); color: var(--ink-muted); max-width: 40ch; line-height: var(--lh-base); }

/* ═══════════════════════════════════════════════
   PROTOTYPE CONTROLS — not product UI.

   Deliberately styled as a dev tool, never as product chrome: mono type,
   dashed border, no accent. Fixed, so it can never push anything, and
   anchored above the input band because the bottom-centre belongs to the
   input and the toast.
═══════════════════════════════════════════════ */
/* ══ `display: flex` BEAT `[hidden]`, SO THE DEV PANEL SHIPPED ═════════════

   `proto()` sets `shell.hidden = !protoOn()`, so without `?proto` in the URL
   the whole control is meant to be absent — and it never was. `[hidden]`
   lives in the UA stylesheet as `display: none`, which any class rule with a
   `display` outranks, so the shell rendered on every page load: a dashed
   32px button in the bottom-right corner of the product.

   Pressing it opened an EMPTY box, because the same guard that failed to
   hide the shell does return early before filling the panel. So the one
   thing a reader could do with it was find a 320px dashed rectangle with
   nothing in it.

   Every sibling in this file that needs the pairing already has it —
   `.proto-panel[hidden]`, `.ntf-panel[hidden]`, `.drop-layer[hidden]`,
   `.s-cal-panel[hidden]`, `.app-call[hidden]` — and this is the one that was
   missed. It is the third time this file has been bitten by the same thing:
   `.s-field { display: block }` beat `[hidden]` and put Topic and Objection
   on every outbound log. */
.proto[hidden] { display: none; }
.proto {
  position: fixed; right: 16px; bottom: 16px; z-index: 950;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
/* ══ QUIET IS NOT THE SAME AS INVISIBLE ═══════════════════════════════════
   Measured against the page: the button's own background **1.08**, its dashed
   border **2.51** — under the 3:1 floor a UI boundary needs to register as a
   shape at all — and a `--d500` icon inside that. A 32px near-transparent
   square with a faint dashed edge, in a corner. It was reported as missing
   twice, and once it WAS rendering, it was still reported as missing: being
   drawn and being findable are two different things.

   The dashed edge stays, because that is what says scaffolding rather than
   product. It just has to clear the floor: 3.54 for the border, and an icon
   a step brighter so the control reads as a control. Still the quietest
   thing on the page, and now actually on it. */
.proto-toggle {
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--r-md);
  border: 1px dashed var(--d500);
  background: var(--panel-bg);
  color: var(--ink-muted); cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.proto-toggle:hover { color: var(--d100); border-color: var(--ink-muted); }
.proto-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.proto-toggle svg { width: 16px; height: 16px; }
.proto-panel {
  order: -1;
  width: 320px; max-height: min(calc(var(--vp-h) * 0.7), 560px); overflow-y: auto;
  padding: 12px;
  /* The dashed border already says "scaffolding, not the product" — and says
     it better than a second typeface did, since the border cannot be mistaken
     for a deliberate style choice. */
  font-size: var(--fs-2xs);
  border: 1px dashed var(--d600); border-radius: var(--r-lg);
  background: var(--panel-bg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(12px);
}
.proto-panel[hidden] { display: none; }
/* Below this the panel and the input band fight for the same corner. */
@media (max-width: 899.98px) { /* 900 layout px */
  .proto { bottom: 76px; }
}

/* ═══════════════════════════════════════════════
   THE FILTER ROW

   Layout only. Every control is the design system's `.v2-dropdown`, which
   is the one select control in this system and carries the whole keyboard
   model — rebuilding it by hand is the thing the library says not to do.

   The row wraps rather than scrolls. A horizontally scrolling filter row
   hides filters behind a gesture, and a filter you cannot see is a filter
   you do not know is on — which is the defect V1's twelve-column table has
   in its own way.
═══════════════════════════════════════════════ */
/* ══ THE WAYS IN ═══════════════════════════════════════════════════════════
   Six named views above the axes, each carrying its count. They are the fast
   path and the axes are the slow one, so they sit first and read louder — a
   count on a chip is the only thing on this row that tells you whether it is
   worth pressing before you press it.

   Bigger than the filter chips below them on purpose: these answer the
   question people arrive with, and the dropdowns answer the one they arrive
   at. */
.s-views {
  display: flex; flex-wrap: wrap; gap: 4px;
  margin-bottom: 8px;
}
.s-view {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 8px 12px; min-height: 24px; cursor: pointer; font-family: inherit;
  border: 1px solid var(--card-border); border-radius: var(--r-pill);
  background: var(--card-bg);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d200);
  transition: border-color var(--t-fast), background-color var(--t-fast), color var(--t-fast);
}
.s-view:hover { border-color: var(--card-border-hover); background: var(--accent-dim); }
.s-view:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* `All` is not a seventh question — it is the absence of one, so it sits
   after a gap rather than in the run. `auto` would push it to the far right
   of a wrapping row, which puts it on its own line at narrow widths and
   makes it look like a control that belongs to something else. */
.s-view-all { margin-left: 12px; }
.s-view.is-on {
  border-color: var(--accent); background: var(--accent-dim); color: var(--d50);
}
.s-view-n {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.s-view.is-on .s-view-n { color: var(--accent-text); }

.s-filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  margin-bottom: 12px;
}
/* ── Search inside a filter ──
   The library's dropdown ships letter typeahead, which finds a value you can
   already spell. A list of fourteen lists is the case where you cannot.
   See ../GAPS.md. */
.dd-search {
  display: flex; align-items: center; gap: 4px;
  padding: 8px; margin: -2px -2px 4px;
  border-bottom: 1px solid var(--hairline);
  position: sticky; top: -2px; z-index: 1;
  background: var(--panel-bg, var(--card-bg));
}
.dd-search svg { color: var(--ink-muted); flex-shrink: 0; }
.dd-search input {
  flex: 1; min-width: 0;
  background: none; border: 0; outline: none;
  font-family: var(--font-sans); font-size: var(--fs-xs); color: var(--d50);
}
.dd-search input::placeholder { color: var(--ink-placeholder); }
/* DETACHED, not hidden: `display:none` would still leave it in the
   library's `.v2-dropdown-option` query, and arrow keys would walk through
   rows nobody can see. */
.v2-dropdown-option.dd-off { display: none; }
.dd-none {
  padding: 12px;
  font-size: var(--fs-xs); color: var(--ink-muted); font-style: italic;
}
.dd-none[hidden] { display: none; }

/* The count beside an option. Tabular, so the column of numbers down an
   open panel lines up and can be compared at a glance. */
.s-dd-n {
  margin-left: auto; padding-left: 12px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.v2-dropdown-option.selected .s-dd-n { color: inherit; opacity: 0.7; }

/* ── The date range ──
   The panel holds the design system's `.cal` unchanged. Three states are
   ADDED, because `.cal` is a single-date picker and a range has ends —
   built to sit on top of `.selected` rather than replace it, so a one-day
   range still looks like the library's selected day. (../GAPS.md) */
.s-cal-wrap { position: relative; }
.s-cal-btn {
  display: inline-flex; align-items: center; gap: 4px; height: 32px;
  padding: 0 12px; border-radius: var(--r-md); cursor: pointer;
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); background: none; border: 1px solid var(--card-border);
  white-space: nowrap;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-cal-btn:hover { color: var(--d50); border-color: var(--card-border-hover); }
.s-cal-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-cal-btn.active-filter { color: var(--accent); border-color: rgba(var(--accent-rgb), 0.4); background: var(--accent-dim); }
.s-cal-panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 940;
  width: 280px; padding: 12px;
  background: var(--panel-bg); border: 1px solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(12px);
}
.s-cal-panel[hidden] { display: none; }
.s-cal-field { display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px; margin-bottom: 8px; border-bottom: 1px solid var(--hairline); }
.s-cal-field-label {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-muted);
}
.s-cal-field .seg { width: 100%; }
.s-cal-field .seg-btn { flex: 1; }
/* `.cal` bakes `width: 260px` — the width its specimen was drawn at, not a
   property of the component. Third instance of this in the library, after
   `.timeline` and `.type-card.is-compact`. Released. (../GAPS.md) */
.s-cal { width: 100%; box-shadow: none; border: 0; padding: 0; background: none; }
/* The three range states, layered over `.selected`. */
.cal-day.in-range { background: rgba(var(--accent-rgb), 0.12); border-radius: 0; }
.cal-day.range-start { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.cal-day.range-end { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.s-cal-hint { font-size: var(--fs-xs); color: var(--ink-muted); margin-top: 8px; line-height: var(--lh-base); }
.s-cal-clear {
  margin-top: 8px; padding: 4px 8px; cursor: pointer;
  font-family: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  color: var(--accent); border-radius: var(--r-xs);
}
.s-cal-clear:hover { background: var(--accent-dim); }
.s-cal-clear:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }


/* ═══════════════════════════════════════════════
   THE CHIP BAR — only what a dropdown cannot say
═══════════════════════════════════════════════ */
.s-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 16px; }

/* A `.s-scope-chip` stood here for one pass, naming the campaign as a scope
   rather than drawing it as another removable axis. It went with the bar:
   inside a campaign this surface now renders no filter row and no chips, so
   there is nothing for a scope to distinguish itself from. */
.s-chip { cursor: pointer; }
.s-chip svg { width: 11px; height: 11px; margin-left: 4px; opacity: 0.7; }
.s-chip:hover svg { opacity: 1; }
.s-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-chip-clear {
  font-family: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); cursor: pointer; padding: 4px 8px; border-radius: var(--r-xs);
  transition: color var(--t-fast);
}
.s-chip-clear:hover { color: var(--d100); }
.s-chip-clear:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ═══════════════════════════════════════════════
   THE WORKING SURFACE — four tabs and their cuts

   AN UNDERLINE, NOT A PILL, and the reason is that this surface carries pill
   controls that mean something else: the cut chips a row below are pills, and
   so is every filter chip in the product. Identical controls read as one
   control however far apart they sit — and these two rows sit eight pixels
   apart, doing different jobs. The tab changes WHAT THE LIST IS OF; the chip
   narrows what is in it. Two jobs, two shapes.

   THE STRIP IS A ROW ABOVE ITS CUTS, and the cuts are a row above the cards:
   uniform connectedness down a single column, so the chip row reads as
   belonging to the open tab rather than to the page.
═══════════════════════════════════════════════ */
.s-tabs-block { gap: 0; }
.s-tabstrip {
  display: flex; align-items: stretch; gap: 2px; flex-wrap: wrap; min-width: 0;
  border-bottom: 1px solid var(--card-border);
  margin-bottom: 16px;
}
.s-tab {
  appearance: none; border: none; background: transparent; cursor: pointer;
  display: inline-flex; align-items: baseline; gap: 4px;
  /* One step above the cut chips below it, which sit at 12px. It is the
     control that changes what the list is OF, and size is how that gets said
     without a third kind of container. */
  font-family: var(--font-sans); font-size: var(--fs-base); font-weight: var(--fw-semibold);
  /* 8/16/8 rather than 7/14/9. The odd numbers were an eyeballed optical
     nudge for the 2px live border, and they put three values off the scale
     in one declaration. The border is compensated where it is drawn — by
     the -1px margin below — not by shaving a pixel off the padding. */
  color: var(--ink-muted); padding: 8px 16px; white-space: nowrap;
  /* The rule is on the button, not on a pseudo-element of the strip, so the
     live tab keeps it while the strip's own hairline runs behind the rest —
     two borders on the same line, and the darker one wins where they meet. */
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-tab:hover { color: var(--d100); }
.s-tab.is-on { color: var(--d50); border-bottom-color: var(--accent); }
.s-tab:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; border-radius: var(--r-sm); }
/* HOW MANY ARE BEHIND EACH TAB, BEFORE YOU PRESS IT. A tab that will not say
   its size is a door you have to open to find out whether it was worth it. */
.s-tab-n { font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.s-tab.is-on .s-tab-n { color: var(--ink-muted); }
/* THE VERB IS NOT IN THE STRIP. `.s-build` sat at the end of this row with
   `margin-left: auto`, and its rules outlived the control they styled —
   `css-audit` reads `.s-build-page` as covering `.s-build`, so a dead rule
   with a live prefix is exactly the orphan it cannot see. Making a list now
   happens on the Lists tab's own surface, in `.s-mklist` below. */

.s-tabcuts { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 20px; }
/* The count sits inside the chip rather than after it, so pressing the
   number and pressing the label are the same target. */
.s-cut-n { font-variant-numeric: tabular-nums; opacity: 0.65; }
.s-tab-more { margin-top: 16px; }

/* The one thing you can make from this surface, on the one tab it belongs
   to — and on the reading's own line rather than a row of its own. The
   reading is capped at 76ch and leaves the right of a wide page empty; a
   second row would have bought nothing but 40px of height.

   Its `margin-bottom` matters only when the reading is absent (a book with
   one list, or none), where the button is the flex line's only item and has
   to hold the same 24px the reading would have held. Beside a reading it is
   inert — `align-items: flex-start` and the taller item decide the row. */
.s-lists-top { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
.s-mklist { margin-left: auto; flex-shrink: 0; margin-bottom: 24px; }

/* ═══════════════════════════════════════════════
   THE RESULT LINE — what came back, directly above it

   One row: the count sentence, and opposite it the only control here that
   does not change what is in the list.
═══════════════════════════════════════════════ */
.s-result { margin-bottom: 20px; }
/* The scope, when it is not a place you can press back into. */
.s-scope-static { color: var(--ink-muted); font-weight: var(--fw-semibold); }
/* The way out of a narrowed list sits above its count, where a page's Back
   sits above its name — same position, same gesture, so it reads as the
   same thing on a surface that is not a page. */
.s-result .s-back { margin-bottom: 12px; }
.s-result-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  justify-content: space-between;
}
/* Centre, not baseline: the run is a 32px count, a middle dot and a 13px
   button, and a baseline shared between three sizes puts the small ones on
   the big one's feet. Same reasoning as `.s-group-head`. */
.s-result-main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }

/* ══ A LIST'S OWN HEAD ═════════════════════════════════════════════════════
   Same anatomy as the campaign's, because a list is the same kind of thing:
   a named object with an owner, a description and verbs. Reusing
   `.s-sheet-kind` and `.s-sheet-name` is what makes the two read as
   siblings rather than as two inventions. */
/* ══ THE NAME GETS THE WIDTH ═══════════════════════════════════════════
   Two columns put the verbs beside the title, and three of them wanted
   330px of a 666px header — so "Amsterdam & Randstad, 200+ staff" had 336px
   at the 32px anchor size and broke across two lines, mid-phrase. A title
   that wraps is the one thing on the surface that must not: it is the
   anchor, and an anchor you read in two goes is not one.

   One column. The verbs sit under the name rather than in front of it,
   which is also the order they are wanted in — what this is, then what you
   can do to it. The primary is still the only filled control here. */
.s-listhead {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 8px; margin-bottom: 20px;
}
.s-listhead-main { min-width: 0; width: 100%; }
/* ── THE KIND LABEL AND THE DRAFT DECISION SHARE A ROW ──
   For a drafted list the state chip sits at the left where the kind label
   already was, and the two verbs sit at the right — same row, so the eye
   sees the state and the answer as one thing. On any other list `s-listhead-
   top` still runs, but it only holds the kind label, and `justify-content:
   space-between` puts nothing where the decide row would have been. */
.s-listhead-top {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-width: 0; width: 100%;
}
.s-listhead-decide { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.s-listhead-drop { white-space: nowrap; }
.s-listhead-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* ── FULL WIDTH, AND NO FACT MAY BREAK IN HALF ──
   These sat in the left column, which the buttons on the right squeezed to
   338px of a 666px header — so a run needing about 700px broke wherever it
   ran out: "Engy Saleh owns / it · made 3 weeks ago". Under both columns
   now, laid out as atomic units with a gap where the `·` glyphs were, so a
   wrap falls between two facts and never through one. */
/* ── AND LEGIBLE, NOT MERELY PASSING ──
   13px at `--d400` measured 6.08:1. That clears AA and it is still WRONG
   here: the product's own comparable meta line — `.rail-row-say`, the
   findings under the rail's conclusion — is 9.07, so this line was set
   dimmer than the convention for exactly the same kind of text. Body size
   and the meta colour, which is what it should have been all along. */
.s-listhead-facts {
  width: 100%;
  display: flex; flex-wrap: wrap; gap: 4px 20px;
  font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-relaxed);
}
.s-lf { white-space: nowrap; }
.s-listhead-facts b { color: var(--d100); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
/* What the list is for, at body size and full width under both columns —
   it is the one piece of prose on the surface and the thing a re-run reads
   back, so it is not a caption. */
.s-listhead-crit {
  width: 100%;
  /* `--d200`. It was `--d300` (9.07) and the facts under it have just come
     UP to 9.07 — two lines at one weight is no ranking, and of the two this
     is the one that says what the list is. */
  font-size: var(--fs-base); color: var(--d200);
  line-height: var(--lh-relaxed); max-width: 72ch;
}
/* `.is-sub` is gone with the line it sized. The count stepped down when a
   list was open, because the list's name is the anchor and a surface gets
   one — and a count that has to shrink to avoid competing with the header
   above it and the reading below it, both of which state the same number,
   was a line arguing for its own removal. */
@media (max-width: 619.98px) { /* 620 layout px */
  .s-listhead { grid-template-columns: minmax(0, 1fr); }
}
/* ══ ONE ANCHOR PER SURFACE, AND IT IS THE H1 ══════════════════════════════

   The scale reserves 32px for "the ONE number a surface is about", and after
   the type pass exactly two rules used it — so the record page placed one of
   its 146 text nodes above body size, and the campaign page two of 39. A
   six-step scale running on two steps is the defect the scale was written to
   fix, wearing the scale as a costume.

   The rule that makes it structural rather than a matter of taste: THE H1 IS
   THE ANCHOR. Every surface has exactly one — that was enforced in the last
   pass — so "one anchor per surface" needs no discipline to hold, and there
   is no argument to have about which element earns it.

   Where the surface is about a number the h1 already IS one: `.s-result-count`
   reads "118 accounts". Where it is about a thing, the thing's name is the
   subject and takes the step: a record's organization, a campaign's name, the
   briefing's own title.

   `.s-stage-fig` deliberately stays at the title step. Several figures at
   32px is several anchors, which is none — they are a series to be compared,
   and the anchor is the one thing that is not. (`.s-pipe-fig` made the same
   argument at book scope and went with the pipeline block.) */
/* The narrow-viewport step-down for these four lives at the FOOT of this
   file, not here. Three of them are declared further down, and a media query
   carries no extra specificity — placed above them it loses to the very rules
   it exists to override, silently, which is exactly what it did. */

/* ══ THE ANCHOR, AND IT IS THE FIRST ONE ═══════════════════════════════════
   The scale reserves 32px for "the ONE number a surface is about", argued at
   the top of this file against AiMY QA's 32-over-11 ratio — and then nothing
   ever set it. `--fs-3xl`, `--fs-4xl` and `--fs-5xl` were read zero times in
   either stylesheet, so the top three steps of a six-step scale did not
   exist in practice and the surface ran on two.

   "118 accounts" is exactly what the step was declared for: the count IS the
   list's subject. `.s-result-main` aligns on the baseline, so the scope and
   the waiting figure beside it sit on the number's baseline rather than its
   box, which is what keeps a 32px figure in a 13px line from looking dropped
   in. */
.s-result-count {
  font-family: var(--font-display);
  font-size: var(--fs-3xl); font-weight: var(--fw-extrabold); color: var(--d50);
  letter-spacing: var(--ls-tighter); line-height: var(--lh-tight);
}
.s-result-scope { font-size: var(--fs-sm); color: var(--ink-muted); }
/* The separator is not a bullet and not a pipe: it joins two halves of one
   sentence rather than splitting a list. */
.s-result-dot { color: var(--ink-muted); font-size: var(--fs-sm); }
/* A pointer, not an alarm. The card borders already carry the alarm, and a
   red pill beside the count competes with the number it is qualifying. */
.s-result-need {
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); cursor: pointer; padding: 2px 0;
  border-bottom: 1px solid var(--d700);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-result-need:hover { color: var(--err); border-color: var(--err); }
.s-result-need:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ── Permission is never silent ──
   A count that quietly shrinks is a lie about the size of the world. This is
   the line that stops it being one, so it is quiet but never faint: it must
   survive being skimmed past, and it must not read as an error. */
.s-disclosure {
  display: flex; align-items: baseline; gap: 4px;
  margin-top: 12px; padding: 8px 12px;
  border: 1px solid var(--d700); border-radius: var(--r-md);
  font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base);
}
.s-disc-ico { flex: none; transform: translateY(2px); color: var(--ink-muted); }
.s-disc-text strong { color: var(--d200); font-weight: var(--fw-semibold); }
/* A client is not in a degraded state, they are in a bounded one — so the
   engagement is named in the accent rather than warned about in amber. */
.s-disclosure[data-client="1"] { border-color: rgba(var(--accent-rgb), 0.3); }
.s-disclosure[data-client="1"] .s-disc-ico { color: var(--accent); }
.s-disc-eng { color: var(--accent); font-weight: var(--fw-bold); }

/* ═══════════════════════════════════════════════
   TASKS — AiMY's work while it is still happening
═══════════════════════════════════════════════ */
.s-task-card .tc-type { display: inline-flex; align-items: center; gap: 4px; color: var(--ai-text); }

/* ── THE STATE, AT THE HEAD ──
   `Needs you` decides whether this card is your problem, and it was the
   first item of a middle-dot meta run — the same size and ink as who
   started it. `orderedTasks` ranks the whole list on it, so the list was
   sorted by the one thing the card would not show. */
.s-task-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.s-task-st {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  white-space: nowrap;
}
.s-task-st.tone-err  { color: var(--err-text); }
.s-task-st.tone-warn { color: var(--warn-text); }
.s-task-st.tone-ok   { color: var(--ok-text); }
.s-task-st.tone-neutral { color: var(--ink-faint); }

/* Progress, under the sentence that already said it in words. The bar is the
   glance; the sentence is the fact. A bar on its own is a shape you have to
   guess at, and a task list read at a glance is exactly where a guess costs.

   The figure now sits on the bar's own line rather than inside the sentence
   above it, so the glance and the fact are one glance. */
.s-task-prog {
  display: flex; align-items: center; gap: 12px;
  margin: 8px 16px 0;
}
.s-prog {
  display: flex; flex: 1; height: 4px;
  border-radius: 2px; overflow: hidden; background: var(--d800);
}
.s-prog-done { background: var(--ok); }
.s-prog-fail { background: var(--err); }
.s-task-count {
  font-size: var(--ty-meta); color: var(--ink-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.s-task-count b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
.s-task-fail { color: var(--err-text); }

/* The status distribution band is gone from both cards — a 4px full-width
   bar reads as progress, and neither card carried a legend for it. The task
   card's genuine progress bar keeps the space the band used to share. */
.s-task-card .s-prog { margin-bottom: 12px; }

/* ── How sure a fact is, on the record's facts line ──
   Not `.conf-badge`: that is a 24px pill with a fill, a border and a meter,
   and this run is 19px of quiet text. A word at the caption step, in the
   tone, sitting on the run's own baseline. */
/* Not `--fs-2xs`: the scale reserves 11px for captions, "ALL CAPS AND
   TRACKED, never lowercase", and this is a lowercase word. It is subordinate
   on three axes instead — a step down in size from the value it qualifies, a
   step down in weight, and several down in colour — which is what the old
   badge was trying to achieve with a fill, a border and a meter. */
.s-prov {
  margin-left: 4px;
  font-size: var(--fs-sm); font-weight: var(--fw-medium); white-space: nowrap;
}
.s-prov.tone-muted  { color: var(--ink-muted); }
.s-prov.tone-ok     { color: var(--ok-text); }
.s-prov.tone-warn   { color: var(--warn-text); }
/* `--accent-text`, not `--accent`: measured 3.77 dark / 4.29 light at this
   size, both under the 4.5 floor. The token is the contrast-corrected pair of
   the same purple and predates this work — not a re-theme. */
.s-prov.tone-accent { color: var(--accent-text); }
/* `.s-edit` pulls itself -2px so its pencil icon does not shove the `·` that
   separates two facts. What follows here is not that dot but a word belonging
   to the same fact, so the pull is given back and the gap is 4px whether the
   value was editable or not — measured 2px against 4px before this. */
.s-edit + .s-prov { padding-left: 2px; }

/* What is running on THIS record, at the top of it. Same shape as the
   Knowledge line — a sentence with the objects set into it — because it is
   the same kind of statement: something exists elsewhere that bears on what
   you are reading. It takes the accent border, not the neutral one, because
   unlike the Knowledge line this is news. */
.s-runline {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px;
  padding: 12px 16px; margin-bottom: 12px;
  border: 1px solid rgba(var(--accent-rgb), 0.3); border-radius: var(--r-md);
  font-size: var(--fs-sm); color: var(--ink-muted); line-height: var(--lh-base);
}
.s-run-st { font-weight: var(--fw-semibold); }

.s-task-nums { display: flex; gap: 20px; flex-wrap: wrap; margin-top: 8px; }
.s-task-num { font-size: var(--fs-xs); color: var(--ink-muted); }
.s-task-num b {
  display: block; font-family: var(--font-display);
  font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--d100);
  line-height: var(--lh-tight);
}
.s-task-num.is-fail b { color: var(--err); }
/* Skipped is neither done nor left. Folding it into either would make the
   figures stop adding up to the run they describe. */
.s-task-num.is-skip b { color: var(--warn); }
.s-task-num.is-left b { color: var(--ink-muted); }

/* ══ WHAT A SESSION CAME TO ═══════════════════════════════════════════════

   Rows, not a chart. Five outcomes is not a distribution worth drawing, and
   the doctrine's block budget spends charts on things a sentence cannot
   say. Every row is a DOOR — the figure and the word are one button —
   because a count you can read and cannot open is exactly what v5 took off
   this product. */
/* The wrapper, because this block is read in two places now — the run
   sheet, where a heading sits above it, and the canvas, where it is the
   last turn of the session's own thread. One gap rather than a margin-top
   per child, so it does not need a different first-child rule in each. */
.s-sess { display: flex; flex-direction: column; gap: 12px; }
.s-sess-head { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }
.s-sess-head b {
  font-family: var(--font-display); font-weight: var(--fw-semibold);
  color: var(--d100); font-variant-numeric: tabular-nums;
}
.s-sess-rows { display: flex; flex-wrap: wrap; gap: 8px; }
.s-sess-row {
  display: flex; align-items: baseline; gap: 8px; text-align: left;
  padding: 8px 12px; border-radius: var(--r-md); cursor: pointer;
  background: var(--card-bg-raised); border: 1px solid var(--card-border);
}
.s-sess-row:hover { border-color: var(--card-border-hover); }
.s-sess-n {
  font-family: var(--font-display); font-size: var(--fs-lg);
  font-weight: var(--fw-bold); color: var(--d100);
  font-variant-numeric: tabular-nums; line-height: var(--lh-tight);
}
.s-sess-lab { font-size: var(--fs-xs); line-height: 16px; }
/* The way back off a skip. It names them, because "call these 3" over three
   people nobody can see is a control asking to be trusted. */
.s-sess-skips { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }

/* ══ THE THREE QUESTIONS, AND THE CALLS UNDER THEM ════════════════════════

   Sections separated by a hairline and a caption rather than by four boxes.
   The figures at the top are already a group and these are readings OF that
   group, so boxing each one would put a border between a count and the
   sentence explaining it — Common Region working against the thing it is
   supposed to hold together.

   The caption is the caption step every block on this surface uses, so a
   reader who knows the record page knows this. */
.s-sess-sec {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  padding-top: 16px; border-top: 1px solid var(--hairline);
}
.s-sess-cap {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.s-sess-say { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }

/* One call and its log. The name and the disposition read as a heading over
   the card rather than as a row of the card, which is what keeps a run of
   three of these from reading as one long table. */
.s-sess-call { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.s-sess-call + .s-sess-call { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--hairline); }
.s-sess-call-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.s-sess-skip-say { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }
.s-task-next { margin-top: 8px; font-size: var(--fs-xs); color: var(--ink-muted); }
.s-task-block { margin-bottom: 16px; }
.s-task-choice { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.s-work-p { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }
.s-work-note { margin-bottom: 12px; }

/* The count on the Running tab. Work that has stopped and is waiting on
   somebody is the one thing here that gets worse while nobody looks. */
/* ═══════════════════════════════════════════════
   WHAT AiMY DOES — the draft's sources, and the enrichment decision
═══════════════════════════════════════════════ */

/* Above the draft, never below it. A person reads the paragraph first and
   decides whether to trust it second; putting the provenance underneath
   means the decision is made before the evidence is seen. */
.s-basis {
  padding: 12px; margin-bottom: 16px;
  border: 1px solid var(--d700); border-radius: var(--r-md);
  background: var(--d900);
}
.s-basis-title {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium); color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: var(--ls-wide); margin-bottom: 8px;
}
.s-basis-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: var(--fs-xs); line-height: var(--lh-base);
}
.s-basis-row + .s-basis-row { margin-top: 8px; }
.s-basis-what { flex: none; width: 160px; color: var(--ink-muted); }
.s-basis-val { color: var(--ink-muted); }

.s-draft {
  width: 100%; resize: vertical; margin-top: 8px;
  font-family: inherit; line-height: var(--lh-base);
}

/* Enrichment, per field. The library's `.decision-zone` holds it, and each
   row is its own accept — a single "take all of this" over four values of
   differing confidence is one decision standing in for four. */
.s-enrich { display: flex; flex-direction: column; gap: 12px; }
.s-enrich-row {
  display: grid; gap: 4px;
  grid-template-columns: 1fr auto;
  grid-template-areas: "head head" "val take" "src src";
  padding: 8px; border: 1px solid var(--d700); border-radius: var(--r-md);
}
.s-enrich-head { grid-area: head; display: flex; align-items: center; gap: 8px; }
.s-enrich-what { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d200); }
.s-enrich-val { grid-area: val; width: 100%; }
.s-enrich-take { grid-area: take; align-self: center; }
.s-enrich-src { grid-area: src; font-size: var(--fs-xs); color: var(--ink-muted); }

.s-slots { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }

/* The read-only note where a record's actions would be. It occupies the same
   row so the page does not reflow into a shape nobody recognises. */
/* ARCHIVED, STATED WHERE IT CHANGES THE READING. The whole page below it is
   an insight, a next step and a timeline that all still describe live work —
   so this sits between the name and them, filled rather than outlined, because
   it is the frame those blocks are read inside and not another one of them. */
.s-arch {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; padding: 8px 12px;
  border-radius: var(--r-md); background: var(--d800); border: 1px solid var(--d700);
  font-size: var(--fs-sm); color: var(--ink-muted);
}
.s-arch-st {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); letter-spacing: .06em; text-transform: uppercase;
  color: var(--d100);
}
.s-arch-say { flex: 1 1 auto; min-width: 200px; }
/* Pushed to the end of the row rather than following the sentence, so it holds
   one edge however the sentence wraps. */
.s-arch-act { margin-left: auto; }

.s-readonly {
  display: flex; align-items: baseline; gap: 4px; margin-top: 16px;
  font-size: var(--fs-xs); color: var(--ink-muted);
}
.s-readonly .s-disc-ico { transform: translateY(2px); }

/* A record that would not open. It is not an error — nothing went wrong —
   so it takes the empty state's shape rather than the error screen's, with
   the lock as the one signal that this is a boundary and not an absence. */
.s-denied .s-disc-ico { width: 20px; height: 20px; color: var(--ink-muted); margin-bottom: 12px; }
.s-denied .btn { margin-top: 16px; }

/* ── Looking as ──
   Reuses the notification panel's shell, because it is the same object: a
   panel hung off a control in the top bar. */
.as-panel { width: 340px; }
.as-item {
  display: flex; align-items: flex-start; gap: 8px; width: 100%;
  padding: 8px 12px; text-align: left; cursor: pointer;
  font-family: inherit; color: var(--d200);
  border-bottom: 1px solid var(--d800);
}
.as-item:hover { background: rgba(255,255,255,0.03); }
:root[data-theme="light"] .as-item:hover { background: rgba(16,24,40,0.03); }
.as-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.as-item.is-current { background: var(--accent-dim); }
.as-item-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.as-name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--d100); }
/* The rule, not the label. Which tier somebody is in tells you nothing;
   what that tier can see is the whole content of the choice. */
.as-rule { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }
.as-you {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--accent);
  text-transform: uppercase; letter-spacing: var(--ls-wide);
}
.as-tier {
  flex: none; font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: var(--ls-wide);
  padding: 2px 8px; border-radius: var(--r-sm); border: 1px solid var(--d700);
}
.as-note { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }
/* The scope switch is a filter wearing a segmented control, so it sits at the
   weight of the chips beside it rather than the weight of the view switcher. */

/* ── More ──
   Five controls on the row, the rest behind one. Eleven dropdowns is not a
   filter row, it is a wall: nothing in it is findable because everything in
   it is the same size and weight. */
.s-more-wrap { position: relative; }
.s-more-btn {
  display: inline-flex; align-items: center; gap: 4px; height: 32px;
  padding: 0 12px; border-radius: var(--r-md); cursor: pointer;
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); background: none; border: 1px solid var(--card-border);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-more-btn:hover { color: var(--d50); border-color: var(--card-border-hover); }
.s-more-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-more-btn.active-filter { color: var(--accent); border-color: rgba(var(--accent-rgb), 0.4); background: var(--accent-dim); }
.s-more-n {
  font-variant-numeric: tabular-nums; font-size: var(--fs-2xs);
  background: var(--accent); color: #fff;
  border-radius: var(--r-pill); padding: 0 8px; min-width: 16px; text-align: center;
}
.s-more-panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 940;
  width: 460px; max-width: calc(var(--vp-w) - 32px); padding: 16px;
  background: var(--panel-bg); border: 1px solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(12px);
}
.s-more-panel[hidden] { display: none; }
.s-more-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.s-more-grid .v2-dropdown { min-width: 0; }
.s-more-clear {
  margin-top: 12px; padding: 4px 8px; cursor: pointer;
  font-family: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  color: var(--accent); border-radius: var(--r-xs);
}
.s-more-clear:hover { background: var(--accent-dim); }
.s-more-clear:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Include widens, exclude narrows, and the colour says which way a term
   cuts before you have read it — as V1's chips do. */
.s-kw { cursor: pointer; }
.s-kw-inc { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok) 30%, transparent); color: var(--ok); }
.s-kw-exc { background: var(--err-bg); border-color: color-mix(in srgb, var(--err) 30%, transparent); color: var(--err); }

/* The derived axis, on the control's face. A derived value is fine; one
   that will not say what it derived is not. */
.s-cal-axis {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.75;
}
.s-cal-dates { font-variant-numeric: tabular-nums; }
.s-cal-axis-said { font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--d50); }
.s-cal-why { font-size: var(--fs-base); color: var(--ink-muted); margin-top: 2px; line-height: var(--lh-base); }
/* The campaign's name in the sentence IS the way into its brief. */
.s-scope-link {
  font-family: inherit; font-size: var(--fs-sm); font-weight: var(--fw-semibold);
  color: var(--d100); cursor: pointer;
  border-bottom: 1px solid var(--d600);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-scope-link:hover { color: var(--accent); border-color: var(--accent); }
.s-scope-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ═══════════════════════════════════════════════
   THE CARD — product-owned bits only

   The card itself is the design system's `.type-card`. What is here is the
   status tint and the three-tier type scale, both of which are decisions
   about THIS product's list rather than about the component.

   The tint is the library's own idiom, not an invention: `.bcard.p1/.p2/.p3`
   puts the whole border at low alpha on the card's existing 1px. Same
   information, no new mechanism, no content shift, and hover still wins.
═══════════════════════════════════════════════ */
/* ══ COLUMNS, NOW THAT THE CARD IS A FIXED HEIGHT ═════════════════════════
   This was one column, and the argument was real: `minmax(320px, 1fr)` put
   three across, a grid row is as tall as its tallest member, and one card
   with a long next-step line set the height of the two beside it — 247
   records over 32 screens, most of that height empty.

   Both halves of that have changed. The card is the compact two-line shape
   now and measures 102–114px across a page of them — a 12px spread, not the
   ragged one the rule was written against — and `align-items: start` ends
   the height coupling outright, which is what should have answered the
   objection in the first place rather than giving up the width.

   And the width is the cost: a 102px card stretched to 1451px on a wide
   screen is half a screen of nothing per record. `420px` minimum keeps the
   name, the sentence and the owner on their own lines, which is the width
   the card was designed at; below that it falls back to one column on its
   own. Groups are unaffected — a group's header is a sibling of this, not a
   member of it. */
/* `min(420px, 100%)`, not a bare `420px`. A `minmax` floor is a floor even
   when the container is narrower than it: at 375 the column stayed 420 and
   every card hung 114px past its own grid, clipped by nothing because the
   parent's overflow is visible. `min()` lets the floor give way at exactly
   the width where it stops fitting, which is what a floor is for. */
.s-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr));
  gap: 16px; align-items: start;
}

/* ══ THE ACCUMULATED READING ═══════════════════════════════════════════════

   Everything AiMY has to say about this set, once, above one flat list —
   replacing a full-width bar per finding, each with its own `Pick` and `Ask`
   pair, with that finding's rows underneath.

   On the sets this surface is reached with, the bars outnumbered the
   records: two findings of one lead each rendered two paragraphs and four
   buttons to introduce two rows, and cut the list into fragments too short
   to be a list. It also spent the ranking — `ordered()` sorts the whole set
   by what costs most to ignore, and grouping re-sorted it into taxonomy
   buckets, so a lead ninety-seven days silent could sit below one a single
   day late.

   Same construction as the campaign page's sections, which is the point:
   the reading, the summary that qualifies it, then the evidence. */
.s-findings {
  background: var(--card-bg-raised); border-radius: var(--r-lg);
  padding: 16px; margin-bottom: 24px; max-width: 76ch;
}
/* ── AND WHERE IT IS THE SURFACE'S ONE AiMY PANEL, IT LOOKS LIKE ONE ──
   On a list's own page this block absorbed the offers, so it is no longer a
   reading beside a recommendation — it is both, and it is the only place on
   that surface where AiMY speaks in the first person and proposes a write.
   `.slv` is the established treatment for exactly that, so this takes its
   ground, its border and its padding rather than inventing a third panel.

   Full width, because the rows are now count-clause-verb triples with a
   control on the right edge: at 76ch the verbs stopped sharing a column with
   the wider offer sentences, and a column that does not align is not a
   column.

   A MODIFIER, NOT THE COMPONENT. The briefing's Lists tab draws this block
   too, and `.slv` is already on that surface — two accent panels on one page
   is exactly the doubling `.s-insight`'s own note argues against. There the
   block stays a quiet raised card, because there it is a reading and nothing
   more. */
.s-findings.is-panel {
  background: var(--aimy-wash);
  border: 1px solid var(--aimy-wash-edge);
  padding: 20px; max-width: none;
}
.s-findings-say {
  font-size: var(--ty-body); font-weight: var(--fw-medium);
  line-height: var(--lh-base); color: var(--ink-primary);
  margin: 8px 0 16px; text-wrap: pretty;
}
.s-findings-say b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
/* One finding means the set IS the finding — arriving from a gate, every
   record here has it by construction. The block keeps the mark, the sentence
   and the bulk verbs, and drops the list that would restate the sentence
   and the ratio that would divide a number by itself. */
.s-findings.is-one .s-findings-say { margin-bottom: 12px; }
.s-findings-list { display: flex; flex-direction: column; }
/* Count, clause, and the one bulk verb — the same left-to-right reading as
   the campaign page's ranked rows, at a size that stacks five deep without
   becoming a wall. */
/* ── TWO COLUMNS: the statement, and the verb that answers it ──
   Three was one too many. The count had a 32px gutter of its own and the
   sentence began 44px in, which made a row read as a figure and then a
   separate remark about something — two things sharing a line rather than
   one statement with a subject. The count is inside the sentence now, and
   the only column left is the one that has to share an edge down the block:
   the control. */
.s-finding {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: 16px;
  padding: 8px 0;
}
/* At the reading step, not the caption step. These are the findings the whole
   block exists to state, and they were set two steps below the sentence
   introducing them — small enough that the eye took the block as a legend
   rather than as the content. */
.s-finding-say {
  font-size: var(--ty-heading); color: var(--ink-secondary);
  line-height: var(--lh-base); min-width: 0;
}
/* The subject of the sentence: full ink, heaviest weight, lining figures so
   a column of them still lines up despite being inline. */
.s-finding-say b {
  color: var(--ink-primary); font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}
/* Selection, not navigation — you are already inside the set, so the useful
   verb is "work these together" rather than another door into them. */
.s-finding-pick {
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary); cursor: pointer; white-space: nowrap;
  padding: 4px 8px; border-radius: var(--r-pill);
  transition: color var(--t-fast), background-color var(--t-fast);
}
.s-finding-pick:hover { color: var(--ink-primary); background: var(--card-bg); }
.s-finding-pick.is-on { color: var(--accent-text); }
.s-finding-pick:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ── AND THE ROWS THAT GO AND DO SOMETHING TAKE A BORDER ──
   Same row, heavier verb. `Pick these 3` stages a selection you are already
   inside; `Get revenue` sends AiMY to a data supplier and writes what comes
   back. The entry-mode ladder puts those two tiers apart and this is the one
   place they sit in the same list, so the difference has to be visible: bare
   uppercase for the selection, a bordered control for the write. */
.s-finding-go {
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-bold);
  color: var(--ink-secondary); cursor: pointer; white-space: nowrap;
  padding: 4px 12px; border-radius: var(--r-pill);
  border: 1px solid var(--card-border); background: none;
  transition: color var(--t-fast), border-color var(--t-fast), background-color var(--t-fast);
}
.s-finding-go:hover { color: var(--ink-primary); border-color: var(--card-border-hover); background: var(--card-bg); }
.s-finding-go:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* One second of working state, so a press that goes to a supplier says it is
   going rather than changing the page a beat later with no cause on screen. */
.s-finding-go.is-busy { opacity: 0.6; cursor: default; }
/* The findings too small to earn a row, named rather than hidden. */
.s-findings-tail {
  font-size: var(--ty-body); color: var(--ink-secondary);
  padding: 8px 0 0; max-width: 62ch;
}
.s-findings-tail b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
.s-findings-acts { margin-top: 12px; }

/* ══ THE LEAD WORKLIST ═════════════════════════════════════════════════════

   Two aligned columns per row, and the alignment is the whole design: what
   sits in each is fixed, so the eye reads DOWN a column instead of across
   each row in turn.

       Signify                                   30 days late
       what we sent 7 weeks ago never arrived    Sara Nabil      [Fix it]

   LEFT is who and what happened; RIGHT is how bad and whose. Copilot Money
   puts the merchant over its account and the amount over its category,
   Evernote the task over its note with the due date in its own column,
   Better Stack the incident over its detail with the length at the edge.
   All three are the same shape for the same reason.

   ONE COLUMN of rows, not a grid of cards. A grid is for comparing one thing
   against another — which is what the campaigns and tasks tabs do, and why
   they keep theirs — but a set arrived at from "show me the 4 that are
   stuck" is a worklist, and its order is the answer.

   `gap: 0` and a hover band: rows that touch read as one list, rows with a
   gap between them read as small cards, which is what these were. */
.s-lrows { display: flex; flex-direction: column; }
/* `.s-lrow*` is gone with `leadRow`. One row draws leads everywhere now —
   `.s-brow` — so the family that drew the thinner one has nothing left to
   style. `.s-lrows` and `.s-lrow-st` survive: the first is the column the
   call queue still stacks in, the second is the state chip that queue's rows
   wear, and both are rendered. */
.s-lrow-st {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  margin-top: 2px;
}
.s-lrow-st.tone-err  { color: var(--err-text); }
.s-lrow-st.tone-warn { color: var(--warn-text); }
.s-lrow-st.tone-ok   { color: var(--ok-text); }
/* ── CARD PROXIMITY, WHICH IS WHAT WAS ACTUALLY WRONG ──

   The library stacks a card at 12 / 8 / 6 / 12 — head, title, summary, then
   the governance row. Four gaps within three pixels of each other, so
   NOTHING GROUPS: the type label, the name, the description and the meta all
   float at the same distance and the eye is given no reason to read them as
   anything but four separate lines. That is the detachment, and it was never
   about the sizes.

   Three groups, and the spacing says so:
     identity  — type · name · what it is        tight, 4 between name and snippet
     standing  — status · next step · owner      16 away, a new group
     AiMY      — the insight                     its own bounded region

   The ratio rule: 4 inside, 16 between. */
.s-card { display: flex; flex-direction: column; cursor: pointer; }
.s-card .tc-summary { padding-top: 4px; }
.s-card .tc-gov { margin-top: 16px; }
/* The whole card opens the record — the same rule the table row follows, and
   the same hover value, so the two views answer a pointer identically.
   Background rather than border, because the border is carrying the status
   tone below and a hover that overrode it would erase the alarm. */
.s-card:hover { background: rgba(255,255,255,0.03); }
:root[data-theme="light"] .s-card:hover { background: rgba(16,24,40,0.03); }
.s-card:hover .s-card-title { color: var(--accent); }
.s-card.st-err  { border-color: color-mix(in srgb, var(--err) 20%, transparent); }
.s-card.st-warn { border-color: color-mix(in srgb, var(--warn) 20%, transparent); }
.s-card.st-ok   { border-color: color-mix(in srgb, var(--ok) 16%, transparent); }
/* st-neutral takes the default border: "no issues" is the absence of a
   signal, not a signal of its own. */

/* ══ THE IDENTITY IS ONE LINE ══════════════════════════════════════════════
   Tick, name, qualifier, ICP, status — read left to right as one sentence
   about who this is, instead of a head row, a title block and a sub-line
   stacked three deep. The name takes the space it needs and the qualifier
   takes what is left, which is why the two are `flex` rather than a grid:
   "Rick Brouwer" beside "Head of Customer Support · BearingPoint" wants a
   different split from "SMART Photonics" beside "Semiconductors · 240 · Eindhoven".

   `min-width: 0` on both, or a long organization name refuses to ellipsise
   and pushes the status off the card. */
/* ══ ONE LINE MEANS NOWRAP, AND THE SLOTS SAY SO ══════════════════════════
   Everything below already declared the right intent — the snippet shrinks
   and ellipsises, the status is `flex: none; margin-left: auto` — and the
   status still fell to a second line at full width, alone, at whatever x the
   snippet happened to end. The library's `.tc-head` sets `flex-wrap: wrap`
   and nothing here overrode it above 620, so the row was free to break
   exactly where the rules said it must not.

   `align-items` goes to CENTRE with it. Baseline aligns text baselines, and
   two of the four items are not text: a 15px checkbox and a padded pill have
   no baseline to share, so the checkbox sat 3px above the name it belongs to
   and the pill sat lower than both. Centre is what a row of mixed boxes and
   words actually wants. */
/* `.s-card-line` and `.s-card-idy` were the lead card's head — a tick, a
   name with its firmographics, and a status pill on one non-wrapping row,
   with the identity growing downward so the pill kept the right edge. All of
   it careful, and all of it in service of a shape this surface no longer
   uses: `leadRow` is one line with the urgency at its head, and the
   firmographics that filled the identity slot are on the record. */
.s-card-title {
  flex: 0 1 auto; min-width: 0; text-align: left; cursor: pointer;
  font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--d50);
  letter-spacing: var(--ls-tight); line-height: var(--lh-snug);
}
.s-card-title:hover { color: var(--accent); }
.s-card-title:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The qualifier follows the name and wraps under it when it has to. It
   carries four facts — sector, size, city, how many people — and ellipsis
   was silently dropping the last two on sixteen of thirty-three cards. */
.s-card-snip {
  /* Was a bare `13px`. The value is right and the literal was not: a size
     written outside the scale is a size the scale cannot move. */
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base);
}
/* The status is the card's one computed claim, so it never shrinks and never
   wraps — it is the thing the eye is looking for. */
/* The pill and the ICP chip held their slot against a shrinking identity in
   the card head. In `.s-lrow-idy` they are wrap items in a flex row that is
   allowed to wrap, so there is nothing to hold them against. */
/* Every tier differs from its neighbour in BOTH size and colour, and there
   is no uppercase anywhere on this card. */
/* THE CARD'S TWO HALVES STOP TOUCHING.

   Its four parts each carried their own top padding and nothing separated
   them, so the identity (type · name · role) and the state (status · next
   step · owner · last touch) ran together as one paragraph — measured at 0px
   between every pair. The rule below is the seam: above it is who this is,
   below it is where it stands, and they are answers to different questions.
   The library's `.tc-gov` already draws that line on its own cards; ours was
   overriding it away. */
.s-card-meta {
  /* `margin-top: auto` was pushing this to the bottom of a stretched grid
     cell. In a single column there is nothing to stretch to. */
  /* Two slots that never break: the facts, then the flag. The facts wrap
     inside their own slot; the flag keeps the right edge and the first
     line. See `.s-card-line` for why baseline lost. */
  display: flex; align-items: flex-start; gap: 8px; flex-wrap: nowrap;
  padding: 8px 16px;
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--ink-muted);
  line-height: var(--lh-relaxed);
}
/* THE RUN IS THE THING THAT GIVES WAY. Three facts and two separators used
   to be five loose flex items, so a long next-step pushed the FLAG over the
   edge and the flag is the one item that must not move — it is the finding
   the group is named after. One shrinkable item, one fixed. */
/* `.tc-gov-run` wrapped the card's meta run so it could shrink and ellipsis
   while the flag chip kept its slot. The row's meta is a grid cell now, so
   the shrinking is the grid's job. `.tc-gov-sep` stays — the row still
   separates owner from last-touch with it. */
/* The flag chip that sat at the end of this facts line is gone. It was the
   grouped card grid's share of the insight, and `leadRow` says which finding
   put a record on screen in the row itself — so `flagChip()` lost its only
   call site, `wiring-audit` reported it UNMOUNTED, and the function and both
   of its rules went out together. */
/* ═══════════════════════════════════════════════
   THE STATUS IS THE PRODUCT'S ONE COMPUTED CLAIM, AND IT LOOKED LIKE TEXT

   It was a bold coloured run inside a `·`-separated meta line, which put the
   single fact this whole surface exists to derive at the same visual weight
   as the owner's name and the date beside it. Every screen answers "where
   does this stand" first; the answer was the least distinguishable thing on
   the card.

   It is now a marked object: a tone dot, a tinted ground, a hairline in its
   own colour. Not a `.chip` — chips here are filters, and a status is not
   something you clicked. Not `.trust-state` either, which belongs to
   Knowledge and means something else. Its own thing, in one place, used by
   the card, the table, the record and the task sheet.
═══════════════════════════════════════════════ */
/* ══ AND THE OTHER LABEL FAMILY IS THE SAME LABEL ═════════════════════════
   This took the caption size and the caption tracking and left the caption
   CASE behind — 11px of sentence case with 6% of tracking between the
   letters, which is the one combination the token's own comment forbids:
   "ALL CAPS AND TRACKED, never lowercase". Tracking is a correction for
   capitals; on lowercase it is just gaps.

   With the case added, this and the tag are the same object down to the
   pixel — same size, weight, tracking, case, padding, radius, line box —
   and differ only in which ground they stand on. Two hosts, one label. */
.s-meta-st {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  white-space: nowrap; vertical-align: middle;
}
/* ══ THE DOT IS GONE ══════════════════════════════════════════════════════
   It said "the dot carries the tone at full strength; the ground carries it
   at 12%", and that was an argument for having the tone twice. Read the
   variants below: each sets `color`, `background` AND `border-color` from
   its tone, on a pill whose text is the status word. Four carriers of one
   fact, and the smallest of them was a 6px circle.

   The accessibility half of the old argument survives untouched — "colour
   alone never distinguishes them, the word is always there" is still true,
   because the word was never the dot. */
/* ══ AND THE SAME RULE HERE ═══════════════════════════════════════════════
   The status beside a record's name is a label, and it was wearing the
   ghost button's border — `--card-border` on a raised ground, which is what
   Handover wears two lines below it. It reads as a control you cannot
   press. The border goes; the ground stays and does the work.

   These three also still carried the saturated borders the tokens above
   left behind — 28% of #f04438 around text that was #d2827b then and is
   the migration finishing here. */
.s-meta-st.tone-err   { color: var(--err-text);  background: var(--err-bg); }
.s-meta-st.tone-warn  { color: var(--warn-text); background: var(--warn-bg); }
.s-meta-st.tone-ok    { color: var(--ok-text);   background: var(--ok-bg); }
.s-meta-st.tone-neutral { color: var(--ink-muted); background: var(--card-bg-raised); }

/* ══ SKIPPED IS NOT A STATUS, AND MUST NOT LOOK LIKE ONE ══════════════════

   It borrows the warn tone and none of the pill's ground, so it cannot be
   read as the computed status sitting eight pixels to its right. The
   difference is the whole point: the status is derived from the record and
   outlives every session, and this is true of ONE run and leaves with it. */
/* Renamed to `.s-lrow-skip` with the row that carries it — the rule survives
   verbatim, the selector moved. */

/* On the record it is the headline fact, so it is sized as one. */
.s-rec-status .s-meta-st {
  font-size: var(--fs-sm); padding: 4px 12px 4px 8px; letter-spacing: normal;
}
/* The record's own status pill had a bigger dot to match its bigger text.
   Both went with the dot itself; the asymmetric padding that made room for
   it goes too, or the word sits off-centre in its own pill. */
.s-rec-status .s-meta-st { padding: 4px 12px; }
/* An overdue next step is the one meta fact that outranks the others,
   because it is the one with a date that has already passed. */
/* `.s-meta-due` reddened "40 days overdue" inside the card's meta run —
   third of three facts, at caption weight, on the line under the name. The
   fact survives and led the row instead: `.s-lrow-urg`. */
/* `.s-override-dot` was here — the mark that said a status had been set by
   hand, so the escape hatch could not be mistaken for a fact. Both are gone.
   Nothing on this surface can be set by hand, so there is nothing to mark. */
/* The matched ICP is Knowledge's object, reachable from the card without
   leaving it. Quiet: it is provenance, not a status. */
.s-icp {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: var(--ls-wide);
  color: var(--ink-muted); cursor: pointer;
  border: 1px solid var(--card-border); border-radius: var(--r-xs);
  padding: 2px 4px;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.s-icp:hover { color: var(--accent); border-color: rgba(var(--accent-rgb), 0.4); }
.s-icp:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.s-empty { margin-top: 24px; }

/* ═══════════════════════════════════════════════
   THE RECORD PAGE — and the one docked thread

   A record is a page, so the briefing rail and the filter row step aside
   while one is open. `.is-record` on the body is the only switch: two
   regions hiding themselves independently is two regions that can disagree
   about whether a record is open.
═══════════════════════════════════════════════ */
/* CENTRED. `max-width` with no auto margin pins the page to the left of a
   stage that has just lost its sidebar — 780px of content against ~1220px
   of room, and the reported "everything is left aligned, what is this?"

   The campaign page is wider because pass 11 gave it a card grid: at 780px
   `repeat(auto-fill, minmax(320px, 1fr))` resolves to two columns and the
   filter row above them wraps. Prose does not follow it out — a description
   set to the width of a card grid is a description nobody finishes. */
.s-rec { max-width: 780px; margin-inline: auto; }
/* A campaign page has no companion column — the rail only docks for a
   record or a selection — so a cap here is a hole rather than a measure. Its
   prose is capped below, which is where the reading happens. */
.s-rec.s-camp-page { max-width: none; }
.s-camp-page .empty-state-desc,
/* The page's own summary. NOT the cards' — `.s-card-snip` is already bounded
   by the card it sits in, and capping it there would only shorten a line that
   is 55 characters wide. Measured uncapped at the page level: 322. */
.s-camp-page .tc-summary:not(.s-card-snip) { max-width: 68ch; }

/* min-width:0 so a long domain or a monospaced email cannot push the grid
   column wider than its track and shove the thread off screen. */
.s-rec-main { min-width: 0; }

/* A CONTROL IS CHROME, NOT CONTENT. This was --fs-sm at 16, which is the
   body step: the way back off a record was set at the same size as the
   record. bdr.css already lifted its INK off --d400 on the argument that it
   is a navigation control and not a footnote; the size never followed. 14 is
   the meta step and the right one for a control that names a destination
   rather than saying anything. The 14px mark beside it now matches it. */
.s-back {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); cursor: pointer; padding: 4px 0; margin-bottom: 12px;
  transition: color var(--t-fast);
}
.s-back:hover { color: var(--d50); }
.s-back:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-back svg { width: 14px; height: 14px; }

.s-rec-head { margin-bottom: 24px; }
.s-rec-kind {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.s-rec-kind svg { width: 12px; height: 12px; }
/* ══ THE MASTHEAD ══════════════════════════════════════════════════════════

   Same move the campaign page took, for the same reason: the name of the
   thing you have open sets the ceiling, and nothing under it may outrank it.
   At 32 it was level with the campaign title before that page was rebuilt —
   which meant two surfaces disagreed about how loud a name is, and a reader
   moving between them had no fixed point.

   `--ty-masthead` is the top of the scale and this is what it is for.
   `text-wrap: balance` because an organization name can run long and a
   46px orphan is the one wrap a masthead cannot take. */
.s-rec-name {
  font-family: var(--font-display);
  font-size: var(--ty-masthead); font-weight: var(--fw-extrabold);
  color: var(--ink-primary); letter-spacing: var(--ls-tighter);
  line-height: 1.05; margin-top: 4px; text-wrap: balance;
}
@media (max-width: 899.98px) { /* 900 layout px */
  .s-rec-name { font-size: var(--ty-lead); }
}
.s-rec-kind { color: var(--ink-quiet); font-size: var(--ty-meta); }
/* The status sits beside the name, qualifying the record's identity rather
   than introducing the work. On its own line below it was competing with
   AiMY's insight for the one position on the page that gets read first. */
.s-rec-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.s-rec-title .s-rec-name { margin-top: 0; }
/* The `translateY(-2px)` was a hand correction for the baseline this row no
   longer uses — a nudge on top of a misalignment is two things to keep in
   step. Centre alignment does it, and does it at every size. */
.s-rec-title .s-meta-st { flex: none; }

/* ── CAPTIONED BLOCKS ──
   Every block below the insight is a caption in the left column and its
   content in the right: WHY, THE COMPANY, CONTACTS, HISTORY, SHARED WITH.
   The caption column makes the page scannable down a single edge, which is
   what a record page is for — you arrive knowing which of five things you
   came to read, and one eye movement finds it.

   ── AND THE COLUMN STAYS, WHICH IS A DIVERGENCE FROM THE CAMPAIGN PAGE ──

   That page moved its row titles ABOVE their content. This one keeps the
   gutter, and the difference is what the two blocks are for: the campaign's
   identity rows are a narrative preamble read once, top to bottom, where a
   title above its content is the reading order; a record page is a REFERENCE
   SHEET you arrive at knowing which field you want, and a shared left edge
   is how you find it in one eye movement. The campaign block's gutter also
   failed for a reason this one does not have — `align-items: center` put its
   caption beside the middle of a tall row, and these are `start`-aligned.

   What was wrong here is the captions themselves, and it is the same fault
   in both places: 11px at `--d500`, the faintest ink in the product, on the
   labels carrying the entire wayfinding. Thirteen and extrabold in the quiet
   ink, and the column widens to 132 to hold them without wrapping — a
   caption that wraps to two lines stops being a landmark. */
.s-rec-block, .s-rec-why {
  display: grid; grid-template-columns: 132px minmax(0, 1fr);
  gap: 4px 20px; align-items: start;
  margin-top: 32px;
}
.s-rec-why { margin-top: 24px; }
.s-rec-cap {
  font-family: inherit; margin: 0;
  font-size: var(--ty-meta); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-quiet);
  line-height: var(--lh-base);
}
@media (max-width: 719.98px) { /* 720 layout px */
  /* Below this the gutter costs more than the scan edge is worth: 132 of a
     narrow column leaves the content too little to be read. */
  .s-rec-block, .s-rec-why { grid-template-columns: minmax(0, 1fr); gap: 8px; }
}
.s-rec-why-body, .s-rec-body { display: grid; gap: 4px; min-width: 0; }
.s-rec-body { gap: 8px; }

/* History states its size and stays shut. Open, it is the longest thing on
   the page; closed, it is one line that says whether opening is worth it. */
.s-rec-hist-sum {
  font-size: var(--fs-sm); color: var(--ink-muted); cursor: pointer;
  list-style: none; display: inline-flex; align-items: center; gap: 4px;
  min-height: 24px;
}
.s-rec-hist-sum::-webkit-details-marker { display: none; }
.s-rec-hist-sum::after {
  content: ''; width: 6px; height: 6px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--t-fast);
}
.s-rec-hist[open] .s-rec-hist-sum::after { transform: translateY(1px) rotate(-135deg); }
.s-rec-hist-sum:hover { color: var(--d200); }
.s-rec-hist-sum:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }
.s-rec-hist[open] .s-timeline { margin-top: 12px; }
.s-rec-because { font-size: var(--fs-sm); color: var(--ink-muted); }

/* Under 900px the caption goes above its content. A 112px column against a
   200px content column is not a two-column layout, it is a squeeze. */
@media (max-width: 899.98px) { /* 900 layout px */
  .s-rec-block, .s-rec-why { grid-template-columns: minmax(0, 1fr); gap: 4px; }
}

/* ═══════════════════════════════════════════════
   WHAT AiMY SAYS — the insight block

   Ported from AiMY QA's `.attn-done` (QA/index.html ~8063), which is the
   component that makes that product read as AI-native rather than as a
   dashboard with a chat box. NEITHER `.attn-done` NOR `.aimy-done` IS IN THE
   SHARED LIBRARY — both are QA-local, so this is a second product inventing
   the same thing, which is a gap in the system rather than a choice. Filed
   in ../GAPS.md. `.work-state` and `.entry-action` ARE in the library and
   are used from it unchanged, which is why only the wrapper is here.

   Three departures from QA's version, all forced by this file's own rules:

   · Its tokens are QA's `--wash-*` scale, which the extracted library does
     not carry. `--accent-dim` and the accent rgb triple are the local
     equivalents, and css-audit §4 is what caught the difference.
   · Its type is 10.5–11.5px. On the scale here that is 12 and 13.
   · Its `.lnk` is 22px tall. 24 minimum, SC 2.5.8.

   THE TEXT GETS ITS OWN ROW. `flex: 1 1 100%` rather than sharing a line
   with the mark and the chip: the sentence is the thing being read and a
   sentence starting 120px in, wrapping under its own label, is a paragraph
   pretending to be a table cell. Mark and state above it, action below —
   who is speaking, then what they found, then what to do.
═══════════════════════════════════════════════ */
/* ══ THE MARK BELONGS TO THE FIRST LINE ═══════════════════════════════════
   `align-items: center` centres the mark against the WHOLE text block, so on
   a four-line insight the AiMY glyph floated thirty pixels down beside the
   middle of a sentence — the mark that says who is speaking, drifting away
   from where the speaking starts. Measured spread: 64px.

   Top-aligned, with the mark's box made exactly one line tall so the glyph
   centres itself on that line: `xMidYMid meet` letterboxes the 18×20 viewBox
   inside it and puts the optical centre on the line's centre, with no margin
   left to go stale when the type scale moves. The margin this replaces was
   4px, derived against a 1.5 line-height and a 16px glyph — the file says
   1.45 and the box was 18px tall, so the mark sat 2.85px below its own
   sentence. Derive it, do not measure it once and write the number down. */
.s-insight {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px;
  margin-top: 12px; padding: 12px;
  border-radius: var(--r-lg);
  background: rgba(var(--accent-rgb), 0.06);
  border: 1px solid rgba(var(--accent-rgb), 0.16);
}
.s-insight-mark { width: 16px; height: calc(var(--fs-base) * var(--lh-base)); flex-shrink: 0; }
/* The state pill sits on the same first line as the mark. Without this it
   top-aligns against the block and the two marks disagree by the difference
   between an 18px glyph and a 24px pill. */
.s-insight .work-state { flex: none; align-self: flex-start; }
/* ══ THE MARK LEADS THE SENTENCE, IT DOES NOT SIT ABOVE IT ════════════════

   This was `flex: 1 1 100%`, which forced the text onto a row of its own and
   left the mark alone on the one above it — a 16px glyph with a whole line
   to itself, and a gap between AiMY's signature and the thing it is
   signing. It read as a header over a paragraph rather than as AiMY saying
   something.

   `1 1 200px` puts the mark and the first line together and lets the text
   take the rest of the row, so wrapped lines align to the text column and
   the glyph reads as what starts the sentence. The basis is what keeps a
   compact card honest: below about 200px the text still wraps to its own
   line rather than being squeezed into a column three words wide.

   The actions stay at `1 1 100%` — they are a row, not a continuation. */
.s-insight-txt {
  flex: 1 1 200px; min-width: 0;
  font-size: var(--fs-base); color: var(--d200);
  line-height: var(--lh-base);
}
/* The facts inside the sentence, and nothing else. `<strong>` is pinned to
   800 further up this file; this is the same emphasis at the weight the
   surrounding 500 can carry without the line going lumpy. */
.s-insight-txt b { color: var(--d50); font-weight: var(--fw-semibold); }
.s-insight-acts { flex: 1 1 100%; display: flex; gap: 8px; flex-wrap: wrap; }
/* Both of these are declared with the tiers at the top of this file, because
   they ARE the tiers — `.s-insight-lnk` is secondary and `.primary` is the
   surface's one primary. What is left here is only what is local to the
   insight block.

   THE TEAL IS GONE. It was chosen so the primary would not read as a
   selected state against the block's accent ground — a real problem, solved
   by adding a fifth actionable colour to a surface that already had four.
   The primary is the brand fill every other primary uses; against an accent
   tint it reads as the filled thing, not as a selection, because nothing
   else on the surface is filled at all. */
.s-insight-lnk { border-radius: var(--r-pill); white-space: nowrap; }
.s-insight-lnk.primary { border-radius: var(--r-pill); white-space: nowrap; }
.s-insight-lnk:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* On a card the block is the last thing and the card's own padding closes
   it, so it gives back the margin the record page needs. */
.s-card .s-insight { margin-top: 12px; }
.s-rec .s-insight { margin-bottom: 16px; }

/* ══ THE LEAD ══════════════════════════════════════════════════════════════

   The same block on the one surface that has room for it. Everything below
   is a REMOVAL plus a scale: the tint, the border and the padding go, and
   what is left is a figure, a line, a consequence and one filled control.

   THE TINT STAYS. A pass of this took it out, on the reasoning that a
   coloured box is how a flat page fakes ranking and that this page ran the
   same box twice, four hundred pixels apart, on two findings of different
   kinds. The second half of that was the real fault and it is fixed
   elsewhere: the offering block is a pull-quote now, so there is exactly one
   tinted panel on the surface.

   With the pair broken, the tint is doing its actual job — marking the one
   place AiMY speaks in the first person and recommends something. That is
   what this product has meant by an accent panel since v3, and taking it
   away made AiMY's recommendation look like a heading somebody had set
   large. The panel says WHO is talking; the scale inside it says how much
   of the page this is worth. Both, not one instead of the other.

   It is still within the accent budget, which asks for one recommended move
   per surface and now gets exactly one. */
.s-insight.is-lead {
  display: block;
  margin: 24px 0 32px; padding: 24px;
  background: rgba(var(--accent-rgb), 0.06);
  border: 1px solid rgba(var(--accent-rgb), 0.16);
  border-radius: var(--r-lg);
}
/* AiMY's signature on the finding, at the size a signature is. It was the
   first thing in a 14px sentence, which gave the mark the same weight as the
   words — the mark says WHO IS SPEAKING and belongs on the line above, with
   the work state it is declaring. */
.s-lead-mark {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}
.s-insight.is-lead .s-insight-mark { width: 14px; height: 14px; }
/* Baseline, not top. Two type sizes this far apart share a baseline or they
   share nothing — top-aligned, the 18px line floats level with the figure's
   cap height and reads as a separate element that happens to be adjacent. */
.s-lead-line {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px;
  margin-bottom: 16px;
}
/* THE ONE FIGURE THIS PAGE IS ABOUT.

   QA sets its hero figures at 40-52px against 11px body and that is what
   makes an eleven-pixel card legible; this is the same move against 14px
   body, which lands at 60 and a ratio of 4.3 to 1. Tracking is negative
   because at this size it is a real optical correction rather than the
   imperceptible one the note at the top of this file rules out below 24px.
   Leading under 1 because a single line of figures has no descender pair to
   clear and 1.25 would open a gap the block cannot use. */
.s-lead-n {
  font-size: var(--ty-lead); font-weight: var(--fw-extrabold);
  letter-spacing: var(--ls-tighter); line-height: 0.9;
  color: var(--ink-primary);
  font-variant-numeric: tabular-nums;
  flex: none;
}
.s-lead-say {
  flex: 1 1 260px; min-width: 0;
  font-size: var(--ty-body); font-weight: var(--fw-medium);
  line-height: var(--lh-base); color: var(--ink-secondary);
  text-wrap: pretty;
}
/* The denominator is not the figure. It reads with the sentence, in the
   sentence's colour, so "12" stays the thing the eye lands on. */
.s-lead-of { color: var(--ink-faint); font-weight: var(--fw-bold); }
/* ── THE CONSEQUENCE, WHICH IS THE HEADLINE ──

   "The sequence is running past them regardless" is the line that makes the
   figure matter, and it was the tail of a 14px sentence. It is the loudest
   TEXT in the block now — under the figure, above the action, at the deck
   step — because a number nobody has been told the meaning of is a number
   nobody acts on. */
.s-lead-deck {
  font-size: var(--ty-body); font-weight: var(--fw-bold);
  line-height: var(--lh-tight); color: var(--ink-primary);
  max-width: 52ch; text-wrap: balance;
  margin-bottom: 20px;
}
.s-lead-acts { display: flex; flex-wrap: wrap; gap: 8px; }
@media (max-width: 619.98px) { /* 620 layout px */
  .s-lead-n { font-size: var(--ty-display); }
  .s-lead-say, .s-lead-deck { font-size: var(--ty-heading); }
}

/* ══ THE PULL-QUOTE ════════════════════════════════════════════════════════

   The offering block's reading, and the second of the two accent panels this
   page used to carry. Identical treatment to the lead four hundred pixels
   above, on a finding of a completely different kind and a fifth of the
   evidence — so the page said "this matters most" twice and meant it once.

   A quote, not a panel: a raised surface with the mark on the line above,
   and no accent. It reads as AiMY's reading OF the rows beneath it, which is
   what it is, rather than as a second announcement competing with the first.

   No stripe down the side either. That was the same decorative `border-left`
   the ranked rows had just dropped, reintroduced two hundred lines later on
   the block sitting right beneath them. The raised surface holds it in
   without drawing anything — elevation by surface step, which is how this
   ecosystem separates a contained thing from its ground. */
.s-insight.is-quote {
  display: block;
  margin: 0 0 20px; padding: 16px;
  background: var(--card-bg-raised); border: 0;
  border-radius: var(--r-lg);
  max-width: 62ch;
}
/* The mark and the state on one line above the reading, the way the lead
   does it — a 14px glyph alone on a line of its own was a signature with
   nothing beside it to sign. */
.s-insight.is-quote .s-insight-mark { width: 14px; height: 14px; }
.s-insight.is-quote .s-lead-mark { margin-bottom: 8px; }
.s-insight.is-quote .s-insight-txt {
  display: block; font-size: var(--ty-body); color: var(--ink-secondary);
  line-height: var(--lh-base);
}
.s-insight.is-quote .s-insight-txt b { color: var(--ink-primary); }
/* ── THE PROPOSAL, SET APART FROM THE READING ──
   Two different claims: what the evidence says, and what to do about it. Run
   together they read as one paragraph, which is how "the offering is what to
   change" ended up sounding like a conclusion instead of a preamble to one.
   A labelled block, on the raised surface's own step up, so the section
   named "What to offer better" visibly contains an answer to it. */
.s-offer-fix {
  margin-top: 12px; padding: 12px;
  border-radius: var(--r-md); background: var(--card-bg);
  font-size: var(--ty-body); line-height: var(--lh-base);
  color: var(--ink-secondary); max-width: 62ch;
}
.s-offer-fix b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
.s-offer-fix-cap {
  display: block; margin-bottom: 4px;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint);
}
.s-insight.is-quote .s-insight-acts { margin-top: 12px; }

/* ═══════════════════════════════════════════════
   OBSTACLES AND OPPORTUNITIES

   A labelled stack, one fact per line — the pattern the Clay screenshots use
   for a record's details, and the one this product now uses everywhere a set
   of facts about a single thing has to be read. Never a grid: a flag, its
   evidence and its action are three different kinds of thing, and columning
   them makes the reader scan across for a relationship that reads better
   along.

   The two axes are told apart by the RULE ON THE LEFT rather than by their
   own container. Common Region would need two boxes for two kinds of line
   that sit in one list, and the tone already carries the difference — this
   just makes it structural as well as chromatic.

   Proximity: 6px inside a line, 16px to whatever follows the stack. The
   ratio rule in css-audit §6, which is why the margin is not 8.
═══════════════════════════════════════════════ */
.s-flags {
  display: flex; flex-direction: column; gap: 4px;
  margin-top: 12px; margin-bottom: 16px;
}
.s-flag {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding-left: 12px; border-left: 2px solid var(--hairline);
}
.s-flag.is-obstacle { border-left-color: var(--warn); }
.s-flag.is-opportunity { border-left-color: var(--ok); }
.s-flag-name { font-size: var(--fs-xs); font-weight: var(--fw-bold); flex-shrink: 0; }
.s-flag-why { font-size: var(--fs-xs); color: var(--ink-muted); min-width: 0; }
/* Last, quiet, and only ink — the line is about the fact, not about the
   control for putting it away. */
.s-flag-x {
  margin-left: auto; flex-shrink: 0;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ink-muted); cursor: pointer; padding: 2px 4px;
  border-radius: var(--r-xs);
  transition: color var(--t-fast), background-color var(--t-fast);
}
/* `--accent-dim`, not `--wash-6`. The wash scale is QA's, not the library's —
   carried across with the flag markup and caught by css-audit §4 before it
   could render as "no hover at all". Sales' three other inline-button hovers
   all use `--accent-dim`, so this follows them. */
.s-flag-x:hover { color: var(--d200); background: var(--accent-dim); }
.s-flag-x:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Facts are separate spans with a dot drawn between them rather than one
   string with " · " baked in, so a fact can wrap as a unit and the last one
   never trails a dot into empty space. */
/* ── The company, in three ranks ──
   It was one wrapping run at 13px/400/--d400 — six facts of five kinds, all
   claiming the reader equally. The ranks are size, then what and where, then
   background; each is a row, and the separation between rows is 4px against
   the 8px between facts inside one, so a rank reads as a group. */
.s-rec-facts { display: flex; flex-direction: column; gap: 4px; line-height: var(--lh-base); }
.s-rec-facts > div { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 4px; }
/* The separator trails the fact it follows rather than leading the one
   after. A ::before sits first inside the NEXT item's box, and these are
   flex rows that wrap, so every wrapped line opened on a punctuation mark.
   And on every child, not between spans only: paired as span + span it
   separated a bare fact from a marked one and left two marked ones
   touching, which is how an account masthead came to read "5 people here1
   you can call now". */
.s-rec-facts > div > *:not(:last-child)::after {
  content: '·'; color: var(--ink-muted); margin-left: 8px;
}
/* The two figures every ICP is written against. The only thing in the block
   that is bigger, heavier and brighter than the rest — which is also what
   lets a qualifier sit beside one without needing a box to be seen.

   ── AND THE RANKS WERE ONE PIXEL APART ──

   Three ranks, at 14 / 13 / 13. The structure was right and the steps were
   not: a reader cannot see a one-pixel difference as a level, so `32k staff`
   and `Founded 2016` arrived at the same volume and the ranking existed only
   in the stylesheet. It is the cluster this product's whole type pass was
   about, surviving inside a block that had already been rebuilt once to fix
   exactly this.

   Block heading against body, and three inks under it: size and ink for the
   lead, ink alone between mid and back. Not higher — the next step above is
   16 too, and firmographics do not outrank the thing that is overdue. */
.s-facts-lead {
  font-size: var(--ty-heading); font-weight: var(--fw-bold);
  color: var(--ink-primary); font-variant-numeric: tabular-nums;
}
.s-facts-mid  { font-size: var(--ty-meta); color: var(--ink-secondary); }
/* When it was founded, and how our record of it began — the second is not a
   fact about the company at all. Neither changes a decision.

   `--d400`, not the caption step. `--d500` measured **3.54:1 in dark**, under
   the 4.5 floor — a known, product-wide value that every 11px section caption
   already sits at and that v5 recorded as a design-system decision rather than
   a per-surface one. A caption is a label you glance at; this is content, and
   putting content at a caption's contrast is a step past that precedent
   rather than a match for it. 6.83 dark / 5.60 light, and still a clear step
   under the rank above it. */
.s-facts-back { font-size: var(--ty-meta); color: var(--ink-faint); }
.s-rec-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.s-rec-actions .btn svg { width: 13px; height: 13px; }
.s-rec-head .inline-note { margin-top: 12px; }

/* The next step, raised. It is the only thing on this page with a date in
   the future, and the only one with a date in the past that matters. */
.s-next {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 16px; margin-bottom: 24px;
  background: var(--card-bg-raised);
  border: 1px solid var(--card-border); border-radius: var(--r-lg);
}
.s-next.is-over { border-color: color-mix(in srgb, var(--err) 30%, transparent); background: var(--err-bg); }
/* ── THE ONE FIGURE THIS RECORD IS ABOUT ──

   `30 days overdue` is the most consequential fact on the page and it was
   set at the meta step, third in a run after the step's name and its date.
   The worklist row this record is reached from leads with the same number,
   so a reader who clicked `40 days late` arrived at a page that mentioned it
   in passing — the figure got quieter as they got closer to it.

   Block heading step, in the error tone the block already carries. Not the
   display step: the name above is the masthead and nothing here outranks
   it, which is the rule this page now shares with the campaign. */
.s-next-what { font-size: var(--ty-heading); font-weight: var(--fw-bold); color: var(--ink-primary); }
.s-next-when { font-size: var(--ty-meta); color: var(--ink-faint); }
.s-next.is-over .s-next-when {
  font-size: var(--ty-heading); font-weight: var(--fw-bold);
  color: var(--err-text); font-variant-numeric: tabular-nums;
}
.s-next .btn { margin-left: auto; }

/* Grouped by a hairline and space, not by a box. A card should exist where
   elevation means something — these are sections of one page, all at the
   same level, and boxing each one draws five containers around content that
   is already contiguous. Space above is larger than space below, because a
   heading belongs to what follows it. */
.s-block { padding-top: 24px; margin-bottom: 0; border-top: 1px solid var(--hairline); }
.s-block:first-of-type { border-top: 0; padding-top: 0; }
.s-block + .s-block { margin-top: 24px; }
.s-block-sub { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }
.s-none { font-size: var(--fs-base); color: var(--ink-muted); }
/* ══ THE TEXT ACTION ═══════════════════════════════════════════════════════

   THE "BLUE UNDERLINED LINK" COMPLAINT, AND IT WAS THREE FAULTS AT ONCE.

   `font-size: inherit` meant its seventeen call sites rendered at whatever
   surrounded them — up to 15px, LARGER than every real button on the page,
   which sit at 12 to 13. Then it stacked three emphasis mechanisms on one
   element: accent colour, underline, and semibold. Butterick is explicit on
   both — never underline in a UI, and bold OR italic, never both. If
   everything is emphasised, nothing is.

   And there were four of these — `.s-inline-btn`, `.s-straight-act`,
   `.s-hist-sum`, `.s-work-cancel` — with two underline offsets and two
   different accent tokens between them.

   One style, one declared size. `--accent-text` rather than raw `--accent`:
   that token exists precisely because `--accent` fails contrast as small
   text, and it was used twice against `--accent`'s twenty-four. The
   underline returns on hover and focus, where it is a state rather than a
   decoration. */
.s-inline-btn, .s-straight-act, .s-hist-sum {
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--accent-text); cursor: pointer;
  text-decoration: none;
  transition: color var(--t-fast);
}
/* ── EXCEPT WHERE IT IS SITTING IN A ROW OF FACTS ─────────────────────────
   A declared size is right for an action standing on its own. In the
   masthead's facts row it made the row alternate: "VP Engineering" 16,
   "Fenborg Software" 14, "Software" 16, "Brussels, BE" 16 — the size
   changing on whether the fact happened to be a door rather than on what
   kind of fact it was. The row is one rank and reads as one line, so the
   items in it take the line's size and the colour goes on carrying which of
   them you can press. */
.s-rec-facts .s-inline-btn, .b-qcard-where .s-inline-btn { font-size: inherit; }

.s-inline-btn:hover, .s-straight-act:hover, .s-hist-sum:hover {
  color: var(--accent); text-decoration: underline;
  text-decoration-color: rgba(var(--accent-rgb), 0.4);
  text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.s-inline-btn:focus-visible, .s-straight-act:focus-visible, .s-hist-sum:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px;
}
/* ══ AN INLINE ACTION THAT CANNOT BE TAKEN ═══════════════════════════════

   The family had no disabled state because nothing had ever disabled one:
   these are sentence verbs, and a sentence verb was always either drawn or
   not. `Skip this one` is the first that has to stay on the page while
   being unpressable — the brief it sits in is an append-only block that
   cannot re-render when the call it describes starts ringing, so the button
   outlives the moment it was valid.

   Greyed to body colour rather than dimmed to a ghost. It is still part of
   a sentence you are meant to read, and 45% of an accent is a colour that
   reads as broken rendering rather than as a decision — which is why the
   one existing precedent, `[data-commit-go][disabled]`, can afford opacity
   and this cannot: that one is a filled button in a modal you are looking
   at, this one is three words inside a paragraph.

   The underline goes with the hover, because an underline that appears
   under a control which does nothing is the affordance lying twice. */
.s-inline-btn[disabled] {
  color: var(--ink-muted); cursor: not-allowed; text-decoration: none;
}
.s-inline-btn[disabled]:hover { color: var(--ink-muted); text-decoration: none; }

.s-people { display: flex; flex-direction: column; gap: 2px; }
.s-person {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 8px 12px; border-radius: var(--r-md); cursor: pointer;
  transition: background-color var(--t-fast);
}
.s-person:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .s-person:hover { background: rgba(16,24,40,0.04); }
.s-person:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.s-person-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.s-person-name { font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--d50); }
.s-person-role { font-size: var(--fs-xs); color: var(--ink-muted); }
.s-person-st { margin-left: auto; font-size: var(--fs-xs); flex-shrink: 0; }

.s-reach { display: flex; flex-direction: column; gap: 4px; }
.s-reach-row { display: flex; align-items: center; gap: 12px; }
.s-reach-what { font-size: var(--fs-xs); color: var(--ink-muted); width: 60px; flex-shrink: 0; }
/* ── An address is not code ──
   Phone, email and LinkedIn were set in JetBrains Mono, which said "literal
   string, read it character by character" about three values nobody reads
   that way — every mail client in the world sets an address in its body face.
   It also broke the block's own voice: a monospaced row under a proportional
   label, beside proportional facts, in a product with one text face. */
.s-reach-val { font-size: var(--fs-sm); color: var(--d100); min-width: 0; overflow-wrap: anywhere; }

.s-camps { display: flex; flex-wrap: wrap; gap: 4px; }
.s-camp { cursor: pointer; }
.s-camp:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-share { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: var(--fs-sm); color: var(--ink-muted); }
.s-share-own { display: inline-flex; align-items: center; gap: 4px; color: var(--d100); }
.s-share-with { color: var(--ink-muted); }
/* "Nour Wael owns it" — the name is the fact, the verb qualifies it, so they
   are ranked rather than set as one run at one weight. */
.s-share-role { color: var(--ink-muted); }

/* What Knowledge holds: four rows at most, each naming one object. It was a
   single wrapped sentence, on the argument that a lead page is not a place to
   browse documents — still true, and the reason this is rows and not the block
   of cards it replaced. But a sentence cannot name four things without
   becoming a list read sideways, and counting them instead named none. */
.s-kbline {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 12px 16px;
  border: 1px solid var(--d700); border-radius: var(--r-md);
  font-size: var(--fs-sm); color: var(--ink-muted); line-height: var(--lh-base);
}
.s-kbline-mark { flex: none; color: var(--ai-text); transform: translateY(2px); }
.s-kbline-list { flex: 1 1 auto; min-width: 200px; display: flex; flex-direction: column; gap: 4px; }
/* The title is the control and takes the row; its kind and its trust follow on
   the same line and wrap under it rather than squeezing the name. */
.s-kbrow { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.s-kbrow-name { text-align: left; }
/* Borrowed from the Knowledge line when both were one wrapped sentence, and
   named after it long after only the running-work line still ran that way. */
.s-runline-text { flex: 1 1 auto; min-width: 200px; }
.s-runline-sep { color: var(--ink-muted); }
/* What kind of thing it is — quieter than the title it qualifies. */
.s-kbline-kind { color: var(--ink-muted); }

/* A review that has been asked for stops being a control and says who asked
   and when — the same move `.cite-action.is-flagged` makes for a report, and
   for the same reason: a live button on a request already sent invites
   sending it twice. */
.s-kb-card .is-asked {
  background: none; border-color: var(--card-border); color: var(--ink-muted);
  cursor: default; pointer-events: none;
}
/* The two card actions wrap rather than squeeze in a 400px rail. */
.s-kb-card .tc-action { display: flex; flex-wrap: wrap; gap: 4px; }

/* Read in the rail (400px) or the canvas, so it fills its column. */
.s-kb-card { max-width: none; }
.s-kb-card + .s-kb-card { margin-top: 8px; }
.s-kb-card .tc-summary { margin: 0; padding: 8px 16px 0; }

/* ═══════════════════════════════════════════════
   THE COMMIT SURFACE

   Every consequential write goes through this. The effects panel is above
   the button, never beside it: a consequence you read after deciding is a
   consequence you did not read.
═══════════════════════════════════════════════ */
.s-commit-back { display: flex; align-items: center; justify-content: center; }
.s-commit { width: min(520px, calc(var(--vp-w) - 32px)); max-height: calc(var(--vp-h) - 48px); overflow-y: auto; }
/* A block quotation — what the rep actually typed. It had a background, a
   left rule and a radius, which is three devices saying "this is quoted"
   where one does. Butterick's rule for a block quote is indent and reduced
   size, no quotation marks, because the indent already signals it; and for
   rules generally, try space above and below first. The raised surface and
   the inset are the whole of it. */
.s-commit-quote {
  font-size: var(--fs-base); color: var(--d200); line-height: var(--lh-base);
  padding: 12px 16px; margin-bottom: 16px;
  background: var(--card-bg-raised);
  border-radius: var(--r-sm);
}
.s-effects {
  padding: 12px; border-radius: var(--r-md);
  background: var(--card-bg-raised); border: 1px solid var(--card-border);
}
.s-effect {
  display: flex; gap: 8px; font-size: var(--fs-xs); line-height: var(--lh-base);
  color: var(--d200); padding: 4px 0;
}
.s-effect::before { flex-shrink: 0; font-weight: var(--fw-bold); }
.s-effect.is-ok::before   { content: "+"; color: var(--ok); }
.s-effect.is-warn::before { content: "!"; color: var(--warn); }
/* The skip line is mandatory, not decoration: an operation that silently
   no-ops on part of its scope reports a success nobody has reason to
   distrust. */
.s-effect.is-skip::before { content: "–"; color: var(--ink-muted); }
.s-effect.is-skip { color: var(--d200); }

.s-pick { display: flex; flex-direction: column; gap: 2px; margin-bottom: 16px; }
.s-pick-row { padding: 8px; border-radius: var(--r-md); cursor: pointer; }
.s-pick-row:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .s-pick-row:hover { background: rgba(16,24,40,0.04); }
/* The qualifier inside a choice — a role, a count, what is already there.
   One step below the name it follows, and no dimmer: at `--d500` the thing
   telling you WHICH of two similar campaigns to pick was the least legible
   text on the surface that asked you to pick. */
.s-pick-role { color: var(--d200); font-size: var(--fs-xs); }
/* Two textareas inside a commit dialog, and one rule. `.s-raise` is the
   read-only tier's question; `.s-why` is a disqualification reason. Same
   box, same behaviour — separate class names because the audits read the
   names to find the paths, and one name on two flows makes a leak in either
   of them unfindable. */
.s-why, .s-raise { width: 100%; resize: vertical; margin-bottom: 16px; }
.s-commit .decision-zone { margin-bottom: 16px; }

.s-toast { position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%); z-index: 960; }

/* ═══════════════════════════════════════════════
   THE CHANGE MARKER

   A toast is a receipt: it says something happened somewhere else. This
   says WHERE — the exact element that changed, tinted for a moment.
═══════════════════════════════════════════════ */
/* A TRANSITION, not a keyframe. This fires on every write and writes here
   come in bursts — a keyframe restarts from zero when re-triggered, so two
   quick changes made the first one snap back to full tint. A transition
   retargets from wherever it had got to. */
.s-changed {
  border-radius: var(--r-md);
  background-color: rgba(var(--accent-rgb), 0.16);
  transition: background-color 1400ms var(--ease-out) 200ms;
}
.s-changed.is-settling { background-color: transparent; }
/* Under reduced motion the tint holds instead of fading — the signal
   survives, the movement does not. */
@media (prefers-reduced-motion: reduce) {
  .s-changed { transition: none; background-color: rgba(var(--accent-rgb), 0.12); }
  .s-changed.is-settling { background-color: rgba(var(--accent-rgb), 0.12); }
}

/* ═══════════════════════════════════════════════
   MOTION — the craft pass

   Four corrections, each with a reason rather than a preference.
═══════════════════════════════════════════════ */

/* 1 · Hover is gated. Touch devices fire :hover on tap, so an ungated hover
   transition makes every tap on a phone leave a stuck hover state. */
@media (hover: none) {
  .s-card:hover, .s-person:hover, .s-frow:hover, .s-pick-row:hover,
  .s-camp-chip:hover, .proto-link:hover { background: none; }
}

/* 2 · The commit surface enters. Nothing in the real world appears from
   nothing, and this modal carries the consequence of every write on the
   surface — arriving instantly reads as an error dialog rather than a
   decision. Origin stays CENTRE: a modal is not anchored to a trigger. */
@media (prefers-reduced-motion: no-preference) {
  .s-commit-back { animation: sFade 160ms var(--ease-out); }
  .s-commit { animation: sRise 200ms var(--ease-out); transform-origin: center; }
  @keyframes sFade { from { opacity: 0; } }
  /* From 0.97, never from 0 — an element that scales up from nothing has no
     shape to come from. */
  @keyframes sRise { from { opacity: 0; transform: scale(0.97); } }

  /* 3 · Things that live on an edge enter from it, so the motion says where
     they came from and where dismissing sends them. The docked rail was the
     original case and is gone; the canvas's chat column sits on the same
     edge and inherits the same reasoning. */
  /* ON OPEN, AND WITH AN EXPLICIT RESTING STATE.

     Inherited from the docked rail as `from { translateX(-12px) }` with no
     `to` and no fill mode, attached to the element unconditionally. Measured
     after it should have finished: `matrix(1, 0, 0, 1, -12, 0)` — the column
     sat TWELVE PIXELS OFF the left edge permanently, cropped by the
     overlay's own bounds. It ran while the canvas was still closed, and
     what it reverted to was never what the layout wanted.

     Two corrections, and either alone would have fixed the symptom: it now
     runs when the canvas OPENS, which is when the motion means anything, and
     the keyframes state where they end rather than leaving it to whatever
     the cascade happens to say. */
  /* OPACITY ONLY. Two attempts to make a translate settle correctly both
     left the column at `matrix(1, 0, 0, 1, -12, 0)` — twelve pixels off the
     left edge, permanently, cropped by the overlay's bounds. Adding a `to`
     state and `fill-mode: both` did not fix it either, which is the point at
     which a decorative transform has cost more than it is worth.

     A transform that can strand an element outside its own container is a
     transform with a failure mode; opacity has none. The motion still says
     the column arrived, and nothing it animates can change where the column
     IS. */
  .aimy-overlay.open .overlay-chats { animation: sFadeIn 220ms var(--ease-out); }
  @keyframes sFadeIn { from { opacity: 0; } }
}

/* 4 · The peek and any anchored panel scale from their trigger, not from
   their own centre. A popover belongs to what opened it. */
.peek, .ntf-panel { transform-origin: top right; }
.v2-dropdown-panel { transform-origin: top left; }

/* ═══════════════════════════════════════════════
   THE BRIEFING — what changed, and one thing to do about each

   Not counts and not a log: current-state counts live in the Status
   filter, which is where somebody looking for them will go, and repeating
   them here would be the same fact twice.
═══════════════════════════════════════════════ */
/* `.brief` and its scrollbar went with the rail. The briefing is a surface
   inside `.page-scroll`, which already scrolls. */

/* ═══════════════════════════════════════════════
   THE PAGE

   One column of blocks, not a dashboard. The blocks that are PROSE cap at a
   readable measure; the card grid does not, because `auto-fill` answering a
   wider viewport with more columns is the whole point of it. The cap lives
   on the things that are read, which is the rule this file already argued
   for `.page-inner`.

   32 between blocks, 12 under a heading — the ratio rule with room to
   spare, because these are the largest groups on the surface and the gap
   between them is doing the most work.
═══════════════════════════════════════════════ */
/* ── THE PAGE USES THE PAGE ──

   It was one 72ch column pinned left, which on a 1920 screen is a third of
   the width doing all the work and two thirds holding nothing — and because
   nothing sat beside anything, every block stacked and the page ran on for
   screens. The measure cap was right for the prose and wrong as a layout:
   it capped the COLUMN when it should only ever have capped the LINE.

   So the blocks lay out on a grid and the cap moves to the text inside them.
   Two columns where two things are read side by side, full width where a
   thing is a set of cards. `auto-fit` rather than a media query, because the
   question is how much room there is, not what device this is. */
/* TWO COLUMNS, DECLARED — not `auto-fit`.

   `repeat(auto-fit, minmax(340px, 1fr))` gave FOUR columns at 1920 and the
   findings measured 30 CHARACTERS, which is the 22ch briefing rail all over
   again in a wider frame. `auto-fit` asks "how many will fit", and the
   answer for prose is never "as many as possible" — a column of text has an
   upper bound as well as a lower one.

   Two, above the width where two fit. The cap keeps the pair from running to
   1600px each on a very wide screen, where the eye loses the start of the
   next line on the way back from the end of the last. */
.s-home {
  display: grid;
  /* minmax(0, 1fr), NOT 1fr, AND THE DIFFERENCE IS WHETHER THE PAGE CAN BE
     PUSHED. `1fr` is `minmax(auto, 1fr)`, and that auto minimum is the
     largest min-content among the blocks in the track — so one block that
     cannot narrow does not overflow its own box, it WIDENS THE COLUMN, and
     every sibling stretches to the new width with it. Measured on the
     manager's campaign at a 391px layout: `.b-cmeta` reported 792px of
     min-content and the back link, the title and four blocks that each fit
     perfectly well were all drawn 792 wide and cut off at 391.

     The block that could not narrow is fixed where it lives (bdr.css §56).
     This is the guard that keeps the next one from doing the same thing to
     the whole page: a track here may never be wider than the column it is
     in, and a block that overruns overruns alone. */
  grid-template-columns: minmax(0, 1fr);
  gap: 32px;
  align-items: start;

  /* CAPPED AND CENTRED, AND THE SECOND HALF IS NOT OPTIONAL.

     This is the only content cap above the 1536 anchor, so it is the only one
     the UI scale can reach: the scale resolves 16:9 and 16:10 to exactly 1536
     of layout and the cap never binds, but an ultrawide gives back its extra
     width — measured on a 5120x1300 32:9 panel, a 2835px layout, and the home
     page stopped at 1560 with 886px of dead space down the right-hand side.

     That is the defect §THE MEASURE CAP BELONGS TO PROSE argues out above, in
     the same words the report used ("everything is left aligned, what is
     this?"), and the answer there is the answer here. The cap is right — two
     columns of blocks do not want 2400px — and a cap without an auto margin is
     not a cap, it is a left-pin. */
  max-width: 1560px;
  margin-inline: auto;
}
@media (min-width: 1100px) { /* 1100 layout px */
  .s-home { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.s-block { display: flex; flex-direction: column; min-width: 0; }
/* ══ EVERY HOME BLOCK IS WIDE, AND THAT IS CORRECT ═══════════════════════
   A pass through here let the row-list blocks — calls, campaigns, obstacles
   — fall into the two columns declared above, on the reasoning that a list
   of rows is a column already. Measured, that was wrong: a role's home
   usually carries ONE such block, so it drew a 530px column of campaigns
   beside 530px of nothing. Pairing only works when there is reliably a
   sibling to pair with, and `FUNCTIONS[fn].home` does not guarantee one.

   The columns belong on the record lists, where the number of things is
   the corpus rather than a role's block plan — see `.s-grid`. */
.s-block-wide, .slv { grid-column: 1 / -1; }
/* A report section dropped on a home page is wide too. `.s-exec-sec` is
   built for the one-column report, where a figure row and a ledger run the
   whole measure; in the home grid's two tracks it would be drawn at half of
   it. The floor page has written `s-block-wide` beside the class by hand
   since it was built — this is that, said once, so the next section
   borrowed from the report does not have to remember. */
.s-home > .s-exec-sec { grid-column: 1 / -1; }
/* Space above a heading, more than below it: a heading belongs to what
   follows. The block gap supplies the above, so this only has to set the
   below and stay out of the way. */
.s-block-h {
  font-family: var(--font-display);
  font-size: var(--fs-lg); font-weight: var(--fw-bold);
  color: var(--d50); letter-spacing: var(--ls-tight);
  line-height: var(--lh-tight); margin-bottom: 12px;
}
/* THE CAP IS ON THE LINE, NOT ON THE COLUMN. `.slv-main` is the paragraph;
   the block around it spans the page and the score sits at the far end of
   it. Everything else is already inside a grid column narrow enough that a
   second cap would only ever fight the first. */
.slv-body, .s-pool { max-width: 72ch; }

/* ── What AiMY can start ──
   Each one is a proposal with an artefact behind it, so it gets a bounded
   region of its own — Common Region, because five proposals in a flat
   column read as five paragraphs of the same thought. */

/* ── Campaigns, the things that actually surface ──
   Three-up on a wide screen, one-up on a narrow one, each carrying its own
   distribution and its own insight. `280px` rather than the grid's 320
   because a campaign card is mostly one sentence and a bar. */
/* 360 rather than 300: at 300 a full-width row fitted FIVE campaign cards,
   and a card carrying a sentence at five-up is a card whose sentence wraps
   six times. Four is the practical ceiling for this content. */
/* ══ THE GAP BETWEEN CARDS EXCEEDS THE PADDING INSIDE THEM ═══════════════
   Every card grid on this surface had it the other way round — 16px of air
   inside a card and 12px between two of them — so the space that separates
   two cards was smaller than the space that separates a card's own text
   from its own border. Proximity says the closer pair groups, and the
   closer pair was ACROSS the boundary: four cards read as one panel with
   lines through it. 24 against 16 puts the grouping back inside the card,
   which is the only place it means anything. */
.s-campos {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  gap: 24px; align-items: start;
}
.s-campo {
  display: flex; flex-direction: column;
  padding: 16px; border-radius: var(--r-lg);
  border: 1px solid var(--card-border); background: var(--card-bg);
  transition: border-color var(--t-fast);
}
.s-campo:hover { border-color: var(--card-border-hover); }
/* 4 under the title, 12 to the bar: the meta belongs to the name above it,
   not to the distribution below. */
/* The insight is the last thing and closes the card, so it gives back the
   margin it carries elsewhere. */
.s-campo .s-insight { margin-top: 12px; }

/* The pool, stated rather than enumerated. */
.s-pool { font-size: var(--fs-base); color: var(--d200); line-height: var(--lh-base); }
.s-pool b { color: var(--d50); font-variant-numeric: tabular-nums; }
.s-pool .s-inline-btn { margin-left: 8px; }

/* ── The rail: the briefing, and nothing that grows with the corpus ──

   240 → 280 when these lines gained a chip, a sentence fragment and a verb
   rather than a name and a number. 280 → 320 with the agenda: an agenda row
   carries a work-state chip AND a finding sentence AND a named verb, and at
   280 the sentence had about 250px, which broke "AiMY stopped mid-way
   through Find a slot with four universities" across five lines.

   Still not wide enough for prose, which is why nothing here is prose. */
/* A TOKEN, BECAUSE THE FLOAT BAR HAS TO CLEAR THIS AND MUST NOT GUESS IT.
   The bar is fixed and centred on the window; below the anchor it has to step
   out of the rail's way, and the width it steps by is this one. Two literals
   that have to agree is how they stop agreeing. --sidebar-width is not this
   token: that is the design system's, overridden above and read by nothing. */
:root { --rail-w: 320px; }
/* ══ IT HAD TO STOP SCROLLING ══════════════════════════════
   Measured on a 698px window: the rail wanted 703px inside 661px of room,
   so three widgets and a footer put a scrollbar on a column that should
   just sit there. 24px top and bottom is canvas padding on a 241px column;
   16 is enough and gives back 16 of the 42. The rest comes from the
   conclusion, below. */
.app-rail {
  width: var(--rail-w); flex-shrink: 0;
  display: flex; flex-direction: column;
  padding: 16px;
  overflow-y: auto;
  /* ══ THE GUTTER IS ALWAYS THERE, SO NOTHING MOVES WHEN THE SCROLLBAR IS ══

     `overflow-y: auto` on a column whose height depends on what is open
     means the scrollbar appears and disappears — and its 5px comes out of
     the CONTENT box, not the element. Measured: opening the history took
     every child of the rail from 247px to 242px, so the conclusion, the
     evidence pill, every finding and the history block all reflowed and
     re-wrapped from one press on something at the bottom of the column.

     `stable` reserves the gutter whether or not it is occupied. The rail is
     280px either way; what changes is that it is 274px of content always
     rather than 279 sometimes. `#pageScroll` has the same latent shift and
     only hides it by always overflowing, so it gets the same treatment. */
  scrollbar-gutter: stable;
  background: transparent;
  border-right: 1px solid var(--hairline);
}
/* ── WHOSE CONTEXT THIS IS ──

   It was an eyebrow over a name, and the comment here argued the name was
   "not a second page title competing with the one four hundred pixels to the
   right". It was exactly that, only smaller — and the rail's own reading
   already speaks about whatever the page is showing, so nothing was lost by
   taking both away. */
.rail-read { display: flex; flex-direction: column; gap: 16px; }

/* ── THE CONCLUSION ──

   `.bcard` is the library's conclusion · evidence · action component and
   this is the first thing in the product to use it. What is overridden is
   only what a 248px measure needs: the library sets the conclusion at 12px,
   which is off this product's type scale, and the card is the one piece of
   prose in the rail so it gets the body step.

   `.bcard:hover` lifts and shadows — right for a card in a grid you are
   choosing from, wrong for the only card in a column, where a lift on
   mouse-over reads as something about to happen. */
/* The state, in its own tone, since the dot that used to carry it is gone.
   The library's three severities are what a work state has to paint with. */
.rail-state.p1 { color: var(--err); }
.rail-state.p2 { color: var(--warn); }
.rail-state.p3 { color: var(--ok); }
.rail-card { padding: 16px; }
.rail-card:hover { transform: none; box-shadow: none; border-color: var(--card-border); }
/* THE MEASURE IS THE REASON, NOT THE SCROLLBAR. 16px is the body floor and
   this file does not leave it lightly — but the floor comes with a named
   exception for a narrow measure, and this column is 241px. At --fs-base
   that is about 28 characters a line, well under half of comfortable, and
   the sentence broke to three lines. At --fs-xs it is about 32 and breaks
   to two, which reads better AND returns the 27px that stopped the rail
   scrolling. A side rail is denser than the canvas beside it; that is what
   a side rail is for. */
.rail-conclusion {
  font-size: var(--fs-xs); font-weight: var(--fw-medium);
  line-height: var(--lh-base); color: var(--d100); margin: 0 0 12px;
}
.rail-conclusion b { font-weight: var(--fw-semibold); color: var(--d50); }
.rail-act { width: 100%; justify-content: center; }

/* ── THE FINDINGS ──
   A sentence and one way in, the same anatomy as `.s-straight` on the page.
   The tone is on the left edge, because a 248px column has no room for a
   pill and the edge is the cheapest carrier of a severity there is. */
.rail-rows { display: flex; flex-direction: column; gap: 12px; }
.rail-row {
  padding-left: 12px; border-left: 2px solid var(--d700);
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
}
.rail-row.tone-err  { border-left-color: var(--err); }
.rail-row.tone-warn { border-left-color: var(--warn); }
.rail-row.tone-ok   { border-left-color: var(--ok); }
.rail-row-say { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }
/* An agenda row carries a work state; a finding does not. The chip sits
   above the sentence rather than beside it because at 320px a chip and a
   sentence on one line leaves the sentence about 190px, which is four words
   before it wraps under the chip anyway — and a wrap that lands under a chip
   reads as a second paragraph. */
.rail-row .work-state { margin-bottom: 2px; }
/* The way to the rest of the agenda, at the foot of the rows it caps.

   Aligned to the list's OUTER edge, not indented to the row text. The first
   cut indented it to match — which needs 14px, the rows' 2px border plus
   12px padding, and 14 is not on the 4px scale. Chasing it to 12 or 16 would
   have been a near-miss nobody could explain later. It is the right edge
   anyway: this control is about the list, not an item in it, so it belongs
   where the list begins rather than where its sentences do. */
.s-rail-more { margin-top: 4px; align-self: flex-start; }

/* The campaign's history, under everything it is context for. No rule of its
   own any more: it used to draw the hairline that separated it from the
   findings, and `.s-hist` is a bordered block now, so the two together were
   a boundary and then another boundary eight pixels later. */
.rail-hist { display: block; }

/* ── WHAT IS STANDING ──

   Four figures, no header and no sentence. The heading that was here said
   the same words as the page's heading four hundred pixels to the right, and
   the paragraph under it wrapped to nine lines in a 248px measure.

   A figure, its name, and a way in. Each row is the target — a number you
   cannot open is a number you have to go and find. */
.rail-figs { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rail-fig {
  display: flex; align-items: baseline; gap: 8px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 8px; border-radius: var(--r-md);
  font-family: inherit;
  transition: background-color var(--t-fast);
}
.rail-fig:hover { background: var(--accent-dim); }
.rail-fig:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* Tabular figures on a fixed column so four numbers of different widths make
   one edge. Ragged digits down a narrow rail read as four unrelated facts. */
.rail-fig-n {
  min-width: 2.2ch; text-align: right;
  font-size: var(--fs-lg); font-weight: var(--fw-bold);
  line-height: var(--lh-tight); letter-spacing: var(--ls-tight);
  color: var(--d50); font-variant-numeric: tabular-nums;
}
.rail-fig-cap { font-size: var(--fs-xs); color: var(--ink-muted); flex: 1 1 auto; }
.rail-fig-trend {
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ok); font-variant-numeric: tabular-nums;
}
.rail-fig-trend.is-worse { color: var(--err); }
/* The first figure is the one that gets worse while nobody looks — but only
   where the figures are openable sets. A lead's touchpoint count has no
   worse-or-better about it. */
.rail-figs .rail-fig:first-child:not(.is-flat) .rail-fig-n { color: var(--warn); }
/* A figure with nowhere to go. A lead's own counts are facts about the
   record you are already looking at, so there is no set to open — and a
   button that navigates nowhere is worse than a line of text. */
.rail-fig.is-flat { cursor: default; }
.rail-fig.is-flat:hover { background: none; }

/* The evidence zone. `.bcard` is conclusion · evidence · action and the
   product has only ever drawn two of the three, so anything qualifying a
   conclusion had to hang under the card as a figure block of its own. */
.rail-evidence { margin: 0 0 12px; }

/* The refusal offers two lists — what it can be asked and what it can be told
   to do — and without a caption each they read as one long run of chips. */
.s-ans-cap {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
  margin: 16px 0 8px;
}
.s-ans-cap:first-of-type { margin-top: 12px; }

/* Which suppliers were asked, and how often each answers — the same reading
   the enrichment waterfall has carried since v4, on the surface that until
   now asserted a match count from nowhere. */

/* ── A FOURTH CONFIDENCE TIER, AND THE ONLY ONE A LOOKUP CANNOT PRODUCE ──
   The library styles high · medium · low, which are all a provider's answer
   about how sure it is. `stated` is a person having typed it, which outranks
   every lookup — so it takes the accent rather than the ok/warn/err ladder:
   it is not a better guess, it is not a guess. */
.conf-stated { color: var(--accent); border-color: rgba(var(--accent-rgb), 0.24); background: var(--accent-dim); }
.conf-stated .conf-meter i { background: var(--accent); }
/* ── What a campaign is for ──
   Set as the campaign's own sentence rather than as another meta line: it is
   the objective, and everything else on the page is a means to it. Bordered
   left rather than boxed, so it reads as a statement the page is making and
   not as a panel beside it. */
/* ══ THE STRIPE IS GONE ════════════════════════════════════════════════════

   A 2px accent rule down the left of the goal, and another down the left of
   progress. It is on the doctrine's own list of things that fail Level 1
   (`00_AiMY_Knowledge_to_Action_Doctrine.md:416`, "decorative border-left
   stripes"), and it was carrying the accent on two blocks that take no
   action — which is the other budget it was over.

   What the stripe was actually for is separation, and the block gets that
   from the caption column beside it and the space around it. Nothing needed
   replacing; the rule just went. */
.s-goal-text {
  font-size: var(--ty-body); color: var(--ink-secondary); line-height: var(--lh-base);
  max-width: 72ch;
}
/* ── THE LEDE ──
   The goal is the claim every figure below is measured against, so it is
   read first and set to be read: the deck step, primary ink, and a measure
   short enough to hold a line of sight. Knowledge does the same to a
   document's opening paragraph (`knowledge.css:3763`) for the same reason.

   A lede is set SHORT. 50ch at the deck step is about 460px, which both
   keeps the eye's return sweep honest and leaves the caption beside it
   rather than wrapping the row — at 62ch the goal alone was wider than the
   block's own measure and pushed its own label onto the line above. */
.s-goal-text.is-lede {
  font-size: var(--ty-body); font-weight: var(--fw-medium);
  color: var(--ink-primary); line-height: var(--lh-base);
  max-width: 50ch; text-wrap: pretty;
}

/* ── What we are selling ──
   Kind then name, one per row. The kind leads because Service, Solution and
   Product are what the reader is trying to tell apart — that distinction is
   the complaint this block answers. */
.s-sells { display: flex; flex-direction: column; gap: 8px; }
.s-sell { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.s-sell-kind {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-muted); min-width: 64px; flex-shrink: 0;
}
.s-sell-name { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d50); }

/* ── Who is on it, by function ──
   A label column and the people beside it. The old flat row of avatars could
   not say who was doing which job, which is the question the diagram asks in
   those words. 6 inside a row, 12 between — the ratio rule. */
.s-crew { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }
/* ══ THE TITLE SITS ON TOP OF ITS ROW ══════════════════════════════════════

   A 76px caption column beside the content, vertically centred. That worked
   while every row held one line and broke the moment one did not: against
   Team's three lines the caption sat beside the second, and against the
   tally it landed beside the BAR — four figures floating above their own
   label, reading as though they belonged to the row above.

   Baseline alignment does not rescue it either. The value cell is itself a
   flex container, so the row takes ITS baseline, and a multi-line block
   inside one synthesises that from the last line — which put GOAL level with
   the third line of the goal.

   So the title goes above what it names, full width, and the alignment
   question stops existing: there is no cross-axis to get wrong. It is also
   the shape these titles needed anyway — a row title at the head of its row
   reads as a title, where the same words in a narrow left gutter read as a
   margin note.

   The control follows the content it changes, on the same line, instead of
   being pushed to a far right edge by `margin-left: auto` — which is what
   put `Change` a full column away from the Team it changes. */
.s-crew-row { display: block; }
/* The titled bar: name, a rule spanning what the control acts over, the
   control. Same construction as `.s-kicker` a rank above it, which is what
   makes a row title read as a row title rather than as small text that
   happens to be bold. */
.s-crew-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }

/* ── The campaign's identity, as five titled rows ──
   What the Set up stage was: five blocks, five headings, five buttons and a
   page of its own. A title, its values, and the one control that changes
   them — grouped, in the shape the section heads use one rank up.

   ── IT IS A COLUMN, NOT A FULL-WIDTH TABLE ──
   Uncapped, the row stretched the whole stage and put `Change` about 1,400px
   from the `TEAM` it changes. Capped at the measure the goal already uses,
   the rows are one column and every control is inside the group it belongs
   to.

   The gap between rows is 24 against the 12 inside them: two to one, which
   is what tells a reader that the four lines under `Team` are one thing and
   the row below is another. It was 12 against 8 and the block read as nine
   evenly-spaced lines rather than five groups. */
.s-camp-id {
  gap: 16px; margin-bottom: 0;
  max-width: 88ch;
}
/* ══ THE GAP INSIDE A NAME IS NOT THE GAP BETWEEN GROUPS ═════════════════
   Both were 4px, so an avatar sat as close to its own name as one person sat
   to the next group's caption — "ES Engy Saleh Marketing LH Lina Haddad"
   with no boundary anywhere in it. 16 between the items in this run, 8
   inside a person, which is the ratio every other group on the surface
   uses. */
.s-id-vals { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; min-width: 0; }
/* Stacked: one line per function or per offering, under the row's own title. */
.s-id-vals.is-stacked { flex-direction: column; align-items: stretch; gap: 12px; }
/* ── LABEL ABOVE, PEOPLE UNDER, ONE LEFT EDGE ──
   The flush-right 68px column gave every label a different starting x, so
   the three groups had no shared margin and the names sat in a stepped
   indent. Stacked, everything in the row starts at the same edge and "who is
   on BDR" is a glance down it. */
.s-id-line { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
/* ── A FIGURE AND WHAT IT COUNTS, INSIDE AN IDENTITY ROW ──
   Persona and the weekly rate are both "this number, of that thing", which
   is what every stat on this page is — but at the rank a row inside the
   identity block carries rather than the tally's. The figure takes full ink
   and the heaviest weight, the noun stays at secondary: rank by weight, not
   by fading the half that says what the number means. */
.s-persona {
  display: inline-flex; align-items: baseline; gap: 4px;
  font-size: var(--fs-base); color: var(--d200); white-space: nowrap;
}
.s-persona b {
  color: var(--d50); font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}
/* ── WHO IT IS FOR, AND WHO TURNED UP ──
   The aim leads the row because it is the claim the counts beside it are
   measured against — a campaign holding five VPs of Engineering is a fact,
   and whether that is drift depends entirely on this line. */
.s-persona-want {
  font-size: var(--fs-base); color: var(--d200); white-space: nowrap;
}
.s-persona-want b { color: var(--d50); font-weight: var(--fw-semibold); }
/* A title on it that the campaign did not aim at. Toned rather than hidden:
   it is not an error, it is the drift, and the reader decides whether it
   matters. */
.s-persona.is-off b { color: var(--warn-text); }
.s-persona-off {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--warn-text);
  white-space: nowrap;
}

/* ══ WHERE THE MONEY WENT, PER PERSON ══════════════════════════════════════
   A total answers "how much" and none of "which of these to stop", which is
   the question a cost is read for. Three columns: who, what they did, what it
   cost — the figure last and right, so the column can be compared down the
   block, which is the whole reason it is a column.

   Ranked dearest first by `campaignCost`, so the order is the finding. */
/* ══ A CAMPAIGN'S LISTS ════════════════════════════════════════════════════
   Find and Enrich both draw the same row — which list, and one fact about
   what it is doing here. The name is the target because a list is a thing
   you open, and the fact under it is the only thing the stage is asking
   about, so there is nothing to rank against it and it sits plainly below. */
.s-clists { display: flex; flex-direction: column; margin-top: 8px; }
/* Two columns where a row carries a verb: the name and its fact stack on the
   left, the control holds the right edge so the buttons share one down the
   block — which is the only reason a column is a column. */
.s-clist {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; column-gap: 16px; row-gap: 2px;
  padding: 12px 0; border-bottom: 1px solid var(--hairline);
}
.s-clist-facts { grid-column: 1; }
.s-clist-act { grid-column: 2; grid-row: 1 / span 2; justify-self: end; }
.s-clist:last-child { border-bottom: 0; }
.s-clist-name {
  font-family: inherit; font-size: var(--fs-base); font-weight: var(--fw-semibold);
  color: var(--d50); text-align: left; background: none; border: 0; padding: 0;
  cursor: pointer; align-self: flex-start;
  transition: color var(--t-fast);
}
.s-clist-name:hover { color: var(--accent-text); }
.s-clist-name:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }
/* One step down and secondary ink — not faded, because it carries the only
   number on the row and the number is the reason the row is here. */
.s-clist-facts { font-size: var(--fs-xs); color: var(--d200); }
.s-clist-facts b { color: var(--d50); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }

.s-money-crew { display: flex; flex-direction: column; margin-top: 12px; }
.s-money-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline; gap: 16px; padding: 8px 0;
  border-bottom: 1px solid var(--hairline);
}
.s-money-row:last-child { border-bottom: 0; }
.s-money-who {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  font-size: var(--fs-base); color: var(--d50); font-weight: var(--fw-semibold);
}
/* What they did, not dimmed to make the name louder — the name is already
   heavier and the figure is already to the right. */
.s-money-sub { font-size: var(--fs-xs); color: var(--d200); white-space: nowrap; }
.s-money-fig {
  font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--d50);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The caveat under the return. One line, at the size of the block's own
   supporting copy — it qualifies every figure above it and a reader who
   misses it reads all four wrong. */
.s-money-note {
  font-size: var(--fs-xs); color: var(--d200);
  line-height: var(--lh-base); margin-top: 12px; max-width: 72ch;
}
@media (max-width: 719.98px) { /* 720 layout px */
  .s-money-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
  .s-money-sub { grid-column: 1; }
}
/* `.s-id-item` was the offering row — a name, a Knowledge link and a `×`
   sharing one flex line, where the only thing binding the three together was
   the gap between them. `.s-sell-pill` binds them with a border instead, so
   the row wrapper has nothing left to do. */
/* ══ A FIXED COLUMN, AND NO RIVER IN IT ═════════════════════════════════
   Fixed so the names line up down the block and "who is on BDR" is a glance.
   RIGHT-ALIGNED because left-aligned it was 84px of box holding 42px of
   "Owner" — 42px of dead space plus the 12px gap put 54px between a label
   and the value it labels, on every line. Flush right, every label ends at
   one edge and every value starts 12px after it, so the pair is tight and
   the column is still a column. */
.s-id-fn { flex: none; }
.s-id-people { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; min-width: 0; }
/* ══ A CONTROL HAS TO LOOK LIKE ONE ════════════════════════════════════════

   With the rule removed from between them, `Change` landed twelve pixels
   after `TEAM` and read as the second word of the title — two pieces of
   13px text side by side, one of them pressable and nothing saying which.

   It goes to the row's right edge, which is where every one of these has
   sat since the block was built and is what makes `Add`, `Change` and
   `Change` scan as one column of controls. The row is capped at the block's
   own measure, so "the right edge" is a few hundred pixels away rather than
   the fourteen hundred an uncapped row once put between them.

   And it gets a press target: a padded box that fills on hover. A bare text
   link inside a heading is the one place this product's tertiary tier
   genuinely needs an edge, because the thing beside it is not a sentence. */
.s-crew-head > .s-inline-btn {
  flex: none; margin-left: auto;
  padding: 4px 8px; border-radius: var(--r-sm);
  transition: color var(--t-fast), background-color var(--t-fast);
}
.s-crew-head > .s-inline-btn:hover { background: var(--card-bg-raised); }
/* ── SENTENCE CASE, AND THAT IS THE POINT ──
   These were 11px bold uppercase — the same treatment as the row caption
   containing them, one shade apart — so `TEAM OWNER … MARKETING … BDR …
   SALES …` read as eight captions of equal rank and nothing said which were
   the structure. Four things in this block are captions now, and they are
   the four rows. */
/* `--d400`, not `--d500`. Measured against the page: `--d500` is 3.54:1,
   under the 4.5 floor, on the labels that are the entire reason this row is
   scannable — the second-dimmest thing in the block was the thing that says
   what each line is. `--d400` is 6.08. */
/* ══ AN OFFERING IS AN OBJECT, SO IT HAS AN EDGE ═══════════════════════════

   A pill: the kind, the name, and the two controls that belong to it, inside
   one border. Before this they were four loose spans in a wrapping row and
   the only thing grouping an offering with its own `×` was proximity — which
   fails the moment the row wraps, and wrapped on any narrow column.

   The kind leads because it is the classifier and it is short; the name is
   the value and carries the weight. `--card-bg-raised` rather than a border
   alone: elevation by surface step is how this ecosystem separates a
   contained thing from its ground. */
.s-sell-pill {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 4px 8px 4px 12px;
  border: 1px solid var(--card-border); border-radius: var(--r-pill);
  background: var(--card-bg-raised);
  max-width: 100%; min-width: 0;
}
.s-sell-pill-kind {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint); flex: none;
}
.s-sell-pill-name {
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--ink-primary); min-width: 0;
}
.s-sell-pill .s-camp-out { margin-left: 0; }

/* ── WHO IT IS FOR, LEGIBLY ──
   The value here is one or two words and it decides who may see the whole
   campaign, so it is set at the deck step in primary ink — the same rank as
   the goal it sits four rows under. It rendered at the meta step in the
   faint ink, which made the row a title with nothing under it. */
.s-client-name {
  font-size: var(--ty-deck); font-weight: var(--fw-bold);
  color: var(--ink-primary); letter-spacing: var(--ls-tight);
}
/* The qualifier follows the value rather than replacing it. */
.s-client-note {
  font-size: var(--ty-meta); color: var(--ink-faint);
}

/* ── AND NOW THEY CAN BE CAPS AGAIN ──

   These were made sentence-case deliberately: at the time the row captions
   were ALSO 11px bold uppercase, one shade apart, so eight captions at equal
   rank left nothing saying which were the structure and which were inside
   it. That reasoning was right for that scale.

   The row titles are 13 extrabold with a rule now, a clear rank above these,
   so the collision is gone — and the function labels can go back to the
   micro-label idiom the rest of the page uses for exactly this job. Caps and
   tracking make them a different TEXTURE from the names beside them, which
   is what separates a label from a value at one pixel of size difference. */
/* ── UPPERCASE RANKS IT; THE INK DOES NOT HAVE TO ──
   Every caption on this page was small, uppercase, tracked, bold AND dim —
   four devices for one job, and the dimming was the one that cost legibility
   for nothing. The first three still say "this is a label"; the ink stops at
   secondary so the label can be read. */
.s-id-fn {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary);
}

/* The fold is gone: it hid three lines behind a press on a block that is
   only four rows, and a disclosure earns its place by hiding something
   large. Team stopped being large when the unfilled functions stopped
   taking a line each. */
/* ══ THE GOAL OUTRANKS A NAME ═══════════════════════════════════════════
   At `--d200` it measured 11.41:1 — exactly what a person's name measures,
   so the sentence the whole campaign exists to serve read at precisely the
   weight of "Engy Saleh". It is the one thing in this block somebody has to
   read, and now it is the brightest thing in it. */
.s-camp-id .s-goal-text { color: var(--ink-secondary); }
.s-camp-id .s-goal-text.is-lede { color: var(--ink-primary); }

/* ══ THE TALLY ═════════════════════════════════════════════════════════════

   What replaced the eleven-number paragraph. Figures, then the proportion,
   then the one sentence — and the order is the point: the figures are what a
   glance takes, the bar is what the figures cannot show, and the sentence is
   what neither can say.

   The figures are `.s-afs` from the executive surface, one step louder. That
   row sets its values at `--fs-2xl` because it is one of six blocks on a
   page read standing up; here it is the second thing on the page and the
   figure it is competing with is the lead at 60, so it takes the display
   step and the gap between the two stays legible. */
.s-tally { display: flex; flex-direction: column; gap: 16px; }
/* Shared with the funnel's figures: the campaign's two figure rows say what
   happened and what came of it, and reading the same is the point. */
.s-tally-figs, .s-fn-figs { margin-bottom: 0; gap: 20px; }
.s-tally-figs .s-af-val,
.s-fn-figs .s-af-val { font-size: var(--ty-display); letter-spacing: var(--ls-tight); }
.s-tally-figs .s-af-cap,
.s-fn-figs .s-af-cap { color: var(--ink-secondary); }
/* The goal keeps its marker and the biggest loss keeps its tone — the two
   things the rows carried that a bare figure does not. */
.s-fn-figs { margin-bottom: 24px; }
/* The unit, over the row rather than in every caption. `.s-reasons-head`
   carries 12px of padding for the rows it usually sits over; these figures
   have none, so the label lines up with the first of them. */
.s-fn-unit { padding-left: 0; }
.s-fn-figs .s-af-cap { display: flex; align-items: baseline; gap: 8px; }
.s-fn-figs .s-af.is-goal .s-af-cap { color: var(--ink-primary); }
.s-fn-figs .s-af.is-weak .s-af-val { color: var(--warn-text); }
/* The channel breakdown, spanning the grid under the figure it breaks down.
   `Touchpoints 25` is honest but incomplete: eleven of those were calls and
   eight were meetings, and a campaign being worked on the phone is a
   different campaign from one being mailed at. */
.s-tally-mix {
  grid-column: 1 / -1;
  font-size: var(--ty-meta); color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* ── THE PROPORTION, DRAWN ──

   Segments grow by their own count rather than a computed percentage, so the
   arithmetic lives in one place and a one-lead segment cannot round to
   nothing. No animation, so no `width` to thrash: flex does the division at
   layout time and there is nothing here that moves. */
.s-tally-bar {
  display: flex; height: 8px; gap: 2px;
  border-radius: var(--r-pill); overflow: hidden;
}
.s-tally-seg { display: block; min-width: 2px; border-radius: var(--r-pill); }
.s-tally-seg.is-live { background: var(--d600); }
.s-tally-seg.is-won  { background: var(--ok); }
.s-tally-seg.is-lost { background: var(--err); }
/* Find's status spread reuses this bar, and its segments come from
   `TAX.status`, which speaks in tones rather than in this block's three
   named outcomes. */
.s-tally-seg.tone-ok      { background: var(--ok); }
.s-tally-seg.tone-warn    { background: var(--warn); }
.s-tally-seg.tone-err     { background: var(--err); }
.s-tally-seg.tone-neutral { background: var(--d600); }
.s-tally-seg.tone-info    { background: var(--info); }
/* Standing alone under Find's heading it needs the margin the Progress
   block's flex column was giving it. */
.s-stage .s-tally-bar { margin-bottom: 16px; }

/* Status is carried by colour AND text, never colour alone — the design
   system's accessibility rule, and the reason the legend is not optional. */
.s-tally-keys { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.s-tally-key {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-faint);
}
.s-tally-key::before { content: ""; width: 8px; height: 8px; border-radius: var(--r-pill); flex-shrink: 0; }
.s-tally-key.is-live::before { background: var(--d600); }
.s-tally-key.is-won::before  { background: var(--ok); }
.s-tally-key.is-lost::before { background: var(--err); }

/* ══ THE CONCLUSION IS AiMY'S, SO IT CARRIES AiMY'S MARK ═══════════════════

   "4 organizations have had three messages and answered none of them. That
   is the biggest drag on it." `.s-tally-drag` styled this as prose — first
   at body size under the legend, where it read as a footnote, then at the
   deck step, which made it visible but still unattributed.

   Visibility was only half of it. The four figures above are the campaign's
   own arithmetic; this ranks five obstacles, picks one, and calls it the
   thing holding the campaign back. That is a reading, and the doctrine's
   rule is that AiMY's work is never rendered as the product's plain output.
   It is an `.s-insight.is-quote` now — the same treatment as the offering
   block's reading, because it is the same kind of claim — and the rule that
   used to style it has nothing left to style. */
.s-tally .s-insight.is-quote { margin-top: 8px; margin-bottom: 0; }
/* `--d400`, not `--d500`. These four captions are what makes the header
   readable as rows — Goal, Selling, Team, For — and at the dimmest step on
   an 11px caps run they were the least legible thing on the surface while
   being the thing that says what each row is. */
/* ══ THE ROW TITLES CARRY THEIR ROWS ═══════════════════════════════════════

   GOAL, PROGRESS, SELLING, TEAM, FOR at eleven pixels in the faint ink, and
   the rows they name now run to a lede at 18 and a figure at 24. A label
   that much quieter than its content stops reading as a title and starts
   reading as a caption that got lost — and these are the only wayfinding
   this block has.

   Thirteen and extrabold in the quiet ink, on the line above what it names.
   Not primary: they title the rows, they do not compete with them, and this
   block's emphasis belongs to the goal and the figures. */
.s-crew-cap {
  flex: none;
  font-size: var(--ty-meta); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary);
}
/* An empty function says so. A row that simply vanishes when nobody holds it
   answers "who is doing the BDR work here" with silence, which reads as
   somebody rather than as nobody.

   AND IT HAS TO BE LEGIBLE TO SAY IT. `--d600` at `--fs-base` measured
   2.51:1 — the FAINTEST thing in the block and simultaneously the LARGEST,
   set bigger than the 13px labels beside it. An absence is information here;
   it was rendered as though it were noise. On the row's own size step now,
   at `--d400` (6.08), with the italic kept because that is what marks it as
   a gap rather than a name. */
/* "us", "nothing recorded" — a real value, not an apology. It was italic at
   the meta step in the faint ink, which made the answer to "who is this for"
   the quietest thing in its own row. Italic was carrying "this is a default
   rather than a choice"; the row title already carries that, and italic on a
   two-letter word at 13px reads as a rendering fault. */
.s-crew-none { font-size: var(--ty-body); color: var(--ink-secondary); }

/* `.s-trail*` went with `trailBlock`. It drew the sequence — the steps a
   campaign sends and the waits between them — and Reach is the calls to be
   made now. `.s-block-say` survives it: it was declared inside that block
   and is the line every sub-block on the campaign page uses to say what it
   holds. Secondary ink rather than faint, on the rule this pass has been
   applying everywhere — a line that explains the block is not the half to
   dim. */
.s-block-say {
  font-size: var(--ty-meta); color: var(--ink-secondary);
  max-width: 62ch; margin: -8px 0 16px;
}

/* ═══════════════════════════════════════════════
   ANALYTICS AS CARDS

   Clay reports campaign numbers as five metric cards and three engagement
   panels — not a table, on a screen whose entire job is numbers. The figure
   leads and is large; the caption names it; the qualifier sits under both,
   because a rate without its denominator is a number you cannot argue with.
═══════════════════════════════════════════════ */
/* 16 between against 12 inside — the same rule at metric-card scale. */
.s-metrics {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(132px, 100%), 1fr));
  gap: 16px; margin-bottom: 12px;
}
.s-metric {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px; border-radius: var(--r-lg);
  border: 1px solid var(--card-border); background: var(--card-bg);
}
/* Each metric card is about exactly one number, which is the definition of
   the anchor step. At the title step they read as the name of something
   rather than as the figure itself. */
.s-metric-v {
  font-size: var(--fs-3xl); font-weight: var(--fw-extrabold);
  line-height: var(--lh-tight); letter-spacing: var(--ls-tight);
  color: var(--d50); font-variant-numeric: tabular-nums;
}
.s-metric-cap { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d200); }
.s-metric-sub { font-size: var(--fs-xs); color: var(--ink-muted); }
/* Tone tints the ground, not the figure: a red number reads as an error in
   the number rather than as a fact about the thing it counts. */
.s-metric.tone-ok { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok) 24%, transparent); }
.s-metric.tone-warn { background: var(--warn-bg); border-color: color-mix(in srgb, var(--warn) 24%, transparent); }
.s-metric.tone-err { background: var(--err-bg); border-color: color-mix(in srgb, var(--err) 24%, transparent); }

/* Replies, one row per category. */
.s-replies { display: flex; flex-direction: column; gap: 2px; }
.s-reply-row {
  display: flex; align-items: baseline; gap: 12px;
  width: 100%; min-height: 24px; padding: 8px; border-radius: var(--r-sm);
  text-align: left; cursor: pointer; transition: background-color var(--t-fast);
}
.s-reply-row:hover { background: var(--accent-dim); }
.s-reply-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* `.s-reply-name` and `.s-reply-n` are gone with the campaign's replies
   list, which is `.s-reason` now like every other count-and-a-door on that
   page. The row shell survives for the surfaces that still use it. */

/* ── The decision zone's two halves ──
   What it does, then a ruled-off "Does not" — separated because they are
   different claims, and the second is the one a reader skips when it is
   formatted like the first. */
/* Reversibility and who is deciding. Quiet, last, and always present — an
   action that will not say whether it can be undone is one you must assume
   cannot be. */
.s-effects-undo { flex: 1 1 100%; }

/* ═══════════════════════════════════════════════
   FIND COMPANIES

   Criteria as removable chips over a count, rather than a filter builder
   over a preview grid. The `×` on an active chip and the `+` on a dropped
   one say which way pressing it goes — a chip that only changes colour
   makes you press it to find out.
═══════════════════════════════════════════════ */
.s-find { display: flex; flex-direction: column; }

/* ══ THREE STEPS, ONE OPEN ═════════════════════════════════════════════════

   The block held nine sections at one visual level: 1,927px of content in a
   379px scroll viewport, 27 controls, and a `Save 8` pinned to the foot
   above 1,100px nobody had seen — the rows it counted began at 330px, the
   name at 619px, the owner at 690px.

   A closed step keeps its head and shows ITS ANSWER on the same line, so
   all three answers are legible above the confirm at all times; only the
   open one draws a body, which is what makes that fit. Airbnb's listing
   rail is the reference — `Location 10640`, `Photos 5 photos` — and Circle's
   checklist is the same move in a single column.

   Hairlines between, not boxes. Three bordered cards inside a bordered
   panel inside a bubble is three nested regions to say one thing, and the
   step heads already carry a number apiece. */
/* The number IS the step marker. A tick would say a question was answered
   and not what the answer was, and the answer is the whole point of the
   line it sits on. */
/* What you answered, on the head, right-aligned so the three read down a
   column rather than trailing three different name lengths. */

/* ── WHERE YOU START, AND THAT IT IS A CHOICE ──
   Label over strip rather than beside it: at this width a right-hand segment
   would leave each button about forty pixels, and the two names are the
   whole point of the control. */
/* ── WHAT AiMY UNDERSTOOD, AS CHIPS YOU CAN TAKE BACK OFF ──
   Not a menu you picked from: these are the criteria read out of the sentence
   you typed, so each carries the × that says "you read that wrong". */
.s-find-crit { display: flex; flex-wrap: wrap; gap: 4px; }
.s-crit-x { margin-left: 8px; opacity: 0.6; font-weight: var(--fw-bold); }

/* Same 15px box as every other pick on a commit surface. */

/* The reading sits directly under the sentence it was read from, and the
   note beside the button says how much was understood — so a description
   that yields nothing says so where you are typing, not further down. */
.s-find-said { min-height: 56px; resize: vertical; line-height: var(--lh-base); }
.s-find-new { flex: 1; min-width: 0; }
/* Yours reads as yours. Same size and same shape as AiMY's, one border
   apart — a criterion you wrote is not a different KIND of criterion. */

/* ═══════════════════════════════════════════════
   THE WATERFALL

   An ordered chain per field, numbered, with each link's hit rate on its
   face. The order is the whole idea — best first, first answer stops the
   chain — so the numbers are shown rather than implied by position alone.
═══════════════════════════════════════════════ */
.s-water { display: flex; flex-direction: column; gap: 16px; }
.s-water-field { display: flex; flex-direction: column; gap: 8px; }
.s-water-head { display: flex; align-items: baseline; gap: 8px; }
.s-water-name { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50); }
.s-water-n { font-size: var(--fs-2xs); color: var(--ink-muted); }
.s-water-chain { display: flex; flex-direction: column; gap: 2px; list-style: none; margin: 0; padding: 0; }
.s-water-step {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px; border-radius: var(--r-sm);
  background: var(--card-bg);
}
.s-water-i {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--ink-muted);
  font-variant-numeric: tabular-nums; flex-shrink: 0;
}
.s-water-p { font-size: var(--fs-xs); color: var(--d200); min-width: 0; }
.s-water-hit { margin-left: auto; flex-shrink: 0; font-size: var(--fs-xs); color: var(--ink-muted); }

/* ── Organizations and contacts ──
   A count, an insight, and a handful of names. Names as chips rather than
   cards: they are evidence for the sentence above them, not things to be
   read one by one — which is the distinction that keeps this block from
   becoming the inventory it replaced. */
.s-group-count { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); max-width: 72ch; }
.s-group-count b { color: var(--d50); font-variant-numeric: tabular-nums; }

/* `.s-camp-count` was the blocked figure, set in the caption step at the far
   right of the heading row — the argument being that "20 of 31" is what the
   rows add up to, so a second thing at heading size would read as a second
   heading. The premise was right and the conclusion went one step too far:
   the answer to "do not make it a heading" is not "make it the smallest and
   faintest thing in the block, in the one column a text-heavy page is not
   read in". `.s-sect-fig` is that figure, on the left, at its own step —
   which is neither a heading nor a whisper. */

/* `.s-offer-say` was the reading, set under its own evidence at caption
   weight — the one sentence on that block anybody was meant to act on,
   styled as a footnote. It is an `.s-insight` now: AiMY's mark, the accent
   panel, the action last. A conclusion that looks quieter than the counts it
   was drawn from is a conclusion nobody reads. */

/* ── Lead lists, for Marketing ── */
/* 24 between lists against 4 inside one — the ratio rule, and it is what
   stops four lists reading as one paragraph of criteria. */
/* ── A LIST IS A CARD NOW, NOT A ROW IN A TAB ──
   It was a bare flex column: no ground, no border, four facts and three grey
   buttons, stacked inside an opened tab where the tab's own frame did the
   containing. On the page there is no frame, so 12 lists would read as one
   long column of text — Law of Common Region, which is the strongest
   grouping cue there is and the one this had none of. */
/* Two across, and `auto-fill` rather than a literal `repeat(2, 1fr)` so the
   second column collapses on a narrow viewport instead of squeezing two
   cards into 200px each. 360 is the width at which the criteria line stops
   breaking mid-phrase — measured, not chosen. */
.s-lists {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  gap: 16px; align-items: start;
}
.s-listrow {
  display: flex; flex-direction: column; gap: 4px;
  padding: 16px; border-radius: var(--r-lg);
  border: 1px solid var(--card-border); background: var(--card-bg);
  /* The stretched link's containing block, and the hover feedback for a
     target that is now the whole card rather than one line of it. */
  position: relative;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.s-listrow:hover { border-color: var(--card-border-hover); }
/* The name's hit area, grown to the card. `inset: 0` on the card's padding
   box covers the facts and the reading; the two controls below sit above it
   on their own stacking context. */
.s-listrow-name::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
/* Focus rings the CARD, because the card is what the name now covers — and
   it is drawn on the stretched pseudo-element rather than through `:has()`,
   which this file has twice had stop matching silently. A call on the button
   itself would circle one line of text and claim a target that is 210px
   tall. */
.s-listrow-name:focus-visible { outline: none; }
.s-listrow-name:focus-visible::after { outline: 2px solid var(--brand); outline-offset: -2px; }
/* 4 inside against 16 between — the ratio rule, so three cards read as three
   things rather than as one column. */
/* The operations, last and quiet. A list you cannot act on is a list nobody
   needs to see — but the verbs are not what you read the card for, so they
   sit under the facts that tell you whether to use them. */
/* `position: relative` lifts these above the stretched link, so each keeps
   its own target and its own tab stop. */
.s-listrow-ops {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 8px;
}
/* ══ THE NAME AND THE STATE, ON ONE LINE ═══════════════════════════════════
   The chip was a block inside the reading, so across six cards the states
   sat at six different heights — aligned to the length of the criteria above
   them rather than to each other. Nothing about a set of cards is more worth
   reading down a column than which of them is finished, and it was the one
   thing the column could not carry. Right-aligned against the name, the same
   place the task card's state went in this pass. */
.s-listrow-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
/* ── THE CARD'S OWN MASTHEAD ──
   `--fs-lg` against a 14px reading is 1.14:1, which is not a hierarchy, it
   is a rounding error — and the card's largest element was therefore
   whichever sentence happened to be longest. `--ty-title` puts a real step
   between the name and everything under it, and keeps the house rule the
   campaign page settled: the name of the thing is the top of its own scale,
   above any figure drawn inside it. */
.s-listrow-name {
  font-family: var(--font-display);
  font-size: var(--ty-title); font-weight: var(--fw-bold); color: var(--ink-primary);
  letter-spacing: var(--ls-tight); line-height: var(--lh-tight);
  text-align: left; cursor: pointer; min-height: 24px;
}
.s-listrow-name:hover { color: var(--ai-text); }
/* No call on the button. The rule below `.s-listrow-name::after` already
   draws the focus call on the stretched pseudo-element, because that is what
   the target actually is — and a second `:focus-visible` rule further down
   this file was overriding it back onto one line of text, which is the whole
   defect that comment was written to prevent. One call, on the card. */
/* ── WHAT IT COLLECTS ──
   A subtitle, so it sits tight under the name rather than floating as its
   own paragraph: the card's gap is 4px and only the beats below it buy
   space. */
.s-listrow-crit { font-size: var(--ty-meta); color: var(--ink-quiet); line-height: var(--lh-relaxed); max-width: 72ch; }
/* ══ THE FIGURE THE CARD IS ABOUT ══════════════════════════════════════════
   `1,240 matched the criteria · Engy Saleh owns it · runs itself` — one run
   of 13px `--d400` prose in which the only emphasised thing was the owner's
   name. The number the card exists to state was set at the faintest step on
   it, unbolded, while the least decision-relevant fact in the line carried
   `--d100` at 800.

   Both counts now sit here, both labelled, and the emphasis is on the two
   numbers rather than on the person. Tabular figures because six of these
   stack in a column and a column of proportional digits cannot be compared
   down its own length. */
.s-listrow-fig {
  font-size: var(--ty-meta); color: var(--ink-faint); margin-top: 8px;
}
.s-listrow-fig b {
  font-size: var(--ty-heading); font-weight: var(--fw-semibold);
  color: var(--ink-primary); letter-spacing: var(--ls-tight);
  font-variant-numeric: tabular-nums;
}
/* ── What AiMY concluded, between the figure and the verbs ──
   Where a reader wants the thing that makes the verb make sense: the figure
   says what the list HOLDS, this says what is wrong with it, the button acts
   on what it just said. Same conclusion → action order as `.bcard` and the
   rail's rows. */
.s-listrow-say {
  font-size: var(--ty-body); color: var(--ink-secondary);
  line-height: var(--lh-base); max-width: 72ch; margin-top: 8px;
}
.s-listrow-say b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
/* Whose it is, and whether it keeps itself — attribution, so it sits under
   the sentence it qualifies at the quietest step the card uses. */
.s-listrow-facts { font-size: var(--ty-meta); color: var(--ink-faint); }



/* ══ THE LIST BUILDER ══════════════════════════════════════════════════════

   A page, not a panel — the canvas version caps at `min(72vh, 520px)` and put
   nine sections inside 379px of scroll.

   AND NOT AN ACCORDION EITHER. The first cut of this page was five collapsing
   steps holding six grids of options with a count on every chip: eighty-odd
   controls to answer a question you could have said in a sentence. A grid of
   options asks you to recognise the product's vocabulary before you are
   allowed to use it. So the criteria are TYPED, AiMY reads them back as chips
   you can take away, and the only things you press are two cards and one
   button. */

/* ── TWO CARDS, NOT A CHOICE LIST ──
   Each carries its own reason, because a card with only a name makes you
   press one to find out what it does. */
/* The page's own stack. One rhythm for every state it has — the two cards,
   the sentence and its suggestions, the rows arriving, and the list at the
   end — so moving between them does not re-space the page under you. */
.s-build-page .s-rec-main { display: flex; flex-direction: column; gap: 16px; }
.s-build-page .s-sheet-head { padding-bottom: 0; }

/* 24 between, 20 inside. More space around a card than within it, or two of
   them read as one surface with a line down the middle. */
.s-ways { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 24px; margin-top: 8px; }
.s-way {
  display: flex; flex-direction: column; gap: 8px;
  padding: 20px; text-align: left; cursor: pointer;
  border: 1px solid var(--card-border); border-radius: var(--r-lg);
  background: var(--card-bg); font-family: inherit;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.s-way:hover { border-color: var(--card-border-hover); background: var(--card-bg-raised); }
.s-way:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-way-name {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-display); font-size: var(--ty-title); font-weight: var(--fw-bold);
  color: var(--ink-primary); letter-spacing: var(--ls-tight);
}
.s-way-why { font-size: var(--ty-meta); color: var(--ink-faint); line-height: var(--lh-base); }
.s-way-why b { color: var(--ink-secondary); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }

/* ── WHAT IT HEARD, WHERE THE FIELD USED TO BE ──
   The textarea has gone to the bar at the foot of the surface. What is left
   is the sentence itself, at the step it deserves: it is the whole input to
   everything below it, so it reads as a statement rather than as the value
   of a field. Empty, it says where to type instead of leaving a blank. */
.s-said {
  font-size: var(--ty-body); color: var(--ink-primary);
  line-height: var(--lh-base); max-width: 72ch; margin: 0;
}
.s-said.is-empty { font-size: var(--ty-body); color: var(--ink-secondary); }

/* ── AND THE NAME IS A FIELD, NOT A CAPTION ──
   Sized and coloured by the `h1` it sits inside, so it reads as the heading
   it replaces until you hover it. The rule underneath is the whole
   affordance: no box, no label, no pencil — the heading simply turns out to
   be typeable, which is what every product that lets you name a thing before
   making it does. */
.s-build-name {
  width: 100%; font: inherit; color: inherit;
  letter-spacing: inherit; line-height: inherit;
  background: none; border: 0; border-radius: 0; padding: 0;
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-fast);
}
.s-build-name:hover { border-bottom-color: var(--card-border-hover); }
.s-build-name:focus { outline: none; border-bottom-color: var(--accent); }
.s-build-by { width: 15px; height: 15px; accent-color: var(--accent); }

/* ══ WHAT AiMY NOTICED, AND THE ONE PRESS THAT APPLIES IT ══════════════════
   Not a pre-fill. Each row is something derived from the book or the index,
   and the verb beside it is what accepting it would do — so the criteria on
   screen are always the ones you asked for. */
/* ── AiMY's nudges WHILE the criteria are being written ──
   This block no longer draws on a saved list: there it was a second marked
   panel stacked over `.s-findings`, one reading the list and one reading the
   leads in it, with the count line wedged between them. Those rows were
   already the shape `.s-finding` draws, so they are `.s-finding`s now.

   What is left is the moment it was built for — the describe step, where
   there is no list yet and nothing to count, only a criterion worth adding.
   No count column for that reason. The verb is `.s-finding-go`, the same one
   the merged block uses, because both send AiMY to do something. */
.s-sugg {
  background: var(--card-bg-raised); border-radius: var(--r-lg);
  padding: 16px; max-width: 76ch;
}
.s-sugg-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: 12px; padding: 8px 0;
}
.s-sugg-row + .s-sugg-row { border-top: 1px solid var(--hairline); }
.s-sugg-say { font-size: var(--ty-meta); color: var(--ink-secondary); line-height: var(--lh-base); }
.s-sugg-say b { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* ══ THE LIST ARRIVING ═════════════════════════════════════════════════════
   A search that returns instantly reads as a lookup. This one is going out to
   four named suppliers, and the rows landing one after another is what that
   actually looks like — it is also the only moment the flow has to show its
   work, because afterwards the list simply exists.

   `transform` and `opacity` only, under 300ms, and the whole thing is skipped
   at the source under `prefers-reduced-motion` — `buildRun` writes
   immediately rather than animating faster. */
.s-stream-cap { font-size: var(--ty-meta); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.s-stream-n { color: var(--ink-primary); font-weight: var(--fw-bold); }
.s-stream { display: flex; flex-direction: column; max-height: 420px; overflow-y: auto; }
.s-stream-row {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 0; border-bottom: 1px solid var(--hairline);
  animation: s-stream-in 200ms var(--ease-out, ease-out) both;
}
.s-stream-name { font-size: var(--ty-body); font-weight: var(--fw-bold); color: var(--ink-primary); }
.s-stream-facts { font-size: var(--ty-meta); color: var(--ink-faint); }
.s-stream-row.is-yours .s-stream-facts { color: var(--accent-text); }
@keyframes s-stream-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .s-stream-row { animation: none; }
}

/* ══ THE TOTAL, ONCE ════════════════════════════════════════════════════════
   A hairline above rather than a box: the foot ends the page, it is not a
   separate region. */
.s-build-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 16px;
  border-top: 1px solid var(--card-border); padding-top: 16px; margin-top: 8px;
}
.s-build-total {
  flex: 1 1 24ch; min-width: 0;
  font-size: var(--ty-body); color: var(--ink-secondary); line-height: var(--lh-base);
}
.s-build-total b {
  color: var(--ink-primary); font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}
.s-build-go { white-space: nowrap; }
/* `.s-build-keep` and `.s-build-drop` went with the interstitial preview
   stage. Deciding to keep or throw a list now happens on the drafted list
   itself — see `.s-listhead-decide` below. */

/* ══ THE STEPS, FOLDED ══════════════════════════════════════════════════
   Once the list exists the criteria stop being a form and become a caption on
   the thing they produced — still pressable, because refining is the
   commonest next move. */
.s-build-done { display: flex; flex-direction: column; gap: 16px; }
/* `.s-build-sum` and its two children are gone. They drew the saved list's
   criteria as chips directly above `listHead`, which draws the same criteria
   as its own editable line — the same facts twice, with two different ways
   to change them eight pixels apart. */

/* `.s-build-ai` is gone. It drew the campaign fit as a standalone marked
   paragraph ending "Adding this list there is one press below" — an insight
   with no control, pointing at one somewhere else. It is a row of `.s-sugg`
   now, where every other thing AiMY offers about a list already lives, and a
   row there comes with its own press. */

/* ══ STACKED, AND ALL FOUR ACTUALLY STACKED ════════════════════════
   `column` + `align-items: stretch` was half the job and read as none of it.
   Two of these four are bare buttons and stretch — with their labels centred,
   because that is what a button does with a label it has room for. The other
   two are wrapped in `.b-menu-wrap`, which is `inline-flex`, so they took
   their natural width and sat left. Four controls at four different
   horizontal positions, none of them lining up with anything: Add to campaign
   on the left, Save as draft near the middle, Assign people back on the left,
   Discard near the middle again.

   ONE LEFT EDGE FOR THE THREE THAT ARE WORDS, and the primary keeps the
   centred label a filled button wants. That is the only difference between
   them here, and it is the one that says which is which. */
@media (max-width: 719.98px) { /* 720 layout px */
  .s-build-foot { flex-direction: column; align-items: stretch; gap: 12px; }
  /* The wrapper has to stretch before the control inside it can. It is
     `inline-flex` for the menu it anchors, which is right everywhere the
     foot is a row and wrong the moment it is a column. */
  .s-build-foot > .b-menu-wrap { display: flex; }
  .s-build-foot > .b-menu-wrap > .b-menu-open { width: 100%; }
  .s-build-foot .s-inline-btn,
  .s-build-foot .b-menu-open:not(.s-build-go) {
    justify-content: flex-start; text-align: left;
  }
}


/* ══ THE ROWS A TABLE WOULD HAVE HEADED ════════════════════════════════════

   Four fixed tracks, so the eye reads DOWN a column instead of across each
   row in turn — and no header, because every value says what it is on its
   own: "480 staff", "founded 2012", "bunq.com". That is the whole difference
   from a table. A table needs a legend at the top and loses it the moment you
   scroll; these rows carry their own.

   Two lines per cell, the second quieter: what it is over where to find it,
   what sector over what place, how big over how old, what state over whose. */
.s-brows { display: flex; flex-direction: column; }

/* ══ ONE ROW, AND YOU CAN SEE ITS RANK FROM ACROSS THE DESK ════════════════

   Everything on this row was within one step of everything else — a bold
   14px name over 13px and 11px greys — so it read as a paragraph with a
   heading rather than as a record with fields. Four steps now, and they are
   far enough apart to be steps:

     20  the name          what it is, and the thing you press
     14  what they do      the line the row exists for
     13  where and when    the facts that qualify it
     11  the addresses     the only things here you would copy

   And the right edge carries the two figures you compare between rows — the
   headcount, and the revenue the enrichment offer above fills in. */
.s-brows { display: flex; flex-direction: column; }
/* Three columns. A fourth held the verb for one pass and cost every row
   192px of reserved nothing at the right edge, whether or not it had a verb
   to put there — the price a fixed button column charges for keeping the
   figures beside it aligned. The verb sits under the row it belongs to now,
   so the figures end the row and the row ends where the figures do. */
.s-brow {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start; column-gap: 24px;
  padding: 16px 12px; margin-left: -12px; border-radius: var(--r-md);
  border-bottom: 1px solid var(--hairline);
  transition: background-color var(--t-fast);
}
.s-brow:last-child { border-bottom: 0; }
.s-brow:hover { background: var(--card-bg-raised); }
.s-brow.is-picked { background: var(--accent-dim); }
.s-brow-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.s-brow-nopick { width: 0; }

.s-brow-name {
  font-family: var(--font-display);
  font-size: var(--ty-title); font-weight: var(--fw-bold); color: var(--ink-primary);
  letter-spacing: var(--ls-tight); line-height: var(--lh-tight);
  text-align: left; cursor: pointer; background: none; border: 0; padding: 0;
  transition: color var(--t-fast);
}
.s-brow-name:hover { color: var(--accent-text); }
.s-brow-name:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* The one move, after the four lines that say who this is — the row's own
   ladder runs 20 → 14 → 13 → 11 and closes, and the verb opens a new beat
   under it. 8px rather than the block's 4px gap, because it is a different
   kind of thing from the facts above it and not the next one of them. */
.s-brow-ops { margin-top: 8px; }
/* ══ RANK BY SIZE AND WEIGHT, NOT BY FADING ════════════════════════════════

   The ladder ran 20 → 14 → 13 → 11 and each step also lost ink: primary,
   secondary, faint, quiet. Two devices doing one job, and the cheaper one —
   dimming — carried most of it, so the bottom of every row was a pale 11px
   grey that had to be leaned into to read. A URL you have to lean in for is
   a URL you will not check, which was the whole argument for putting the
   addresses on the row.

   Now the ladder is 20 → 16 → 14 → 14 and the ink stops at SECONDARY. Size
   and weight rank it; nothing on the row is dimmed to make something else
   look important. The smallest thing here is 14px, which is the body step,
   not a caption step. */
.s-brow-desc { font-size: var(--ty-heading); color: var(--ink-primary); line-height: var(--lh-base); }
.s-brow-facts { font-size: var(--ty-body); color: var(--ink-secondary); }
.s-brow i { font-style: normal; color: var(--ink-secondary); }

.s-brow-links { display: flex; flex-wrap: wrap; gap: 4px 16px; min-width: 0; margin-top: 2px; }
/* Addresses at the body step, not the caption step. Not uppercased and not
   truncated to a word: a URL you cannot read is a URL you cannot check, and
   at 11px in quiet grey that was true of both of these. They rank below the
   facts above them by being links rather than by being fainter. */
.s-brow-link {
  font-size: var(--ty-body); color: var(--ink-secondary);
  text-decoration: none; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 100%;
  transition: color var(--t-fast);
}
.s-brow-link:hover { color: var(--accent-text); text-decoration: underline; text-underline-offset: 3px; }

.s-brow-side { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; text-align: right; }
/* The two figures you compare BETWEEN rows, so they take the largest step
   on the right and full ink — and the two under them stop at secondary
   rather than fading away. Same rule as the left: rank by size, not by
   dimming. */
.s-brow-fig {
  font-size: var(--ty-deck); font-weight: var(--fw-bold);
  color: var(--ink-primary); font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-tight); white-space: nowrap;
}
.s-brow-rev {
  font-size: var(--ty-body); color: var(--ink-secondary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.s-brow-tag {
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary); white-space: nowrap; margin-top: 4px;
}
@media (max-width: 719.98px) { /* 720 layout px */
  .s-brow { grid-template-columns: auto minmax(0, 1fr); row-gap: 8px; }
  .s-brow-side { grid-column: 2; align-items: flex-start; text-align: left; }
}

/* ── The call queue, for Sales ──
   Name, who they are, why now, and the control that starts it. The reason
   sits between the person and the button on purpose: it is what makes the
   button pressable rather than arbitrary. */
/* 12, not 16. A row is two lines now rather than four, and the separation
   that read as generous around 38px of content read as loose around 76px —
   the gap plus the divider's padding was nearly as tall as the person. */
.s-queue { display: flex; flex-direction: column; gap: 12px; }
/* ── TWO LINES, NOT FOUR ──
   Name·role·account on the first, why·draft-state on the second, and the
   Call button beside both. It was four grid rows — 110px a person, so six
   people were 659px and the Reach node ran past the screen it is supposed
   to be readable on. */
.s-qrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; align-items: baseline; }
.s-qrow-id {
  grid-column: 1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
}
.s-qrow-name {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50);
  text-align: left; cursor: pointer; min-height: 24px;
}
.s-qrow-name:hover { color: var(--ai-text); }
.s-qrow-name:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Both are lowercase sentences, so both belong on the meta step rather than
   the caps-only caption step. */
.s-qrow-sub { font-size: var(--fs-xs); color: var(--d200); }
/* WRAPS, NEVER TRUNCATES. Why this person is worth ringing and whether
   there is a line to read them are one question — should I call this one
   next — so they share a line, and a narrow column moves the second half
   down rather than clipping it. */
/* `--d200`, not `--d400`. This line carries the reason a person is ranked
   where they are and what last happened to them — the two things a caller
   reads before pressing Call — and it was set two steps below the name it
   qualifies in ink as well as size. Size ranks it; dimming it as well made
   the deciding half of the row the faint half. */
.s-qrow-why {
  grid-column: 1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  font-size: var(--fs-xs); color: var(--d200); line-height: var(--lh-relaxed);
}
.s-qrow-because { min-width: 0; }
/* What last happened to them, beside why they are ranked — two halves of one
   answer to "should I call this one next", so they share a step and a line
   and are told apart by the row's own gap. */
.s-qrow-last { min-width: 0; }
/* WHETHER there is a line, not what it says — and attributed, because
   "AiMY drafted" and "Lina wrote" are different claims. */
.s-qrow-lead {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.s-qrow-none { color: var(--warn); }
/* Two rows, and the Call button stands beside both. */
.s-qrow-go { grid-row: 1 / span 2; grid-column: 2; align-self: center; }
/* ── ONE PERSON PER ROW, AND YOU CAN SEE WHERE ONE ENDS ──
   Six people ran together as one block of text: the queue is a flex column
   with a gap and no boundary, so "Femke…" and "Lieke…" were separated by the
   same distance as the lines inside each of them. A hairline is the cheapest
   thing that says where a person stops. */
.s-queue > .s-qrow + .s-qrow {
  border-top: 1px solid var(--hairline); padding-top: 12px;
}

/* ── ONE SENTENCE AND ONE WAY IN ──

   These rows replaced three columns of counts. The sentence carries its own
   figure, so nothing here is a number in its own cell — and the action is a
   link rather than a button, because on a client's surface every one of them
   opens a set and none of them writes.

   A rule down the left in the row's own tone: Common Region at the cheapest
   possible weight. Boxing eight of these would draw eight containers around
   content that is already contiguous, and the tone is the one thing that
   separates "22 addresses are wrong" from "3 have just been funded". */
.s-straights { display: flex; flex-direction: column; gap: 16px; }
.s-straight {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  padding-left: 12px; border-left: 2px solid var(--d700);
  max-width: 72ch;
}
.s-straight.tone-err  { border-left-color: var(--err); }
.s-straight.tone-warn { border-left-color: var(--warn); }
.s-straight.tone-ok   { border-left-color: var(--ok); }

/* ══ THE REASON ROW ════════════════════════════════════════════════════════

   One target, the full width of the block: the figure, the sentence, and a
   chevron that says it opens. What it replaces was four columns — figure,
   track, sentence, link — and each of the three that are gone was reporting
   something the figure had already reported.

   THE TRACK. Obstacle counts on a real campaign run 4 / 4 / 3 / 3 / 2.
   Against the leader that draws as 100 / 100 / 75 / 75 / 50 — five bars
   within a quarter of each other; against the campaign it draws five stubs.
   QA argues for a compressed scale where six portfolios sit between 79 and
   94 per cent, because there the eye genuinely cannot rank them unaided.
   Single digits beside single digits are not that case, and drawing a bar
   anyway put a column of travel between a number and its own sentence.

   THE UNIT, five times. THE LINK, repeating the figure it sat beside —
   "Show me the 4" next to a 4, five identical targets with nothing ranking
   them, on rows whose whole purpose is to be ranked.

   Hover fills the row rather than underlining a word inside it: the target
   is the row, so the feedback is the row. It bleeds 12px left of the text
   column so the fill reads as a band the content sits in, and the figures
   stay on the same left edge as every section head above them.
*/
/* ── DENSITY IS THE POINT ──

   Five rows at 60px each, in a 76ch container with the chevron two hundred
   and fifty pixels past the end of the sentence, is not a list — it is five
   paragraphs with numbers. Mobbin's analytics breakdowns (Semrush's topic
   table, Amplitude's segment rows) all run one line per row at a tight
   rhythm, because a breakdown is read by COMPARING rows and comparison
   wants them close enough to hold in one look.

   One line each, 40px tall, and the container capped where the sentences
   actually end so the chevron sits at the end of the content rather than at
   the end of the page. `gap: 0` — the hover band is the separation, and two
   bands touching read as a list where two bands with a gap read as cards.

   ── AND THE COLUMN IS SIZED TO THE NUMBER ──

   40px of column plus a 16px gap put 56px between the start of the row and
   its sentence, for figures that are one digit wide. Half of every row was
   the space between its two halves. 32 and 12 — enough for a two-digit
   count, which is the largest this walk can produce on a campaign of any
   size worth reading — and the pair reads as one line instead of two
   columns that happen to be adjacent. */
.s-reasons { display: flex; flex-direction: column; }
/* The column header, sitting directly over the figures on the same left edge
   — the row's own 12px padding is what lines it up, so the label and the
   numbers under it share an origin without a second set of numbers to keep
   in step. Named once here rather than repeated on every row. */
.s-reasons-head {
  padding: 0 12px 8px;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint);
}
.s-reason {
  display: grid; grid-template-columns: 32px minmax(0, 1fr) 16px;
  align-items: baseline; gap: 12px;
  width: 100%; max-width: 70ch;
  margin-left: -12px; padding: 8px 12px;
  background: none; border: 0; border-radius: var(--r-md);
  font-family: inherit; text-align: left; cursor: pointer;
  transition: background-color var(--t-fast);
}
.s-reason:hover { background: var(--card-bg-raised); }
.s-reason:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* ── THE ROW WHOSE SET YOU ARE LOOKING AT ──
   Only Find's status spread needs this: those rows narrow the list beneath
   them rather than opening a new surface, so one of them is always the
   answer to "what am I looking at". A filled surface and the accent, the
   same pair the pipeline cell uses for the same reason — it is a selection,
   and this page says selection one way. */
.s-reason.is-on { background: var(--accent-dim); }
.s-reason.is-on .s-reason-say { color: var(--ink-primary); }
.s-reason.is-on .s-reason-go { transform: rotate(90deg); }
/* ── THE UNIT LEFT THE ROW ──

   `ORGS` under every figure, five times, on rows that all count the same
   thing — and the one that read `ADDRESSES` was wrong: `inWayOf` walks
   accounts, so it is three organizations with a bad address. The noun is
   stated once in the section's qualifier, which is what lets the row be a
   single line and every sentence share one left edge. */
.s-reason-n {
  font-size: var(--ty-display); font-weight: var(--fw-extrabold);
  line-height: var(--lh-tight); letter-spacing: var(--ls-tight);
  color: var(--ink-primary); font-variant-numeric: tabular-nums;
}
/* ── THE SENTENCE IS THE ROW, NOT ITS CAPTION ──
   Body against a 20px figure read as a label under a number. These are the
   findings — the thing a reader is here to take — so they take the block
   heading step, and the figure goes to display to keep the rank. 24 over 16
   against 800 over 400 in two inks: three axes of difference, which is what
   a rank needs when the sizes are only one step apart. */
.s-reason-say {
  font-size: var(--ty-heading); font-weight: var(--fw-medium);
  line-height: var(--lh-base);
  color: var(--ink-secondary); text-wrap: pretty;
}
/* Visible standing, not revealed on hover: an affordance that only exists
   under a pointer does not exist on a touch screen. */
.s-reason-go {
  width: 16px; height: 16px; color: var(--ink-faint);
  transition: color var(--t-fast), transform var(--t-fast);
}
.s-reason:hover .s-reason-go { color: var(--ink-quiet); transform: translateX(2px); }

/* ── THE SAME ROW, QUIETER ──
   The offering block reads a fifth of the evidence this one does. Same
   construction, one step down on the figure and a narrower measure:
   repetition WITH variation, which reads as "same kind of thing, less of
   it" rather than as the same block twice. */
.s-reasons.is-quiet .s-reason { max-width: 56ch; }
.s-reasons.is-quiet .s-reason-n { font-size: var(--ty-title); }
.s-reasons.is-quiet .s-reason-say { font-size: var(--ty-body); }

@media (max-width: 719.98px) { /* 720 layout px */
  /* The 40px figure column holds; only the measure gives way. */
  .s-reason, .s-reasons.is-quiet .s-reason { max-width: none; gap: 12px; }
}

/* ══ THE FUNNEL ════════════════════════════════════════════════════════════

   Five rows, one population, narrowing. Every row counts ORGANIZATIONS and
   every bar is drawn against the top of the funnel, which is what makes the
   rows divide into each other — the three figures this replaced counted
   events, a percentage of deliveries and a percentage of email opens, and no
   two of them shared a denominator.

   The bar is honest here for the same reason it is honest on the coverage
   meters and dishonest on the ranked rows: there is a real whole, every row
   is a part of it, and the narrowing IS the finding. A funnel drawn as five
   bare numbers is five numbers a reader has to subtract.

   The goal row is marked, because a campaign is measured against what it was
   for and this one says so in its own goal statement — a scoping call. It
   was the one figure the tab never carried. */
/* ── THE FUNNEL IS A FIGURE ROW NOW ──
   `.s-funnel`, `.s-fn-row`, `.s-fn-name`, `.s-fn-pct`, `.s-fn-n` and
   `.s-fn-of` are gone with the table they drew. Four rows of `name · rate ·
   N of M` asked to be read; the results are given at a glance instead, in
   `.s-afs` — the same anatomy the logged figures above them use. What
   survives is the goal's marker, because a figure row has nowhere else to
   put it. */
.s-fn-goal {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--accent-text);
}
/* Bounces are not a stage. Stated beside the funnel they distort, with the
   fix attached, rather than as a step nobody passes through. */
.s-fn-note {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 12px;
  max-width: 70ch; margin-top: 16px;
  font-size: var(--ty-meta); color: var(--ink-secondary);
}
.s-fn-note b { color: var(--warn-text); }

/* `.s-cover*` went with Enrich's per-record rows. The stage listed every
   thin organization and every unreachable person with a meter each — the
   enrichment run drawn as a worklist. It reports its LISTS' gaps now and
   opens the list to fix them, which is the surface the supplier offers are
   on; see `.s-clists`. */

/* ══ THE SECTION HEAD ══════════════════════════════════════════════════════

   A figure, a kicker and the line the kicker introduces. The figure is left
   of both and the whole head sits above a rule, so a section announces
   itself the way a story on a page does rather than by being the next thing
   after some space.

   ── NO RULES. SPACE. ──

   A pass of this hung a hairline off every title and a border over every
   section head, and the page ended up striped: a line under the masthead, a
   line per section, a line trailing each of five row titles, a line trailing
   each of three sub-block titles. Every one of them was doing a job that
   space does better, and together they read as scaffolding rather than as
   structure — which is what "separators all over the place" describes.

   Knowledge's widgets file states the rule this violates outright: related
   things are separated by SURFACE STEP and space, never by a drawn line, and
   the only borders that earn their place are a card's outer edge and the
   focus call. It was in the research for this pass and got ignored.

   So the separation is 48px above a section and 24px under its head, against
   16px between the rows inside it — three to one, which is a boundary the
   eye reads without anything being drawn. The proximity check in
   `css-audit.js` enforces the same ratio mechanically.

   ── AND IT STACKS ──

   Two arrangements failed before this one. A fixed 64px gutter with the
   figure in it put the kicker and the headline in a column beside a number,
   three elements starting at three different heights. Baseline-aligning the
   figure with the headline instead — the lead's construction — worked for
   the lead, whose figure OPENS the sentence beside it ("12 · of 24 have
   something in the way"), and failed here, where the figure and the headline
   are two separate statements: a 24px number beside a two-line sentence
   wraps into an L and neither reads first.

   What fixed it was shortening the headline, not moving the figure. Once
   the deck is "blocked, for five different reasons." — one line, with the
   working moved down to the qualifier — the figure genuinely IS the first
   word of it, and the baseline construction the lead uses works here for
   the same reason it works there: `12 of 24 · blocked, for five different
   reasons.` reads as one sentence because it is one.

   `flex-wrap` is deliberately off. If a deck ever does run long, it wraps
   under itself inside its own column rather than dropping below the figure
   — which is what turned this into an L the first time. */
/* ══ THE LADDER, WHICH IS THE WHOLE OF THE SECTIONING ═══════════════════
   No rules, no borders, no boxes: the only thing separating one region of
   the campaign page from the next is how far apart they are, so the gaps
   have to be far enough apart from each other to be READ as different.

   They were not. 24 between the rows inside a section, 24 between blocks,
   48 between sections — 2:1, which reads as "a bit more" — and ZERO around
   the identity block, the largest thing on the page. The biggest structural
   boundary had the smallest gap, which is exactly what "everything is
   leaking into everything" describes.

   64 / 24 / 16. A 4:1 ratio between the outer step and the inner one is not
   a judgement call for the reader to make. */
.s-sect-head { margin: 64px 0 24px; }
/* Two exceptions, both narrowed to a DIRECT child so a head nested inside a
   block cannot claim them — the first cut used `:first-of-type` unscoped,
   which every block's own head matched, and the 64 landed nowhere.

   The first head on the page follows the title and the recommendation, so it
   takes the header's gap rather than a section break's. */
.s-camp-page .s-rec-main > .s-sect-head:first-of-type { margin-top: 32px; }
/* The rule that paired the two findings when they were stacked is gone with
   the stack. It also beat the panel's own reset on specificity, so the right
   column's head sat 24px below the left one's — two columns of one panel,
   starting at different heights. */
.s-sect-main { display: flex; align-items: baseline; gap: 12px; }
/* ── THE KICKER ──

   Eleven pixels, weight 700, `--d500` — the faintest and smallest thing in
   the section it was naming, one shade off the background and set below the
   body text it introduced. A label a reader has to hunt for is not doing the
   job a label exists for.

   Thirteen, extrabold, in the FAINT ink — a kicker names the section, it
   does not compete with the headline under it, and the 48px above it is
   already saying "new section" louder than any rule could. */
.s-kicker {
  display: block;
  font-size: var(--ty-meta); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}
/* The line the kicker introduces, and the loudest text in the section. */
.s-sect-deck {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--ty-body); font-weight: var(--fw-medium);
  line-height: var(--lh-base); color: var(--ink-primary);
  max-width: 46ch; text-wrap: pretty; margin: 0;
}
/* ── A QUALIFIER IS NOT A DISCLAIMER ──

   Set at the meta step in the faint ink, this read as small print — and what
   it actually holds is the two things the section cannot be acted on
   without: what the figures COUNT ("organizations, counted by reason", and
   why they therefore add to more than the total), and what a fix is worth
   ("clearing the biggest would free 4"). The second of those is the most
   directly actionable sentence in the block, and it was the quietest.

   Body prose in the secondary ink: a clear step under the headline, and
   plainly legible rather than plainly ignorable. The figures inside it take
   primary ink, because those are the parts a reader is deciding on. */
.s-straight-say {
  font-size: var(--fs-base); color: var(--d100);
  line-height: var(--lh-base); font-variant-numeric: tabular-nums;
}
.s-straight-act { min-height: 24px; }
/* A row that states a count and is not a way in. Same shape as the reply
   rows, minus the hover that would promise a click. */
.s-reply-row.is-flat { cursor: default; }
.s-reply-row.is-flat:hover { background: transparent; }

/* ═══════════════════════════════════════════════
   THE CALL PANEL

   A shell region, which is the whole design decision. A call is live and
   timed and you move around during one — open the record, check the brief,
   read what was said last time — so a container that unmounts on navigation
   cannot hold it. This sits beside the main column and outlives every URL
   change, because `DB.call` is not URL state: a call is not a view.
═══════════════════════════════════════════════ */
:root { --call-width: 300px; }
/* ══ THE PANEL ITSELF NEVER SCROLLS ═══════════════════════════════════════

   It was `overflow-y: auto`, and the moment the brief was added the panel
   grew past the viewport and took `End` with it — the one irreversible
   control here, the one whose mispress costs you the call, below the fold
   on a surface used by somebody who is talking to another person and has
   about a second of attention to spare. A control you have to go looking
   for is not a control on a live call.

   So the panel is a fixed column and exactly ONE thing inside it scrolls:
   the transcript, which is the only part that grows without bound. Head,
   name, brief, session controls, note and the handset row are all fixed,
   and `End` is pinned by `.call-tools { margin-top: auto }`. */
.app-call {
  width: var(--call-width); flex-shrink: 0;
  /* ══ EVERY ROW BUT ONE IS FIXED, SO EVERY ROW BUT ONE IS A COST ═════
     `.call-lines` is `flex: 1 1 0`: it gets exactly what the fixed rows
     leave. So the gap between those rows and the padding around them come
     straight off the transcript, which is the only thing on this panel that
     is actually being read. 12 and 14 rather than 16 and 20 — still the
     scale's own steps, and twenty pixels back for the words. */
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px 16px;
  overflow: hidden;
  border-left: 1px solid var(--hairline);
  background: var(--card-bg);
}
/* Everything that is not the transcript keeps its own height. Without this
   a flex item's `min-height: auto` refuses to shrink below its content, and
   the column overflows again with the scrollbar simply removed — which is
   worse than scrolling, because then `End` is unreachable. */
.app-call > * { flex-shrink: 0; }
.app-call[hidden] { display: none; }

.call-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The name and the clock, one line, opposite ends. `baseline` and not
   `center`: they are two sizes — the name at the body step, the clock a step
   up and extrabold — and two different sizes centred on one row sit on two
   different lines of type. On their shared baseline they read as one line,
   which is what they are. `min-width: 0` on the name so a long one ellipses
   into the gap instead of pushing the clock off the panel. */
.call-who-top { display: flex; align-items: baseline; gap: 8px; }
.call-who-top .call-name { flex: 1 1 auto; min-width: 0; }
.call-who-top .call-head { flex: 0 0 auto; margin-left: auto; }
/* Before the call the head has nothing in it, and an empty flex child still
   takes the row's 8px gap — which would push a long name to wrap one word
   early for a box holding nothing. */
.call-who-top .call-head:empty { display: none; }
/* ══ THE WAY BACK TO THE PAGE, AND IT IS .call-tool's SKIN ═══════════════
   It sits in .call-tools, last, past End — that row is everything you can
   press during a call, and the head above it is the three things the call is
   SAYING. So it wears what the rest of that row wears: Record, Mute and Hold
   are 44px circles for the reason written on .call-tool, which is that a
   thumb finds a 44px circle without looking, and that reason is not weaker
   for the control that puts the call away.

   `display: none` at every width and painted only in the narrow block below:
   above it the page is beside the call already, and a control offering to
   reveal something already on screen is a control that does nothing. */
.call-min {
  display: none;
  flex: 0 0 auto;
  align-items: center; justify-content: center;
  width: 44px; height: 44px;
  border-radius: 50%; border: 1px solid var(--card-border);
  background: none;
  color: var(--d200); cursor: pointer;
  transition: background-color var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.call-min svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; }
.call-min:hover { background: var(--accent-dim); color: var(--d50); }
.call-min:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The live dot pulses. It is the one thing on this surface that is true
   right now rather than true as of the last paint, and motion is how that
   reads without a word for it. */
.call-live {
  width: 6px; height: 6px; border-radius: 50%; background: var(--err);
  flex-shrink: 0; animation: callPulse 1.6s ease-in-out infinite;
}
@keyframes callPulse { 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .call-live { animation: none; } }
/* ══ AND THE CLOCK IS A VALUE, NOT A HEADING ══════════════════════
   18px extrabold was right while this had a row to itself and was the first
   thing on the panel: it was the heading, and the name was the line under
   it. On the name's own line that ranking is upside down — the subject of
   this panel is the person, and how long you have been talking to them is a
   figure about the call.

   So it comes down to the meta step and keeps its weight, which is what
   stops it reading as another qualifier under the name. Tabular still, and
   more load-bearing now than it was: at 14px a proportional digit set shifts
   the whole row every time a 1 turns into a 2. */
.call-timer {
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
  color: var(--d50); font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
}
/* Who you are speaking to, as one group. 4 inside, and the panel's own 16
   between — the ratio rule, so the name and its qualifiers read as one
   thing rather than three lines. */
.call-who-block { display: flex; flex-direction: column; }
.call-name { font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--d50); line-height: var(--lh-tight); }
/* 2 rather than 4. The ratio rule the block's own note states is about the
   gap INSIDE the group being smaller than the one around it, and the panel's
   own gap came down to 12 — so this comes down with it or the group stops
   reading as one thing. */
.call-sub { font-size: var(--fs-xs); color: var(--ink-muted); margin-top: 2px; line-height: var(--lh-tight); }
.call-num { font-size: var(--fs-xs); color: var(--ink-muted); font-variant-numeric: tabular-nums; margin-top: 2px; line-height: var(--lh-tight); }

/* ══ IT RINGS BEFORE IT IS LIVE ═══════════════════════════════════════════

   The dot changes COLOUR rather than shape, because red already means a
   call that is happening and amber already means something not settled
   everywhere else on this surface — the status tones, the run states, the
   effect lines. Slower, too: a ringtone is slower than a heartbeat, and the
   rhythm is the only thing distinguishing the two states without a word.

   No reduced-motion rule of its own. `.call-live` already has one twelve
   lines up, and `animation: none` there clears the name — a duration on a
   nameless animation animates nothing, which is the correct answer here. */
.call-live.is-dialing { background: var(--warn); animation-duration: 2.2s; }
/* `.call-live.is-ready` was here: a grey, still dot for a call nobody had
   placed. There is no dot before the call now — the head is not drawn at
   all until there is something for it to report — so the state that rule
   dressed does not reach the screen. */

/* WHICH ONE OF HOW MANY. The only figure in this head that is about the RUN
   rather than about the call, so it sits apart from the clock, in the
   caption step. A session counter at the timer's weight would make the
   panel look like it is counting two things at once. */
.call-of {
  margin-left: auto; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-muted); font-variant-numeric: tabular-nums; line-height: 16px;
}

/* PP THE BRIEF AND THE RUN CONTROLS LEFT THIS PANEL PPPPPPPPPPPPPPPPPPPPPPP

   `.call-brief`, `.call-brief-row`, `.call-brief-cap` and `.call-sess` were
   here and are gone. The brief is preparation and lives in the canvas with
   the rest of the preparation; the run's controls live on that brief, under
   a sentence naming the run, because in this panel they had no visible
   object and read as pausing or stopping the call in front of you.

   The rail is one call again: who, what is being said, and the handset. */

/* ── Recording, and the notice that gates it ──

   The notice is a question the Record button raises rather than a panel
   standing on screen for the length of the call — the reasoning is beside it
   in `callPanel`. It sits directly above the row that raised it and is
   raised off the surface, because it is the one thing here waiting on an
   answer.

   Its two answers go UNDER the sentence rather than beside it: "Not yet" and
   "I have told them" are different lengths, and a row of two unequal buttons
   beside a paragraph reads as one button and an afterthought.

   `margin-top: auto` moves from the tools to whichever of the two is first,
   or the free space in the column would be split between two auto margins
   and float the question away from the row it belongs to. The adjacent
   sibling is the whole rule — no `:has()` needed for a thing that is
   literally the element before. */
.call-consent {
  display: flex; flex-direction: column; gap: 12px;
  padding: 12px; margin-top: auto; margin-bottom: 12px;
  border-radius: var(--r-lg);
  background: var(--card-bg-raised);
  border: 1px solid var(--card-border);
  box-shadow: var(--shadow-md);
}
.call-consent-say { font-size: var(--fs-xs); color: var(--d200); line-height: var(--lh-relaxed); }
.call-consent-acts { display: flex; gap: 8px; justify-content: flex-end; }
.call-consent + .call-tools { margin-top: 0; }
/* ON AIR, IN THE COLOUR THE LIVE DOT ALREADY USES. `is-on` is the accent
   state the other tools carry for "this is doing something"; recording is
   doing something to somebody else, so it takes the same red as the timer's
   live dot and the End button rather than the surface's own accent. That red
   is `--err` now, which is what the dot was using all along. */
.call-tool.is-rec {
  color: var(--err);
  border-color: color-mix(in srgb, var(--err) 40%, transparent);
  background: color-mix(in srgb, var(--err) 16%, transparent);
}
.call-tool.is-rec:hover {
  background: color-mix(in srgb, var(--err) 16%, transparent);
  color: var(--err);
}
.call-tool.is-rec svg { fill: currentColor; }

/* ── The transcript ── */
/* THE ONE SCROLLER. `flex: 1 1 0` with `min-height: 0` so it takes exactly
   the space the fixed rows leave and no more — the `max-height: 40vh` it
   carried was a cap on the wrong thing: it bounded the transcript and let
   everything else push the panel past its own edge. `min-height: 0` is what
   lets a flex item shrink at all; without it the transcript refuses to go
   below its content height and the overflow moves back up to the panel. */
.call-lines {
  flex: 1 1 0; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 8px;
}
/* ══ ONE STEP DOWN, AND THE LEADING STAYS ════════════════════════
   The body step in a 300px column is about six words to the line, so a
   transcript set at it spends most of its height on line breaks. At the meta
   step the same exchange is a third shorter and more of it is on screen,
   which is the only thing this panel is being asked for.

   `--lh-relaxed` is untouched on purpose. Smaller type in a narrow measure
   needs MORE leading, not less — this is read at a glance by somebody who is
   mid-sentence with another person, and lines that run together are lines
   they lose their place in. The height comes back from the type, not from
   the space between it. */
.call-line { font-size: var(--fs-xs); color: var(--d200); line-height: var(--lh-relaxed); }
.call-line.is-them { color: var(--ink-muted); }
/* WHO SAID IT, IN THEIR OWN CASE. It was uppercase with the caption's wide
   tracking, which is this build's treatment for a LABEL — a word naming a
   field. This is not one: it is a person's name, and a name set in capitals
   is being shouted rather than ranked. The tracking went with the capitals,
   because letters are spaced out to make caps readable and sentence case
   does not want it.

   The rank is carried by weight alone now, which is what is left when case
   goes and is enough: bold against the regular line under it, at the same
   size and the same colour. */
.call-who { display: block; font-weight: var(--fw-bold); color: var(--ink-muted); }
/* The transcript's empty state is in the transcript, so it is set like it. */
.call-none { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-relaxed); }

/* ── The handlers: round icon targets, centred, in the order every phone
   uses. Centred rather than stretched because a round button that grows to
   fill a column stops reading as a phone control and starts reading as a
   form. */
.call-tools {
  display: flex; gap: 12px; flex-wrap: wrap; align-items: center;
  justify-content: center; margin-top: auto;
}
.call-tool {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  /* 44px, which is the tap-target floor and the size a thumb finds without
     looking — the whole reason these stopped being words. */
  width: 44px; height: 44px;
  border-radius: 50%; border: 1px solid var(--card-border);
  color: var(--d200); cursor: pointer;
  transition: background-color var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.call-tool svg { width: 20px; height: 20px; }
.call-tool:hover { background: var(--accent-dim); color: var(--d50); }
.call-tool:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* On means the thing is DOING something — muted, held — so it is filled
   rather than outlined. A struck-through microphone that looks identical
   whether or not you are muted is the state this control exists to show. */
.call-tool.is-on {
  background: var(--accent-dim); color: var(--d50); border-color: var(--accent);
}
/* The one control that ends a conversation with a person on the other end
   is the only filled thing in the panel — and the only one that keeps its
   word, because it is irreversible and a mispress costs you the call.

   It keeps FULL STRENGTH when the label family does not. The family is for
   words that describe something, and it is quiet on purpose; this is not
   describing anything, it is an alarm, and hanging up on somebody mid-call
   is the case an alarm exists for. Muted, it read as a suggestion. */
.call-end {
  /* .call-tool's geometry, to the pixel, because it is one of them now — the
     fourth circle in a row of circles. What it does NOT share is the skin:
     the others are outlined and quiet because they are reversible, and this
     one is filled because it is not. That is the whole difference, and it is
     the only difference worth drawing. */
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  border-radius: 50%;
  /* ══ THE FAMILY'S RED, AND ONE STEP FIRMER THAN RECORDING ═════════
     `.call-tool.is-rec` is this exact recipe — `--err` as glyph, a 40%
     edge, a 16% wash — and the two sit in the same row of the same size.
     A red circle beside a red circle is a row with two of something, so
     the edge goes to full strength here and the wash up two points.
     Recording is a state you can turn off; this is the one press on the
     panel you cannot take back, and it is drawn as the firmer of the two
     rather than the louder. The glyphs differ as well: a filled dot, and
     a handset with a line through it. */
  border: 1px solid var(--err);
  background: color-mix(in srgb, var(--err) 18%, transparent);
  color: var(--err); cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}
.call-end svg { width: 20px; height: 20px; }
/* Darkened FROM the resting value rather than pinned to a literal. It was
   `#d92d20`, which is what `--err` resolves to in LIGHT — so in light theme
   the hover and the resting state were the same colour and the one control
   that hangs up on a person had no feedback at all. */
/* The wash deepens rather than the fill darkening — there is no fill left to
   darken, and a tint that gains weight under the cursor is the same gesture
   the outlined tools make with `--accent-dim`. */
.call-end:hover { background: color-mix(in srgb, var(--err) 30%, transparent); }
.call-end:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ══ START CALL TAKES END'S PLACE, AND ITS SHAPE ══════════════════════════

   Same geometry, opposite tone. It is the same slot in the same row doing
   the opposite job, and the pair reads as one control with two states
   rather than as two controls that happen to be the same size — which is
   what it is: the only thing this rail can do, before and after.

   Brand rather than error, because opening a line is not the irreversible
   half. Ending one is. */
/* ══ DIALLING IS NOT ANSWERING, AND ONLY ONE OF THEM IS GREEN ═══════════
   This was green for a while, on the argument that answer and start are
   the same verb in two places. They are not. Green is what a RINGING
   phone offers — Jakob's Law, the banner's two circles, a decision
   somebody else started and you have seconds to make. Nothing is ringing
   here. You picked this person off a queue, read their brief, and pressed
   a button when you were ready; the pressure is entirely yours and the
   colour should not borrow urgency the moment does not have.

   So it is back to brand, which is also the note directly above: opening
   a line is not the irreversible half. Ending one is, and that is the
   only control in this panel wearing an alarm. */
.call-go, .call-go:hover { border-color: var(--brand); }
/* AND START IS STILL A BUTTON WITH A WORD ON IT. The wide geometry used to
   live on `.call-end` and this rule only recoloured it; End is a circle now,
   so the shape moves here, to the control that was always the one wanting
   it. `order: 1` and a full basis put Start on a line of its own under the
   chevron, which is where it has always sat and is the only thing a ready
   panel offers. */
.call-go {
  background: var(--brand);
  /* White, and stated rather than inherited. `.call-end` is a tinted circle
     now and its `color` is `--err`; this shares that rule for its geometry
     and shares nothing else. Left unsaid, the one affirmative control on the
     panel drew its label and its handset in the colour of hanging up. */
  color: #fff;
  flex: 1 1 100%; order: 1; gap: 8px;
  width: auto; height: auto; min-height: 44px; padding: 8px 12px;
  border-radius: var(--r-md);
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
}
/* Darkens rather than lightens. White on brand is 4.56:1 against the
   14px bold label — over the 4.5 floor and not by much — so the hover
   this rule used to lighten toward #fff would have taken it under. */
.call-go:hover { background: color-mix(in srgb, var(--brand) 88%, #000); }

/* ── What the rest of the shell does while a call is up ──
   The canvas clears the panel rather than covering it: the prep thread and
   the call have to be legible together, which is the reason they are in two
   containers at all. */
body.is-calling .aimy-overlay { right: var(--call-width); }
/* AND THE CHAT COLUMN GOES. `display: none`, not disabled — switching
   session mid-call swaps the surface underneath a live conversation, and a
   disabled control still occupies the space, still reads as usable, and
   still invites the click that has to be refused. The only honest state for
   a control whose every use during a call is a mistake is absent. */
body.is-calling .overlay-chats { display: none; }

/* ══ AND BELOW 720 LAYOUT PX THE CALL IS THE WHOLE WIDTH ════════════════

   `.app-call` is 300px and `flex-shrink: 0`, so what the page column gets is
   whatever is left — and on a phone that is nothing. Measured at a 459px
   layout, which is what a 390px phone carried back when width still scaled to
   a 0.85 floor; the same phone is 390 layout px now, so the case is stronger
   rather than weaker. With a call up at that width the main column was
   159 layout px wide, every line in it cut mid-word, and the float bar's own
   clamp — correct arithmetic on a false premise — resolved --fb-w to 111px, so
   the composer was a pill with three icons and no room to type in. The call's
   own Start control and that composer then overlapped, which is the one
   collision this panel's rules exist to prevent.

   720 IS WHERE THE COLUMN STOPS BEING A COLUMN, and it is already this file's
   word for that. What the page is FOR beside a live call is reading — the
   record, the brief, what was said last time — and reading wants 45 to 75
   characters, which at this body size is about 420px. 420 + 300 + the gutters
   is 720, and at 719 the column would be under its own measure. So above this
   they sit side by side, and below it the call takes the surface.

   THE PAGE IS HIDDEN, NOT SQUEEZED. A zero-width flex sibling still scrolls,
   still takes focus and still answers the keyboard, so leaving it in place
   trades a visible defect for an invisible one. `display: none` is also what
   .overlay-chats above gets, and for the same reason: a surface whose every
   use during a call is a mistake is absent rather than disabled. */
@media (max-width: 719.98px) { /* 720 layout px */
  :root { --call-width: 100%; }
  /* ══ .page-scroll, NOT .app-main — THE CANVAS LIVES IN THERE ══════════
     `.aimy-overlay` is a child of .app-main (index.html), so hiding the
     column took the canvas with it: at a 459px layout with a call up, the
     overlay computed `position: fixed; top: 60px; left: 0; right: 0` and
     measured 0x0, because `display: none` on an ancestor removes a fixed
     descendant like any other. The brief AiMY posts at call time was there
     and unreachable, and so was the control that closes it.

     The scroll column is the thing that must not stay behind a full-width
     call — it still scrolls, still takes focus and still answers the keyboard
     at zero width — and it is the thing that is named. .app-main keeps its
     box, collapses to nothing beside a 100% call panel, and goes on hosting
     the canvas, which is fixed and does not care what its parent measures. */
  body.is-calling .page-scroll { display: none; }
  .app-call { border-left: 0; }
  /* The reserve exists to keep the composer off a panel parked on the right.
     There is no right-hand panel here — the call IS the surface — so the
     reserve would clamp --fb-w to nothing. */
  body.is-calling { --fb-reserve: 0px; }
  /* AND THE COMPOSER GOES WITH THE COLUMN IT BELONGED TO. `End` is pinned to
     the bottom of this panel by `.call-tools { margin-top: auto }`, and a
     fixed bar at `bottom: 20px` lands on it — the one control here whose
     mispress costs you the call, under a bar you did not ask for. */
  body.is-calling .aimy-float-wrap { display: none; }
  body.is-calling .aimy-overlay { right: 0; }

  /* ══ AND THE CALL CAN STEP BACK, SO THE PAGE IS NEVER GONE ═════════════
     Taking the whole width is right and it is not the whole answer: a caller
     opens the record, checks the brief and reads what was said last time
     DURING the call — that is the reason this is a shell region rather than
     a modal — and none of it was reachable once the column stepped aside.

     So the region has a second form at this width instead of one form and a
     hole. Minimised it is a strip under the masthead: the live dot, the
     clock, the name, and End. The page is underneath, whole, with its own
     composer back. Nothing is removed — the rest of the panel is one press
     on the same chevron away, and the three things a live call has to keep
     saying are the three the strip carries.

     `flex-direction: row` is the whole mechanism: .app-call is already a flex
     column with `margin-top: auto` pinning End to the bottom of a full-height
     panel, and turning the axis makes that same rule pin End to the RIGHT of
     a strip. One declaration, and the control that must never be hunted for
     stays where the thumb already is. */
  .call-min { display: inline-flex; }

  body.is-call-min .page-scroll { display: block; }
  body.is-call-min .aimy-float-wrap { display: flex; }
  /* A TOKEN, BECAUSE THREE THINGS READ THIS HEIGHT and they must not drift:
     the strip is this tall, the canvas starts under it, and anything fixed to
     the top of the column after them has to clear both. Declared at 0 on
     :root above so every other state gets the arithmetic for free. */
  body.is-call-min { --call-strip: 60px; }
  body.is-call-min .app-call {
    flex: 0 0 auto;
    height: var(--call-strip);
    flex-direction: row;
    align-items: center;
    gap: 12px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--hairline);
  }
  /* The panel stacks ABOVE the page rather than beside it. .app-body is a
     flex row; `wrap` plus a full-width basis is what makes a row into two,
     and `order` is what decides which is on top. */
  body.is-call-min .app-body { flex-wrap: wrap; align-content: flex-start; }
  body.is-call-min .app-call { order: -1; width: 100%; }
  body.is-call-min .app-main { flex: 1 1 100%; min-height: 0; }

  /* What the strip does not carry, said as what it DOES. Naming the parts to
     hide means naming them again every time the panel grows one — and the
     first draft of this rule named four that do not exist, leaving the
     transcript and the note standing in a row 190px tall. Three children are
     the strip; everything else folds. Still in the tree, still one press away
     on the same chevron: this is the panel folded, not the panel cut. */
  body.is-call-min .app-call > :not(.call-who-block):not(.call-tools) {
    display: none;
  }
  /* One name fewer than it used to carry: the clock moved inside this block,
     so the strip keeps the ROW — name and clock together — rather than the
     name alone with the clock as a separate child of the panel. */
  body.is-call-min .call-who-block > :not(.call-who-top) { display: none; }
  body.is-call-min .call-tool { display: none; }

  /* ══ AND THE WAY BACK SITS BESIDE End, NOT UNDER IT ════════════════
     `.call-go` is `flex: 1 1 100%; order: 1`, so it takes a line of its own
     and sorts after anything at order 0 — which put the chevron on the line
     ABOVE the button, centred, reading as a control belonging to whatever sat
     over it. Ordered last and given a basis it can share, the two sit on one
     line with Start taking everything the 44px circle does not. */
  /* End is the handset at every width now — this block used to be where that
     was true, and the three rules that made it true here are the base rules.
     What is still only true here is Start: the strip is one line, so it takes
     what the chevron leaves rather than a line of its own. */
  .call-tools .call-end.call-go { flex: 1 1 auto; }
  .call-min { order: 2; }
  /* The clock is the strip's subject, so it keeps its size while the name
     beside it gives way — a name is a string of unknown length and the only
     thing here that can be trusted to end. */
  body.is-call-min .call-who-block { min-width: 0; flex: 1 1 auto; }
  body.is-call-min .call-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body.is-call-min .call-head { flex: 0 0 auto; flex-wrap: nowrap; }
  body.is-call-min .call-tools { margin: 0 0 0 auto; flex-wrap: nowrap; gap: 8px; }
  /* Folded, both controls come down to the strip's own height. 34 is the
     masthead's icon button, which is what this row is beside once the panel
     is a bar under it. */
  body.is-call-min .call-end { height: 34px; width: 34px; min-height: 0; }
  body.is-call-min .call-end svg { width: 15px; height: 15px; }
  body.is-call-min .call-end.call-go { width: auto; padding: 0 14px; }
  body.is-call-min .call-min { width: 34px; height: 34px; }
  body.is-call-min .call-min svg { width: 16px; height: 16px; }
}

/* The brief AiMY writes before the call. */
.s-brief-call { display: flex; flex-direction: column; gap: 8px; }
.s-callp { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }

/* ══ THE RUN, ON THE BRIEF THAT BELONGS TO IT ═════════════════════════════

   A rule down the left rather than a box, the same device `.s-straight`
   uses for a count with an action: the brief above it is already a group,
   and boxing three buttons inside a block is a container around content
   that is contiguous with what it sits under.

   The sentence is the point. `Pause after this call` in the call panel had
   no object on screen and read as pausing the call; here it sits directly
   beneath "Call 3 of 25 on Calling 25 people", and the buttons inherit the
   noun from the line above them. */
.s-brief-run {
  display: flex; flex-direction: column; gap: 8px;
  margin-top: 16px; padding-left: 12px;
  border-left: 2px solid var(--accent);
}
.s-brief-run-say { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }
.s-brief-run-say b { color: var(--d100); font-variant-numeric: tabular-nums; }
.s-brief-run-acts { display: flex; flex-wrap: wrap; gap: 12px; }
.s-callp b {
  display: block; font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--ink-muted);
}
/* Who said to remember it. Quieter than the line, because the judgement is
   the thing you read and the attribution is what you check afterwards. */
.s-callp-who { color: var(--ink-muted); }
/* PP THE FORM THAT USED TO BE HERE PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP

   `.s-obj-pick`, `.s-obj-chip`, `.s-obj-name`, `.s-obj-tick`, `.s-opp-tick`,
   `.s-prop-pick`, `.s-callsay` and its foot and reading line, `.s-callmem`,
   and the whole `is-aiset` argument, are gone with the modal they dressed.

   Logging a call is a conversation now: AiMY says what it read, the card
   underneath is the one the record will carry, and a correction is a
   sentence rather than a tick. There is nothing left to style because there
   is nothing left to fill in.

   `is-aiset` is worth one line of epitaph. It existed to mark what AiMY had
   filled in, was stripped of every visual treatment on the argument that a
   ticked option is a ticked option, and survived only because the code
   needed it to know which of its own ticks it could replace. With no ticks,
   that bookkeeping has no subject — the same rule now holds over VALUES, in
   `callLogCorrect`, where an axis you have spoken to is yours. */

/* ══ WHAT A CALL PRODUCED ═══════════════════════════════════════════════
   One block, drawn in two places — the canvas the moment you hang up, and
   the record's Calls list months later. Labels down a fixed column so six
   facts of different kinds can be read across without hunting for the one
   you want; the caption is the quiet half and the value is the loud one,
   which is the same ranking `.s-id-line` settled on. */
.s-callsum { display: flex; flex-direction: column; gap: 8px; }
.s-callsum-rows { display: flex; flex-direction: column; gap: 4px; }
.s-callsum-row { display: flex; align-items: baseline; gap: 12px; }
.s-callsum-cap {
  flex: 0 0 96px; text-align: right;
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.s-callsum-val { font-size: var(--fs-xs); color: var(--d100); min-width: 0; }
/* What was said, in the caller's words — the longest thing here and the one
   most likely to be read whole, so it gets the body step and the measure. */
.s-callsum-note {
  font-size: var(--fs-base); color: var(--d200); line-height: var(--lh-base);
  max-width: 72ch; margin-top: 4px;
}
/* The durable line, marked as different from the call it was captured on. */
.s-callsum-mem {
  font-size: var(--fs-xs); color: var(--d200); line-height: var(--lh-base);
  padding-left: 12px; border-left: 2px solid var(--accent);
}
.s-callsum-mem .s-plan-cap { display: block; }

/* The list of them on a record. Bordered, because each is a self-contained
   account of one conversation and they are read one at a time. */
.s-calls { display: flex; flex-direction: column; gap: 12px; }
.s-call {
  padding: 12px 16px; border-radius: var(--r-lg);
  border: 1px solid var(--card-border); background: var(--card-bg);
}
/* ── The closed line: when, who, how it went ──
   The three facts that tell one call from another. `flex` on a `<summary>`
   because the default marker would sit outside the row and push the date
   off its left edge; the chevron is drawn instead, on the same construction
   `.s-trace-sum` uses. */
.s-call-sum {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  cursor: pointer; list-style: none;
}
.s-call-sum::-webkit-details-marker { display: none; }
.s-call-sum::before { content: "▸ "; color: var(--ink-muted); }
.s-call[open] .s-call-sum::before { content: "▾ "; }
.s-call-sum:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--r-md); }
.s-call-when { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d200); font-variant-numeric: tabular-nums; }
.s-call-by { font-size: var(--fs-xs); color: var(--ink-muted); }
.s-call-by.is-ai { color: var(--ai-text); font-weight: var(--fw-bold); }
/* How it went carries the tone, because it is the one fact on the line that
   is good or bad news rather than a label. */
.s-call-out { font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
/* Pushed to the far end: it qualifies the date and is the least of the four,
   so it neither competes for the left edge nor crowds the outcome. */
/* Who was called. First in the closed line and the only bold thing in it,
   because on an account it is what tells two calls apart. */
.s-call-who { font-weight: var(--fw-semibold); color: var(--d100); }
.s-call-ago { margin-left: auto; font-size: var(--fs-2xs); color: var(--ink-muted); }
.s-call-body {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--hairline);
}

/* ── Picking cards ──
   The tick sits in the card's own head row, before the type. Quiet until
   hovered or checked, because on a hundred cards a permanently visible
   control is a hundred controls; the card is the target either way. */
.s-pick-tick { display: inline-flex; align-items: center; flex-shrink: 0; margin-right: 8px; }
.s-tick {
  width: 14px; height: 14px; cursor: pointer;
  accent-color: var(--accent);
  opacity: 0.35; transition: opacity var(--t-fast);
}
.s-card:hover .s-tick, .s-tick:checked, .s-tick:focus-visible { opacity: 1; }
/* A picked card says so on its border rather than with a tint: a tint on a
   card that already carries a status tone is two colours arguing about what
   the card means. */
.s-card.is-picked { border-color: var(--accent); }

/* ── The lists ──
   Clay's saved views. A name and a count, and the one that is open says so —
   which is the only navigation state this rail carries, because a list is
   the one thing here you can BE inside. */
/* Each finding is one target. 6 inside, 16 between — the ratio rule, and it
   is what stops five findings reading as one paragraph in a narrow column. */

/* ══ THE RAIL NO LONGER STEPS ASIDE ═══════════════════════════════════════

   It was `body.is-record .app-rail { display: none }`, with an exception for
   `:has(.rail-camp)`. The reason was sound at the time and is now false: the
   rail held four counts about the whole book, which beside a single record
   were answering a question about somewhere else.

   It holds that record's own conclusion now — why this one is where it is —
   and a campaign's own reading, so hiding it removes the thing the surface
   is most about. The premise went, so the rule goes with it.

   AND THE EXCEPTION HAD ALREADY BROKEN ONCE. It keyed on `.rail-camp`, a
   class the rail stopped rendering when the four scopes were unified — so
   the campaign rail, complete and repainting, drew nothing on screen. That
   is the second time a `:has()` exception has silently un-shown this panel;
   there is no third, because there is no exception. */
/* ═══════════════════════════════════════════════
   THE RAIL, ON A NARROW SCREEN — A DRAWER, NOT A DELETION

   This was `display: none`, and the rail's own comment two screens up is the
   argument against it: it holds "a short, complete index of the containers —
   what campaigns there are, what is running, and the pool everything else sits
   in", and it came BACK from being deleted once already because "with no rail
   there was nowhere to SEE what exists. Campaigns could only be reached by
   asking for them, which is a fine way to reach a thing you already know the
   name of and a poor way to learn there are seven."

   Removing it below 1080 removed exactly that, on exactly the screens with the
   least room to discover anything. Same defect, narrower window.

   So it becomes a drawer, which is what AiMY Knowledge does with its briefing
   at 1040 (knowledge.css §THE BRIEFING, ON A NARROW SCREEN) — same mechanism,
   same scrim, same `is-open` class, and the same reason. It stops taking room
   in the row and starts opening over it.

   --rail-w GOES TO 0 AND THAT IS THE WHOLE RECENTRING. The float bar's clamp
   reads it, so the bar reclaims the room the rail gave up without a second
   rule that has to remember this one exists. Knowledge zeroes --sidebar-width
   in the same place for the same reason.
═══════════════════════════════════════════════ */
@media (max-width: 1079.98px) { /* 1080 layout px */
  :root { --rail-w: 0px; }

  .rail-toggle { display: inline-flex; }

  .app-rail {
    position: fixed; z-index: 700;
    top: var(--topbar-height); bottom: 0; left: 0;
    /* --vp-w, not 100dvw. Inside `zoom: k` a viewport unit is k viewports wide
       — see §EVERY WIDTH BREAKPOINT. The 56px it leaves is the strip of the
       page behind that says the drawer is over something, not the page. */
    width: min(320px, calc(var(--vp-w) - 56px));
    /* The column sits on the ambient layer and draws no ground of its own,
       which is right for a column and wrong for a panel over content. */
    background: var(--panel-bg);
    border-right: 1px solid var(--glass-border);
    box-shadow: var(--shadow-xl);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    /* visibility, not display: it keeps the closed drawer out of the tab order
       AND leaves something for the transform to animate. */
    transform: translateX(-100%); visibility: hidden;
  }
  .app-rail.is-open { transform: none; visibility: visible; }

  .rail-scrim {
    display: block;
    position: fixed; z-index: 690;
    top: var(--topbar-height); right: 0; bottom: 0; left: 0;
    background: rgba(10, 14, 18, 0.5);
    opacity: 0; pointer-events: none;
  }
  .rail-scrim.is-open { opacity: 1; pointer-events: auto; }
}
:root[data-theme="light"] .rail-scrim { background: rgba(16, 24, 40, 0.28); }

/* The start state lives inside the motion query and the end state outside it,
   which is this file's own pattern (§ENTRY STAGGER): a surface that never
   animates is a surface that is simply there. */
@media (prefers-reduced-motion: no-preference) {
  .app-rail { transition: transform 220ms var(--ease-out), visibility 220ms; }
  .app-rail.is-open { transition: transform 220ms var(--ease-out), visibility 0s; }
  .rail-scrim { transition: opacity 220ms var(--ease-out); }
}

/* ══ YOUR CAMPAIGNS ════════════════════════════════════════════════════════

   ROWS, NOT CARDS, and the reason is what they differ by. A card is for
   choosing between things that differ in KIND; six campaigns differ in one
   fact — which stage each is waiting in — and a row lets that fact line up
   down a column where the eye can compare six of them at once. As cards this
   block was 1,500px; as rows it is about 300.

   The whole row is the target and it lands on the stage the campaign is
   waiting in, so the thing you would do next is the thing you pressed. */
.s-camp-rows-head {
  display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.s-camp-rows-head .s-block-h { margin-bottom: 0; }
.s-camp-rows-head .btn { margin-left: auto; }



/* ── THE REST OF THE BOOK, AS DOORS ──
   What replaced 1,231px of inlined tab. Three targets, each a count and a
   noun, which is the whole of what a destination has to say before you press
   it. The count leads because it is the thing that differs — "Contacts" is
   the same word every day and "247" is not. */
.s-doors { display: flex; flex-wrap: wrap; gap: 8px; }
/* A door that says what is behind it needs two lines, so it stacks — and
   each one takes a share of the row rather than shrinking to its number, or
   "69 Organizations" and "153 Contacts" would be two different widths for
   two things of equal rank. */
.s-door {
  flex: 1 1 220px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  padding: 12px 16px; cursor: pointer; text-align: left;
  border: 1px solid var(--card-border); border-radius: var(--r-lg);
  background: var(--card-bg); font-family: inherit;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.s-door-head { display: flex; align-items: baseline; gap: 8px; }
/* The two most urgent names behind the door, at meta weight — they are the
   REASON to press it, not the label of it, so they sit under the count
   rather than competing with it. Wraps rather than truncating: a name cut in
   half is worse than a second line. */
.s-door-peek {
  font-size: var(--fs-xs); color: var(--ink-muted);
  line-height: var(--lh-base);
}
.s-door:hover { border-color: var(--card-border-hover); background: var(--accent-dim); }
.s-door:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-door-n {
  font-family: var(--font-display);
  font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--d50);
  font-variant-numeric: tabular-nums; letter-spacing: var(--ls-tight);
}
.s-door-label { font-size: var(--fs-xs); color: var(--ink-muted); }
/* `Close` is the only thing on the right of the strip now — `Find companies`
   sat beside it and was a write in the navigation. */
.s-tab-shut { margin-left: auto; }
.s-camp-rows { display: flex; flex-direction: column; gap: 2px; }
.s-camp-row {
  display: grid; grid-template-columns: minmax(0, 15rem) auto auto minmax(0, 1fr) 13px;
  align-items: center; gap: 16px;
  width: 100%; text-align: left; cursor: pointer; font-family: inherit;
  padding: 12px; border-radius: var(--r-md);
  transition: background-color var(--t-fast);
}
.s-camp-row:hover { background: var(--accent-dim); }
/* YOUR OWN MOVE READS AS YOURS, AND THE WHY LINE ALREADY SAYS IT.

   This carried a red left edge as well, on the argument that a background
   would be saying it twice. The edge was the third thing saying it: the
   ordering puts your rows at the top, the why line is already red and
   semibold, and a red rule down the side of a row nobody has asked about
   reads as an alert rather than as ownership.

   Same rule as the disclosure line and the read-only tiers — one signal per
   fact. The colour and weight on the sentence carry it. */
.s-camp-row.is-mine .s-camp-row-why { color: var(--err); font-weight: var(--fw-semibold); }
.s-camp-row.is-nobody .s-camp-row-why { color: var(--warn); }
.s-camp-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.s-camp-row-name {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50);
  min-width: 0;
}
/* The stage is the column the eye scans, so it is fixed-width and never
   wraps — a ragged middle column is the thing that stops a table being read
   as one. */
.s-camp-row-stage {
  display: inline-flex; align-items: center; gap: 4px; flex: none;
  min-width: 7.5rem;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--warn);
}
.s-camp-row-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--warn-bg); border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
  font-variant-numeric: tabular-nums; letter-spacing: 0;
}
/* ── HOW BIG, AND HOW FAR ──
   Weight and size carry the hierarchy, not colour: the figure is the thing
   being compared down the column, and the noun beside it only says what the
   figure counts. Fixed width so the numbers line up across eight rows —
   a column of figures you have to hunt for is a column of prose. */
.s-camp-row-stat {
  display: inline-flex; align-items: baseline; gap: 12px; flex: none;
  min-width: 10.5rem;
}
.s-camp-row-fig {
  display: inline-flex; align-items: baseline; gap: 4px;
  font-size: var(--fs-xs); color: var(--d200); white-space: nowrap;
}
.s-camp-row-fig b {
  font-size: var(--fs-base); font-weight: var(--fw-semibold); color: var(--d50);
  font-variant-numeric: tabular-nums;
}
.s-camp-row-why { font-size: var(--fs-base); color: var(--d200); min-width: 0; }
/* WHY IT IS THIS FAR UP, on the rows that have a reason. Amber rather than
   red: a closing window is a reason to look, and `is-mine` already owns red
   for the rows you are personally holding up.

   It declares its own colour rather than inheriting, so it survives
   `.is-mine .s-camp-row-why` recolouring the clause beside it — whose move
   it is and how long it has been theirs are two facts, and letting the first
   repaint the second would merge them into one. */
.s-camp-row-when { margin-left: 8px; color: var(--warn); font-weight: var(--fw-semibold); }
.s-camp-row-go { width: 13px; height: 13px; color: var(--ink-muted); flex: none; }
.s-camp-row:hover .s-camp-row-go { color: var(--accent); }
/* Under 900 the reason is the first thing to go — the name and the stage are
   what the row is for, and a truncated reason is worse than none. */
@media (max-width: 899.98px) { /* 900 layout px */
  .s-camp-row { grid-template-columns: minmax(0, 1fr) auto 13px; }
  .s-camp-row-why { display: none; }
}

/* The agenda's card shape is gone with the page block it drew. It is
   `.rail-row` now — a work state, a sentence and a named verb — which is
   the same anatomy at a quarter of the width. `.s-agenda` (the grid) went
   with the cards it laid out. */

/* ── In flight ──
   One line each. These are checked, not read, so they are rows rather than
   cards — and a row that is a button is the whole target, which is why
   nothing inside it is separately clickable. */
.s-flight { display: flex; flex-direction: column; gap: 2px; }
.s-flight-row {
  display: flex; align-items: baseline; gap: 12px;
  min-height: 24px; padding: 8px 12px;
  border-radius: var(--r-sm); cursor: pointer; text-align: left; width: 100%;
  transition: background-color var(--t-fast);
}
.s-flight-row:hover { background: var(--accent-dim); }
.s-flight-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.s-flight-name {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d100);
  min-width: 0;
}
.s-flight-fact { margin-left: auto; flex-shrink: 0; font-size: var(--fs-xs); color: var(--ink-muted); }

/* ── The chips ──
   A count on the face of every one, so what is worth pressing is legible
   before it is pressed. */

/* ── The opener, ported from AiMY QA's `.slv` ──
   Mark, title and how long you were away; then the paragraph, with the one
   number that anchors it pulled out beside. QA's own note on why it replaced
   a score hero applies unchanged: "a score is orientation you already had;
   this is what moved while you were away." */
.slv {
  background: var(--aimy-wash);
  border: 1px solid var(--aimy-wash-edge);
  border-radius: var(--r-lg);
  padding: 20px;
  margin-bottom: 32px;
}
.slv-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
/* THE MARK IS SIZED OFF THE TITLE IT STANDS BESIDE. It was a fixed 18×20 —
   right when the title was 13px and absurd once the title went to 32, where
   an 18px mark reads as a stray glyph rather than as the speaker. `em` on a
   parent set to the title's own size ties the two together, so the mark
   follows the anchor down at 620 without a second rule. */
.slv-head svg { width: 0.9em; height: 1em; flex-shrink: 0; align-self: center; }
/* TITLE SIZE, BECAUSE IT IS THE TITLE. It was `--fs-xs` — 13px, the meta
   step — which put the name of the whole surface at the same volume as the
   supporting line under a card name. This is the surface's anchor. */
.slv-head { font-size: var(--fs-3xl); }
.slv-head .slv-title {
  font-family: var(--font-display);
  font-size: 1em; font-weight: var(--fw-extrabold); color: var(--d50);
  letter-spacing: var(--ls-tight); line-height: var(--lh-tight);
}
/* Last, quiet, and to the right: the gap is what qualifies the paragraph,
   not what introduces it. */
.slv-time { margin-left: auto; font-size: var(--fs-xs); color: var(--ink-muted); }
/* The score panel that sat to the right of this paragraph is in the rail
   now — it was a STANDING figure inside a block about the last three days,
   and the rail was already carrying an identical copy of it. What is left is
   the narrative, which is all this block was ever for. */
.slv-body { min-width: 0; }
.slv-line {
  font-size: var(--fs-base); color: var(--d100);
  line-height: var(--lh-base); margin: 0;
}
/* ── AND FOUR STATEMENTS ARE NOT ONE PARAGRAPH ──
   `margin: 0` is right for the one-line case this block was built for and
   wrong the moment it holds two. "What we think should change" draws one
   line per engagement — on the overview that is four verdicts about four
   different things, measured at 0.0px apart, so a reader met a wall of
   text and had to find the sentence boundaries themselves.

   8, against the 12 `.slv-head` keeps above the body. Less than the gap
   to the heading, so the lines still group under it as one answer rather
   than reading as separate blocks; more than nothing, so each verdict is
   its own. A third of a line box, which is the smallest break the eye
   reads as deliberate. */
.slv-line + .slv-line { margin-top: 8px; }
/* ── AND THE ONE RUN THAT IS NOT A LINK STOPS OUTWEIGHING THE FIVE THAT ARE ──
   `Three things need you` is a clause, not a figure: it opens nothing. At
   `--d50` extrabold it was the brightest AND heaviest thing in a sentence
   containing five links, so the strongest signal on the line was the one
   place there was nothing to press.

   It keeps its emphasis — it is the hinge of the sentence — one step down in
   both axes, so within this paragraph weight-plus-underline means pressable
   and weight alone means important. */
.slv-line strong { color: var(--d100); font-weight: var(--fw-semibold); }

/* ── A FIGURE IN THE SENTENCE IS A WAY INTO THE SET IT COUNTS ──

   These sit four and five to a paragraph, so accent-colouring them all would
   be five coloured phrases in one sentence — a paragraph that has stopped
   being a paragraph. THAT HALF STILL HOLDS and is why the text stays `--d50`.

   The other half was argued from a premise that is false. It read: "there is
   nothing here that underlining does that the weight is not already doing —
   these are `--d50` bold against `--d100` body text, the brightest and
   heaviest thing in the sentence." Measured, they are not. The same sentence
   renders `<strong>Three things need you</strong>` — `--d50` at weight 800 —
   so the one run that is NOT pressable is heavier than the five that are:

       colour   #eef2f6   #eef2f6      (identical)
       size     14px      14px         (identical)
       weight   700       800          (the LINK is the lighter one)
       resting decoration: none, on both

   Law of Similarity: identical treatment states identical function. And a
   hover-only underline is a signifier that does not exist until after you
   have guessed — Norman's point exactly, and the reason the ecosystem's own
   `.link.inline` in `aimy-ds.css` carries a RESTING underline.

   It also did not match `.s-inline-btn`, which the old comment claimed it
   did. That one is `--accent-text` at rest: it has a resting signifier, it is
   just colour rather than decoration. So every text action in the product
   marks itself at rest except this one.

   The line is drawn in the accent at low alpha rather than in the text
   colour: it ties the mark to where hover is going without colouring five
   phrases, which is the paragraph's whole constraint.

   Tabular so the figures do not jitter when the corpus changes under them. */
.slv-n {
  font-family: inherit; font-size: inherit;
  color: var(--d50); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: rgba(var(--accent-rgb), 0.45);
  text-underline-offset: 3px; text-decoration-thickness: 1px;
  transition: color var(--t-fast), text-decoration-color var(--t-fast);
}
.slv-n:hover {
  color: var(--accent);
  text-decoration-color: rgba(var(--accent-rgb), 0.9);
}
.slv-n:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* ── AND ONE OF THEM LEAVES THE PRODUCT ──
   Every other mark in this paragraph is accent-coloured and lands
   somewhere on this desk. This one opens AiMY QA in a new tab, and a
   reader should be able to tell that from the mark rather than from the
   hostname once they have arrived.

   `--info` is the blue the design system already contrast-checked in both
   themes — #7ea7ff on the dark card, #067dc2 on the light one — so this
   is not a new colour, it is the one that already means "this is
   elsewhere". After `.slv-n:hover`, which is the same specificity and
   would otherwise take the accent back on hover.

   The mark is sized in `em` and not px: it sits inside a sentence, so it
   is a piece of type and has to move with the type around it. */
/* `nowrap` because the mark broke away from the name it belongs to and
   stood alone at the start of the next line, which reads as a bullet
   rather than as part of a link. The run is a fixed product name plus a
   13px glyph, so keeping it whole moves the wrap one word earlier and
   cannot overflow. */
.slv-out {
  color: var(--info); text-decoration-color: rgba(var(--info-rgb), 0.5);
  white-space: nowrap;
}
.slv-out:hover { color: var(--info); text-decoration-color: rgba(var(--info-rgb), 0.95); }
.slv-out svg {
  width: 0.8em; height: 0.8em; margin-left: 5px;
  vertical-align: -0.04em; flex: none;
}

/* ── WAYS TO START ──

   `.slv-signals` was here: four buttons repeating the sets the paragraph
   above already links. What replaced them is a different KIND of thing — the
   paragraph says what changed, these say what to begin — so the group is
   captioned and separated by a rule rather than sitting 16px under the
   sentence as one more line of it.

   24 above the caption against 4 between a label and its reason: the ratio
   rule at two levels, so the four openers read as one group of four
   two-line items rather than as eight loose phrases. */
.s-starts-wrap { margin-top: 24px; padding-top: 20px; border-top: 1px solid rgba(var(--accent-rgb), 0.18); }
.s-starts-cap {
  display: block; margin-bottom: 12px;
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
/* Declared columns, not `auto-fit`. Four openers in a row that reflows to
   two is four; one that reflows to three leaves a widow, and the reason
   lines are what get squeezed when it does. */
/* 16 between against 12 inside — the card-grid rule, argued at `.s-campos`. */
.s-starts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 1179.98px) { /* 1180 layout px */ .s-starts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 619.98px) { /* 620 layout px */ .s-starts { grid-template-columns: minmax(0, 1fr); } }

.s-start {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  min-width: 0; text-align: left; cursor: pointer;
  padding: 12px; border-radius: var(--r-lg);
  border: 1px solid var(--card-border); background: var(--card-bg);
  font-family: inherit;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.s-start:hover { border-color: var(--accent); background: var(--accent-dim); }
.s-start:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-start-label { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50); }
/* The verb and, where the door opens onto a set, how big it is. The figure
   sits at the far end on the label's baseline, a step up in size and weight
   from the verb, so a row of four reads as "how much of each" before any of
   the reasons are read. */
.s-start-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  width: 100%; min-width: 0;
}
.s-start-n {
  flex: none;
  font-family: var(--font-display); font-size: var(--fs-xl); font-weight: var(--fw-extrabold);
  line-height: 1; letter-spacing: var(--ls-tight); color: var(--d50);
  font-variant-numeric: tabular-nums;
}
.s-start:hover .s-start-n { color: var(--accent); }
/* The reason is what stops this being a menu, so it is not decoration and it
   does not truncate — it wraps.

   IT WAS ALSO BIGGER THAN THE THING IT EXPLAINS. --fs-base is 16 and the
   label above it is --fs-xs at 14, so the card read upside down: the answer
   louder than the question. Levelling it at 14 leaves weight and ink to
   carry the rank, which is the two axes the type system asks for — 600
   against 400, --d50 against --ink-muted. */
.s-start-why { font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base); }

.s-brief { display: flex; flex-direction: column; gap: 16px; }
/* The work-state chip leads, and it leads on ITS OWN LINE. A dot encoded
   priority in a colour nobody has a key to; the chip says the word. But
   putting it in a first grid COLUMN cost the sentence 90px of the 470 it
   had, and measured at 1280 that dropped the finding to 41 CHARACTERS —
   under the readable floor, in the block whose whole job is a sentence.

   Same anatomy as `.s-insight` for the same reason: who is speaking, then
   what they found, then what to do. The text gets the column. */
/* BODY, not meta. It was 13px because the rail was 220px wide and 13 was as
   much as fitted; on a 72ch surface a finding is the main text of the page
   and reads at the body size like any other prose. */
/* A quiet briefing is a real state and the best one this rail can report.
   It was unreachable while any entry was pushed unconditionally. */

/* ═══════════════════════════════════════════════
   WORK IN THE CANVAS

   Not a modal in a different place. A work block stays in the thread after
   it runs, its result lands under it, and the conversation that produced it
   is still there to read — a modal's whole contract is that it disappears
   and takes its reasoning with it.
═══════════════════════════════════════════════ */
/* ── AiMY's own controls ──
   Anything AiMY authored, or is about to do on your behalf, carries the mark
   and the AI blue. An AiMY affordance that looks like an ordinary button is
   the same defect as an AiMY touchpoint attributed to a rep: the surface
   hiding who acted.

   THE MARK CARRIES THE GRADIENT; THE LABEL DOES NOT. `--ai` runs from
   #0066ff to #45d3e6, and clipped to text the label starts at the dark end —
   measured 3.58:1 against the card, under the 4.5 that small text needs.
   `--ai-text` is the library's own token for AI-blue TYPE and measures
   7.22:1. The gradient was the wrong token for the job, not the wrong idea:
   it is for surfaces and marks, and the mark beside this label is already
   a gradient-filled SVG. */
.s-ai-btn { display: inline-flex; align-items: center; gap: 4px; }
.s-ai-btn svg { flex-shrink: 0; }
/* ══ THE MARK ON A BRAND-FILLED CONTROL ═════════════════════════════════
   The AiMY mark is a purple radial gradient, which reads on every ground
   this product has except one: the filled accent primary, where it is purple
   on purple. Measured on the list card's "Fill them in" — the only thing
   marking that button as AiMY's, invisible.

   `--aimy-mark` is declared on the symbol's own `fill` with the gradient as
   its fallback (see index.html), so this is the single place that opts out
   of it. Custom properties are the one thing that inherits into a `<use>`
   shadow tree; a `fill` attribute of `url(#aimy-rg)` cannot be reached from
   outside at all.

   Every other surface is untouched — nothing else sets the variable, so
   nothing else stops being the brand gradient. */
.s-insight-lnk.primary, .em-direct, .btn-brand { --aimy-mark: currentColor; }
.s-trace-sum.s-ai-btn { color: var(--ai-text); font-weight: var(--fw-bold); }
.ntf-foot-btn.s-ai-btn { justify-content: center; }

/* THE BLOCK FITS THE SURFACE IT LIVES IN.

   Measured: the create-a-campaign block ran **1031px** with the owner reveal
   open, inside a **578px** thread — 452px taller than the room it had. The
   thread was pinned to its bottom, so what you saw was the assignee tail, the
   effects and the buttons, with the block's own title 479px above the fold
   and the REQUIRED name field 321px above it. That is the reported "weird
   scroll when I choose to assign someone": nothing jumped, you were simply
   already at the bottom of something far too tall.

   Modals got this treatment in pass 6 and the canvas block never did. The
   body scrolls inside a bounded box; the head and the actions are pinned, so
   Create and Cancel are reachable at any scroll position and at any window
   height — rather than being visible only by the accident of the thread
   sitting at its end. */
.s-work {
  min-width: 0;
  display: flex; flex-direction: column;
  /* The thread's own height, less its padding and the turns above. Capping
     against the viewport keeps this honest on short windows, which is where
     the defect was worst. */
  max-height: min(calc(var(--vp-h) * 0.72), 520px);
}
.s-work-head, .s-work-actions { flex: none; }
.s-work-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.s-work-mark { display: flex; flex-shrink: 0; }
.s-work-title {
  font-family: var(--font-display);
  font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--d50);
  letter-spacing: var(--ls-tight);
}
.s-work-lede { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); margin-bottom: 12px; max-width: 65ch; }
.s-work-body {
  margin-bottom: 16px;
  /* `min-height: 0` because a flex item's default `auto` refuses to shrink
     below its content — without it the cap above does nothing and the box
     grows to 1031px again. */
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Clear of the scrollbar, so a radio label never sits under it. */
  padding-right: 4px;
}
/* A settled block has no body and no actions, so it must not keep a cap that
   would leave it a mostly-empty box. */
.s-work.is-done { max-height: none; }
/* 16px, measured at 0. The effects panel has no bottom margin and this row
   had no top one, so Cancel and the confirm sat welded to the box stating
   what they would do — the two things in a decision that most need to read
   as separate. */
.s-work-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
/* Work that has run stops being a control and becomes a record of what was
   done. Live buttons on finished work invite running it twice. */
.s-work.is-done { opacity: 0.72; }
.s-work-said {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ok);
  padding-top: 8px; border-top: 1px solid var(--hairline);
}
.s-work-evidence { margin-bottom: 16px; }
.s-work-ev-h {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted);
  margin-bottom: 8px;
}
.s-seed-list { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }

/* ── WHAT AiMY READ OFF THE AUDIENCE ──
   One sentence above the fields it filled, so a full form reads as a
   proposal rather than as somebody else's decision. */
.s-seed-say {
  display: flex; align-items: flex-start; gap: 8px;
  font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base);
  margin-bottom: 20px; max-width: 72ch;
}
.s-seed-say .s-insight-mark { width: 14px; height: calc(var(--fs-base) * var(--lh-base)); flex: none; }

/* ── The contacts it would start with ──
   A row per person: the tick, who they are, and why they are on this list.
   6 inside a row against 4 between rows would invert the ratio, so the rows
   sit tight and the block is what separates them from the next field. */
.s-seed-cons { display: flex; flex-direction: column; gap: 2px; }
.s-seed-con {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 8px; min-height: 32px; border-radius: var(--r-sm);
  transition: background-color var(--t-fast);
}
.s-seed-con:hover { background: var(--accent-dim); }
.s-seed-con:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: -2px; }
.s-seed-con input { flex: none; }
.s-seed-con-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.s-seed-con-name { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d100); }
.s-seed-con-role { font-size: var(--fs-xs); color: var(--d200); }
/* The reason, last and to the right — it qualifies the person, it does not
   introduce them. Hidden under 520px, where a two-column row stops being one. */
.s-seed-con-why {
  margin-left: auto; flex: none; text-align: right;
  font-size: var(--fs-base); color: var(--ink-muted); max-width: 22ch;
}
@media (max-width: 519.98px) { /* 520 layout px */ .s-seed-con-why { display: none; } }

/* `.s-add-*` went with the add-contacts panel. It put `Add ones we have`
   beside `Add an existing list` on Find, and the two were one offer. */


/* ═══════════════════════════════════════════════
   ANSWERS — computed, and citing what they counted
═══════════════════════════════════════════════ */
.s-ans { min-width: 0; }
.s-ans-title {
  font-family: var(--font-display);
  font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--d50);
  letter-spacing: var(--ls-tight); margin-bottom: 12px;
}
.s-ans-body { display: flex; flex-direction: column; gap: 2px; }
/* `baseline` sat the count, the name and the control on the type baseline of
   whichever ran tallest, so a row with a two-line name pulled its number out
   of line with every row above it. These are three unrelated things sharing
   a row; they align on the row, not on each other's text. */
.s-ans-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 0; border-bottom: 1px solid var(--hairline);
}
.s-ans-row:last-child { border-bottom: 0; }
.s-ans-name {
  display: flex; flex-direction: column; gap: 2px;
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--d100);
  min-width: 0; text-align: left;
}
button.s-ans-name { cursor: pointer; }
button.s-ans-name:hover { color: var(--accent); }
button.s-ans-name:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-ans-sub { color: var(--ink-muted); font-weight: var(--fw-medium); font-size: var(--fs-xs); }
.s-ans-fact { margin-left: auto; font-size: var(--fs-xs); color: var(--ink-muted); text-align: right; font-variant-numeric: tabular-nums; }

/* ── Who is worth ringing, in the canvas ──
   Two lines and a phone. Name and role on the first, the reason on the
   second, the verb on the right of both — so the eye reads down the names,
   and the hand goes to one column. Divided rather than carded: six people
   compared on one axis is a list, and six rectangles would make each of them
   a decision of its own. */
.s-call { display: flex; flex-direction: column; }
/* The session control sits under the six it will work through, not over
   them: you read who is in the queue and then decide to work it, which is
   the order the block is already written in. */
.s-call-foot { display: flex; margin-top: 12px; }
.s-call-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 4px 12px;
  padding: 8px 0; border-bottom: 1px solid var(--hairline);
}
.s-call-row:last-child { border-bottom: 0; }
.s-call-who { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
/* The reason spans the name column only, under it — a why that sat beside
   the name pushed the phone off the row at the canvas's width. */
.s-call-why {
  grid-column: 1; font-size: var(--fs-xs); color: var(--ink-muted);
  line-height: var(--lh-base); min-width: 0;
}
/* The phone spans both rows, so it is one target beside the person rather
   than one per line. */
.s-call-go { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
.s-call-go svg { width: 13px; height: 13px; }
.s-ans-go { flex-shrink: 0; }

/* `.s-ans-hit` and `.s-ans-bar` are gone with the campaign's status spread.

   The row-as-control argument they carried was right and it survives — it is
   what `.s-reason` does, on all four of the lists that each used to draw
   their own version of it. The BAR does not survive: it was flex 0 0 84px
   against a status split running 9/6/5/3/1, which is five near-identical
   stubs, and the same reasoning retired the ranked bars in the two sections
   above. `.s-ans-row` keeps its rules — the executive surface still draws
   it, and there the spreads are wide enough for a bar to mean something. */

/* ── Reschedule: two faces of one date ──
   Quick picks for the four answers people actually give, and a real date
   field for the fifth. Picking either updates the other and the stated
   effect above it. */
/* ══ RESCHEDULE, AS A CANVAS BLOCK ═════════════════════════════════════════

   `.s-when` and `.s-when-quick` held the modal's four quick-pick buttons
   beside a date field — two controls setting one value, so the first
   decision was which of them to use. Both are gone with the modal.

   What is left is AiMY's proposal in the confirm and one date field behind a
   disclosure for the case where you want another. `<details>` because the
   edit path should cost a press only when taken: doctrine §3 requires Edit
   to exist, not to be paid for by everyone who accepts. */
.s-work-say {
  font-size: var(--ty-body); line-height: var(--lh-base);
  color: var(--ink-secondary); max-width: 62ch; margin-bottom: 16px;
}
.s-work-say b { color: var(--ink-primary); font-weight: var(--fw-semibold); }
.s-when-own > summary {
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
  color: var(--ink-quiet); cursor: pointer; list-style: none;
  padding: 4px 0; transition: color var(--t-fast);
}
.s-when-own > summary::-webkit-details-marker { display: none; }
.s-when-own > summary::marker { content: ""; }
.s-when-own > summary:hover { color: var(--ink-primary); }
.s-when-own > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }
.s-when-own[open] > summary { margin-bottom: 8px; }
.s-when-date { width: 100%; color-scheme: dark light; }

/* A refusal that offers somewhere to go. */
.s-ans-none, .s-ans-lede { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }
.s-ans-try { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 12px; }
.s-ans-note { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); margin-top: 12px; }
/* A number with no way to see behind it is the same unsourced confidence
   this product exists to remove, wearing a more objective face. */
.s-ans-cite {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--hairline);
  font-size: var(--fs-xs); color: var(--ink-muted);
}
.s-ans-cite .cite-action { margin-left: auto; }

/* The table's stylesheet went with the table — 6070 characters of
   .s-table, .s-td-*, .s-cols-*, .s-pager and .s-page-*. Argued in
   sales.js §THE TABLE IS GONE. */

/* "…and 4 more." Three surfaces cut a list short and say so — the campaign
   list on a record, and the two `many` verbs. It lived in the table's
   stylesheet under the name `.s-cols-note`, which was never what it was for:
   the column manager happened to be the first thing that needed it. Renamed
   to what it does, and moved out of a block that no longer exists. */
/* ══ A NOTE RANKS BY SIZE, NOT BY FADING ══════════════════════════════════
   `--d500` is two steps below the dimmest ink role this product has a name
   for, and nine surfaces reached for it to mean "supporting" — so the
   supporting half of every commit body was the half you had to lean in to
   read. One step of SIZE below the copy above it does that job; losing the
   ink as well was the second device doing the first one's work badly. */
.s-more-note { font-size: var(--fs-xs); color: var(--d200); margin-top: 4px; }
/* ── AND THE COMMIT SURFACE GETS A BODY VOICE ──
   Every commit wanting prose reached for `.s-more-note` — a FOOTNOTE class
   — because there was nothing else, so the one paragraph a decision surface
   exists to make readable was set at the smallest step in the dimmest ink.
   This is the lead: what is being decided about, at a size that says so. */
.s-commit-lead {
  font-size: var(--fs-lg); font-weight: var(--fw-medium);
  color: var(--d50); line-height: var(--lh-base);
  margin: 0 0 4px; text-wrap: pretty;
}
.s-commit-lead b { font-weight: var(--fw-semibold); color: var(--d50); }

/* The tones the status word shares with the cards and the rail. */
.tone-err { color: var(--err-text); }
.tone-warn { color: var(--warn-text); }
.tone-ok { color: var(--ok-text); }
.tone-neutral { color: var(--ink-muted); }

/* 2275 characters of docked-rail CSS removed — .app-talk, its widths and
   breakpoints, body.is-docked, and the rail's own head and close button.
   The chat column inside the canvas replaces all of it; argued in
   index.html and in sales.js §THREADS. What survives below is what the
   THREAD still uses wherever it is drawn: the chips, the empty state and
   the turn styles. */


/* Bottom padding clears the float bar, which is fixed and overlaps the foot
   of this column. Without it the last turn hides behind the input that
   produced it. */
/* ── The touchpoint timeline, inside the rail ──
   It was a second column on the right of an open record, which put two
   conversation-shaped rails on one page. They are one thing: the
   touchpoints are what has been said TO this lead and the turns are what
   has been said ABOUT it — same subject, same order, one rail. */
.s-timeline { max-width: none; margin-bottom: 4px; }
.s-timeline .tl-dot { display: flex; align-items: center; justify-content: center; }
.s-timeline .tl-dot svg { width: 9px; height: 9px; }

/* ══ THE HISTORY IS THE RECORD, SO IT IS SET TO BE READ ════════════════════
   The library's timeline sets its note SMALLER than the heading it explains,
   and both below this product's body step. That block is the one thing on a
   record that answers "what has actually passed between us" — nine
   touchpoints on an open account, the densest and most-read thing on the
   surface — and it was the smallest type on the page.

   Title and body share the body step and separate on WEIGHT, which is this
   file's rule wherever size has run out of room. The timestamp drops to
   meta, where a supporting figure belongs.

   Scoped to `.s-timeline`, so the library's timeline is untouched everywhere
   else — including the campaign lifecycle in the rail, which is 220px wide
   and genuinely does want the smaller setting. */
.s-timeline .tl-title { font-size: var(--fs-base); }
.s-timeline .tl-body  { font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base); }
.s-timeline .tl-time  { font-size: var(--fs-xs); color: var(--ink-muted); }
.s-touch-who { color: var(--ink-muted); font-weight: var(--fw-medium); }
/* AiMY's own work is never disguised as the rep's. The --ai gradient is the
   library's marker for an AI actor and the avatar gradient is its marker for
   a human; they are not interchangeable. */
.s-touch-by { color: var(--ink-muted); font-weight: var(--fw-semibold); }
/* Same correction as the AiMY controls: the AI blue is a TEXT token, and
   the gradient clipped to type starts below the contrast floor. The marker
   still reads as AiMY's — it is the only blue name in a column of grey
   ones, and it is bold. */
.s-touch-by.is-ai { color: var(--ai-text); font-weight: var(--fw-bold); }
.s-touch.is-ai .tl-dot { border-color: rgba(var(--accent-rgb), 0.5); }
/* Why we reached out, in the slot the campaign name otherwise holds. One
   step up from the rest of the meta line because it is the only part of it
   that is a REASON rather than a label — and it appears on a minority of
   rows, so it has to be findable among them. */
.s-touch-why { color: var(--ink-muted); font-style: italic; }

.s-trace { margin-top: 8px; }
.s-trace-sum {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink-muted);
  cursor: pointer; list-style: none; padding: 2px 0;
  transition: color var(--t-fast);
}
.s-trace-sum::-webkit-details-marker { display: none; }
/* How many lines are behind it — a count beside the noun rather than baked
   into a sentence, so "Transcript" stays the label and the number stays the
   qualifier. Same shape as `.s-tab-n` and `.s-view-n`. */
.s-trace-n {
  margin-left: 8px; font-size: var(--fs-2xs); color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.s-trace-sum::before { content: "▸ "; }
.s-trace[open] .s-trace-sum::before { content: "▾ "; }
.s-trace-sum:hover { color: var(--d200); }
/* ══ A CLOSED DISCLOSURE HAS TO STAY CLOSED ═══════════════════════════════
   The library ships `.agent-steps { display: flex }`, and a class selector
   outranks the UA's `details:not([open]) > *:not(summary) { display: none }`
   — so every collapsed "What AiMY did" laid its full step list out anyway
   and spilled it 225px below a summary claiming to be 23px tall. Measured on
   40 traces: the whole point of the disclosure, defeated by one inherited
   declaration.

   Stated at the same specificity the library uses, scoped to the closed
   state, so opening still gets the library's flex column. */
.s-trace:not([open]) .agent-steps { display: none; }
.s-trace .agent-steps { padding: 4px 0 0 12px; }
.s-trace .agent-step-ico { font-size: var(--fs-2xs); }
/* `.agent-step-label` and `.agent-step-meta` are both inline in the library,
   and the meta carries a `margin-top` an inline box ignores — so the two ran
   together as one string. Given a block context. (../GAPS.md) */
.s-step-text { display: flex; flex-direction: column; min-width: 0; }

/* ── The transcript, folded under the touchpoint it came from ──
   `.call-line` and `.call-who` are the live panel's own classes, reused so a
   line read during the call and the same line read a month later are set
   identically. What the panel does not carry is the closed state and the
   indent, which the trace beside this already defines — so this only adds
   those two, at the trace's own specificity. */
.s-trace:not([open]) .s-said-lines { display: none; }
.s-said-lines { display: flex; flex-direction: column; gap: 8px; padding: 8px 0 0 12px; }
.s-said-lines .call-line { font-size: var(--fs-sm); }

/* `.ov-open` and `.ov-open-h` are gone with the heading they drew. A canvas
   is a conversation and opens with a message from AiMY — `openingSay`,
   rendered through `turnHtml` like everything else it says. Nothing on this
   surface is set above the body step, which is what a chat reads like.
   `.s-talk-empty` went with them; the sentence is a turn now. */

/* ── A question in the thread, and its shortcuts ──
   The hint is what AiMY would say if you took its suggestion, shown as the
   quoted sentence rather than pre-filled into the box: a field that arrives
   full is a decision somebody made, and one you can read and take is a
   proposal. */
.s-cb-hint {
  margin-top: 8px; padding-left: 12px;
  border-left: 2px solid var(--accent);
  font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base);
  max-width: 60ch;
}
/* ══ A QUESTION THAT SHOWS ITS WORKING ════════════════════════════════════

   The proposed call log is the record's own card, drawn inside the turn that
   asks whether it is right. It gets a ground and a rule of its own so it
   reads as the THING being asked about rather than as more of the sentence
   asking — the same device `.s-work-evidence` uses to put a record's history
   above a decision about it. */
.s-callsum-in-turn {
  margin-top: 12px; padding: 12px; border-radius: var(--r-md);
  background: var(--card-bg-raised); border: 1px solid var(--card-border);
}
.s-cb-opts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.s-cb-opt {
  font-family: inherit; cursor: pointer;
  padding: 8px 12px; border-radius: var(--r-xl);
  border: 1px solid var(--card-border); background: var(--card-bg-raised);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d100);
  transition: border-color var(--t-fast), background var(--t-fast);
}
.s-cb-opt:hover { border-color: var(--accent); background: var(--accent-dim); }
.s-cb-opt:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Answered, not removed: the thread is a record and what you chose between
   is part of it. Spent reads as spent — no hover, no pointer, no border. */
/* ══ THE WAY OUT IS NOT ONE OF THE ANSWERS ═════════════════════════════════
   Every turn carries a control that leaves the conversation for the page. It
   was drawn as a third option beside Companies and People, so a question with
   two answers looked like a question with three — and the one that was not an
   answer sat at the same weight as the two that were.

   Borderless and quiet: still there on every turn, still one press, and no
   longer competing with the thing actually being asked. */
.s-cb-opt.is-quiet {
  border-color: transparent; background: none;
  color: var(--ink-faint); font-weight: var(--fw-medium);
}
.s-cb-opt.is-quiet:hover { color: var(--ink-primary); background: none; border-color: var(--card-border); }
.s-cb-opt.is-spent {
  cursor: default; color: var(--ink-muted);
  border-color: transparent; background: none;
}

/* ══ MESSAGES ARE THE FAMILY'S, NOT THIS PRODUCT'S ═══════════════════════
   `.s-turn` was a local message shape: a right-aligned tinted box for you, a
   bare paragraph for AiMY, no avatars, no tails, no typing state. The
   library ships `.chat-msg` / `.msg-avatar` / `.msg-bubble` and both sibling
   products use it, so the one surface in Sales that IS a chat was the one
   that did not look like one.

   These are QA's numbers where QA and the library disagree — a 28px avatar
   at 8px radius against the DS's 30px circle, a 12px bubble against 16, and
   14px text against 13. Matching what is on screen in the sibling beats
   matching what is in the file, and 14 is this product's body step, so the
   two agree for once. */
.chat-msg { display: flex; gap: 12px; align-items: flex-start; padding: 0 4px; }
.chat-msg.user { flex-direction: row-reverse; }
.msg-avatar {
  width: 28px; height: 28px; border-radius: var(--r-md);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  flex-shrink: 0; margin-top: 2px;
}
.msg-avatar.aimy-av {
  background: rgba(70, 40, 140, 0.35);
  border: 1px solid rgba(var(--accent-rgb), 0.3);
}
.msg-avatar.aimy-av svg { width: 14px; height: 16px; }
.msg-avatar.user-av { background: var(--grad-avatar); color: #fff; }
.msg-bubble {
  max-width: 540px; padding: 12px 16px; border-radius: var(--r-xl);
  font-size: var(--fs-base); font-weight: var(--fw-medium);
  line-height: var(--lh-base); color: var(--d50);
}
/* The tail points at its speaker: the square corner sits on the avatar's
   side, which is the whole reason a bubble reads as coming from somebody. */
.chat-msg.aimy .msg-bubble {
  background: var(--card-bg-raised); border: 1px solid var(--card-border);
  border-radius: var(--r-xs) var(--r-xl) var(--r-xl) var(--r-xl);
}
.chat-msg.user .msg-bubble {
  background: var(--accent-dim); border: 1px solid rgba(var(--accent-rgb), 0.22);
  color: var(--d100);
  border-radius: var(--r-xl) var(--r-xs) var(--r-xl) var(--r-xl);
}

/* ── AiMY composing ──
   Three dots in an AiMY bubble, which is what both siblings draw. The pause
   is what makes an answer read as a reply rather than as a lookup. */
.typing-dots { display: flex; gap: 4px; padding: 4px; align-items: center; }
.typing-dots span {
  width: 4px; height: 4px; border-radius: 50%; background: var(--d500);
  animation: sTyping 1.2s ease-in-out infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.15s; }
.typing-dots span:nth-child(3) { animation-delay: 0.3s; }
@keyframes sTyping {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-4px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .typing-dots span { animation: none; opacity: 0.6; }
}

/* A write, in the thread, with its own way back. This is the half V1 has
   nowhere: it shows the conversation and never what the conversation did. */
/* The receipt is not a message — it is what a message DID — so it keeps
   its own full-width shape rather than a bubble with a speaker. */
.chat-msg.is-change { align-items: stretch; padding: 0 4px; }
/* The whole border at low alpha, not a tinted left edge. This is the
   library's own idiom — `.bcard.p1/.p2/.p3` — and Knowledge records having
   tried the left edge and rejected it: the pattern it was copied from sets
   `border-left-color` on a border its base rule has commented out, so it is
   dead in the system it came from. The `±` mark carries the accent, so the
   signal is still colour AND text rather than colour alone. */
.s-change {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 12px; border-radius: var(--r-md);
  background: var(--card-bg-raised);
  border: 1px solid rgba(var(--accent-rgb), 0.22);
  font-size: var(--fs-xs); color: var(--ink-muted); line-height: var(--lh-base);
}
.s-change-mark { color: var(--accent); font-weight: var(--fw-bold); flex-shrink: 0; }
.s-change-text { min-width: 0; }
.s-change-text strong { color: var(--d50); }
.s-change-undo {
  margin-left: auto; flex-shrink: 0;
  font-family: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  color: var(--accent); cursor: pointer; padding: 2px 8px; border-radius: var(--r-xs);
  min-height: 24px;
}
.s-change-undo:hover { background: var(--accent-dim); }
.s-change-undo:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }


/* ══ THE BUILD, IN THE CANVAS ═════════════════════════════════════════
   The criteria card's rules lived here. The card was the first of two forms
   asking for one name, and it is gone; only `.s-crit-rows` survives, because
   the read-out it drew is now a field inside the one form that remains. ══ */

/* The audience panel. STICKY, because the whole reason the flow moved here
   is that you could not see what your answers were doing — a preview that
   scrolls out from under the question is the same defect with extra steps. */

/* The build's own message. Being a `.chat-msg` gives it the same 640px cap as
   everything else in the thread; the bubble then has to FILL that cap rather
   than shrink to its content, or the block still does not line up — measured,
   452px against the commit block's 610px on the same left edge. */
/* Cancel is not a button. Three controls of equal weight is three decisions
   of equal weight, and two of these are ways out of one. It reads as the
   quiet one, sits hard left, and keeps a 24px target. */
.s-work-cancel {
  min-height: 24px; padding: 0 var(--sp-2); margin-right: auto;
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-xs); color: var(--ink-muted);
}
.s-work-cancel:hover { color: var(--d200); text-decoration: underline; text-underline-offset: 3px; }
.s-work-actions.has-alt { flex-wrap: wrap; }

/* ═══════════════════════════════════════════════
   LISTS — chips, assignees, the scope bar
═══════════════════════════════════════════════ */
.s-camps { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.s-camp-chip { cursor: pointer; display: inline-flex; align-items: center; gap: 4px; }
.s-camp-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The campaign's state, said on the chip: a draft is safe to assemble and a
   running one sends on your behalf. Computed, so there is no field anybody
   has to remember to set — the same rule the lead statuses follow. */
.s-camp-state {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.08em; text-transform: uppercase;
}
/* Every campaign chip carries a way out of that campaign. You could add and
   never remove, which is a loop with a start and no finish. */
/* ══ A GROUP, AND ITS ONE SENTENCE ═════════════════════════════════════════
   The finding sits above the records it owns, in the same anatomy the card
   used to carry per record: AiMY's mark, the work state, the sentence, then
   the actions as links. Reading it is reading one thing instead of thirty-one
   copies of one thing.

   It is sticky. A group of thirty scrolls past its own heading, and a card
   whose flag chip says "Gone quiet" with no heading in view has lost the
   sentence that explains why it is grouped at all. */
/* `.s-group-head` and its parts are gone with the per-finding headings. It
   was a sticky, blurred panel carrying the AiMY mark, a work-state pill, the
   finding as a paragraph and two verbs — careful work, and all of it in
   service of a structure that put a full-width bar above every group. On the
   sets this surface is reached with, that meant more bars than records.

   `.s-findings` accumulates the same sentences into one block above one flat
   list, and the stickiness is not needed there: a heading has to follow you
   down thirty rows, a summary you have already read does not.

   `.s-group` and `.s-group-more` survive — `.s-group` still wraps other
   surfaces' sections, and `Show the other N` now pages the single list. */
.s-group { margin-bottom: 24px; }
.s-group-more { margin-top: 8px; }

/* `.s-flagchip` was what a card kept of the insight when its group heading
   carried the sentence — two words in the finding's tone. Both are gone with
   the grouping: `.s-findings` states every finding once above one flat list,
   and `leadRow` writes which one applies into the row. */

/* ── WHICH CAMPAIGNS THIS RECORD IS IN, AND WHERE IT STANDS IN EACH ──
   Two lines per campaign, not one 342px strip. The grid puts the name and its
   state on the first line with the remove pinned to the right edge, and the
   record's position on the second, under the name it qualifies. */
.s-camprows { display: flex; flex-direction: column; gap: 8px; }
.s-camprow {
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline;
  column-gap: 8px; row-gap: 4px;
  padding: 8px 12px; border-radius: var(--r-md);
  /* The card pair, matched. `--d700` is opaque and `--card-border-hover` is
     14% white, so starting from one and hovering to the other moved the
     border between two different families and read as a colour change rather
     than as the boundary strengthening. Both ends come from the design
     system's card boundary now — the same pair `.s-listrow` uses for the same
     job, a row you press. */
  border: 1px solid var(--card-border); background: var(--card-bg);
  /* The stretched link's containing block, and the hover feedback for it. */
  position: relative; transition: border-color var(--t-fast);
}
.s-camprow:hover { border-color: var(--card-border-hover); }
/* The name covers the row, so pressing anywhere opens the campaign at this
   record's stage — the `::after` idiom rather than `:has()`, which this file
   has twice had stop matching silently. */
.s-camprow-name {
  font-family: inherit; background: none; border: 0; padding: 0; text-align: left;
  font-size: var(--fs-base); font-weight: var(--fw-semibold); color: var(--d50);
  cursor: pointer;
}
.s-camprow-name::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.s-camprow-name:focus-visible { outline: none; }
.s-camprow-name:focus-visible::after { outline: 2px solid var(--brand); outline-offset: -2px; }
/* A step under the name, level with the position line below it: the campaign
   is the subject and its state is one of two facts about it. Tone and weight
   are what separate it from the name, not size — at 14px/400 beside 14px/600
   it was competing for the same rank. */
.s-camprow-state { justify-self: start; font-size: var(--fs-xs); }
/* Above the stretched link, and pinned to the far edge so two rows of
   different name lengths still put it in one place. */
.s-camprow .s-camp-out { position: relative; z-index: 1; justify-self: end; margin-left: 0; }
/* The second line, spanning all three columns under the name. */
.s-camprow-at {
  grid-column: 1 / -1;
  font-size: var(--fs-xs); color: var(--ink-muted);
}
.s-camprow-sep { color: var(--ink-muted); }
.s-camprow-add { align-self: flex-start; margin-top: 4px; }
/* ══ A CONTROL YOU CANNOT SEE IS NOT RESTRAINT ══════════════════════════
   `--d600` measured 2.51:1 — the same near-invisible value the empty states
   were using, on a thing that is not decoration but the only way to take an
   offering off a campaign. "Support ×" read as "Support" with a smudge after
   it. `--d400` is 6.08 and still quieter than the name it sits beside; the
   hover to `--err` is what says it removes. */
.s-camp-out {
  width: 20px; min-height: 24px; margin-left: -2px;
  font-family: inherit; font-size: var(--fs-sm); color: var(--ink-muted); cursor: pointer;
  transition: color var(--t-fast);
}
.s-camp-out:hover { color: var(--err); }
.s-camp-out:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* A note a rep added to AiMY's work. AiMY's own entry is never edited —
   rewriting the record of what it did is the one thing this refuses. */
/* A raised surface and nothing else — the same treatment `.s-commit-quote`
   and `.s-sheet-brief` already use for an attached aside. This one kept a
   left rule the other three had dropped, which made one of four identical
   jobs look different for no reason. */
.s-touch-note {
  margin-top: 8px; padding: 8px;
  font-size: var(--fs-base); color: var(--ink-muted); line-height: var(--lh-base);
  background: var(--card-bg-raised);
  border-radius: var(--r-xs);
}
.s-touch-note-who { font-weight: var(--fw-bold); color: var(--d100); }
/* ══ EDITING IN PLACE ══════════════════════════════════════════════════════

   THE AFFORDANCE IS ALMOST NOTHING, AND THAT IS THE POINT. These fields are
   read a hundred times for every time one is changed, so an editable name
   that looks like a text box would put a form around the whole record. It
   inherits its type entirely — an editable `<h1>` is still the `<h1>` — and
   says it can be changed only on hover and on focus.

   The pencil is the exception: a value that reveals its affordance ONLY on
   hover is a value nobody with a touchscreen ever discovers. It sits at 0.35
   at rest, which is present without being a second thing to read. */
.s-edit {
  font: inherit; color: inherit; letter-spacing: inherit; text-align: inherit;
  display: inline; cursor: text;
  border-radius: 3px; padding: 0 2px; margin: 0 -2px;
  transition: background-color var(--t-fast), box-shadow var(--t-fast);
}
.s-edit:hover { background: var(--accent-dim); }
.s-edit:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.s-edit-pen {
  width: 0.72em; height: 0.72em; margin-left: 0.3em;
  opacity: 0.35; vertical-align: baseline;
  transition: opacity var(--t-fast);
}
.s-edit:hover .s-edit-pen { opacity: 0.9; color: var(--accent); }
/* An absent value still has to be a target, so it says what is missing
   rather than rendering as nothing you could press. */
.s-edit-none { color: var(--ink-muted); font-style: italic; }

/* The live input takes the SIZE of what it replaced but not its weight — a
   30px extrabold heading typed into is a heading you cannot see the caret
   in. Full width on the long fields, because a goal is two sentences. */
.s-edit-live { display: block; }
.s-edit-in {
  font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  letter-spacing: 0; line-height: var(--lh-base);
  width: 100%; max-width: 52ch;
}
textarea.s-edit-in { max-width: 68ch; resize: vertical; }

/* ── WHAT THIS TOUCHPOINT DID ──

   The line that makes a computed status legible. It sits between what
   happened and the note about it, tight to both, because it is neither a
   heading nor an aside — it is the consequence of the line above.

   Not coloured as a whole. The VERB is quiet and the outcome carries the
   tone, so a row that raised a bounce reads amber on two words rather than
   on a sentence; four amber sentences down a timeline is a page that looks
   broken rather than a history with two problems in it. */
.s-fx-line { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 4px; }
.s-fx { display: inline-flex; align-items: baseline; gap: 4px; font-size: var(--fs-xs); }
.s-fx-verb { color: var(--ink-muted); }
.s-fx-name { font-weight: var(--fw-semibold); }
/* "nothing changed" is a real answer and the commonest one — most calls do
   not move a record — so it is drawn quietly rather than omitted. A row that
   is silent about its effect reads as a row whose effect was not computed. */
.s-fx-none { font-size: var(--fs-base); color: var(--ink-muted); }

/* 8 above against the 3 the effect line sits at: the controls belong to the
   whole row, not to the note directly above them. */
.s-touch-ops { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }

/* Two columns for six short fields. One column made a six-field correction
   as tall as the dialog it sits in, and every one of these is a word. */
.s-fix-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 559.98px) { /* 560 layout px */ .s-fix-grid { grid-template-columns: minmax(0, 1fr); } }
.s-fix-grid .s-field { max-width: none; }
.s-fix-at, .s-fix-dir { width: 100%; }
.s-sheet-kind .s-camp-state { margin-left: 8px; }

.s-assignees { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.s-assignee {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-xs); color: var(--d200);
}
.s-assignee.is-owner { color: var(--d50); font-weight: var(--fw-semibold); }
.s-sheet-head-main { min-width: 0; }
.s-new-name, .s-new-desc, .s-new-aim, .s-note, .s-addr, .s-kb-why { width: 100%; }
.s-note, .s-kb-why { resize: vertical; }
/* THE CHECKMARK IS NOT CENTRED IN ITS OWN BOX, and it is the library's rule.

   `.ds-choice input[type="checkbox"]:checked::after` places a 5×9 element at
   a hard-coded `left: 5px; top: 1.5px` and rotates it 45°. Rotation happens
   about the element's centre, so the tick's centre is wherever that offset
   put it — measured, **1.5px left and 3px above** the box's centre in an
   18px box. Small, and visible: the tick sits high and to the left in every
   picker, every add-contacts row and every decision zone at once.

   The offsets were presumably tuned by eye at one size and then frozen. The
   fix is to stop expressing the position as a distance from a corner and
   express it as what it actually is — centred — so it stays centred at any
   box size and any glyph size.

   The radio is unaffected: `inset: 4px` is symmetric, so it centres itself.

   Filed in ../GAPS.md. */
.ds-choice input[type="checkbox"]:checked::after {
  left: 50%; top: 50%;
  /* Rotate first, then pull back by half — the order matters, because
     `translate` after `rotate` would move along the rotated axes. */
  transform: translate(-50%, -50%) rotate(45deg);
  /* The tick's ink is its bottom and right borders, so its optical centre
     sits a hair below its geometric one. One pixel up, and no more. */
  margin-top: -1px;
}

/* EVERY TICK AND RADIO ON THIS SURFACE.

   This rule used to carry `min-height: 24px` beside `height: 15px`, and said
   in its own comment that SC 2.5.8 is about the target rather than the ink —
   then grew the ink. `min-height` beats `height`, so every checkbox rendered
   **15×24** and every radio **18×24**; and a radio is `border-radius: 50%`,
   so an 18×24 radio drew an **ellipse** with the library's inset dot
   stretched inside it. That was on screen in every picker, every decision
   zone, every table row and every dialog.

   The ink is now the drawn size and nothing else. The 24px target lives on
   the box around the control — which is the pattern this file already uses
   for `.filter-chip` a hundred lines above, and which a label gives for
   free, since clicking a label is clicking its input. */
.s-tick, .s-add-tick, .s-new-assign, .s-new-owner,
.s-new-con, .s-new-sell, .s-new-client, .s-task-pick, .s-enrich-tick,
.s-log-reply, .s-log-obj, .s-list-to {
  width: 15px; height: 15px; accent-color: var(--accent);
}
/* Inside `.ds-choice` the library draws its own 18px control at a higher
   specificity; it wins on size and should. Only the target is ours. */
/* `min-height: 24px` gives the WCAG target — and it QUIETLY REMOVED the
   protection that stops a flex item shrinking below its own content.

   A flex item's default is `min-height: auto`, which is what makes a column
   refuse to squash its children. Naming an explicit minimum replaces that
   default, so inside `.s-pick-scroll` (a 280px column with overflow) every
   row shrank to exactly 24px while its content stayed 38px — and the names
   and subtitles overlapped each other down the whole list.

   `flex-shrink: 0` says the thing `min-height: auto` was saying. Measured:
   24px squashed, 54px correct. */
.ds-choice { min-height: 24px; flex-shrink: 0; }
/* The table's ticks have no label of their own, so the cell is the target. */

.s-scope { margin-bottom: 16px; }
.s-scope .ss-num { font-variant-numeric: tabular-nums; }
.s-scope .ss-actions { display: inline-flex; gap: 8px; margin-left: auto; }

/* ═══════════════════════════════════════════════
   THE COMMIT SURFACE — form pieces for the list operations
═══════════════════════════════════════════════ */
/* `.s-commit-banner` and `.s-pick-scroll` went with the add-contacts
   panel — the only surface that ever drew either. */
.s-field { display: block; margin-bottom: 16px; }
/* ── `hidden` HAS TO WIN, AND IT DID NOT ──
   `[hidden] { display: none }` lives in the UA stylesheet, so a class rule
   setting `display` on the same element beats it. `.s-field` did, which meant
   the Topic and Objection fields on "Log a touchpoint" — hidden on purpose,
   `f.hidden = !inbound`, because an outbound message has no reply category
   and no objection — were `hidden: true` and `display: block` at once, 93px
   and 57px tall, on every outbound log.

   Measured: the fields showed, they took a choice, and `run()` discarded it
   (`inbound ? ... : null`). Pick "Not interested" on a call you made, press
   Log it, and the touchpoint records nothing — a control that accepts input
   and throws it away, which is the defect this pass keeps finding.

   Stated at class specificity rather than with `!important`: same weight as
   the rule it corrects, later in the file, so it wins by order. */
.s-field[hidden] { display: none; }
.s-field-label {
  display: block; margin-bottom: 4px;
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink-muted);
}
.s-field .field-input, .s-field .ds-textarea { width: 100%; }
/* ── The qualifier beside a field label ──
   `Rememberoptional`. The label and the note are two inline spans with
   nothing between them, so every field carrying one read as a single
   run-together word. A margin rather than a space in the markup, because a
   literal space inside a template is invisible to whoever edits it next and
   the first person to reformat the line deletes it. */
.s-field-note { margin-left: 8px; font-weight: var(--fw-medium); color: var(--ink-muted); }

/* ── Owner: one line, and the other five behind a reveal ──
   Six radios for an answer that is "you" nearly every time. `<details>` is
   the native form of a closed control that opens in place, so it is the one
   used rather than a toggle rebuilt out of a button and a class. */
.s-owner-swap {
  margin-left: auto; font-size: var(--fs-xs); color: var(--ink-muted);
  border-bottom: 1px solid var(--d700);
}
.s-owner[open] .s-owner-swap { color: var(--accent); border-color: var(--accent); }

/* ── Governance: four questions, one line, all four defaults stated ──
   The summary is the ANSWER, not a label — "You own it, nobody else is on
   it, for our own pipeline, nobody watching" is the whole of what the
   sixteen controls inside would say if you left them alone, so the common
   case is readable without opening anything. */
.s-gov { margin-bottom: 16px; }
.s-gov-sum {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; cursor: pointer;
  padding: 8px 12px; border: 1px solid var(--d700); border-radius: var(--r-md);
  list-style: none;
}
.s-gov-sum::-webkit-details-marker { display: none; }
.s-gov-sum:hover { border-color: var(--ink-muted); }
.s-gov-sum:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-gov-say { font-size: var(--fs-xs); color: var(--ink-muted); min-width: 0; }
.s-gov[open] .s-gov-sum { margin-bottom: 12px; }
.s-gov[open] .s-owner-swap { color: var(--accent); border-color: var(--accent); }
/* The hide has to be declared. A `display` on the direct child of a closed
   `<details>` overrides the user-agent rule that would have hidden it, so
   `display: flex` here quietly re-showed all sixteen controls and the
   disclosure became decoration. */
.s-gov:not([open]) .s-gov-body { display: none; }
.s-gov-body { display: flex; flex-direction: column; gap: 16px; }

/* The audience chip form is gone from the Find node, and its rules with it.
   It asked "where they stand" and "who they are" and added whatever matched
   — a third list builder, inline, on a stage whose two doors reach the real
   one. Find draws the campaign's LISTS now; see `.s-clists`. */

/* `.s-newstage*` went with the static four-stage preview it drew inside the
   create form. Creating now lands on `.s-flow` — the same four stages
   carrying live figures — so the preview was a diagram of the page you were
   one press away from. */

/* ── Assignees: the shape the campaign sheet already shows them in ──
   Wrapping toggles rather than a stacked list, so six people take two rows
   instead of six and the form reads at the weight of the decision. */
.s-assign-grid { display: flex; flex-wrap: wrap; gap: 4px; }
.s-assign-chip {
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  padding: 4px 8px 4px 4px; min-height: 32px;
  border: 1px solid var(--d700); border-radius: var(--r-pill);
  font-size: var(--fs-xs); color: var(--ink-muted);
  transition: border-color var(--t-fast), background var(--t-fast);
}
/* ══ A TWO-LINE CHIP IS NOT A PILL ═══════════════════════════════════════
   One rule was serving two shapes. With an avatar and a single line the chip
   is 34px tall and `--r-pill` is exactly right. With a name STACKED over a
   description it is 46px — and a 9999px radius on 46px is a 23px corner, so
   both lines run straight into the curve with nothing to clear it: the 4px
   left padding exists to seat an avatar flush, and a chip with no avatar
   spends it on nothing. That is the clipping.

   `:has(.s-pick-role:not(:empty))` asks the question the shape actually
   depends on — is there a second line — rather than a class every call site
   has to remember to pass. The `:not(:empty)` is load-bearing: three call
   sites were rendering the span from `p.role`, which REPS do not carry, so
   five assignee chips held an EMPTY element. Without the guard they matched
   as two-line and lost their pill at 34px tall. (Those three now read
   `roleOf`, so the span has content — but a shape rule that depends on a
   value being non-blank should say so rather than assume it.)

   A rounded rectangle, and horizontal padding that clears its own corner. */
.s-assign-chip:has(.s-pick-role:not(:empty)) {
  border-radius: var(--r-xl);
  padding: 8px 12px;
}
/* Except where an avatar is one of the two lines' neighbours: it seats flush
   against the left edge and the radius is what gives it room, so only the
   right side needs the wider padding. */
.s-assign-chip:has(.s-pick-role:not(:empty)):has(.avatar) {
  padding: 4px 12px 4px 4px;
}
.s-assign-chip:hover { border-color: var(--ink-muted); }
/* KEYBOARD ONLY. `:focus-within` fires for a mouse click too — the chip's
   hidden checkbox takes focus and the call stays until you click somewhere
   else, so a chip you had just picked showed a `--brand` outline AND the
   `--accent` checked fill at once: two states drawn for one fact, and the
   one that lingers is the one that means nothing.

   `:has(input:focus-visible)` leaves the call for the people who need it —
   the input is `opacity: 0` but still in the tab order, so Tab reaches it and
   the browser sets `:focus-visible` — and leaves a mouse click showing only
   what it changed. Every other focus style in this file is already
   `:focus-visible`; this was the one that was not. */
.s-assign-chip:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 1px; }
/* Checked reads as picked, not as focused: the accent fills the border and
   tints the ground, which is how every other chosen chip here behaves. */
.s-assign-chip:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); color: var(--d100); }
.s-assign-chip input { position: absolute; opacity: 0; pointer-events: none; }
.s-assign-name { display: flex; flex-direction: column; line-height: var(--lh-snug); }
/* The channel, chosen where its consequence is stated rather than on a
   control beside an input. AiMY reads it out of the words; this is where it
   says what it read and lets you correct it. */
.s-ch-pick { width: 100%; }
.s-ch-pick .seg-btn { flex: 1; }
.s-ch-note { font-size: var(--fs-xs); color: var(--ink-muted); margin-top: 8px; }
/* Shown and disabled, never hidden: a contact you cannot FIND is worse than
   one you cannot select, because you go looking for it. */
.s-pick-row.is-already { opacity: 0.45; cursor: not-allowed; }
.s-pick-row.is-already:hover { background: none; }

/* ═══════════════════════════════════════════════
   THE CAMPAIGN SHEET — a filter, with a brief behind its name
═══════════════════════════════════════════════ */
/* THE SHEET FITS THE WINDOW, AND ITS MIDDLE IS WHAT SCROLLS.

   Measured before this: the campaign sheet rendered 928px tall in a 720px
   viewport, `max-height: none`, and the OVERLAY scrolled instead. So the
   thing that moved was the whole card — title, state, and the close button
   with it — and 256px of content sat below the fold with no indication it
   was there. Scrolling to reach it scrolled away the way out.

   Head and foot are pinned because they are the two things you need at every
   scroll position: what am I looking at, and what can I do about it. */
.s-sheet-head { display: flex; align-items: flex-start; gap: 12px; padding: 24px 24px 16px; flex: none; }
.s-sheet-head .modal-close { margin-left: auto; }
/* `--d400`, not `--d500`. This is the word that says what the surface IS —
   LIST, CAMPAIGN, RUN — and it measured 3.54:1, under the 4.5 floor and the
   faintest thing on the page it introduces. Same correction the campaign's
   identity captions took, on the same token, for the same reason. */
.s-sheet-kind {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
/* TITLE, one step up from block heading. It was `--fs-xl` — which resolves
   to 16px, the BLOCK HEADING step — so the name of the run you have open sat
   at the same size as the headings of the sections inside it. The campaign
   page had already patched around this with an override of its own; the two
   surfaces use one rule now, because they are the same kind of thing. */
.s-sheet-name {
  font-family: var(--font-display);
  font-size: var(--fs-3xl); font-weight: var(--fw-extrabold); color: var(--d50);
  letter-spacing: var(--ls-tight); margin-top: 2px;
}
.s-sheet-when { font-size: var(--fs-xs); color: var(--d200); margin-top: 4px; }

/* ═══════════════════════════════════════════════
   THE CAMPAIGN PAGE

   Was a 600px modal, and the content had outgrown it: a description, an
   assignee list, a distribution over eight statuses, a plan, a Knowledge
   brief and four actions. Worse than cramped, it was a LAYER — and layers
   stack, which is how "Start it" ended up leaving three surfaces open at
   once with cancel able only to peel the top.

   It is a page now, on the record page's own scale, so the two read as the
   same kind of thing because they are: a named object you opened, with one
   way back.
═══════════════════════════════════════════════ */
.s-camp-page .s-sheet-head {
  display: flex; align-items: flex-start; gap: 16px;
  padding: 0; margin-top: 16px;
}
.s-camp-page .s-sheet-head-main { flex: 1 1 auto; min-width: 0; }

/* ══ THE MASTHEAD ══════════════════════════════════════════════════════════

   The name sets the ceiling. Nothing on this page is allowed to be larger
   than it — not the lead figure, not a stage's count, not a section's own
   number — because the largest thing on a page is what tells a reader where
   they are, and a surface whose biggest element is a statistic reads as a
   chart that has lost its title.

   It goes UP, from 32 to 46. The pass before this took it down to 20 to
   clear room for the figure and got the relationship backwards: the room the
   figure needed was never the title's to give, it was sitting in the flat
   14px middle of the page. Taking the top of the scale for the name and
   re-ranking everything under it is what actually opened the surface.

   Scoped, because the run sheet's name shares this class and has no lead
   under it to outrank — `sales.css` records that unification as deliberate
   and it stays. */
.s-camp-page .s-sheet-name {
  font-size: var(--ty-masthead); letter-spacing: var(--ls-tighter);
  line-height: 1.05; color: var(--ink-primary);
  margin-top: 4px; text-wrap: balance;
}
.s-camp-page .s-sheet-kind { font-size: var(--ty-meta); color: var(--ink-secondary); }
.s-camp-page .s-sheet-when { font-size: var(--ty-meta); color: var(--ink-secondary); margin-top: 8px; }
@media (max-width: 899.98px) { /* 900 layout px */
  .s-camp-page .s-sheet-name { font-size: var(--ty-lead); }
}
/* ── THE CAMPAIGN'S ACTIONS, BESIDE ITS NAME ──

   `.s-camp-foot` was here, and it sat at 4,801px on a 4,762px page — below
   every one of the campaign's member cards, six and a half screens from the
   name it belongs to. These are campaign-level verbs and they belong with
   the campaign's identity, which is at the top.

   The primary keeps its weight and the rest are ghosts, so the row reads as
   one recommended move with two others available rather than as four equal
   choices. Under 900 the group takes its own line: a four-button row and a
   campaign name cannot share a narrow one. */
.s-camp-acts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-left: auto; flex: none;
}
@media (max-width: 899.98px) { /* 900 layout px */
  .s-camp-acts { margin-left: 0; flex: 1 1 100%; margin-top: 12px; }
}
/* ══ THE PIPELINE ══════════════════════════════════════════════════════════

   Four cells, always four wide, always comparable. What this replaces was an
   `<ol>` of full-width rows with the open stage's five hundred pixels of
   work nested INSIDE one of them — so the fourth stage sat a screen and a
   half below the third, and the four figures the block exists to compare
   could not be seen together at all.

   Side by side is what makes them a pipeline rather than a list. The eye
   crosses them in one movement, left to right, which is the direction the
   work actually flows, and the stage under the strip is then free to take
   the whole column.

   No card. The strip is grouped by proximity and a single raised surface
   under the selected cell — the page's one card was this list, and it was
   the only card on a surface that groups with space everywhere else. */
/* ── ONE ROW, HOWEVER MANY NODES THERE ARE ──
   It was `repeat(4, ...)`, written when there were four — so the fifth
   wrapped onto a line of its own and the strip read as four stages and an
   afterthought. `auto-fit` counts the cells it is given, which is the right
   rule for a strip whose length depends on who is reading it: a BDR gets
   four, a sales manager five, and neither should have to know that. */
.s-pipe {
  display: grid;
  /* No `grid-template-columns`: every cell becomes an implicit COLUMN of
     equal width, so the strip is one row for four nodes or five without
     being told how many there are. `repeat(auto-fit, minmax(0, 1fr))` cannot
     do that — `auto-fit` needs a real minimum to count against, and `0`
     gives it nothing to count. */
  grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: 4px; margin-bottom: 24px;
}
.s-pipe-cell {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
  padding: 12px; border-radius: var(--r-md);
  background: none; border: 0;
  font-family: inherit; text-align: left; cursor: pointer;
  transition: background-color var(--t-fast);
}
/* ── AT REST, A PRESSABLE THING HAS TO LOOK PRESSABLE ──
   Four of the five cells were flat until the pointer was already on them, so
   the strip read as one card and four labels and nothing offered the other
   four. Three steps now — a ground at rest, a lighter one under the pointer,
   the accent fill when it is open — and no line anywhere.

   ONLY ON `button`. `div.s-pipe-cell` is the read-only strip a tier that
   cannot open stages gets, and leaving it flat makes the same signal work
   the other way: the ground is what says this one can be pressed. */
/* MEASURED, not chosen by eye. `--card-bg` is rgb(20,27,36) and the ground
   the strip is painted on is rgb(23,26,28) — both at luminance 26, so a
   resting ground drawn in it was invisible and the affordance did not exist.
   `--card-bg-raised` is luminance 37, which reads. */
button.s-pipe-cell { background: var(--card-bg-raised); }
/* `:not(.is-here)`, or the hover ground beats the open cell's accent on
   specificity — `button.s-pipe-cell:hover` outweighs `.s-pipe-cell.is-here`,
   so pointing at the stage you are standing in turned it grey. */
button.s-pipe-cell:not(.is-here):hover { background: var(--accent-dim); }
.s-pipe-cell:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
div.s-pipe-cell { cursor: default; }

/* ── THE SELECTED CELL ──
   The fill does it alone. An underline under a tinted cell was two devices
   saying one thing, and the second was a drawn line on a surface that had
   just had every drawn line taken out of it. The tint is what this ecosystem
   has always meant by an active nav item, and it is the one place on this
   block the accent appears. */
.s-pipe-cell.is-here { background: var(--accent-dim); }

.s-pipe-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* ── THE ORDINAL CARRIES THE STAGE'S CONDITION ──
   Clear, holding work, or holding work that is yours. Which cell happens to
   be selected is navigation state and is drawn by the cell itself, so the
   mark is free to keep saying how the campaign is doing. Colour is paired
   with the cell's own aria-label, never carrying the meaning alone. */
.s-pipe-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%; flex: none;
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-faint); border: 1px solid var(--card-border);
  font-variant-numeric: tabular-nums;
}
/* ── AND THE ORDINAL STOPPED CARRYING STATE ──

   It was green for clear, amber for where the campaign stands, red for
   yours — three hues, no legend anywhere, on a mark whose own cell already
   says the same things in words: `Clear`, `3 waiting`, and the stage head's
   `waiting on you`. Colour that duplicates adjacent text is not redundancy
   for accessibility, it is a third vocabulary to learn for nothing, and the
   design system's rule is that status is carried by colour AND text — not
   that every text state earns a colour.

   What survives is `is-at`: which stage the campaign has actually reached is
   the one thing the figures do not say, since a stage can be clear because
   it is done or clear because nothing has got there yet. */
.s-pipe-cell.is-at .s-pipe-n { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); background: var(--warn-bg); }
.s-pipe-name {
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-secondary); min-width: 0;
}
.s-pipe-cell.is-here .s-pipe-name { color: var(--ink-primary); }

/* The figure and what it counts, on one baseline — the same construction as
   the lead and the section heads, so a number and its unit read as one
   statement everywhere on this page. */
.s-pipe-fig {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: var(--ty-display); font-weight: var(--fw-extrabold);
  line-height: var(--lh-none); letter-spacing: var(--ls-tight);
  color: var(--ink-primary); font-variant-numeric: tabular-nums;
}
/* A condition is not a quantity. `Clear`, `None`, `No goal` are states, set
   at the body step so a word is never mistaken for a figure. */
.s-pipe-fig.is-state {
  font-size: var(--ty-body); font-weight: var(--fw-bold);
  color: var(--ink-faint); letter-spacing: 0;
}
.s-pipe-unit {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-faint);
}

/* `.s-pipe-track` and `.s-pipe-fill` are gone. They drew a bar under the two
   nodes that queue, against the campaign's own size — and on a five-cell
   strip that meant two cells with a rule under them and three without, which
   reads as three cells failing to draw something rather than as two holding
   work. The figure already says how many are waiting, which is the same fact
   the bar was a second copy of. */

@media (max-width: 899.98px) { /* 900 layout px */
  /* Five cells at 900px give each about 170px, which the longest label
     ("Financials") and its figure will not share. Two up, and `grid-auto-flow`
     back to rows so the wrap is what it looks like. */
  .s-pipe { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ══ THE STAGE ═════════════════════════════════════════════════════════════

   The working surface, at the full width of the column instead of indented
   inside a list item. It gets a name because it is no longer a row that was
   doubling as one — and that name is the `h3` the blocks inside it needed,
   which puts Copy, Sequence and Queue back at `h4` where they belong. */
.s-stage-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 12px;
  margin-bottom: 20px;
}
/* The block heading step, not the title step. It is an `h3` under a section
   whose own headline is set at 18 — at 20 the stage outranked the section
   containing it, which is the rank inversion this whole pass has been
   removing everywhere else. Deck 18, stage 16, sub-block 13: one descent. */
.s-stage-name {
  font-size: var(--ty-heading); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight); color: var(--ink-primary);
}
/* Only ever "waiting on you" or "nobody on it" — the two things the Team row
   above cannot say, and both of them are findings, so they are toned. */
.s-stage-who {
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
  color: var(--warn-text);
}
/* What the tab is, in one line. `Find  BDR` was a name and an acronym, and
   between them they said nothing about what the tab holds — the same gap the
   sections had before they got decks, one rank down. Takes the whole row so
   it sits under the name rather than trailing the owner. */
.s-stage-does {
  flex: 1 0 100%;
  font-size: var(--ty-meta); color: var(--ink-faint);
}
/* The stage takes the column. It used to be `padding: 4px 16px 20px` inside
   a bordered list item; there is no list and no border to sit inside now. */
.s-stage { display: block; }
.s-stage-body { display: block; }


/* `.s-mrow` drew one organization on a stage, in a list the Find stage
   inlined under its figures. That list was 579px of a 1045px tab, restating
   rows the workbench already holds, and it went; `memberRow()` outlived it
   with no caller until `wiring-audit` named it, and these rules outlived
   both. `Open all N in the workbench` is what the block was actually for. */
/* ── ④ WRITE ──
   One row per lead, because copy is per person or it is a mailshot. The row
   leads with the name and says why it is here beside it — never written, or
   written before something changed — and the message itself sits under both
   at body size, since it is the only thing on this stage anybody is going to
   read word for word. */
.s-writes { display: flex; flex-direction: column; gap: 2px; }
.s-write { padding: 12px; border-bottom: 1px solid var(--hairline); }
.s-write:last-child { border-bottom: 0; }
/* `.s-stage-head`, `.s-stage-h`, `.s-stage-nobody` and `.s-stage-ask` all
   went with the sentences and the duplication they carried: a question
   printed under a heading that answered it, a 15-word explanation of who is
   not on a stage, and finally the stage's own name and function repeated
   sixteen pixels under the node head that states both. What is left of a
   stage body is the work and the one verb for it. */
.s-stage-acts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-bottom: 16px;
}
.s-stage-primary { flex: none; }

/* A block heading inside a stage. One step under the stage's own, because the
   stage is the page and these are its parts. */
/* Same correction as the row captions, one rank down: these name the parts
   INSIDE a stage — Copy, Sequence, Queue, Organizations, Results — so they
   sit below the section kicker and above nothing else. `--d500` was the
   faintest ink in the product on the labels doing the most wayfinding. */
.s-sub-h {
  font-size: var(--ty-meta); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-secondary);
  margin-bottom: 12px;
}

/* ── What the numbers mean ──
   The reading under the metric cards, marked as AiMY's so it is not mistaken
   for another metric. */

/* The report-to pickers reuse the tick vocabulary — they are choice rows,
   where a visible radio is the affordance. */
.s-camp-client { width: 15px; height: 15px; accent-color: var(--accent); }
.s-sell-pick { cursor: pointer; }

/* ══ A SEGMENTED CONTROL DOES NOT ALSO HAVE RADIOS IN IT ══════════════════

   `Kind` was drawn as `.seg` — the library's segmented control, whose whole
   premise is that the segment IS the affordance — with a 15px radio rendered
   inside each segment. Two affordances for one choice, and the pill sat there
   doing nothing.

   Worse, it could not show its own state. `.seg-btn.active` is a class the
   library expects JavaScript to toggle; nothing here toggles it, and this
   control is driven by `:checked`. So picking `Solution` left all three
   segments looking identical and the only evidence was the radio dot — which
   is why the radio had to be visible, and why it looked like a broken radio
   group rather than a segmented control.

   The input goes away, the `<span>` becomes the segment, and `:checked`
   paints it. No `:has()`: this file has twice had a `:has()` rule silently
   stop matching, and a sibling selector cannot. */
.s-sell-kinds { flex-wrap: wrap; }
.s-sell-kinds .seg-btn { padding: 0; background: none; position: relative; }
.s-sell-kind-in { position: absolute; opacity: 0; width: 0; height: 0; }
.s-sell-kinds .seg-btn > span {
  display: block; padding: 4px 12px; border-radius: var(--r-sm);
  min-height: 24px; line-height: 16px;
  transition: background-color var(--t-fast), color var(--t-fast);
}
.s-sell-kinds .seg-btn:hover > span { color: var(--d100); }
.s-sell-kind-in:checked + span { background: var(--accent-dim); color: var(--accent); }
.s-sell-kind-in:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }


/* A scrolling column, because a campaign with forty people on it would push
   the confirm below the dialog — a commit that cannot show its own button. */
.s-logpick { max-height: 320px; overflow-y: auto; }
.s-logwho { width: 15px; height: 15px; accent-color: var(--accent); }
/* Prose. The quote panel was sized for a paragraph, and a four-character
   description turned it into a mostly empty box. */
.s-camp-page .s-sheet-block { margin-bottom: 24px; }
/* ══ WHAT IS GOING WRONG, BOTH SIDES OF IT, ON ONE ROW ═════════════════════
   In the way and What to offer better were 743px stacked, and they are the
   same shape twice — a sentence and a ranked list of reasons with counts.
   Side by side the comparison is the layout rather than something the reader
   holds in their head across a scroll.

   TWO TRACKS, NOT `auto-fit`. `auto-fit` fits as many 20rem columns as the
   width allows, which on a wide screen is THREE — so the panel drew a third,
   empty column and the two sections took two thirds of it. There are exactly
   two things here and there always will be, so the grid says two. */
.s-wrong {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px;
  align-items: start;
  background: var(--aimy-wash);
  padding: 20px;
  border-radius: var(--r-lg);
  /* THE SECTION BREAK BELONGS TO THE PANEL. It used to come from the first
     column head's own 64px top margin; the panel resets those to zero so the
     two columns start level, which left the whole block sitting 20px under
     the Client row above it. */
  margin: 64px 0 24px;
}
/* The mark spans both columns and reads once, above them. */
.s-wrong-mark { grid-column: 1 / -1; margin: 0; }
/* If only one section draws, it takes the width rather than sitting in half
   of it with air beside it. It is the second child because the mark is the
   first, so `:only-child` cannot say this. */
.s-wrong > .s-sheet-block:nth-child(2):last-child { grid-column: 1 / -1; }
/* ── THE READING, WITH NO CARD AROUND IT ──
   It was a bordered panel inside the panel. What made it legible as AiMY's
   was the card; what makes it legible now is that the whole block is, so it
   is set as prose with the spacing doing the separating. */
.s-offer-read { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }
/* `.s-insight-txt` ships `flex: 1 1 200px`, which inside a ROW is a sensible
   minimum measure and inside this COLUMN is a 200px HEIGHT basis — the
   two-line sentence measured 170px tall with 130px of it empty, which is the
   hole under the reading. Here it is prose in a stack, so it sizes to its
   own text. The type comes with it: it used to be set by the card. */
.s-offer-read .s-insight-txt {
  flex: none; display: block;
  font-size: var(--ty-body); color: var(--ink-secondary);
}
.s-offer-read .s-insight-txt b { color: var(--ink-primary); }
.s-offer-read .s-insight-acts { flex: none; }
/* And the proposal loses its raised ground with the card that held it. It was
   a surface inside a surface to say "this part is different"; inside a panel
   that is already AiMY's, a third ground reads as a third block. The label
   above it and the space around it were always what set it apart. */
.s-offer-read .s-offer-fix { background: none; padding: 0; margin-top: 0; }
/* The section break belongs to the panel, not to the heads inside it — they
   are columns now, and a 64px top margin on a column head is a column that
   starts two-thirds of the way down its own box. */
.s-camp-page .s-wrong > .s-sheet-block { margin-bottom: 0; }
.s-camp-page .s-wrong .s-sect-head { margin-top: 0; }
/* `auto-fit` alone did not stack these: measured at 700px it kept two tracks
   and squeezed them to 290, where "Lost the person who wanted this, and
   nobody has replaced them." is three lines. Below 900 the comparison is not
   worth the wrapping, so they stack — the same width the campaign row drops
   its reason at.

   AND UNTIL NOW IT WAS NOT THAT WIDTH. This was the one query in the file
   written as a bare `900px` while every other threshold was written as
   `layout x 0.85` — the design number left in the machinery's slot, never
   converted. So it fired at 1059 layout, and the two rules this sentence
   calls the same width were 159 layout px apart for as long as both have
   existed. They are both 899.98 now, and with the width floor gone that is
   900 layout px, which is what the sentence always said. */
@media (max-width: 899.98px) { /* 900 layout px */
  .s-wrong { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}
/* The tally closes the flow section; the next head owns the gap after it. */
.s-camp-page .s-tally { margin-bottom: 24px; }
/* The campaign's own title override is gone — `.s-sheet-name` carries the
   title step for both surfaces now. */

/* Who gave the reason, quieter than the reason itself. The words are what
   the next person needs; the attribution is what makes them trustable. */
.s-why-who { color: var(--ink-muted); }
.s-sheet-head .inline-note { margin-top: 8px; }
/* Same block-quote treatment as `.s-commit-quote`, for the same reason:
   these are the two places on the surface that quote somebody's own words. */
.s-sheet-brief {
  font-size: var(--fs-base); color: var(--d200); line-height: var(--lh-base);
  padding: 12px 16px; margin-bottom: 24px;
  background: var(--card-bg-raised);
  border-radius: var(--r-sm);
}
.s-sheet-block { margin-bottom: 24px; }
.s-sheet-h {
  font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--d100);
  margin-bottom: 8px;
}
.s-sheet-foot {
  display: flex; flex-wrap: wrap; gap: 8px; flex: none;
  padding: 16px 24px 24px; border-top: 1px solid var(--hairline);
}

/* The commit surface takes the same treatment. It is the same shape — a
   head that says what this is, a body that can run long, and a foot that
   must never leave — so it should not have its own answer to the same
   question. */
.s-commit {
  max-height: min(calc(var(--vp-h) * 0.88), 900px);
  display: flex; flex-direction: column;
  padding: 0;
}
.s-commit .modal-header { flex: none; padding: 24px 24px 16px; margin: 0; }
.s-commit .modal-body   { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 24px 4px; }
.s-commit .modal-footer {
  flex: none; padding: 16px 24px 24px; margin: 0;
  border-top: 1px solid var(--hairline);
  display: flex; align-items: center; gap: 8px;
}
/* An unmet requirement says what it is, in the footer, beside the button it
   is holding. A disabled control with no explanation is the same defect as a
   dead one — the difference is only that this one is deliberate. */
.s-needs { margin-right: auto; font-size: var(--fs-xs); color: var(--warn-text); }
.s-commit .modal-footer:not(.is-blocked) .s-needs { display: none; }
.s-commit [data-commit-go][disabled] { opacity: 0.45; cursor: not-allowed; }
/* The field the requirement points at carries it too, so the two ends of the
   condition are visible at once however the modal is scrolled. */
.s-commit .modal-footer.is-blocked ~ * .s-why,
.s-commit .modal-body .s-why:placeholder-shown,
.s-commit .modal-body .s-kb-why:placeholder-shown,
.s-commit .modal-body .s-note:placeholder-shown { border-color: color-mix(in srgb, var(--warn) 40%, transparent); }

/* The AiMY mark and the words beside it sit on one optical centre. The mark
   is 13×14 against a 12px uppercase label and a 20px title, so leaving them
   on their own text baselines drops the mark a pixel or two below the line —
   small, and visible on every AiMY-authored surface at once. */
.s-sheet-kind { display: flex; align-items: center; gap: 4px; }
.s-sheet-kind svg { flex: none; }

/* ═══════════════════════════════════════════════
   NON-HAPPY STATES
═══════════════════════════════════════════════ */
.s-skel { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 24px; }
/* Card proportions, so the grid does not reflow when the real thing lands —
   the same reason Knowledge gives, and the same anatomy underneath. */
.s-skel-card {
  min-height: 132px; border-radius: var(--r-xl);
  border: 1px solid var(--card-border); background: var(--card-bg);
  padding: 12px 16px;
  display: flex; flex-direction: column; gap: 8px;
}
.s-skel-line { border-radius: var(--r-xs); }
/* The meta line sits below the rule, where the real one does. */
.s-skel-line.is-meta { margin-top: auto; padding-top: 0; }
.s-skel-card .is-meta { border-top: 0; }
.s-error { margin-top: 48px; }
/* AiMY unavailable does not replace the surface — everything that reads a
   record still works, and a screen saying otherwise would be lying. */
.s-aidown { margin-bottom: 24px; }

/* ═══════════════════════════════════════════════
   PROTOTYPE CONTROL — scaffolding, never product

   Mono type, dashed border, no accent, no brand. It should be impossible
   to mistake for something that ships.
═══════════════════════════════════════════════ */
.proto-sec { margin-bottom: 16px; }
.proto-sec:last-child { margin-bottom: 0; }
.proto-h {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-muted); margin-bottom: 4px;
}
.proto-link {
  display: block; width: 100%; text-align: left;
  font-size: var(--fs-2xs); color: var(--ink-muted);
  text-decoration: none; cursor: pointer;
  padding: 2px 4px; border-radius: var(--r-xs);
  transition: color var(--t-fast), background-color var(--t-fast);
}
.proto-link:hover { color: var(--d100); background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .proto-link:hover { background: rgba(16,24,40,0.05); }
.proto-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* ═══════════════════════════════════════════════
   NARROW WIDTHS

   Two things cannot shrink on their own and both drag the page wider than
   the window, which is the one layout defect that is never acceptable:
   a horizontal scrollbar on the body means every vertical scroll is
   fighting a sideways drift.
═══════════════════════════════════════════════ */
@media (max-width: 719.98px) { /* 720 layout px */
  /* Five tabs at 14px of side padding each will not fit 375px, and wrapping
     them puts the underline on two levels — a tab strip with a second row is
     no longer a strip. It scrolls, on the same rule the ecosystem strip
     below follows: chrome you swipe beats a page that drifts sideways. */
  .s-tabs { overflow-x: auto; scrollbar-width: none; }
  .s-tabs::-webkit-scrollbar { display: none; }
  .s-tab { flex-shrink: 0; }

  /* The count and the view switcher stop being an end-aligned pair. */
  .s-result-row { flex-direction: column; align-items: stretch; }
  .s-result-row .seg { align-self: flex-start; }

  /* The six ecosystem tabs are `white-space: nowrap`, so the strip cannot
     shrink below 475px however much flex says it may. It scrolls instead of
     overflowing — the tabs are chrome, and a chrome strip you swipe is
     better than a page that drifts sideways. */
  .topnav-tabs { min-width: 0; overflow-x: auto; scrollbar-width: none; }
  .topnav-tabs::-webkit-scrollbar { display: none; }
  .topnav-tabs-inner { flex-shrink: 0; }
  .app-topnav { padding: 0 12px; }
}

/* ═══════════════════════════════════════════════
   UTILITIES

   Exempted from css-audit's orphan check: these are styled only in
   combination, or carry no visual weight of their own.
═══════════════════════════════════════════════ */
.s-row { display: flex; align-items: center; gap: 8px; }
.s-gap-2 { gap: 8px; }
.s-gap-3 { gap: 12px; }
.s-gap-4 { gap: 16px; }
.s-hidden { display: none !important; }
/* Visible to a screen reader, not to the eye. */
.s-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ══ INSIDE A THING, NOT ACROSS THE CORPUS ═══════════════════════════════
   A campaign page used to answer "show its accounts" by leaving for the
   global list, and the window-fixed filter tray floated over it the whole
   time. You were inside the campaign and outside it at once.

   Now the campaign holds its accounts, its controls narrow within them and
   count what they narrow, and the corpus-wide tray is not on screen at all
   while a page is. ══ */
body.is-record .filter-tray { display: none; }

/* ══ A PAGE OF FIGURES DRAWS NO DOOR INTO THE BOOK ═══════════════════════
   The executive surface refuses every filter and every drill. The MASTHEAD
   is not one of those: its tab strip switches products, not records, and
   the identity control beside it is how a person stops being the CEO. The
   first cut hid the whole bar and took a way out along with a way in —
   there was then no control anywhere that could change who you looked as.

   So the cut is by what a control points AT, not by which bar it sits on.
   The rail is an index of the book and goes. Two things inside the
   masthead point at what the rail held, and go with it:

     · `.rail-toggle` opens a rail that is not there. A control that cannot
       do its one thing is worse than an absent one — the same rule that
       gives a read-only tier no button rather than a disabled one.
     · the bell enumerates records waiting on a person, and this tier holds
       none. `bell()` already returns early at aggregate grain, so leaving
       the button up would leave a door onto a panel that can never fill.

   `is-figures` is set by `paintChrome` off the declared grain, the same way
   `is-record` above is set off what is open. --rail-w to 0 is the
   recentring the drawer breakpoint already uses: one variable, read by the
   float bar's own clamp, so nothing has to remember this rule exists. */
body.is-figures { --rail-w: 0px; }
body.is-figures .app-rail,
body.is-figures .rail-toggle,
body.is-figures .topnav-bell,
body.is-figures .filter-tray { display: none; }

/* ═══════════════════════════════════════════════
   THE EXECUTIVE SURFACE — A BOUNDED GRID OF TILES

   ══ WHY IT IS NOT FULL WIDTH, AND WHY THAT WAS THE WHOLE PROBLEM ═══════

   The pass before this ran six sections down the full 1560px column. At
   that width a breakdown row put its name at the far left and its figure
   twelve hundred pixels away on the right — reading one row meant crossing
   the screen twice, and the eye lost the line on the way back. Stacking six
   of those produces a REPORT: a thing you sit down with, scroll through,
   and read in order. This surface is read standing up, once, by somebody
   who will not scroll twice.

   So the layout is capped at 1180px and composed of tiles, each holding one
   fact. Nothing stretches to fill. A tile is a quarter of that grid, which
   is a width a caption and a figure can be read across in one movement —
   and the grid can be scanned in any order because no tile depends on the
   one before it.

   THIS IS THE ONE PLACE ON THE SURFACE THAT USES CARDS. The rest of the
   product groups with rules and negative space, correctly — a workbench row
   is one of forty and a border around each would be noise. Here there are
   eight things, they are unrelated to each other, and the border is what
   says where one fact ends and the next begins.
═══════════════════════════════════════════════ */

.s-exec { gap: 0; max-width: 1180px; margin-inline: auto; width: 100%; }

.s-exec-top {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 24px; flex-wrap: wrap; margin-bottom: 20px;
}
/* Left-aligned, never centred. Reading starts here and the control that
   changes what is read sits opposite, which is where a control belongs. */
.s-exec-h {
  font-family: var(--font-display); font-size: var(--fs-2xl);
  font-weight: var(--fw-extrabold); letter-spacing: -0.02em;
  line-height: var(--lh-tight); margin: 0;
}
/* What the page counts, under the name of the page. Sentence case at the
   meta step: it is a line of data, not a label, and the capitals it used to
   wear were the whole of what made it read as a kicker. Declared at the ink
   floor rather than under it, so what this rule says is what renders. */
.s-exec-scope {
  margin: 6px 0 0; font-size: var(--fs-xs); color: var(--ink-muted);
  line-height: var(--lh-base);
}
/* ══ ONE FAMILY, AND THE MONO WAS NEVER LOADED ═════════════════════════

   Every caption and figure on this surface asked for `--font-mono`, and
   this product loads exactly one family — Poppins. So the request fell
   through JetBrains Mono to whatever the machine calls `monospace`:
   Consolas on Windows, Menlo on a Mac. A different typeface per operating
   system, at 32px, on the one number the page is about.

   It should not have been mono in any case. The only thing it was buying
   was digits that do not reflow when a period switches. A monospace would
   have separated too much, in the wrong direction, adding a terminal
   register to a page about money.

   CORRECTION, and it matters well beyond this surface. The sentence that
   stood here said `font-variant-numeric: tabular-nums` buys that "on ANY
   face — it is a property, not a reason for a third family". The first
   half is false and the second is the right conclusion for a wrong reason.
   A property can only switch on a feature the font actually ships, and
   Poppins does not ship one: digit-width spread 63.00 at 100px with the
   property set, against 0.00 for a face that does support it. Measured, in
   the browser, against a control that can fail. So the ~86 declarations of
   it in this file and bdr.css buy nothing at all today, and figures here do
   reflow when their value changes. That is a known, accepted cost of the
   one-face decision — see `--font-sans` in aimy-ds.css for the remedy if it
   is ever judged too expensive.

   So hierarchy comes from weight and size within Poppins, which loads
   400 through 800. Captions sit at 600 because the floor is 400 or heavier
   below 18px and uppercase at the minimal step needs the extra body.

   THE SIZE IN THIS PARAGRAPH WAS STALE. It argued for 11px, which the type
   floor removed — the scale is even from 12 now, and 12 is the
   minimal-component step: a tag, a state, a timestamp, a caption over a
   figure. Never a sentence. Every caption on this surface is one of the
   first four. */
/* ══ IT IS THE SECTION'S HEADING, SO IT IS A HEADING ══════════════
   Three `<section>` elements on this page named their subject in a `<div>`,
   so the outline held two headings for a page with four parts and none of
   the sections had an accessible name. They are `<h2>` now. The treatment
   does not change: small tracked capitals is a section label, and a section
   label is exactly what these are — what made the one at the top of the page
   a kicker was that it sat ABOVE a heading, not how it was set. */
.s-exec-eyebrow {
  font-family: var(--font-sans); font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-faint); margin: 0;
}
.s-exec-top .s-tabcuts { margin-bottom: 0; }
/* On its own row under the section head, hard against the left edge. Inside
   the head it was placed BY the heading, and the heading is one of the two
   things it rewrites — so the chips moved whenever the cut changed, which
   means the control moved out from under the finger that pressed it. Nothing
   on this row changes with the cut, so nothing on it moves. */
.s-cut-by { margin: 12px 0 0; }

/* ══ ATTAINMENT — THE ONE THING ON THE PAGE THAT ANSWERS THE QUESTION ═══

   Booked runs from zero. AiMY's projection continues it in a lighter tone,
   so the two are one length and obviously not one fact. Two markers cross
   the track: where the target is, and where booked should have reached by
   today. The track is scaled to the widest of the three, never to the
   target — a bar whose track IS the target cannot draw a projection that
   beats it, and this quarter's does. */
.s-att { margin-bottom: 32px; }
.s-att-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.s-att-lead {
  font-family: var(--font-sans); font-size: var(--fs-3xl);
  font-weight: var(--fw-extrabold); letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums; line-height: var(--lh-none); color: var(--d50);
}
/* The unit is a separate phrase from the figure and sat hard against it —
   32px at 800 beside 16px at 500 needs more than a word space to read as
   two things. `nowrap` keeps "of €300k" from ever breaking across lines. */
.s-att-of {
  font-size: var(--fs-lg); font-weight: var(--fw-medium); color: var(--ink-quiet);
  letter-spacing: 0; white-space: nowrap; margin-left: 4px;
}
.s-att-pc { font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--d200); font-variant-numeric: tabular-nums; }
.s-att-pc.tone-ok { color: var(--ok-text); }

/* The bar is two layers: a track that clips its fills to the rounded
   corners, and the marks over it. They were one, and `overflow: hidden` —
   which the fills need — cut off the four-pixel overhang that makes a mark
   read as crossing the bar, then cut the target off the end entirely. */
.s-att-bar { position: relative; }
.s-att-track {
  position: relative; display: flex; height: 20px;
  border-radius: var(--r-sm); overflow: hidden;
  background: var(--card-bg); border: 1px solid var(--card-border);
}
.s-att-booked, .s-att-fcast { position: relative; z-index: 1; }
.s-att-booked { background: var(--accent); }
/* The projection has to read as NOT YET REAL from across the room. A flat
   tint of the same hue reads as more of the same bar; the hatch says it is
   a different kind of claim without introducing a second colour. */
.s-att-fcast {
  background: repeating-linear-gradient(115deg,
    rgba(var(--accent-rgb), 0.30) 0 4px, transparent 4px 8px);
}
/* Markers sit ON the track, not under it — a legend two rows down cannot
   say "here", and here is the whole of what they are for. */
.s-att-target, .s-att-pace {
  position: absolute; top: -4px; bottom: -4px; width: 2px; border-radius: var(--r-pill);
}
/* Centred on the position rather than starting at it, or a mark that lands
   on the end of its own bar draws two pixels past it. */
.s-att-target, .s-att-pace { transform: translateX(-1px); }
/* ══ TWO MARKS THAT DIFFER IN KIND, NOT IN SHADE ═══════════════════════
   They were two two-pixel lines a step of grey apart, so which was the
   target and which was today could only be got from the legend — and the
   legend is a row away, which is the one thing a mark is for.

   The commitment CROSSES the bar: full height and four pixels of overhang
   top and bottom, in the brightest ink. The running reference sits INSIDE
   it, no overhang, a step quieter. One is a line drawn across the track,
   the other is a tick in it, and neither needs reading twice. */
.s-att-target { top: -4px; bottom: -4px; background: var(--d50); }
.s-att-pace { top: 3px; bottom: 3px; background: var(--d300); }

.s-att-keys { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 16px; font-size: var(--fs-xs); color: var(--ink-quiet); }
.s-att-key { display: inline-flex; align-items: center; gap: 8px; }
.s-att-key::before { content: ""; width: 10px; height: 10px; border-radius: var(--r-sm); flex-shrink: 0; }
.s-att-key.is-booked::before { background: var(--accent); }
.s-att-key.is-fcast::before { background: rgba(var(--accent-rgb), 0.28); }
/* The swatches carry the same difference the marks do, so the legend teaches
   the bar rather than only naming it. */
.s-att-key.is-target::before { background: var(--d50); width: 2px; height: 14px; border-radius: var(--r-pill); }
.s-att-key.is-pace::before { background: var(--d300); width: 2px; height: 8px; border-radius: var(--r-pill); }
.s-att-key svg { color: var(--ai-text); flex-shrink: 0; }

/* The supporting four. Divided, never boxed. */
.s-afs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; margin-bottom: 32px; }
/* ══ PROXIMITY SAYS WHICH LINE BELONGS TO WHICH ══════════════════
   Three lines at one gap is a stack, not a tile: the caption floated exactly
   as far from its own figure as the qualifier below it did, so the eye had
   to read all three to find out which two were a pair. The caption binds
   TIGHT to the figure it names and the sub-line sits back from both — the
   one arrangement that makes a tile read as a thing rather than as three
   consecutive lines that happen to be near each other.

   And the rule above gets room to be a rule. 16px of padding under a 7%
   hairline reads as a hairline that has slipped; 20 gives it the air that
   makes it deliberate. */
.s-af {
  display: flex; flex-direction: column; gap: 2px;
  padding-top: 20px; border-top: 1px solid var(--card-border); min-width: 0;
}
.s-af .s-af-sub { margin-top: 6px; }
.s-af-cap {
  font-family: var(--font-sans); font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-faint);
}
.s-af-val {
  font-family: var(--font-sans); font-size: var(--fs-2xl);
  font-weight: var(--fw-bold); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--d50); line-height: var(--lh-none);
}
.s-af-val.tone-ok { color: var(--ok-text); }
.s-af-val.tone-warn { color: var(--warn-text); }
/* THE NEGATIVE POLE WAS NEVER DEFINED HERE. Two of the three tones had a
   rule and the third fell through to plain ink, so a figure asking for the
   alarm got the same white as the figures either side of it and lost the
   one thing it was asking for. Nothing caught it: the audit reads class
   attributes, and a tone applied by concatenation is not one it can see. */
.s-af-val.tone-err { color: var(--err-text); }
.s-af-sub { font-size: var(--fs-xs); color: var(--ink-quiet); line-height: var(--lh-base); text-wrap: pretty; }

/* ── Where the projection comes from ── */
.s-odds {
  padding: 20px; border-radius: var(--r-lg);
  background: rgba(var(--accent-rgb), 0.04);
  border: 1px solid rgba(var(--accent-rgb), 0.10);
  /* IT WAS FLUSH AGAINST THE PANELS ABOVE IT — measured, zero pixels. Every
     other group on this page is separated by `.s-exec-sec`'s 40px, and this
     one is not a section, so it never got any. Same rhythm, stated once. */
  margin: 40px 0 24px;
}
.s-odds-cap {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-sans); font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-faint);
}
.s-odds-cap svg { color: var(--ai-text); }
/* The caption and the name of the column under its right end, on one line.
   Baseline, so the two micro-caps labels sit on the same rule; `.s-pan-unit`
   drops the top margin it needs when it hangs under a figure instead. */
.s-odds-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.s-odds-top .s-pan-unit { margin-top: 0; }
.s-odds-rows { display: flex; flex-direction: column; margin-top: 12px; }
/* ══ THE ROWS ARE `.s-pan-p`, THE ROW THIS PAGE ALREADY DRAWS TWICE ═════
   Its own five rules went with the three-column layout they served — a
   figure slot, a middle that flexed, and a hanging sub-line. Resources and
   Campaigns above already shape a name, a qualifier and a figure, and that
   is exactly what a reason, its count and its cost are. All this block adds
   is the rule between rows, which `.s-pan-crew` gets from its own gap. */
.s-odds-rows .s-pan-p { padding: 8px 0; border-top: 1px solid var(--card-border); }
.s-odds-rows .s-pan-p:first-child { border-top: 0; }
/* The doors sit off the note, at the foot of the block, the way every other
   reading on this desk ends. */
.s-odds .s-lead-acts { margin-top: 14px; }
/* `pretty` rather than `balance`: this is a description, and the thing worth
   preventing is one word alone on the last line. */
/* ══ AND IT IS NOT A FIFTH ROW ══════════════════════════════
   Twelve pixels under the last row, which is the gap BETWEEN rows — and set
   in the rank the rows' own counts use. So a sentence about all four rates
   read as the sub-line of the fourth. The ink floor means it cannot be
   separated by dimming it, and shrinking a line that explains what every
   figure above it means would be the wrong axis. It is separated by being
   somewhere else: a hairline and the air a section break takes. */
.s-odds-note {
  font-size: var(--fs-xs); color: var(--ink-faint); line-height: var(--lh-base);
  margin: 20px 0 0; padding-top: 16px; border-top: 1px solid var(--card-border);
  max-width: 72ch; text-wrap: pretty;
}


@media (prefers-reduced-motion: no-preference) {
  /* ══ THE ONE REVEAL A PAGE IS ABOUT, AND THE CEILING ON IT ════════
     700 and 520, on the same hand-rolled curve the peek card was using —
     three literals for one intent. --t-emphasis is the top of the scale and
     it is 500: past that a reveal stops reading as the page arriving and
     starts reading as the page being slow, and this is the surface where
     somebody is looking for a number. */
  .s-att-booked, .s-att-fcast { transform-origin: left; animation: s-att-wipe var(--t-emphasis) var(--ease-out) backwards; }
  .s-att-fcast { animation-delay: 220ms; }
  .s-exec .s-af { animation: s-exec-rise var(--t-emphasis) var(--ease-out) backwards; }
}
@keyframes s-att-wipe { from { transform: scaleX(0); } }

@media (max-width: 1179.98px) {
  .s-afs { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
}
@media (max-width: 619.98px) {
  .s-afs { grid-template-columns: 1fr; }
  .s-att-lead { font-size: var(--fs-2xl); }
}

/* ══ THE WORKINGS UNDER THE HEADLINE ═══════════════════════════════════
   Four sections, each answering one of the questions a total cannot: what
   the money went on, which campaign it went on, which product it bought,
   and how sure the open half is. Divided by rules and space; no boxes. */
.s-exec-sec { padding-top: 40px; }
.s-exec-note {
  font-size: var(--fs-xs); color: var(--ink-quiet); line-height: var(--lh-base);
  max-width: 76ch; margin: 8px 0 20px; text-wrap: pretty;
}
.s-exec-note b { color: var(--d200); }


/* ══ NOT A SECOND SEGMENTED BAR ═══════════════════════════════════════

   The spend split was drawn as a horizontal bar with a colour legend under
   it — which is the attainment track's exact shape, forty pixels away, and
   the two mean nothing like the same thing. One is progress against a
   target; this is four amounts that add up. A reader who has just learnt
   what the first bar means arrives at the second and has to unlearn it.

   Four amounts that add up is a list. Ranked, aligned on the figure, with
   the share as a number rather than a width — and nothing else on this page
   pretending to be the thing above it. */
/* ══ TWO COLUMNS, AND A RULE BETWEEN THEM RATHER THAN A GAP ════════════

   Capped at 68ch this list used half the page and left the other half
   empty — a column of nine short lines down the left of a 1400px surface.
   Two columns use the room, and three groups fall as People and AiMY on
   the left, Suppliers on the right.

   THE SEPARATOR IS A RULE, NOT SPACE. Space alone at this width reads as
   one wide row with an odd gap in the middle — the eye keeps trying to
   pair "People €74k" with "Suppliers €90.77" across it. A hairline says
   these are two lists, which is what they are. Padding sits on both sides
   of it so neither column's figures crowd the line. */
.s-cost {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 0; row-gap: 32px; margin-top: 12px;
}
.s-cost-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: 16px;
  padding: 8px 0; border-top: 1px solid var(--card-border);
}
.s-cost-row:first-child { border-top: 0; }
/* AT THE BODY STEP, NOT THE CAPTION STEP. This whole section answers
   "what did I spend it on", which is one of the four questions the page
   exists for — and it was set two steps below the panels underneath it,
   so the most consequential list on the surface read as a footnote. */
/* ══ A LIST'S HEAD OUTRANKS ITS ITEMS, AND THIS ONE DID NOT ═══════
   "The desk" was 16px REGULAR over children set 14px SEMIBOLD — size said
   parent and weight said child, and a reader resolving that contradiction
   settles it on weight every time. The group head is the strongest text in
   its group after the figure, which is what it is: the thing the nine rows
   under it are nine parts of. */
.s-cost-say { font-size: var(--fs-md); font-weight: var(--fw-bold); color: var(--d50); min-width: 0; }
/* What the row is, under what it is called. The sub-line is where a person
   row says how much of what they cost landed on named work — the one thing
   the figure beside it cannot say on its own. */
/* ══ THESE LINES CARRY THE FINDING, NOT DECORATION ═════════════════════

   Every sub-line here sat at `--d400`, the step this file uses for text you
   are not expected to read — and the most consequential fact in the section
   is one of them: "98 of 1135 hours logged against a campaign". The figure
   beside it means nothing without it.

   `--d400` is for a caption repeating what is already visible. A line that
   says something new belongs a step up, and so do the qualifiers after each
   name: "4 at EUR55/h" is the rate, which is data. */
/* And it does NOT inherit the head's weight. Making the group label bold
   made its own qualifier bold with it, which put the loudest weight on the
   page under the quietest job — a line that says what the figure counts. */
.s-cost-sub {
  display: block; font-size: var(--fs-xs); font-weight: var(--fw-medium);
  color: var(--ink-muted); margin-top: 4px;
}
.s-cost-v {
  font-size: var(--fs-2xl); font-weight: var(--fw-bold); color: var(--d50);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
  text-align: right; min-width: 96px;
}
/* THE GROUP IS THE ROW; THE CHILDREN ARE THE ANSWER TO "OF WHAT". Indented
   and one step down, so a reader can stop at three lines or read nine — the
   thing a flat list of nine equal rows never let them do. */
.s-cost-g { padding: 0; }
.s-cost-g:nth-child(odd)  { padding-right: 40px; }
/* `--card-border` is 7% white — right for a hairline UNDER something, and
   invisible as a rule BETWEEN two columns, which is the one job this line
   has. At `--card-border-hover` it reads without becoming a divider that
   competes with the figures either side of it. */
.s-cost-g:nth-child(even) { padding-left: 40px; border-left: 1px solid var(--card-border-hover); }
.s-cost-g .s-cost-row { border-top: 0; padding: 0; }
.s-cost-kids { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 0 16px; }
.s-cost-kid {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: 16px;
}
.s-cost-kid .s-cost-say { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink-secondary); }
/* ONE STEP OF INK IS NOT A RANK. The label and its own figure were the same
   size, the same weight and one rung apart — the single axis of difference
   this file calls a flat block everywhere else it finds one. The figure
   takes the top of the ramp, because it is the thing being read. */
.s-cost-kid .s-cost-v { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--d50); min-width: 72px; }
.s-cost-note { color: var(--ink-quiet); font-weight: var(--fw-medium); }

/* One question per section, beside the heading it belongs to. A single row
   of three at the foot of the page could only ever be about the page; these
   are about the thing directly above them. */
.s-sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
/* ══ ONE JOB, ONE FACE ═════════════════════════════════════
   This and the chips in `.s-asks` are the same thing — a question you can put
   to AiMY — and they were set at two weights, 400 here against 600 there. At
   400 it also landed in exactly the rank of the section note directly under
   it: same size, same weight, same ink, one line apart, so a control you can
   press and a paragraph you cannot were indistinguishable until the cursor
   crossed one. Semibold, which is what the other half of the pair has always
   been. */
.s-sec-ask {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink-muted);
  background: rgba(var(--accent-rgb), 0.08);
  border: 1px solid rgba(var(--accent-rgb), 0.16);
  border-radius: var(--r-pill); padding: 4px 12px;
  cursor: pointer; font-family: inherit;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}
.s-sec-ask:hover { background: rgba(var(--accent-rgb), 0.14); border-color: rgba(var(--accent-rgb), 0.3); }
.s-sec-ask:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-sec-ask svg { color: var(--ai-text); flex-shrink: 0; }

/* ══ A PANEL IS NOT A CARD, AND THE DIFFERENCE IS AFFORDANCE ═══════════

   These were divided rows with a proportion bar, and rows are the wrong
   shape for the content: a campaign here carries a name, a state, a total,
   four counts and a crew list at individual rates. That is a record, and a
   record wants a container.

   THE OBJECTION IS RIGHT, AND IT IS ABOUT SIGNALS, NOT SHAPE. A container
   that reads as clickable on a page where nothing opens is the same defect
   as a disabled button on a read-only tier — an affordance that promises
   something the surface cannot do. But containment is not what promises it.
   This product already draws the line, and draws it with INTERACTION:

     · `.s-door` and `.s-camp-row` are clickable. Both carry
       `cursor: pointer`, a `:hover` that changes background or border, and
       a `:focus-visible` call — and both are `<button>` elements.
     · `.s-metric` is a panel. Filled, bordered, and none of those three.

   So these follow `.s-metric`: a fill to group, no hover, no pointer, no
   focus call, and a `<div>` rather than a `<button>` — which means they are
   not in the tab order either. A keyboard user is already told the truth;
   the visual now says the same thing.

   NO BORDER, ONLY A FILL. `--card-bg` plus `--card-border` is this
   product's card. The fill alone reads as a region of a page, which is
   what these are, and it is the one signal that separates them from the
   eight bordered things elsewhere on the surface that DO open.

   The proportion bar went with the rows. Ordering by cost is the
   comparison — the dearest campaign is first and its total is the largest
   number in its panel, which is the same fact a bar was drawing twice. */
.s-pans {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  /* MORE SPACE BETWEEN TWO PANELS THAN INSIDE EITHER, or the pair reads as
     one surface and the grid loses its rows. `css-audit` measures exactly
     this — 12px against 20px of padding was under the floor and it said so
     before anybody looked at the page. */
  gap: 24px; margin-top: 16px;
}
.s-pan {
  display: flex; flex-direction: column; min-width: 0;
  background: var(--card-bg); border-radius: var(--r-lg); padding: 20px;
}
.s-pan-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
/* ══ THE PANEL CLIMBS ONE STEP OF ITS OWN SCALE ══════════════════
   It ran 24 / 16 / 14 / 12 with FIVE roles sharing the 14 — the cost, the
   inputs row, every crew name, every crew rate and every crew figure — which
   is a card with one step doing most of its work. The scale has a rung
   between 16 and 24 that this page never used, and a body step above meta
   that these rows should have been on.

   So: the campaign name to --ty-deck, and everything that is a NAME or a
   FIGURE in the crew to --ty-body, leaving --ty-meta for the lines that
   qualify them and --ty-micro for the two capitalised labels. Five ranks on
   five rungs, none invented. */
.s-pan-name { font-size: var(--ty-deck); font-weight: var(--fw-bold); color: var(--d50); min-width: 0; }
.s-pan-state {
  display: block; font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px;
}
/* A product line's state is a VERDICT, so it carries a tone the campaign's
   never needs — running and finished are facts, "not landing" is a call. */
.s-pan-state.tone-ok   { color: var(--ok-text); }
.s-pan-state.tone-warn { color: var(--warn-text); }
.s-pan-state.tone-err  { color: var(--err-text); }
.s-pan-total {
  font-size: var(--fs-2xl); font-weight: var(--fw-bold); color: var(--d50);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* NOTHING IS NOT A FIGURE. A product line that has closed nothing renders
   the word where every other panel renders an amount, and at 24px in the
   brightest ink it read as the loudest number in a row of real ones. The
   class was rendered by the page it came from and defined in neither
   stylesheet, so it has been saying nothing about nothing since it was
   written. It is the absence of a figure, so it is set like the absence of
   one: the caption's weight and the caption's ink. */
.s-pan-total.is-none {
  font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--ink-faint);
  letter-spacing: 0;
}
/* ══ THE ONE PANEL THAT DOES OPEN ══════════════════════════════════════

   Every panel on the executive page is inert because an aggregate tier
   reaches no record. A CLIENT reaches theirs — `filteredCampaigns` returns
   their campaigns and `campPage` renders for them — so a panel that refused
   to open would withhold a door the product already has.

   The rule was never "panels are inert". It was "the signals tell the
   truth", and here the truth is the other way round: a `<button>`, a
   pointer, a hover and a focus call, exactly the three marks `.s-door` and
   `.s-camp-row` carry. Everything else about it is the same component. */
.s-pan-go {
  text-align: left; width: 100%; font-family: inherit; border: 0;
  cursor: pointer; transition: background-color var(--t-fast);
}
.s-pan-go:hover { background: var(--card-bg-raised); }
.s-pan-go:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.s-pan-go .s-pan-head, .s-pan-go .s-pan-facts { display: flex; }
/* The unit under the count, so "6" reads as six of something without the
   word competing with the figure for the same line. */
.s-pan-unit {
  display: block; font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--ink-faint);
  text-align: right; margin-top: 4px;
}

/* The second figure in the head's own column, under the first. Set at the
   meta step and not at the figure step: what a campaign gained is still the
   panel's one loud number, and two shouting figures in one corner is a card
   arguing with itself about which is the headline. Tabular, because it is
   read down a column of fifteen panels against the amounts above it. */
/* The pair, and the pair is the point: what it gained beside what it cost,
   compared across rather than read down. Baseline-aligned so the two figures
   sit on one line however different their sizes, with their labels hanging
   under each. */
.s-pan-figs { display: flex; align-items: baseline; gap: 24px; flex-shrink: 0; }
.s-pan-fig { display: flex; flex-direction: column; }
/* One step down from the gain, not one grammar away from it. The panels are
   ranked by what they gained, so that figure stays the loudest thing in the
   corner — but a cost is the same kind of fact and gets the same kind of
   treatment. */
.s-pan-spent {
  font-size: var(--ty-deck); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight); color: var(--ink-secondary);
  font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right;
}
.s-pan-facts {
  display: flex; flex-wrap: wrap; gap: 4px 20px;
  font-size: var(--fs-xs); color: var(--ink-quiet); margin-top: 12px;
}
.s-pan-facts b { color: var(--d100); font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); }
.s-pan-facts .is-good b { color: var(--ok-text); }
/* The crew, at the rate each of them costs. One per line inside the panel:
   wrapping them into a paragraph made five people read as one sentence,
   and the whole point is that they are five separate costs. */
/* Twelve, not eight. Each row is two lines now, and at 8px the gap between
   two people was smaller than the gap between a person and their own terms
   — so five crew read as ten unrelated lines. */
.s-pan-crew {
  display: flex; flex-direction: column; gap: 12px;
  margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--card-border);
}
/* The block's own name. At the micro step in capitals, which is what this
   page uses for a label over a list — the same treatment the section
   headings and the unit under a figure wear, so it reads as the heading of
   the three rows under it and not as a fourth row. */
.s-pan-restitle {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-faint); margin-bottom: -2px;
}
.s-pan-p {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  font-size: var(--ty-body); font-variant-numeric: tabular-nums;
}
.s-pan-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.s-pan-who b { color: var(--d50); font-weight: var(--fw-semibold); }
/* A COST IS A FIGURE, AND IT WAS WEARING THE FACE OF THE LINE BESIDE IT.
   `.s-pan-cost` inherited its row and landed one step of ink from
   `.s-pan-meta` with the same size and the same weight — one axis of
   difference, which is what a flat block looks like from the inside. Weight
   is what every other figure on this page uses to separate itself from its
   own caption. */
.s-pan-cost { font-weight: var(--fw-semibold); color: var(--ink-secondary); }
/* ONE LEVEL DEEPER THAN THE FACTS ROW, SO IT READS AS ONE LEVEL DEEPER.
   The panel's facts are about the campaign; this is about one person inside
   it, and it restates the arithmetic of the figure beside it — 9.0 hours at
   EUR55 an hour, next to EUR497. A caption repeating what is already visible
   is the definition of the ambient step, and at the attribution step it wore
   the same face as the row above it. */
.s-pan-meta { font-size: var(--ty-meta); color: var(--ink-faint); }

/* ══ THE CREW FOLDS, AND THE MARKER SAYS WHICH WAY ═════════════════════
   Native `<details>`, the same component the design system ships and the
   call trace already uses — keyboard-operable with no state to hold and
   no handler to wire. The default marker is replaced by a chevron that
   rotates, because the default sits outside the flex row and breaks the
   name-and-cost alignment every other line here keeps. */
.s-crew { margin: 0; }
.s-crew-sum {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  list-style: none; cursor: pointer; padding: 0;
  font-size: var(--ty-body); font-variant-numeric: tabular-nums;
  transition: color var(--t-fast);
}
.s-crew-sum::-webkit-details-marker { display: none; }
/* `<summary>` is `display: list-item` by default, and a `display: flex` on
   it is ignored in some engines unless the marker is removed first — which
   is why the cost was landing inline after the text instead of at the far
   edge with every other figure in the panel. */
.s-crew-sum::marker { content: ""; }
.s-crew-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.s-crew-who b { color: var(--d50); font-weight: var(--fw-semibold); }
/* The chevron leads the summary, so the affordance is at the start of the
   line a reader's eye lands on rather than at the far end of it. */
.s-crew-who b::before {
  content: ""; display: inline-block; width: 0; height: 0; margin-right: 8px;
  border-left: 4px solid currentColor;
  border-top: 3px solid transparent; border-bottom: 3px solid transparent;
  transition: transform var(--t-fast); transform-origin: 25% 50%;
  vertical-align: 1px;
}
.s-crew[open] .s-crew-who b::before { transform: rotate(90deg); }
.s-crew-sum:hover .s-crew-who b { color: var(--accent); }
.s-crew-sum:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; border-radius: var(--r-sm); }
/* ══ THE NAMES BELONG TO THE ROW ABOVE THEM ════════════════════════════

   A rule across the top said "new section" — the opposite of what an
   accordion means. Opening one is not moving to the next thing, it is
   looking inside the thing you are already on, and a horizontal separator
   is the one mark that denies that.

   Indentation and a vertical rule say it instead: the names sit inboard of
   their summary and the line runs down beside them, which is the same
   shape the design system's own tree view uses for the same relationship.
   `AiMY` and `Suppliers` stay at the outer margin, so the fold reads as
   one of three items rather than as a container round all of them. */
.s-crew-list {
  display: flex; flex-direction: column; gap: 12px;
  margin: 12px 0 0 4px; padding-left: 20px;
  border-left: 1px solid var(--card-border-hover);
}
.s-pan-p.is-ai .s-pan-who b { color: var(--ai-text); }
.s-pan-p svg { color: var(--ai-text); vertical-align: -1px; margin-right: 4px; }
.s-pan-cost { color: var(--d100); white-space: nowrap; }
/* The absence of a figure, set like one: the row's weight without its
   brightness, so a column of them reads as a column of nothings rather than
   as a column of amounts. Above the ink floor, because it is still content. */
.s-pan-cost.is-none { font-weight: var(--fw-medium); color: var(--ink-secondary); }
.s-pan-none {
  font-size: var(--fs-xs); color: var(--ink-muted);
  margin: 16px 0 0; padding-top: 16px; border-top: 1px solid var(--card-border);
}

  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); margin-left: 12px;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--d400);
}
  font-size: var(--fs-sm); color: var(--d100); white-space: nowrap;
  font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold);
}

/* The AiMY paragraph is the widest thing on this page and the only prose. */
.s-exec .slv { margin-bottom: 32px; }

@media (prefers-reduced-motion: no-preference) {
  .s-exec .s-pan {
    animation: s-exec-rise var(--t-emphasis) var(--ease-out) backwards;
    /* The second stagger with no `min()`. 45ms was also the one offset in
       the build that was not 40. */
    animation-delay: calc(min(var(--i, 0), 6) * var(--t-stagger));
  }
}

/* ══ THE PANELS GO ONE-UP HERE, AND THE HEAD STAYS A ROW ══════════════════
   One column below 1180, because two panels of campaign figures side by side
   stop being comparable long before they stop fitting. */
@media (max-width: 1179.98px) {
  .s-pans { grid-template-columns: 1fr; }
}

/* ══ AND THE HEAD ONLY STACKS WHEN ONE LINE STOPS WORKING ═════════════════
   A pass before this one stacked it at 1179.98, on the argument that a panel
   going full width does not crowd a `space-between` head, it pulls it apart:
   35px between a campaign name and its money at two-up, 582px at one-up. The
   arithmetic is right and the conclusion was wrong. Looked at rather than
   measured — at the widest panel this page ever draws, 931px — the head reads
   as a header band: a name at one end, its figures at the other, both anchored
   by the panel's own edges, with the facts line under them tying the block
   together. It is not two orphans. The picture beat the number.

   SO THE STACK MOVED TO WHERE ONE LINE ACTUALLY FAILS, which is nothing to do
   with the air between the halves. Measured with the head held as a row: 183px
   of air at 600 layout, 83px at 500 — and by 430 the campaign NAME has started
   giving up width to keep the figures on the line, 189px down to 186, and 146
   at 390. A name truncating so a number can stay beside it is the failure. The
   gap never was one.

   600 is before that begins, with the 500 reading still comfortable and the
   430 one already compromised.

   The figure keeps its step down to --fs-lg when it stacks: at 2xl on a line
   of its own it stops being a number beside a name and becomes a headline. */
@media (max-width: 599.98px) { /* 600 layout px */
  .s-pan-head { flex-direction: column; gap: 4px; }
  .s-pan-total { font-size: var(--fs-lg); }
}

/* ══ THE DOOR, NOT THE NARRATOR ═══════════════════════════════════════
   AiMY writes nothing on this page. What it offers is three questions the
   figures above invite and cannot answer — a trade-off, a cause, a
   forecast — which stage into the canvas on press. They sit under the grid
   because they are what you do AFTER reading it, and they use the chip the
   rest of the surface uses so they read as offers rather than as claims. */
.s-asks { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 20px; }
.s-asks-cap {
  font-family: var(--font-sans); font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-faint);
  margin-right: 4px;
}
/* A CHIP MAY NOT BE WIDER THAN THE ROW IT WRAPS IN. `.s-asks` wraps already,
   which handles three chips and not one chip too long for the line: measured
   at a 376px layout, the widest of the three asked for 314 in a 308px row and
   took the block over the edge of the page. `max-width` is inert wherever the
   chip fits, which is every width above that one. */
.s-ask { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; }
.s-ask svg { color: var(--ai-text); flex-shrink: 0; }

@keyframes s-exec-rise { from { opacity: 0; transform: translateY(8px); } }

/* NOT space-between. With seven status chips it looked deliberate; with one
   it threw a lone "Untouched 6" to the opposite edge of the page, a filter
   floating a full column away from the heading it filters. Grouped after the
   title instead, wrapping when it runs out of room — which reads the same at
   one chip and at seven. */
.s-camp-list-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
}
/* THE HEADING IS NOT ONE OF THE ACTIONS. It sat at the same baseline and the
   same 12px gap as the buttons beside it, so "Queue · Let AiMY call 12 · Log
   what happened" read as three links of equal weight and the section had no
   visible title. Pushing everything after the heading to the far edge is
   what makes a heading read as one. */
.s-camp-list-head > :first-child { margin-right: auto; }
.s-camp-list-head .s-sheet-h { margin: 0; }

/* ══ THE SUB-BLOCK TITLE ═══════════════════════════════════════════════════

   The rule above is right that a heading must not read as one of the links
   beside it, and `margin-right: auto` says so by putting the whole width
   between them — which fixes the rank and creates the distance: `Change the
   channels` ended up five hundred pixels from the Sequence it changes.

   The title now carries its own rank by weight and case, so it does not need
   the width to prove it, and the controls sit next to what they act on. No
   rule between them: this page had a hairline hanging off every title for a
   pass and they read as scaffolding.

   ── AND THE VERBS STOP SHOUTING ──

   Six accent links lived in these bars: `Change the channels`, `Call all 3`,
   `Let AiMY call 3`, `Log what happened`, `Draft all 3`, `Add a step`. The
   accent marks the ONE recommended move, and these are the tools of
   whichever stage happens to be open — AiMY's actual recommendation is the
   filled control at the top of the page, and it cannot be the loudest thing
   on the surface while six links share its colour. Quiet ink standing,
   accent on hover and focus where it is a state rather than a claim. */
.s-camp-page .s-camp-list-head { align-items: baseline; }
.s-camp-page .s-camp-list-head .s-sub-h { flex: none; margin-right: 0; margin-bottom: 0; }
.s-camp-page .s-camp-list-head .s-inline-btn { flex: none; }

/* ── THE ACTOR PAIR ──
   `Call all 3` and `Let AiMY call 3` are one job with a different hand on
   it; `Log what happened` is about the past. 8px inside the pair against the
   head's 16px between groups — the two-to-one ratio that says these two
   belong together and the third does not, and the one `css-audit.js` checks.
   Both carry an icon because the actor is the whole difference between them,
   and the phone glyph is the same one the six row buttons use. */
.s-queue-who { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.s-camp-page .s-camp-list-head .s-inline-btn svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: 4px; }
.s-queue-mark { color: var(--accent-text); }

/* ── EVERY TEXT ACTION ON THE PAGE, NOT JUST THESE ──

   Counted on one campaign: `Add`, `Change`, `Change`, `Knowledge`, `Draft
   all 3`, `Change the channels`, `Add a step`, `Ask about the order`, `Call
   all 3`, `Let AiMY call 3`, `Log what happened`, and eight `Show me the N`.
   Nineteen accent controls, and one of them — the filled button under the
   lead — is the recommended move the accent is reserved for.

   The rule is one per surface, so it is applied once at the surface rather
   than nineteen times at the call sites. Standing quiet, accent on hover and
   focus: a door still announces where it goes with its own words, and the
   one thing worth doing first is now the only thing wearing the colour. */
.s-camp-page .s-inline-btn { color: var(--ink-quiet); }
.s-camp-page .s-inline-btn:hover { color: var(--accent-text); }

/* ── THE LAST OF THE STRIPES ──
   `Copy` still drew `.s-straight`'s 2px left border — the pattern the
   doctrine fails a surface on (§11, "decorative border-left stripes"). The
   ranked rows dropped it for a track that carries the same information as a
   length; these rows have one or two entries and nothing to rank, so they
   drop it for nothing at all, which is what a two-row list needs. The base
   class keeps its border for the executive page and the rail. */
.s-camp-page .s-straight:not(.is-ranked) { padding-left: 0; border-left: 0; }
/* `Draft all 3` is a stage tool like the rest of them, and it was the last
   accent link left standing in the flow. */
.s-camp-page .s-straight:not(.is-ranked) .s-straight-act { color: var(--ink-quiet); }
.s-camp-page .s-straight:not(.is-ranked) .s-straight-act:hover { color: var(--accent-text); }

/* The spread is this block's own header — bars that narrow the grid under
   them — so it sits tight to its title and loose from the cards it filters. */
/* `.s-plan-on` held "Sends on AiMY · Phone · Online" above the sequence.
   The channels are on the step cards — one each, named — so the line was
   restating the plan immediately above the plan. `.s-plan-cap` survives: the
   call summary and the find flow both use it as a lead-in label. */
.s-plan-cap {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}

/* `.s-camp-spread` and the `.s-ans-hit.is-on` pair went with the status
   spread. The selected state moved to `.s-reason.is-on`, which is the same
   idea on the row every list on that page now shares. */

/* ══ THE CANVAS AT 375 ════════════════════════════════════════════════════
   `.aimy-overlay` is `position: absolute; inset: 0`, so its box is whatever
   the app shell gives it — and the shell still reserves the sidebar at 375.
   Measured: a **155px-wide thread inside a 375px viewport**, 113px of
   horizontal overflow, with the build flow's own content at 215px.

   The page itself did not overflow, which is why this survived: it is the
   canvas that was squeezed, and until this pass the canvas held one confirm
   block. It now holds the main flow in the product, so it gets a real box.
   Fixed to the VIEWPORT, because a canvas you cannot read is not a surface. ══ */
@media (max-width: 719.98px) { /* 720 layout px */
  /* ══ UNDER THE MASTHEAD, NOT OVER IT ════════════════════════════
     `inset: 0` was the answer to a 155px thread inside a 375px viewport, and
     the half of it that did that work is left and right. The top was
     collateral: the canvas is translucent with a backdrop blur, so covering
     the masthead did not hide it — it drew the canvas's own context chips
     THROUGH the tab strip, two rows of different things on the same pixels,
     and left the logo, the bell and the account pill lit but unreachable.

     It now starts below the masthead, and below a minimised call as well: a
     live call's strip is the one thing on this screen that is true right now,
     and a surface that opens over it takes away the only evidence the line is
     still open. --call-strip is 0 unless one is minimised. */
  .aimy-overlay { position: fixed; inset: calc(var(--topbar-height) + var(--call-strip)) 0 0; }
  /* ══ AND AN EXPANDED CALL IS IN FRONT OF IT ════════════════════════
     Starting a call posts the brief, and posting to the canvas opens it
     (bdr.js, callPrep) — which above this width lands beside the call and at
     this width lands ON it, so pressing Call showed you a brief for a call you
     could no longer see. Two full-width surfaces cannot both be the screen.

     The canvas goes BEHIND rather than away: it is still open, still holding
     the brief, and one press of the chevron puts the call back to a strip and
     reveals it. Lowering the canvas is what makes that reversible — hiding it
     would need a second rule to bring it back and a third to decide when. Its
     z-index of 500 drops under .app-body's 1; the masthead's 100 stays above
     both, which is the order that was wanted here in the first place. */
  body.is-calling:not(.is-call-min) .aimy-overlay { z-index: 0; }
  /* AND THE PANEL HAS TO BE POSITIONED TO BE IN FRONT OF A ZERO. `.app-call`
     is static, and a static box paints in the block layer — under every
     positioned sibling in the stacking context, `z-index: 0` included. So
     dropping the canvas to 0 was not enough on its own: the canvas still
     covered the call, measured, with the panel correct at 459 wide behind it.
     One step up puts it in the positioned layer, above the 0 and below the
     masthead's 100. */
  body.is-calling:not(.is-call-min) .app-call { position: relative; z-index: 1; }
  .s-work, .s-work-body { min-width: 0; }
}

/* ══ PASS 12 FOLLOW-UP ═══════════════════════════════════════════════════ */

/* `body.canvas-open { overflow: hidden }` lived here for one pass and could
   never have done anything: `body` is already `height: 100vh; overflow:
   hidden` (see the shell rules above). The scroll it was meant to stop was
   the canvas THREAD's, fixed where that actually scrolls. */

/* "47 accounts" and "of 118" ran together as "47 accountsof 118": the count
   is `--fs-lg` and the qualifier `--fs-xs`, so a baseline-aligned flex gap
   read as no gap at all against the larger glyphs. Stated explicitly. */

/* `.s-aud-names` went with the audience read-out it wrapped. */

/* `.proto-build` went with the last three things that used it: the build
   stamp, which reads out of the console now, and the two explainers under
   the panel's remaining sections. Nothing renders it, so the rule would be
   dead CSS — and dead CSS is the one kind of wrong the audit cannot see,
   because it only checks that a class USED has a rule, never the reverse. */

/* ══ WHAT HAPPENED, IN THE RAIL ═══════════════════════════════════════════
   A campaign's history, in the same region and the same vocabulary a record's
   touchpoints already use — one rail, whichever thing you opened. ══ */
/* ── ONE LINE UNTIL YOU WANT IT ──
   It was 1,246px of a 1,959px rail: AiMY's conclusion and every finding under
   it were the first third of something you scrolled a screen and a half to
   finish. The summary states what opening it would give — how many, how
   recent — so the decision can be made without making it, which is the only
   thing that makes a disclosure better than a scroll. */
/* ── A BLOCK, NOT A FOOTER ──
   The summary was the 11px uppercase caption step on `--d500` — the faintest
   thing in the product, the treatment field labels get — sitting at the
   bottom of the rail under a button. It read as disabled chrome over the
   record of everything the campaign has done. It is a heading now, on its own
   ground, with a chevron that turns. */
.s-hist {
  margin-bottom: var(--sp-4); margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--r-lg);
}
.s-hist-sum-top {
  list-style: none; cursor: pointer;
  display: flex; flex-direction: column; gap: 2px;
  min-height: 44px; justify-content: center;
}
.s-hist-sum-top::-webkit-details-marker { display: none; }
.s-hist-sum-top:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }
.s-hist:hover { border-color: var(--card-border-hover); }
.s-hist:hover .s-hist-h { color: var(--accent); }
.s-hist[open] .s-hist-sum-top {
  margin-bottom: var(--sp-3); border-bottom: 1px solid var(--hairline); padding-bottom: var(--sp-3);
}
.s-hist-count { font-size: var(--fs-2xs); color: var(--ink-muted); font-variant-numeric: tabular-nums; }

/* The heading takes the body step and the full text colour — it is the
   dominant thing in its own block, and the count under it is the caption. */
.s-hist-h {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--d50);
  transition: color var(--t-fast);
}
/* Same shape as every other disclosure chevron in the library — `.acc-head
   .chev`, `.tree-chev`, `.reasoning .chev` — so a turning chevron means the
   same thing here as it does there, and this one has no timing of its own to
   go out of step with them. */
.s-hist-chev {
  width: 14px; height: 14px; flex: none; margin-left: auto;
  transition: transform var(--t-base) var(--ease-out);
}
.s-hist[open] .s-hist-chev { transform: rotate(180deg); }

/* The two halves inside, each labelled, because they answer different
   questions. These stay on the caption step — they are subordinate to the
   block's own heading, which is the point of giving that heading a step of
   its own. */
.s-hist-h-sub {
  display: block; margin: var(--sp-4) 0 var(--sp-3);
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.s-hist-more { margin-top: var(--sp-2); }
.s-hist-sum {
  list-style: none; display: inline-flex; align-items: center; gap: 4px;
  min-height: 24px;
}
.s-hist-sum::-webkit-details-marker { display: none; }
.s-hist-more[open] .s-hist-sum { margin-bottom: var(--sp-3); }

/* A lifecycle step is quieter than a touchpoint: it is what the campaign did,
   not what was said to somebody. */
.s-hist-item .tl-title { font-weight: var(--fw-semibold); }
.s-hist-item .tl-dot svg { width: 11px; height: 11px; }

/* The campaign's lifecycle sits in the rail, which is 220px wide. It keeps
   the library's sizes; the RECORD's history does not — see §THE HISTORY IS
   THE RECORD, further down, beside the rest of the touchpoint styling. */

/* The distribution reads as a comparison, and a bar 1800px wide with its
   count at the far right is not one — the eye cannot hold the two ends
   together. Capped at a width where the label, the bar and the number are
   one glance. */
.s-camp-page .s-ans-row { max-width: 720px; }

/* `[hidden]` must actually hide. The library gives `.float-badge`
   `display: flex`, and a class selector outranks the user agent's
   `[hidden] { display: none }` — so the badge shipped with `hidden` on it and
   rendered a permanent red "0" regardless. See GAPS.md. */
.float-badge[hidden] { display: none; }

/* HOVER ON THE MARK, THE WAY KNOWLEDGE DOES IT.

   Knowledge v2 reached this first — `assets/knowledge.css`:

       #canvasOpen { cursor: pointer; }
       #canvasOpen:hover { border-color: rgba(139,79,244,0.6); }

   The mark already carries `border: 1px solid rgba(139,79,244,0.3)` from the
   library, so the hover is that same border at twice the opacity: the control
   does not gain an edge on hover, the edge it already has comes forward. That
   is why it reads as one object waking up rather than a box appearing.

   Written against `--accent-rgb` rather than the literal Knowledge used, since
   the two are the same colour and only one of them survives a theme change.

   The mark also joins the press list above. Its own neighbour in the same bar
   — the send button — was already there, so the two controls in one pill
   answered a press differently: one dipped, one did nothing. */
#canvasOpen:hover { border-color: rgba(var(--accent-rgb), 0.6); }

/* THE CONTROLS IN THE AiMY BAR SAY THEY ARE CONTROLS.

   `cursor` is inherited, so stating it on the button is what fixes the unread
   badge the report was about: the badge is a count drawn ON a control, not a
   second control inside it, and giving it its own pointer would have implied
   a target it does not have. */
.aimy-float-icon,
.aimy-float-send,
.overlay-send { cursor: pointer; }

/* Nothing is clickable while AiMY is unreachable, and the cursor should stop
   promising it. `:disabled` already greys them; this stops the hand. */
.aimy-float-icon:disabled,
.aimy-float-send:disabled,
.overlay-send:disabled { cursor: default; }

/* ══ THE ANCHOR, ON A PHONE ════════════════════════════════════════════════
   32px of display face wraps a four-word title at 375px — "Q3 Netherlands —
   QA" took two lines and "Since your last visit" three. One step down, which
   is still two steps clear of the body around it.

   At the foot of the file deliberately: a media query adds no specificity, so
   this has to come after every rule it overrides. Placed with the anchor
   comment further up, it lost to `.s-rec-name` and `.s-sheet-name` and did
   nothing at all. */
@media (max-width: 619.98px) { /* 620 layout px */
  .s-rec-name, .s-sheet-name, .s-result-count { font-size: var(--fs-2xl); }
  /* The head, not the title: the mark is sized in `em` off it, so stepping
     the container down takes the mark with it and the pair stays in
     proportion at every width. */
  .slv-head { font-size: var(--fs-2xl); }
}

/* ── Logging a touchpoint: direction, and what it unlocks ──
   The reply category and the objection only exist on something they said to
   us, so both fields are hidden until the direction says inbound. `hidden`
   does the work; this is only the seam above them. */
/* The two halves of the direction question. They are the same field shape
   and only one of them is ever on screen, so they take the same spacing. */
.s-inbound-only, .s-outbound-only { margin-top: 12px; }
/* The radio itself is the hit target inside the chip, exactly as the other
   `.s-assign-chip` groups on this surface do it. */
.s-log-why { accent-color: var(--accent); }
/* Direction is the same segmented control the channel uses, so the two rows
   read as one question asked twice rather than as two kinds of control. */
.s-dir-pick { display: inline-flex; }

/* The reading under the breakdown, at the width of the list it reads. */
/* ══ THE BLOCK IS FULL WIDTH, THE LINE INSIDE IT IS NOT ════════════════

   Capped at 76ch the callout sat under the left column of a two-column
   grid and read as belonging to it — as if only "People" had a note. It is
   about the whole breakdown, so it spans the whole breakdown.

   AND SO DOES THE LINE. The measure rule this file holds to elsewhere —
   60 to 75 characters — is about long-form text, where the eye has to find
   the start of the next line over and over. This is two lines. Capping it
   left a short callout hugging the left third of a full-width block, which
   read as unfinished rather than as considered, and the cost of a longer
   measure over two lines is close to nothing. */
.s-exec .s-insight { margin-top: 32px; }

/* Below the split the two columns would each be too narrow for a figure
   and its caption on one line, so they stack — and the rule that separated
   them becomes a rule between them. */
@media (max-width: 1179.98px) {
  .s-cost { grid-template-columns: 1fr; row-gap: 0; }
  .s-cost-g { border-top: 1px solid var(--card-border); padding: 16px 0; }
  .s-cost-g:nth-child(odd), .s-cost-g:nth-child(even) { padding-left: 0; padding-right: 0; border-left: 0; }
  .s-cost-g:first-child { border-top: 0; padding-top: 0; }
}
