/* ═══════════════════════════════════════════════════════════════════════
   knowledge.css — AiMY Knowledge v2, product chrome only
   ───────────────────────────────────────────────────────────────────────
   Everything here is layout and product shell. No component lives in this
   file. If a rule here starts describing what a component looks like, it
   belongs in the design system instead — a component built inside Knowledge
   will not travel correctly into a consuming agent (direction §13).

   The shell is legitimately product-owned: design-system.md §navigation
   states the shared topnav is "maintained centrally, outside this system",
   and .app-topnav / .app-sidebar / .app-main exist there only as the
   #shell-css sample. Ported from AiMY/QA so the two agents are structurally
   identical, with two corrections applied on the way across:
     · --transition-fast → --t-fast    (QA used a token that does not exist)
     · --qa-accent       → --accent    (withdrawn, design-system.md §10.8)

   Knowledge keeps the default --accent (#8b4ff4). No re-theme this round.

   Components used by the shell come from the design system, not from here:
   .nav-item · .tabs-strip/.tab · .avatar · .user-pill · .aimy-float-bar ·
   .aimy-overlay. This file positions them; it does not restyle them.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   THE INK SCALE — corrected, and then named

   GAPS.md §1.8 carries a contrast table headed "measured directly against the
   real surfaces, dark theme". Every cell in it is the LIGHT theme's number.
   Measured again, in dark, against the three surfaces this product actually
   paints on:

     token    --card-bg   --card-bg-raised   --body-bg
     --d600     2.49           2.20            2.51
     --d500     3.51           3.10            3.54
     --d400     6.02           5.33            6.08
     --d300     8.99           7.95            9.07

   The section concluded that --d500 "passes AA on every surface at a
   difference the eye does not register" and moved five label classes onto it.
   In the default theme it is 3.10 and fails, which is why the muted text in
   this product could not be read. It also says --d600 is safe for borders
   because the 3:1 non-text threshold applies; at 2.20 it fails that too.

   Two rungs are corrected here rather than in the library, because 85 rules
   in aimy-ds.css use them as a text colour and the library is extracted, not
   authored. An unscoped :root is deliberate: aimy-ds.css declares
   :root[data-theme="light"] at higher specificity, so light — which already
   passes at 4.56 and 3.89 — keeps its own values and only dark moves.

   Above the ramp sits a role layer, because a rule should say what a colour
   MEANS. Each role names a RUNG rather than a hex, and the ramp already
   inverts between themes, so one definition serves both and there is no
   per-theme mapping to drift — drift being exactly what §1.8 is.

     role              dark      worst    light      worst
     --ink-primary     #eef2f6   13.63    #10151b    16.94
     --ink-secondary   #c8d2dc   10.01    #2a3540    11.53
     --ink-quiet       #b0bcca    7.95    #3a4653     8.89
     --ink-faint       #8b9aaa    5.33    #566472     5.60   ← the floor
     --ink-action      #a478f7    4.83    #7a34e8     5.57

   CONTRAST IS NOT THE ONLY CONSTRAINT. A first cut put --ink-faint on the
   raised --d500 (#84929f) because it cleared AA at 4.82. It does, and it was
   still wrong: --ink-quiet was --d400 #8b9aaa, and

     ΔE(CIE76) --ink-quiet vs --ink-faint = 3.47, against a JND of ~2.3

   — two of the five ranks the same colour at 12px, while every other adjacent
   pair sat at 12–20. Raising a rung to fix its contrast necessarily moves it
   into its neighbour, which is how you fix a legibility bug by deleting a
   rank. The four rungs below are evenly spaced instead:

     dark  ΔE 12.19 / 8.33 / 12.93      light  ΔE 15.49 / 7.62 / 12.55

   --d100 and --d200 were sitting unused. The fifth rank was already paid for.

   --ink-action exists because --accent is two things at once: #8b4ff4 is a
   legitimate fill and an illegible text colour — 3.73 dark, 4.29 light, so it
   failed in BOTH themes. GAPS §26 names the confusion; this separates them.
   The fill keeps --accent and does not move.
═══════════════════════════════════════════════ */
:root {
  /* ══ 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);

  /* Neither of these is a product text role — the five --ink-* roles below
     are. They are raised because 85 rules in aimy-ds.css use them as a text
     colour, and a library component the product has not individually
     re-stepped should not render at 3.10:1 while it waits. A floor under the
     parts of the system this product does not own. */
  --d500: #84929f;   /* 5.44 / 4.82 / 5.49 — was #637280 at 3.51 / 3.10 / 3.54 */
  --d600: #6b7b8b;   /* 3.99 / 3.53 / 4.02 — non-text; was 2.49 / 2.20 / 2.51 */

  --ink-primary:   var(--d50);    /* identity — the thing's name, a committed value */
  --ink-secondary: var(--d200);   /* substance — prose, the document's own words    */
  --ink-quiet:     var(--d300);   /* attribution — who, when, where                 */
  --ink-faint:     var(--d400);   /* ambient — separators, counts, timestamps       */
  --ink-action:    #a478f7;       /* accent AS TEXT. The fill stays --accent.       */
  --ink-rule:      var(--d600);   /* NON-TEXT — hairlines, dotted underlines, dots  */

  /* Status AS TEXT. Same split as --ink-action, for the same reason: a hue is
     a fill and a text colour at once, and the two want different values.

     Measured on the surface each one actually paints on, which for a status
     word is its OWN 12-14% tint — not the card. That is the measurement that
     had never been taken, and it changes the answer: in dark only --err fails,
     but in LIGHT all four do, because the library darkened the hues one step
     for "text/icons on white" and a tint is not white.

       light, worst surface     ok 3.22 · warn 3.74 · err 3.73 · info 3.61

     Four words — Conflicting, Out of date, Unowned, No issues — every one of
     them the reason a document is in front of you, and in the light theme not
     one of them cleared AA. The tints, borders, dots and card edges keep the
     original hues and do not move; only text does. */
  --ink-ok:   var(--ok);          /* dark 6.28 / 5.56 / 5.28 / 4.62 — already clear */
  --ink-warn: var(--warn);        /* dark 7.38 / 6.53 / 6.13 / 5.37                 */
  --ink-info: var(--info);        /* dark 6.25 / 5.53 / 5.21 / 4.62                 */
  --ink-err:  #f4736a;            /* dark 6.19 / 5.48 / 5.56 / 4.90 — was 4.08      */
  --ink-cyan: var(--cyan);        /* dark 9.66 on the card, 7.57 on its own tint   */
}
:root[data-theme="light"] {
  /* 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);
  --shadow-sm: 0 0.0625rem 0.1875rem rgba(16,24,40,0.08);
  --shadow-md: 0 0.25rem 1rem rgba(16,24,40,0.10);
  --shadow-lg: 0 0.5rem 2rem rgba(16,24,40,0.12);
  --shadow-xl: 0 1rem 3rem rgba(16,24,40,0.16);

  /* ── AND THE WORST GROUND IS NOT THE CARD ──

     These four were derived against a status tint over a WHITE CARD. Three
     of them fall under 4.5 on the tint over the PAGE, which is two shades
     darker — and a status banner sits directly on the page, with no card
     under it. Found on the webhooks panel: "These webhooks are called when
     knowledge enrichment is triggered from your side" at 4.45.

                       tint on a card    tint on the page
       --ink-ok             5.58              4.99   ok
       --ink-warn           5.02              4.49   under
       --ink-err            5.39              4.80   ok
       --ink-info           4.97              4.45   under
       --ink-cyan           4.67              4.17   under

     Re-derived against the page tint, which is the worse of the two, so the
     value holds wherever the component lands. The nudges are one to three
     points a channel — this is the same colour, measured against the ground
     it is actually printed on.

     --ink-cyan is mine, from the pass two commits ago, and it has the same
     fault for the same reason: I walked it darker until it cleared the card
     and stopped there. */
  --ink-ok:   #0a6d41;   /* 6.41 white / 5.58 card tint / 4.99 page tint */
  --ink-warn: #9b5403;   /* 5.73 / 5.09 / 4.55 — was 4.49 on the page tint */
  --ink-err:  #b81f14;   /* 6.47 / 5.39 / 4.80 */
  --ink-info: #0569a5;   /* 5.88 / 5.11 / 4.57 — was 4.45 on the page tint */
  /* The family was four of five. --cyan is #0d8f95, which measures 3.90 on a
     card and 3.29 on its own tint — the worst of the set — and had no text
     variant at all, so .tag-ai and .tag-teal had nothing to fall back to.
     Derived the same way as the other four: walk the hue darker until it
     clears 4.5 on the tint it is printed on, then stop. */
  --ink-cyan: #0a6d73;   /* 6.09 / 5.14 / 4.59 — was 4.17 on the page tint */
}
:root[data-theme="light"] { --ink-action: #7a34e8; }

/* ═══════════════════════════════════════════════
   THE UI SCALE — one lever, every dimension

   This product was built at one size. Every length in it was an absolute
   pixel, so the interface never changed physical size on a different screen —
   only the amount of empty room around it changed. Measured on the running
   app: at 2560×1400 the content column was 1160px sitting at x=300, leaving
   1100px of dead space to its right, with 13px type. On a tablet the same
   fixed sizes were the only thing that fit, so the rail and half the topnav
   were deleted by `display: none` rather than adapted.

   Both are the same defect. A layout that cannot change size can only change
   what it drops.

   The lever is the root font size, and every length below is stated in `rem`
   against it. One declaration moves type, spacing, radii, the rail, the card
   grid and the input band together — which is what makes a 2560 screen show
   the SAME interface, larger, rather than the same interface stranded.

     viewport   root      scale
     ≤1536      16px      1.00   ← the reference. Identical to what was built.
      1920      20px      1.25
      2560      26.7px    1.67
      3840      40px      2.50
      6144      64px      4.00

   96 is 1536 / 16 — the reference width divided by its own root size. So the
   root is always the viewport's width expressed in the units the layout was
   drawn in, and `--measure-page: 72.5rem` is 75.5% of the window at EVERY
   width, exactly as it is at 1536.

   THERE IS NO CAP, and the first draft of this was wrong to have one. Capped at
   1.4x the interface stopped growing at 2738px and everything past that became
   margin: at a 6144px viewport the layout was a narrow island holding 26% of
   the glass with 4500px of empty space around it. A cap does not prevent a
   large screen from looking wrong, it GUARANTEES it — it just moves the width
   where it starts.

   The floor does the opposite job and stays: at or below 1536 this is 1.00 and
   the reference build renders untouched.

   WHAT THIS COSTS. Browser zoom stops changing the layout. Zooming to 25%
   quadruples the CSS viewport, which quadruples the root, which cancels out —
   so the page looks the same and you cannot zoom out to see more of it. That
   is not a side effect to apologise for, it is the request: one experience,
   the same everywhere, with no zooming.

   A NOTE ON HEIGHT. This tracks width alone, so a very wide, very short window
   shows fewer rows than a 16:9 one — a 21:9 ultrawide gives about 86% of the
   vertical run at 1536. Scaling by min(width, height) would fix that and would
   also break the identity above, because zoom would no longer cancel. Width
   alone is the one that answers what was asked.

   Below the floor the type does not shrink either; the layout adapts instead —
   the rail becomes a drawer, the tab strip scrolls, the grid drops a column.
   Shrinking text to fit a tablet is how you arrive at 10px labels, and this
   file already spends forty lines arguing against those.

   Stated in `rem` rather than `px` so it composes with the reader's own
   default size: someone who runs their browser at 20px gets 1.25× on top of
   whatever the viewport contributes, instead of having it overridden.

   THE CAP IS NOT OPTIONAL. Without it a 3440 ultrawide would land at 1.72×
   and the measure would break — a line of body text is bounded by how far the
   eye tracks, not by how much glass is available.
═══════════════════════════════════════════════ */
html {
  font-size: max(1rem, calc(100vw / 96));
  /* macOS renders text heavier than the design intends, and neither this
     product nor the design system had ever turned that off. It matters more
     after the ladder re-cut than it did before: the whole point of dropping
     every rank one step was to land on a measured ink density, and a Mac
     was quietly adding some of it back. Once, on the root, never per
     component. A no-op on Windows and Linux, correct on a Mac — which is
     also why it cannot be verified from here. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── The library's own scales, re-pointed to the lever ──────────────
   These are declared in aimy-ds.css in px, so nothing that reads them can
   scale. They are re-declared here rather than edited there, because that
   file is a faithful copy of the shared library and its header says so.

   Every value is identical at a 16px root — this is a change of unit, not of
   size, and the 1× snapshot proves it.

   --r-* earns its place: the library consumes the radius tokens 166 times and
   its own --fs-* / --sp-* scales zero times, so re-pointing the radii is the
   one edit here that reaches into library components without touching them. */
:root {
  /* Layout */
  --topbar-height: 3.75rem;   /* 60 */

  /* Radius — read by aimy-ds.css 166 times, so this scales the library too */
  --r-xs:  0.25rem;    /*  4 */
  --r-sm:  0.375rem;   /*  6 */
  --r-md:  0.5rem;     /*  8 */
  --r-lg:  0.625rem;   /* 10 */
  --r-xl:  0.75rem;    /* 12 */
  --r-2xl: 1rem;       /* 16 */
  /* --r-pill stays 9999px. A pill is a shape, not a measurement. */

  /* Type — the library's scale, in rem so it moves with the lever.

     THESE ARE THE LIBRARY'S OWN NUMBERS, AND THEY MOVED. The note above says
     "nothing here changes a value", which was true when the library read
     10 / 11 / 12 / 13 at the bottom. It reads 12 / 14 / 16 / 16 now, and a
     stale copy of a token is worse than no copy at all: every component
     reaching for --fs-2xs was landing on 10px through a shell that had
     already been corrected to 12. Re-synced, and every step is even. */
  --fs-2xs: 0.75rem;   /* 12 — minimal components */
  --fs-xs:  0.875rem;  /* 14 — the smallest sentence */
  --fs-sm:  1rem;      /* 16 */
  --fs-base:1rem;      /* 16 — body */
  --fs-md:  1rem;      /* 16 */
  --fs-lg:  1.125rem;  /* 18 */
  --fs-xl:  1.25rem;   /* 20 */
  --fs-2xl: 1.5rem;    /* 24 */
  --fs-3xl: 1.875rem;  /* 30 */
  --fs-4xl: 2.375rem;  /* 38 */
  --fs-5xl: 2.875rem;  /* 46 */

  /* Spacing — declared by the library, used by it zero times. Live here now. */
  --sp-1:  0.25rem;  --sp-2:  0.5rem;   --sp-3: 0.75rem;  --sp-4: 1rem;
  --sp-5:  1.25rem;  --sp-6:  1.5rem;   --sp-8: 2rem;     --sp-10: 2.5rem;
  --sp-12: 3rem;     --sp-15: 3.75rem;

  /* Elevation. Declared in px by the library and read from there by the
     product, so a card at 4x kept a 16px lift that no longer reads as one.
     Same treatment as --r-*: re-pointed here, identical at a 16px root. */
  --shadow-sm: 0 0.0625rem 0.1875rem rgba(0,0,0,0.3);
  --shadow-md: 0 0.25rem 1rem rgba(0,0,0,0.35);
  --shadow-lg: 0 0.5rem 2rem rgba(0,0,0,0.4);
  --shadow-xl: 0 1rem 3rem rgba(0,0,0,0.5);

  /* The measures. Named, because three separate rules were each carrying
     their own copy of "how wide may a column of prose be". */
  --measure-page: 72.5rem;   /* 1160 — the working set */
  --measure-doc:  58.75rem;  /*  940 — a document sheet */
  --measure-doc-page: 96rem; /* 1536 — a document open in place, WITH its rail.
                                Not a round number: it is exactly the width a
                                1600px window leaves after .page-scroll's
                                padding, so the cap meets the available width
                                at the reference boundary and starts binding
                                from there. At or below it, it cannot bind, so
                                nothing changes; above it, the line length stops
                                climbing with the monitor. */
  --measure-bar:  47.5rem;   /*  760 — the input band */

  /* ── AND A `ch` IS NOT A CHARACTER ──

     Every prose cap in this file is written in `ch`, and `ch` is the width
     of the digit ZERO, which is one of the widest glyphs a face has. Prose
     averages far narrower. Measured in Poppins against 2,178 characters of
     this product’s own document copy:

       one `ch`  10.05px        average prose character  7.88px
       so  N ch  renders  N × 1.274  actual characters

     Which means the caps were all about a quarter wider than the numbers
     they were written as:

       80ch = 102 characters     72ch = 92     66ch = 84

     The note at .doc-scroll .dv-body proves the intent was real characters:
     "at 620px and 13px the body ran to ~95 characters, which is where the
     eye starts losing the line it is on". It then set 66ch, which today
     renders 84 — the author asked for 66 and the unit gave them 84.

     It got worse when the face changed. A `ch` is per-face, so moving the
     product to Poppins re-scaled every one of these caps at once, silently,
     in a commit that was about the typeface.

     One number, stated in `ch` because that is what CSS gives, and labelled
     with what it actually renders. 75 characters is the top of the
     comfortable band rather than the middle — this is a correction of a
     measure that was past the ceiling, not a re-layout of the document. */
  --measure-prose: 59ch;     /* 75 characters in Poppins, at any size */
}

/* ═══════════════════════════════════════════════
   THE DENSITY TYPE SCALE

   GAPS §19 asked for this by name: "worth taking upstream as a documented
   density scale, the way spacing has --sp-*". The library has --fs-2xs
   through --fs-5xl and then never uses it — 265 literal sizes in aimy-ds.css,
   55% of them off its own scale, clustered at the small end (8, 8.5, 9, 9.5,
   10.5, 11.5, 12.5). Twenty-six distinct sizes existed across this repo.

   Five steps, and a floor. The floor is the part that matters: --fs-2xs used
   to hand you 10px without comment, and 10px in a 320px column is decoration
   wearing the clothes of information. It is 12 everywhere now.

   EVERY STEP IS EVEN — 12 · 14 · 16 · 18 · 20 · 28. The scale was 17 / 15 /
   13 / 12, which is a floor with three odd steps standing on it: nothing
   distinguished them from 16 / 14 / 12 except which file they were typed in,
   and one pixel is not a level a reader can see. Rounding up rather than down
   is what protects the floor.

   A 320px rail and a 900px canvas are not the same problem, which is the
   other half of §19. Rather than a second scale, panels re-point the top two
   steps one notch down — same vocabulary, denser setting. The bottom two do
   not move, because a floor that bends is not a floor.

   Hierarchy is carried by step AND weight AND ink together, never by size
   alone. Size alone is what produced a six-run card where nothing led.
═══════════════════════════════════════════════ */
:root {
  --ty-display: 1.75rem;   /* 28 — the document title, and nothing else.
                              Was clamp(22px, 2.4vw, 28px): a private fluid curve,
                              on a different slope from everything around it.
                              The root scale does this job now, once. */
  --ty-title:   1.25rem;   /* 20 — card title, region head              */
  --ty-body:    1rem;      /* 16 — prose, snippet, a value you read for */
  --ty-meta:    0.875rem;  /* 14 — block title, label, attribution      */
  --ty-micro:   0.75rem;   /* 12 — FLOOR: separator, count, timestamp   */

  /* ── LEADING, AND IT WAS INVERTED ──

     The library ships --lh-base 1.55 and --lh-relaxed 1.75, both above the
     1.2–1.45 range the print references give, while the SMALLEST text on
     this surface carries the TIGHTEST leading. 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 correction to the print range is the ground. That range is dark type
     on white and this product is the inverse — light type on a dark ground
     blooms, the glyphs spread slightly into the space around them, and lines
     that measure the same sit visually closer. The standing advice is to pay
     it back on leading, tracking and weight. Only leading is available here:
     tracking on lowercase body is wrong at reading sizes whatever the
     ground, and the weight axis is at its floor already.

     So body takes 1.5, the bottom of the 1.5–1.6 band rather than the top of
     a range computed for the opposite polarity, and the relaxed step — which
     the document body reads — takes 1.6 rather than 1.75. Headings take 1.25
     rather than 1.2, because 1.2 is set solid at 20px and above.

     Identical to Sales, for the same reason the weight ladder is. */
  --lh-tight:   1.25;
  --lh-base:    1.5;
  --lh-relaxed: 1.6;
}
.doc-rail, .app-sidebar, .peek, .props {
  --ty-title: 1.125rem;   /* 18 — block title, region head */
  --ty-body:  1rem;       /* 16 — value, lead-in           */
  --ty-meta:  0.875rem;   /* 14 — attribution, phrase      */
  /* --ty-micro does not move. A floor that bends is not a floor.

     THE DENSE VARIANT IS NOW ONE NOTCH, NOT THREE. On an even scale with a
     12 floor and 16 as the working minimum there are exactly three text
     steps — 12 · 14 · 16 — and a 320px column cannot buy density by
     stepping down through them without landing under the floor. Only the
     title moves, 20 → 18.

     The rest of the density has to come from where this file already says
     it comes from: "hierarchy is carried by step AND weight AND ink
     together, never by size alone". A narrow column is the case that proves
     it, because size is the one axis it has run out of.

     --ty-meta is re-pointed to its own value on purpose. Without this line
     the top two steps move down and --ty-meta stays where it is, which put
     .rail-block-title under the prose it heads. */
}

/* The five ranks. Every text run on a product surface is one of these, and a
   rank differs from its neighbour in at least two of {step, weight, ink} —
   one axis of difference is what "22 type styles, none more than 4.5px apart"
   looks like from the inside.

     L1 identity      what the thing IS
     L2 substance     what you are reading FOR
     L3 structure     names a group or a field
     L4 attribution   who · when · where
     L5 ambient       separators, counts, stamps

   Colour beyond the ramp is spent on exactly two things: status, which also
   tints the card's own border, and actions. Nothing else earns a hue. */

/* ═══════════════════════════════════════════════
   AND THE LIBRARY’S WEIGHTS NEVER RODE THE LADDER DOWN

   Same fault as the section below, on the other axis. aimy-ds.css sets
   font-weight as a bare number 188 times, so when the ladder was re-cut
   for Poppins every one of those sat outside it — and the design system
   went on rendering at weights chosen for Urbanist, which in Poppins run a
   fifth to a quarter heavier.

   MEASURED, not guessed, and measured the same way that section was: 188
   rules carry a raw weight, 34 of them land on an element this product
   actually renders. Walked the grid, the folder tree, the notifications,
   the user menu, the light theme, a document with its rail and versions
   and comments, the report and archive dialogs, both settings pages, the
   gate and an answered question.

   Three of the 34 render ONLY inside the assistant — .aimy-float-hint,
   .filter-chip and .overlay-badge, every instance of them inside
   .aimy-float-wrap or .aimy-overlay. THE CANVAS KEEPS THE LIBRARY’S TYPE.
   That surface is the same component in all three agents, and re-scaling
   it from here makes the assistant look different depending on which
   product you opened it from. Sales settled that one and it settles the
   same way here. The test is where an instance RENDERS, not which banner
   it is filed under: .overlay-sugg-chip is declared in the canvas section
   and every instance of it on the gate sits outside the overlay, so it is
   this product’s.

   The rest 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)

   Listed beyond the 31 confirmed are the library components this product
   mounts but whose surfaces the tour could not reach — the toast, the
   calendar, the memory panel, the change preview, the empty and error
   states. They are the same components under the same ladder; leaving them
   out would mean the first person to open one finds the only block on the
   surface still set for the old face.

   Overridden here rather than edited there, which is the rule the section
   below already states for sizes. Load order makes it work: aimy-ds.css,
   then this.

   .tc-title is NOT here. knowledge.css already sets its weight at § the
   card block, so an override would fork it; it moves there instead.
═══════════════════════════════════════════════ */
/* A DIALOG HEADING IS NOT A BIG TITLE. The library sets .modal-title at 800
   and the last pass dropped it a step with everything else, which left a
   16px heading carrying the weight this product reserves for the 28px name
   of the page. It leads its dialog on size and on 600 against a 400 body,
   which is two axes; extrabold would be a third nothing else there needs. */
.modal-title { font-weight: var(--fw-bold); }

.aimy-toast-title, .aimy-toast-undo, .avatar, .btn,
.cal-dow, .cal-title, .cite, .comment-author, .conf-badge,
.cp-title, .dv-body h4, .dv-notice .dv-notice-link,
.empty-state-title, .entry-action,
.error-state-title, .field-label,
.mem-who, .tag, .tc-approval, .tc-type, .trust-state, .ver-mark,
.ver-tag {
  font-weight: var(--fw-bold);
}

/* A SMALL OUTLINED ACTION IS NOT AN EMPHATIC ONE. .cite-action is "Report a
   problem" and "Archive" in the rail foot, and the ladder drop had left the
   pair at --fw-bold — 600, the weight this product gives a card title. They
   already carry a ground and an edge, which is two axes of their own; the
   third was making a footnote control read as loud as the block heading
   above it.

   .td-action joins it. It is the same kind of object in the trust panel, and
   it was held back one commit only because it was not on the surface being
   looked at — Nour confirmed it should move, so the family is settled in one
   rule rather than split across two weights by which one happened to be on
   screen. */
.cite-action, .td-action { font-weight: var(--fw-semibold); }

/* AND FIVE OF THEM ARE UPPERCASE LABELS, which the caps rule governs: an
   uppercase run is 1.273x the ink height of the same nominal size in
   lowercase, so the library setting these at 14px/700 put a LABEL at 17.8px
   of ink against the 14px value it names. Measured on the archive dialog
   before this: .gov-cr-label and .ss-preview-head both read +27.3% against
   the text under them at a nominal step of zero.

   They take the floor and the weight the product’s own labels take.
   .tc-type stays above, because it is a chip — a kind badge that IS the
   content of its slot, not a name for something else. */
.ai-suggestion-head, .gov-cr-label, .mem-head, .menu-label, .ss-preview-head {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
}

/* THE LIBRARY’S INLINE EMPHASIS GOES ONE STEP FURTHER. Thirteen of its raw
   weights are a <strong> or a <b> inside a sentence, which the rule at
   `strong, b` governs: emphasis ranks under its heading, not beside it. The
   ladder drop alone would have left them at 600, level with every heading
   in the product. Six of these were not in the list above at all — they
   are here so the family is settled in one place rather than half-moved. */
.dv-notice .dv-notice-text strong, .gov-cr-rationale strong, .profile-stat b,
.ss-effect strong, .ss-scope strong, .tc-field-val strong, .tc-gov strong,
.td-row .td-text strong, .trust-line .trust-meta strong, .vc-head strong,
.ver-author strong, .vr-effect strong {
  font-weight: var(--fw-semibold);
}

.ai-menu button, .ai-thinking-label, .cp-passage mark, .ctx-chip, .ds-tab,
.ds-field .field-label, .gov-cr-val, .mem-head .mem-age, .msg-stopped,
.overlay-sugg-chip, .seg-btn, .source-item, .stepper input, .tag-token,
.tree summary, .tree .tree-leaf.selected, .v2-dropdown-btn,
.v2-dropdown-option, .ver-label {
  font-weight: var(--fw-semibold);
}

.ds-choice, .source-domain, .tree .tree-leaf { font-weight: var(--fw-medium); }

/* THE CARVE-OUT I PUT HERE IS GONE, and it was wrong on its own terms.

   It held .btn inside the overlay at --fw-extrabold, on the rule that the
   canvas keeps the library’s type. Nour looked at "New conversation" in the
   overlay and read it as extrabold, which it was — 700 in Poppins.

   Checked in Sales before changing it: sales.css puts .btn in its own
   --fw-bold group with no canvas exception, so Sales has rendered that
   button at 600 since its own sweep. The carve-out was written to keep the
   assistant identical between the products and had achieved the opposite —
   the one component it protected was the one they disagreed about. */

/* ═══════════════════════════════════════════════
   SIX FAILURES AA HAD BEEN CARRYING QUIETLY

   The block above these tokens already made this argument and stopped one
   component short. It fixed the four filter-chip words. It did not fix the
   tags, the confidence badges, the approval marks, the governance labels or
   the danger button, and every one of those prints a status hue as TEXT.

   aimy-ds.css says it in as many words: "these are grounds and marks, not
   sentences. Where a sentence needs the hue, the ink is --ok-text /
   --warn-text / --err-text". This product spells those --ink-*. The rule was
   written down and the components never read it.

   MEASURED in light, on the ground each one is actually painted on — its own
   tinted pill over a white card, not the card:

     .tag.tag-ok             3.47 : 1   needs 4.5
     .conf-badge.conf-high   3.47
     .conf-val               3.47
     .tc-approval.is-approved 3.47
     .gov-cr-proposed .gov-cr-label  3.47
     .btn.btn-err            4.02

   And the whole family fails the same way, not just the instances that
   happened to be on screen: on its own tint --ok is 3.47, --warn 4.02,
   --err 4.02, --info 3.86 and --cyan 3.29. Every --ink-* clears it — 5.58,
   5.02, 5.39, 4.97, 4.67. So the sibling states move with the ones that
   were caught, or the next status to render is the next failure.

   DARK WAS ALREADY CLEAN, which is why this survived: the tour reports zero
   failures there and eight in light. Only the light theme darkens the hues
   for "text on white", and a tint is not white.

   These mirror the library’s own selectors so the specificity matches and
   load order decides — including the :root[data-theme="light"] copies, which
   sit at (0,4,0) and would otherwise beat anything simpler written here.
   Grounds, borders, dots and icons keep the bold hues. Only text moves.
═══════════════════════════════════════════════ */
.tag-ok   { color: var(--ink-ok); }
.tag-err  { color: var(--ink-err); }
.tag-warn { color: var(--ink-warn); }
.tag-info { color: var(--ink-info); }
.tag-ai, .tag-teal { color: var(--ink-cyan); }
.conf-medium { color: var(--ink-warn); }
.conf-low { color: var(--ink-err); }
.tc-approval.is-approved { color: var(--ink-ok); }
.tc-approval.is-pending  { color: var(--ink-warn); }
.gov-cr-current .gov-cr-label  { color: var(--ink-err); }
.gov-cr-proposed .gov-cr-label { color: var(--ink-ok); }
.btn-err  { color: var(--ink-err); }
.btn-warn { color: var(--ink-warn); }
.btn-ok   { color: var(--ink-ok); }
.ws-detected { color: var(--ink-info); }
.ws-staged   { color: var(--ink-warn); }
.ws-completed { color: var(--ink-ok); }
.ws-failed   { color: var(--ink-err); }
.ts-verified { color: var(--ink-ok); }
.ts-due      { color: var(--ink-warn); }
.ts-expired  { color: var(--ink-err); }
.ts-superseded { color: var(--ink-info); }
.td-row.is-ok   { color: var(--ink-ok); }
.td-row.is-warn { color: var(--ink-warn); }
.td-row.is-err  { color: var(--ink-err); }

/* the light theme’s own copies, at the specificity they are written with */
:root[data-theme="light"] .ws-completed,
:root[data-theme="light"] .conf-high    { color: var(--ink-ok); }
:root[data-theme="light"] .ws-staged,
:root[data-theme="light"] .conf-medium  { color: var(--ink-warn); }
:root[data-theme="light"] .ws-failed,
:root[data-theme="light"] .conf-low     { color: var(--ink-err); }
:root[data-theme="light"] .ws-detected  { color: var(--ink-info); }
:root[data-theme="light"] .gov-cr-current .gov-cr-label  { color: var(--ink-err); }
:root[data-theme="light"] .gov-cr-proposed .gov-cr-label { color: var(--ink-ok); }
:root[data-theme="light"] .ts-verified   { color: var(--ink-ok); }
:root[data-theme="light"] .ts-due        { color: var(--ink-warn); }
:root[data-theme="light"] .ts-expired    { color: var(--ink-err); }
:root[data-theme="light"] .ts-superseded { color: var(--ink-info); }
:root[data-theme="light"] .td-row.is-ok   { color: var(--ink-ok); }
:root[data-theme="light"] .td-row.is-warn { color: var(--ink-warn); }
:root[data-theme="light"] .td-row.is-err  { color: var(--ink-err); }
:root[data-theme="light"] .tc-approval.is-approved { color: var(--ink-ok); }
:root[data-theme="light"] .tc-approval.is-pending  { color: var(--ink-warn); }

/* ═══════════════════════════════════════════════
   THE LIBRARY, RE-POINTED TO THE LEVER

   aimy-ds.css states every one of its own sizes as an absolute pixel, and it
   is a faithful copy of the shared library — its header says so, and editing
   it would make the next sync a merge instead of a copy. So the sizes are
   re-declared here.

   This is not "the library, ported". It is the list of library rules that
   actually land on an element this product renders, and it was MEASURED, not
   guessed: 48 library rules carry px that reaches a live element here, and 28
   of those are the `*` reset and icon boxes that follow their component. What
   is left is below.

   Nothing here changes a value. Every number is the library's own, divided by
   16. The point is only that it now moves when the lever moves — a 21.6px
   root with an 11px chip beside it is worse than either alone, because the
   chip does not read as small, it reads as broken.

   NOT restated, because knowledge.css already owns them (verified against
   computed style, not by reading): .type-card, .tc-title, .tc-summary,
   .tc-type font-size, .entry-action padding and font-size, .tag font-size.
   Restating those would fork them.
═══════════════════════════════════════════════ */

/* ── Icons sized by MARKUP, not by CSS ─────────────────────────────
   Every rule above this line moves with the lever because it is a rule. An
   `<svg width="12" height="12">` is not a rule — the presentation attribute
   becomes a used value in CSS pixels, and CSS pixels are the one thing here
   that does not scale. 59 of them render on this page.

   At the reference width they are indistinguishable, which is why they
   survived every earlier pass. At 4x they are the tell: 12px icons inside
   buttons that have grown to 48px, so the whole control reads as a mistake.
   Measured at 6144: `.k-date-btn` was 9 reference pixels narrower than at
   1536, entirely because of its icon.

   Keyed on the attribute's own value, so it is one block for all of them
   rather than a rule per component — including the ones not on screen right
   now (the peek, the canvas, the document rail). Specificity is (0,1,1),
   the same as `.some-class svg`, and this block sits ABOVE every such rule
   in the file, so any component that states its own icon size still wins.

   Attributes stay on the elements: they are the intrinsic size, they are what
   makes the icon correct with CSS off, and the viewBox needs them to be
   internally consistent. This overrides how they are PAINTED, nothing else. */
svg[width="10"]  { width: 0.625rem; }
svg[width="11"]  { width: 0.6875rem; }
svg[width="12"]  { width: 0.75rem; }
svg[width="13"]  { width: 0.8125rem; }
svg[width="14"]  { width: 0.875rem; }
svg[width="15"]  { width: 0.9375rem; }
svg[width="16"]  { width: 1rem; }
svg[width="18"]  { width: 1.125rem; }
svg[width="20"]  { width: 1.25rem; }
svg[width="26"]  { width: 1.625rem; }
svg[width="140"]  { width: 8.75rem; }

svg[height="10"] { height: 0.625rem; }
svg[height="11"] { height: 0.6875rem; }
svg[height="12"] { height: 0.75rem; }
svg[height="13"] { height: 0.8125rem; }
svg[height="14"] { height: 0.875rem; }
svg[height="15"] { height: 0.9375rem; }
svg[height="16"] { height: 1rem; }
svg[height="17"] { height: 1.0625rem; }
svg[height="18"] { height: 1.125rem; }
svg[height="20"] { height: 1.25rem; }
svg[height="26"] { height: 1.625rem; }
svg[height="43"] { height: 2.6875rem; }

/* The input band. .aimy-float-bar's max-width is the one that mattered: the
   wrapper is sized from --measure-bar and the bar inside it was still pinned
   to 760px, so above the reference width the band stopped growing and floated
   in its own wrapper. */
.aimy-float-bar   { max-width: none; gap: 0.625rem; padding: 0.5rem; }
.aimy-float-input { font-size: 0.84375rem; padding: 0 0.25rem; }   /* 13.5 / 4 */
/* 11px, faithfully divided by 16 from the library and still 11px. The floor
   is 12 and it does not bend for a hint. */
.aimy-float-hint  { font-size: var(--ty-micro); padding-right: 0.375rem; }
.aimy-float-icon  { width: 2.25rem; height: 2.25rem; }
.aimy-float-icon svg { width: 1.125rem; height: 1.25rem; }
.float-badge {
  top: -0.1875rem; right: -0.1875rem;
  min-width: 1rem; height: 1rem;
  font-size: 0.5625rem; padding: 0 0.1875rem;
}

/* Buttons */
.btn     { gap: 0.4375rem; font-size: var(--ty-meta);   padding: 0.5rem 1rem; }
.btn svg { width: 0.875rem; height: 0.875rem; }
.btn-sm  { font-size: var(--ty-meta); padding: 0.3125rem 0.6875rem; }
.btn-lg  { font-size: var(--ty-meta);  padding: 0.625rem 1.25rem; }
/* THE BASE, not just the call sites. aimy-ds.css sets .entry-action at 1rem
   with 0.5rem/0.875rem padding, and this file brought every instance it
   renders down to the meta step ONE CALL SITE AT A TIME — .tc-foot-act,
   .rail-act, .rm-end, .type-card.is-compact. The answer surface never got a
   patch, so the apply button was the one place the library default showed
   through: 16px and 42px tall next to 14px ones everywhere else.

   The note at § THE LIBRARY, RE-POINTED says .entry-action padding and font
   size are "NOT restated, because knowledge.css already owns them (verified
   against computed style, not by reading)". That was true of every instance
   the verification could see. It was not true of the base rule, and the one
   surface that inherits it was not on screen when the check ran. */
.entry-action { gap: 0.4375rem; font-size: var(--ty-meta); padding: 0.375rem 0.75rem; }
.entry-action .em-ico { width: 0.875rem; height: 0.875rem; }
.type-card.is-compact .tc-action .entry-action { font-size: var(--ty-meta); padding: 0.375rem 0.75rem; }

/* Chips, tags, dots */
.filter-chip       { padding: 0.25rem 0.75rem; font-size: var(--ty-meta); }
.overlay-sugg-chip { font-size: 0.75rem; padding: 0.4375rem 1rem; }
.seg-btn           { font-size: var(--ty-meta); padding: 0.3125rem 0.875rem; }
.tag               { gap: 0.25rem; padding: 0.125rem 0.5rem; }
.status-dot        { width: 0.5rem; height: 0.5rem; }
.tc-type           { gap: 0.3125rem; }
.tc-type svg       { width: 0.75rem; height: 0.75rem; }

/* Menus and dropdowns */
.menu-item          { gap: 0.625rem; padding: 0.5rem 0.625rem; font-size: var(--ty-meta); }
.v2-dropdown-btn    { gap: 0.3125rem; padding: 0.25rem 0.5rem 0.25rem 0.625rem; font-size: var(--ty-meta); }
.v2-dropdown-btn svg{ width: 0.625rem; height: 0.625rem; }
.v2-dropdown-panel  { top: calc(100% + 0.25rem); min-width: 10rem; padding: 0.25rem; max-height: 16.25rem; }
.v2-dropdown-option { gap: 0.5rem; padding: 0.375rem 0.625rem; font-size: var(--ty-meta); }
.v2-dropdown-option::before { width: 0.3125rem; height: 0.3125rem; }
.dd-label-text      { max-width: 7.5rem; }

/* The canvas, the menus and the tray. These were missed by the first audit
   because that audit only looked at what was ON SCREEN, and the canvas was
   closed — a reminder that "measured, not guessed" is only as good as the
   states you open first. Found by re-running it with every overlay
   instantiated, then by diffing a 6144px render against the reference in
   normalised units: .overlay-thread's 720px cap was 540 reference pixels off
   on its own, which is the thread refusing to grow while the window around it
   did. */
.aimy-float-send, .overlay-send { width: 2.25rem; height: 2.25rem; }
.think-spinner        { width: 0.875rem; height: 0.875rem; }
.filter-tray-inner    { gap: 0.375rem; padding: 0.4375rem 0.75rem; }
.overlay-close        { top: 1.25rem; right: 1.25rem; width: 2.25rem; height: 2.25rem; border-radius: 0.625rem; }
.overlay-close svg    { width: 1rem; height: 1rem; }
.overlay-badge        { top: 1.5rem; gap: 0.375rem; font-size: var(--ty-micro); padding: 0.3125rem 0.875rem; }
.overlay-thread       { max-width: 45rem; padding: 5rem 1.5rem 1rem; gap: 1rem; }
.overlay-suggestions  { gap: 0.4375rem; padding: 2.5rem 0 1.25rem; }
.overlay-input-wrap   { max-width: 45rem; padding: 0 0.75rem; }
.overlay-input-bar    { gap: 0.625rem; border-radius: 2.5rem; padding: 0.5rem; }
.overlay-input        { font-size: 0.84375rem; padding: 0 0.25rem; max-height: 7.5rem; }
.menu                 { top: calc(100% + 0.375rem); min-width: 12.5rem; padding: 0.375rem; }
.menu-label           { padding: 0.375rem 0.625rem 0.1875rem; }   /* size set with the caps family above */
.tc-tags              { gap: 0.3125rem; }

/* Avatars */
.avatar-md { width: 2rem;    height: 2rem;    font-size: var(--ty-meta); }
.avatar-lg { width: 2.75rem; height: 2.75rem; font-size: var(--ty-meta); }

/* ═══════════════════════════════════════════════
   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 — the topnav tabs rendered in
       Arial instead of Urbanist;
     · .tab declares no background at all (its specimen is a <div>), so on a
       <button> the UA's buttonface showed through — light chips on the dark
       shell, which read as the navbar rendering in light mode.

   Tabs are interactive and must be buttons, so this restores the neutral
   canvas the components assume. Nothing here sets a colour, size or spacing;
   it is a reset, not a restyle. Filed in ../GAPS.md.
═══════════════════════════════════════════════ */
input, select, textarea { font-family: inherit; }

/* <strong> pinned to a weight we actually load.
   The UA gives <strong> `font-weight: bolder`, which is RELATIVE: inside a
   600 parent it computes to 900. Briefing conclusions emphasise their key
   figures with <strong> inside .bcard-title (600), so every one of them asked
   for a 900 face the webfont request never included — the browser then
   substitutes 800 and may synthesise the difference on top, which is a
   heavier, muddier stroke than the real cut. 800 is the same face it was
   already landing on, now reached deterministically. */
/* EMPHASIS RANKS UNDER THE HEADING IT SITS BENEATH. This was extrabold,
   which after the ladder re-cut is 700 — and the heaviest heading in the
   product is --fw-bold at 600. Measured on an open document: <b>30 days
   of the purchase date</b> at 16/700 under an <h3> at 20/600. Every <b>
   in the build was tying or beating the thing that introduced it.

   Semibold is a step above the 400 body it interrupts and a step below
   the 600 heading above it, which is what emphasis is for. The exception
   is a <b> that IS its block’s heading — .ntf-head b — and that one is
   left at --fw-bold a few thousand lines down, deliberately. */
strong, b { font-weight: var(--fw-semibold); }

/* Bare <code> falls back to the UA's generic monospace, so the ingestion-error
   codes on Governance rendered in a different typeface from every other
   monospaced string in the product. The library styles .copy-field code and
   .ds-code but never the element itself. */
code { font-family: var(--font-mono); }

/* Element-selector specificity (0,0,1), so any component class that sets these
   still wins — .btn, .entry-action, .filter-chip, .ds-theme-toggle and the
   rest are untouched. What it removes is only what the UA supplies where no
   class does:
     background → .tab showed the buttonface as a light chip on a dark shell
     border     → .link showed `2px outset`, since it only sets border-bottom
     font       → every button rendered in Arial, not Urbanist
     color      → .rq-row computed to black text
   Buttons only: native checkbox and radio need their appearance and the
   library relies on them. */
button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;
}

/* ═══════════════════════════════════════════════
   BUTTON HEIGHTS — deterministic, not inherited

   `font: inherit` on the reset above also inherits LINE-HEIGHT, and none of
   the .btn classes set their own. So a button's height depended on whatever
   container it landed in: .btn-sm rendered 29px inside Governance's table and
   settings rows, and 25px everywhere else. Same class, two heights, which is
   what makes one button look subtly wrong beside another.

   Explicit line-heights reproduce the design system's own measured sizes
   (32 / 25 / 37) and make them independent of context. The library has the
   same latent fragility — its 32px is only 32 because of what it inherits on
   the documentation page. Filed in ../GAPS.md.
═══════════════════════════════════════════════ */
.btn    { line-height: 1rem; }
/* .entry-action is a different component with its own padding, so pairing it
   with a .btn in one row leaves a 2px step. Matched where they sit together. */
.dv-actions .entry-action, .dv-actions .btn { min-height: 2.125rem; }
/* The row used to carry a primary action on the left with these pushed to the
   right by `margin-left:auto`. With the left side gone, the tint and the gap
   drew a large empty panel around two buttons. It is a plain end-of-document
   row now: hairline above, actions where the text is. */
/* No border-top: with Versions collapsed it landed directly under the summary
   and read as a line drawn under a closed section. */
.dv-actions { padding: 1rem 0 0.125rem; background: none; border-top: 0; }
.dv-actions .dv-actions-end { margin-left: 0; }
:root[data-theme="light"] .dv-actions { background: none; }
.btn-sm { line-height: 0.9375rem; }
.btn-lg { line-height: 1.0625rem; }

/* ═══════════════════════════════════════════════
   COPY BUTTON — restored

   Dropped during extraction as documentation chrome, because on the library's
   own page .copy-btn is the "Copy code" control. But .copy-field — a real
   component this product uses — embeds a .copy-btn in its anatomy, so dropping
   it left the settings API-key field with an unstyled button whose unsized SVG
   expanded to 37px. Exactly the failure the extraction header warns about; I
   applied the judgment to the wrong element.
═══════════════════════════════════════════════ */
.copy-btn {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-family: var(--font-sans); font-size: var(--ty-micro); font-weight: var(--fw-bold);
  line-height: 0.875rem; padding: 0.25rem 0.5rem; border-radius: var(--r-sm);
  background: rgba(255,255,255,0.06); border: 0.0625rem solid rgba(255,255,255,0.1);
  color: var(--ink-muted); cursor: pointer; white-space: nowrap;
  transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.copy-btn:hover { background: rgba(255,255,255,0.1); color: var(--d100); border-color: rgba(255,255,255,0.18); }
.copy-btn.copied { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok) 30%, transparent); color: var(--ink-ok); }
.copy-btn svg { width: 0.6875rem; height: 0.6875rem; flex-shrink: 0; }
:root[data-theme="light"] .copy-btn {
  background: rgba(16,24,40,0.05); border-color: rgba(16,24,40,0.12); color: var(--ink-faint);
}
:root[data-theme="light"] .copy-btn:hover { background: rgba(16,24,40,0.10); color: var(--d100); }

/* Small uppercase labels use --d500, not --d600. Measured on the real
   surfaces, --d600 lands at 4.22 / 4.00 / 3.89 against card / raised / body —
   above the design system's stated >=3:1 bar but under WCAG AA's 4.5:1 for
   small text. --d500 is 4.94 / 4.69 / 4.56 and passes everywhere, at a
   difference the eye does not register. See ../GAPS.md. */

/* Numerals in data positions are tabular, so columns of figures line up and a
   changing count does not reflow the text beside it. Proportional digits in a
   metric row make every update look like a layout shift. */
.stat-value, .agg-stat-val, .gov-num, .list-meta, .audit-time, .ver-time,
.ss-num, .agg-val, .rq-count, .nav-count, .dtable td, .conf-val,
.progress-bar-value { font-variant-numeric: tabular-nums; }

.memory-panel { max-width: none; }

/* ═══════════════════════════════════════════════
   AIMY TOAST — corrected to its documented spec

   The library's #canvas-toast anatomy table and the CSS in the same file
   describe two different components. The table (and the published design
   system) specify a bottom-CENTRE toast: accent-tinted icon, row layout with
   a vertical divider, AI-gradient countdown over 5s. The stylesheet still
   implements an older one: bottom-right, green success chip, column layout,
   horizontal divider, --ok fill over 4s.

   The table is the spec people read and the one published, so it wins. Every
   value below is quoted from it, not invented here. Filed in ../GAPS.md — the
   correction belongs in the library, not in each product.
═══════════════════════════════════════════════ */
.aimy-toast {
  bottom: 5.625rem; left: 50%; right: auto;
  transform: translateX(-50%) translateY(20px) scale(0.97);
  flex-direction: row; align-items: center; gap: 0.75rem;
  padding: 0.75rem 1rem;
  visibility: hidden;
  position: fixed; overflow: hidden;
}
.aimy-toast.visible {
  transform: translateX(-50%) translateY(0) scale(1);
  visibility: visible;
}
.aimy-toast-icon {
  width: 1.875rem; height: 1.875rem; border-radius: 0.5625rem;
  background: rgba(var(--accent-rgb), 0.1);
  border: 0.0625rem solid var(--aimy-wash-edge);
}
.aimy-toast-icon svg { width: 0.8125rem; height: 0.9375rem; stroke: none; }
.aimy-toast-body { flex-direction: column; gap: 0.125rem; flex: 1; align-items: flex-start; }
.aimy-toast-title { letter-spacing: -0.005em; margin-bottom: 0; }
/* Vertical rule between the message and the undo, not a horizontal one. */
.aimy-toast-divider { width: 1px; height: 1.75rem; flex-shrink: 0; margin: 0; }
.aimy-toast-progress {
  position: absolute; bottom: 0; left: 0; right: 0;
  height: 0.15625rem; border-radius: 0; background: none;
}
.aimy-toast-progress-fill {
  background: var(--ai);
  transform-origin: left center;
  animation: kToastCountdown 5s linear forwards;
}
@keyframes kToastCountdown { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* Spec says #61adf1 — that is --ai-text, which the CSS renders as --brand.
   Using the token rather than the literal keeps it legible in light mode. */
.aimy-toast-undo { color: var(--ai-text); }
.aimy-toast-undo:hover { color: var(--d50); }
@media (prefers-reduced-motion: reduce) {
  /* The bar states remaining time, so freeze it filled rather than remove it. */
  .aimy-toast-progress-fill { animation: none; transform: scaleX(1); }
}

/* Answer-coverage chart. Bands are labelled in the legend with their current
   share, so the composition is readable without decoding colour — and the
   number beside each label is the thing people actually quote. */
.cov-chart { display: block; border-radius: var(--r-sm); overflow: hidden; }
.cov-legend {
  display: flex; flex-wrap: wrap; gap: 0.375rem 1rem;
  margin-top: 0.5rem; padding-top: 0.75rem;
  border-top: 0.0625rem solid var(--hairline);
}
.cov-key { display: inline-flex; align-items: center; gap: 0.375rem; font-size: var(--ty-meta); }
.cov-swatch { width: 0.5625rem; height: 0.5625rem; border-radius: 0.125rem; flex-shrink: 0; }
.cov-key-label { color: var(--ink-muted); }
.cov-key-val { color: var(--d100); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }

/* ═══════════════════════════════════════════════
   SETTINGS — preferences only

   Deliberately thin. Everything that changes what the corpus DOES is in
   Governance, because the direction classes those as operational decisions
   rather than preferences. Uses the library's .settings-list / .settings-row
   throughout; no settings component is invented here.
═══════════════════════════════════════════════ */
.set-section { margin-bottom: 1.5rem; }
.set-section-title {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wider); text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: 0.5rem;
}
.set-list { max-width: 45rem; }
.set-list .settings-row { gap: 0.75rem; }
.set-list .avatar { flex-shrink: 0; }
.set-list .copy-field { max-width: 16.25rem; }

/* ═══════════════════════════════════════════════
/* ═══════════════════════════════════════════════
   THE CANVAS TAKES THE WINDOW, AND A COLUMN GOES BESIDE THE THREAD

   The design system ships `.aimy-overlay { position: absolute; inset: 0 }`,
   which makes the canvas a child of the surface it is meant to sit above. It
   opened inside `.app-main` only — offset by the briefing rail — and anything
   that scrolled or grew that column carried the canvas with it.

   Both problems close the same way. `fixed` to the viewport under the top bar,
   and `left: 0` rather than an offset, because a canvas that cannot use the
   whole window cannot hold a conversation ABOUT something and show that
   something at the same time. The offsets are the layout's own tokens rather
   than numbers, so they cannot drift from the shell they clear.

   The briefing rail is covered while the canvas is open. That is a deviation
   from doctrine §6.1 — "the product shell (sidebar, topbar) remains
   interactive" — and only half of it: the top bar still is, the rail is not.
   Declared in GAPS.md rather than smuggled through.

   ROW, AND THE LIBRARY'S COLUMN MOVES INSIDE. `display: flex` alone is not
   enough. The library declares `flex-direction: column; align-items: center;
   justify-content: flex-end; padding: 0 0 28px` — one centred stack pinned to
   the bottom, which is exactly right for a thread over an input and exactly
   wrong for a column beside them; left at `column` the chat list stacks ON TOP
   of the canvas. The whole inner contract moves to `.overlay-main` unchanged,
   so the badge, close, basis bar, thread and input keep the geometry the
   system gives them — and they position against `.overlay-main`, because that
   is what is now `position: relative`.

   Ported from AiMY Sales, which argued all of this first and is the only
   product in the ecosystem that had it.
═══════════════════════════════════════════════ */
.aimy-overlay {
  position: fixed;
  top: var(--topbar-height); right: 0; bottom: 0; left: 0;
  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 {
  width: 15rem; flex-shrink: 0;
  display: flex; flex-direction: column; gap: 1.25rem;
  padding: 1.25rem 0.75rem;
  overflow-y: auto;
  border-right: 0.0625rem solid var(--glass-border);
}

/* ── THE SCROLLBAR IS HIDDEN AT REST, NOT ABSENT ──

   This column was the one scrolling surface in the build with no scrollbar
   rule on it at all, so it drew the raw OS bar — a wide, opaque, differently
   coloured strip down the edge of a 240px column, against the four thin ones
   the rest of the product already agrees on (.brief, .sidebar-nav, .ntf-panel,
   .new-menu). The thread beside it does not have this problem because it does
   not use a bar: it fades its last 28px instead.

   THE TRACK STILL RESERVES ITS WIDTH. `scrollbar-width: thin` stays on at all
   times and only the THUMB goes transparent, so appearing costs nothing —
   `scrollbar-width: none` would hand the 4px back to the content and every
   title in the column would re-wrap under the pointer. Hidden, not gone, is
   the whole of the difference.

   It comes back on hover AND on focus-within: keyboard-walking the list with
   Tab scrolls it too, and a scrollbar that only answers a mouse tells half the
   people using it nothing.

   TWO SPELLINGS, ONE BEHAVIOUR, AND ONLY ONE OF THEM RUNS. Where
   `scrollbar-width` is supported the engine uses it and IGNORES the
   ::-webkit-scrollbar pseudo-elements entirely — measured here at a 11px
   reserved gutter rather than the 4px the pseudo asks for. The pseudos are
   kept for the engines that predate that and are dead weight everywhere else,
   which is why neither spelling carries a transition: a fade that happens on
   one browser and not the next is worse than no fade, and `scrollbar-color` is
   not animatable anyway. It appears and it goes. */
.overlay-chats {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}
.overlay-chats:hover, .overlay-chats:focus-within {
  scrollbar-color: rgba(255, 255, 255, 0.06) transparent;
}
.overlay-chats::-webkit-scrollbar { width: 0.25rem; }
.overlay-chats::-webkit-scrollbar-track { background: transparent; }
.overlay-chats::-webkit-scrollbar-thumb { background: transparent; border-radius: 0.125rem; }
.overlay-chats:hover::-webkit-scrollbar-thumb,
.overlay-chats:focus-within::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.06); }

:root[data-theme="light"] .overlay-chats:hover,
:root[data-theme="light"] .overlay-chats:focus-within {
  scrollbar-color: rgba(16, 24, 40, 0.12) transparent;
}
:root[data-theme="light"] .overlay-chats:hover::-webkit-scrollbar-thumb,
:root[data-theme="light"] .overlay-chats:focus-within::-webkit-scrollbar-thumb {
  background: rgba(16, 24, 40, 0.12);
}
.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 spacing scale this project
     audits against, and four pixels at the bottom of a full-height overlay is
     invisible next to a rule that says spacing is chosen once. */
  padding: 0 0 1.5rem;
}

/* ── 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(--d600); }
.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 wrapped title grows downward from the top of the row
     rather than recentring itself and shifting the agent label with it. */
  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) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.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); }
/* 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. */
/* 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. */

/* ═══════════════════════════════════════════════
   THE COMMAND PALETTE

   Above everything, including the canvas and the document overlay, because it
   is the one control that has to work from wherever you are. It is also the
   only thing in this product that is not addressable by URL — a palette is a
   way of reaching a place, not a place.
═══════════════════════════════════════════════ */
.kp { position: fixed; inset: 0; z-index: 900; }
.kp:empty { display: none; }
.kp-scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.45); }
.kp-panel {
  position: absolute; left: 50%; top: 12vh;
  transform: translateX(-50%);
  width: min(34rem, calc(100vw - 2rem));
  display: flex; flex-direction: column;
  border: 0.0625rem solid var(--card-border-hover);
  border-radius: var(--r-xl);
  background: var(--panel-bg);
  box-shadow: var(--shadow-xl);
  overflow: hidden;
}
.kp-input {
  padding: 0.875rem 1rem;
  border: 0; border-bottom: 0.0625rem solid var(--hairline);
  background: none; color: var(--ink-primary);
  font-family: inherit; font-size: var(--ty-body);
}
.kp-input:focus-visible { outline: none; }
.kp-input::placeholder { color: var(--ink-faint); }

.kp-list { max-height: 22rem; overflow-y: auto; padding: 0.375rem; }
.kp-cap {
  padding: 0.5rem 0.625rem 0.25rem;
  font-size: var(--ty-micro); color: var(--ink-faint);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
}
.kp-row {
  display: flex; align-items: baseline; gap: 0.625rem;
  width: 100%; padding: 0.5rem 0.625rem;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer; text-align: left;
  font-family: inherit;
}
/* Selection is a background, not an outline: the palette is driven from the
   keyboard and the highlight moves on every arrow press, so it has to read at
   a glance without redrawing a border each time. */
.kp-row.is-sel { background: var(--brand-dim); }
.kp-row:hover { background: var(--card-bg-raised); }
.kp-label { color: var(--ink-primary); font-size: var(--ty-meta); font-weight: var(--fw-medium); }
.kp-sub {
  margin-left: auto; flex-shrink: 0;
  color: var(--ink-faint); font-size: var(--ty-micro);
}
.kp-none { padding: 1.25rem 0.625rem; text-align: center; color: var(--ink-faint); font-size: var(--ty-meta); }

.kp-foot {
  display: flex; align-items: center; gap: 0.375rem;
  padding: 0.5rem 0.75rem;
  border-top: 0.0625rem solid var(--hairline);
  color: var(--ink-faint); font-size: var(--ty-micro);
}
.kp-key {
  display: inline-flex; align-items: center;
  padding: 0.0625rem 0.25rem;
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-xs);
  font-family: var(--font-mono); font-size: var(--ty-micro);
  color: var(--ink-quiet);
}

@media (prefers-reduced-motion: reduce) { .kp-panel { transition: none; } }

/* ══ 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) var(--ease-out), background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.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; }

.ov-chat-lines { min-width: 0; display: flex; flex-direction: column; gap: 0.0625rem; }
.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; }

/* ══ AND IT IS A CONTROL, NOT A PANEL ════════════════════════════
   `--glass-bg` is `rgba(20,27,36,0.85)` — a PANEL ground, sized for a surface
   that has to be opaque enough to read paragraphs on. Under a 2rem button
   standing on the canvas it disappears: near-black at 85% over a dark
   translucent canvas is just the canvas again, so this read as a bare icon
   floating with no chip beneath it while `.rail-toggle`, the same control one
   screen over, read as a button.

   It now carries what every other small control in this shell carries —
   white 4% on white 8%, the pair `.rail-toggle`, `.gate-rail-btn` and
   `.topnav-bell` already share, with the light-theme inversion that goes with
   it. Which is also what Sales draws here, measured: `rgba(255,255,255,0.04)`
   on `rgba(255,255,255,0.08)`, the reason its toggle has a ground and this
   one did not.

   The transition is `background-color` and `color`, not `all`: `border-color`
   stays instant on purpose, matching `.rail-toggle`. */
.ov-chats-toggle {
  display: none;
  position: absolute; top: 1rem; left: 1rem; z-index: 4;
  width: 2rem; height: 2rem;
  align-items: center; justify-content: center;
  border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 0.0625rem solid rgba(255,255,255,0.08);
  /* --d400 / --d200, the pair `.rail-toggle` and `.topnav-bell` carry and the
     pair Sales gives this same button. It was `--ink-quiet` / `--ink-primary`,
     a rung brighter at rest and three rungs brighter on hover. */
  color: var(--d400); cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
:root[data-theme="light"] .ov-chats-toggle {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}
.ov-chats-toggle:hover { background: rgba(255,255,255,0.08); color: var(--d200); }
:root[data-theme="light"] .ov-chats-toggle:hover { background: rgba(16,24,40,0.07); }
.ov-chats-toggle:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.ov-chats-toggle svg { width: 1rem; height: 1rem; fill: none; stroke: currentColor; stroke-width: 2; }

/* Present at every width, painted at none of them until the drawer exists —
   `.rail-scrim`'s own pattern, and `display` rather than the `hidden`
   attribute for its reason: opening is then one class, and the fade has
   something to transition from.

   BEFORE the query, not after it. Written underneath, this rule and the
   `display: block` inside the query are both (0,1,0) and the later one wins on
   source order — so the scrim stayed `display: none` with the drawer open and
   the whole thing was inert. Measured at 900, 768 and 480: box 0x0, every
   time. `.rail-scrim` sits above its own query at :6165 and that is not a
   coincidence. */
.ov-chats-scrim { display: none; }

/* Below this the column and a readable thread cannot both fit, and the thread
   is what you came for -- so the thread takes the width and the column moves
   behind a toggle. It was `display: none`, which does not put the thread first;
   it removes the only way to reach a different conversation. Same drawer as the
   briefing rail, for the same reason. */
@media (max-width: 900px) {
  .overlay-chats {
    position: absolute; z-index: 3;
    top: 0; bottom: 0; left: 0;
    width: min(15rem, calc(100dvw - 4rem));
    background: var(--panel-bg);
    backdrop-filter: blur(1.125rem);
    -webkit-backdrop-filter: blur(1.125rem);
    box-shadow: var(--shadow-xl);
    transform: translateX(-100%); visibility: hidden;
    /* ══ THE DOOR STAYS ON TOP OF WHAT IT OPENED ══════════════════════
       `.ov-chats-toggle` is `top: 1rem; left: 1rem`, 2rem square, at z-index 4
       against this column's 3 — correct, because while the drawer is open that
       button is how you shut it. But the column's own 1.25rem of padding put
       New conversation directly underneath it: measured at 900, 768 and 480,
       the toggle occupied x 16–48 / y 76–108 and the button x 12–216 / y
       80–104, so the control overlapped the primary action of the panel it
       had just opened, at every width this drawer exists at.

       3.5rem = 1rem of inset + 2rem of button + 0.5rem, which reads as the
       drawer's head with its control in it rather than as a gap. It replaces
       the padding rather than adding to it, so the rows below do not move
       twice. Sales carries the same rule for the same collision. */
    padding-top: 3.5rem;
  }
  .aimy-overlay.chats-open .overlay-chats { transform: none; visibility: visible; }
  .ov-chats-toggle { display: inline-flex; }

  .ov-chats-scrim {
    display: block;
    position: absolute; z-index: 2; inset: 0;
    background: rgba(10, 14, 18, 0.5);
    opacity: 0; pointer-events: none;
  }
  .aimy-overlay.chats-open .ov-chats-scrim { opacity: 1; pointer-events: auto; }
}
:root[data-theme="light"] .ov-chats-scrim { background: rgba(16, 24, 40, 0.28); }
@media (prefers-reduced-motion: no-preference) {
  /* The scrim leaves with the column, not on its own clock — the pairing the
     rail drawer already states. */
  .ov-chats-scrim { transition: opacity var(--t-medium) var(--ease-out); }
  .aimy-overlay.chats-open .ov-chats-scrim { transition: opacity var(--t-slow) var(--ease-out); }
}
@media (prefers-reduced-motion: no-preference) {
  /* visibility is held for the whole close so the column cannot be tabbed into
     while it is sliding out, then flipped at 0s on the way in. That part was
     already right; what was not is that the panel left at exactly the speed it
     arrived. Out on --t-medium, in on --t-slow. */
  .overlay-chats { transition: transform var(--t-medium) var(--ease-out), visibility var(--t-medium) var(--ease-out); }
  .aimy-overlay.chats-open .overlay-chats { transition: transform var(--t-slow) var(--ease-out), visibility 0s; }
}

/* ══ THE MASTHEAD STAYS, AND THE CANVAS OPENS UNDER IT ═══════════════
   Two rules lived here and both are gone, because they only ever existed as
   a pair: `.aimy-overlay { top: 0 }` took the masthead's 60px of height for
   the canvas, and `body:has(.aimy-overlay.open) .app-topnav { display: none }`
   cleaned up after it — the canvas ground is translucent with a backdrop
   filter, so the bar it covered read straight THROUGH it and put two ×
   buttons and two account circles on the same 60px band.

   The second was never a fix for anything but the first. Dropping the first
   dissolves the collision instead of hiding it: the canvas keeps the base
   rule's `top: var(--topbar-height)` at every width, the bar sits above it
   rather than under it, and nothing is drawn on top of anything.

   MATCHED TO SALES, which never took the masthead in the first place. Its
   phone rule is `inset: calc(var(--topbar-height) + var(--call-strip)) 0 0`
   — the same offset plus a minimised call's strip, which this product has no
   equivalent of, so the base rule already IS that rule here.

   The height argument was sound and is simply outranked. Below 900 the
   masthead is the only place the product menu, the bell and the account pill
   exist, and the canvas is where people spend the day: a surface you are in
   all day should not be the one place the rest of the product stops. */

@media (prefers-reduced-motion: no-preference) {
  /* Opacity only. Sales tried a `translateX(-12px)` entrance twice and both
     times the column stranded at the offset, cropped by the overlay's bounds.
     A transform that can leave an element outside its own container has a
     failure mode; opacity has none. */
  .aimy-overlay.open .overlay-chats { animation: kChatsIn var(--t-base) var(--ease-out); }
  @keyframes kChatsIn { from { opacity: 0; } }

  /* THE COMPOSER RISES IN HERE TOO.
     The gate's composer has always arrived — `.gate-composer` in chat.css
     carries gate-rise, 0.75rem up over --t-emphasis after a --t-intent beat.
     The canvas's composer, which is the same three elements in the same
     order, simply appeared. Two ways of opening the same control, and the
     quieter one was on the surface people are in all day.

     Scoped to `.aimy-overlay` deliberately: the gate's composer sits in
     `.gate-stage`, outside this overlay entirely, so the gate keeps its own
     entrance and cannot be caught by this rule and animated twice.

     Its own keyframe rather than a reach for chat.css's — chat.css does not
     load on console.html, and a name defined in two stylesheets that both
     load on index.html is a collision waiting for the day the two stop
     agreeing. */
  .aimy-overlay.open .overlay-input-wrap {
    animation: kComposerRise var(--t-emphasis) var(--ease-out) var(--t-intent) both;
  }
  @keyframes kComposerRise {
    from { opacity: 0; transform: translate3d(0, 0.75rem, 0); }
    to   { opacity: 1; transform: none; }
  }
}

/* ═══════════════════════════════════════════════
   CANVAS THREAD — soften the scroll edge

   The thread's bottom sits flush against the input bar, so a message caught
   mid-scroll is cut by a hard horizontal edge. Next to the input pill's own
   top border that cut reads as a stray rule across the canvas rather than as
   content continuing below.

   A short mask fades the last 28px, which is the honest signal: there is more
   under there. The fade is inset from the bottom 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%);
}
/* At the end of the thread there is nothing to hint at, so drop the fade —
   otherwise the last line of the last answer looks perpetually unfinished. */
.overlay-thread.is-at-end {
  -webkit-mask-image: none;
          mask-image: none;
}

/* ══ THE CANVAS SCROLLS ON THE WINDOW, NOT ON THE COLUMN ══════════
   `.overlay-thread` is the scroller AND the measure — 45rem wide, centred by
   its flex parent — so its scrollbar was drawn against the right edge of the
   prose, a quarter of the surface in from the window with empty canvas either
   side of it. It read as a rule down the middle rather than as the edge of a
   scrolling region, and it had no relationship to the close button it should
   sit under.

   THE MEASURE MOVES TO THE PADDING AND NOTHING ELSE CHANGES. The obvious
   version — drop the max-width, centre the children with `align-items` —
   rewrites how every child of this thread is laid out, and the thread holds
   rows that set their own `align-self`. Padding leaves all of that alone: the
   element is full width, so the bar is at the window's edge, and the content
   box is the same column in the middle of it.

   42rem, not 45. The rule above is `max-width: 45rem` on a border-box element
   already carrying 1.5rem of side padding, so the measure the prose actually
   has is 42 — a calc written off 45 gives it 3rem more and moves every line.

   SCOPED TO THE CONSOLE. The gate draws the same thread inside `.gate-stage`,
   which is its own centred column holding the composer as well, and the same
   two lines there would put a full-width thread inside a 48rem box. chat.css
   does that half. */
.overlay-main .overlay-thread {
  max-width: none;
  padding-left: max(1.5rem, calc((100% - 42rem) / 2));
  padding-right: max(1.5rem, calc((100% - 42rem) / 2));
}

/* ══ THE BADGE AND THE BASIS BELONG TO THE TOP OF THE THREAD ═════════════
   Both float over the conversation. At the top that is what they are for —
   what this surface is, and what it answered from. Scrolled, they are two
   labels sitting on the reading, and the badge in particular was crossing the
   text of whatever passed under it.

   They leave on the first scroll and come back at the top. `visibility` rides
   with the opacity so a faded badge is not still in the tab order, delayed by
   the fade on the way out and flipped at 0s on the way in — the pattern the
   drawers in this file already use.

   `.overlay-close` and `.ov-chats-toggle` deliberately do NOT go with them:
   one is the way out of the canvas and the other the way to another
   conversation, and a control you need is not chrome you are done with. */
.aimy-overlay .overlay-badge,
.aimy-overlay .overlay-context-bar {
  transition: opacity var(--t-base) var(--ease-out), visibility 0s;
}
.aimy-overlay.is-scrolled .overlay-badge,
/* `.open.is-scrolled` for the basis bar, not `.is-scrolled` alone. Its reveal
   rule at :2494 is `.aimy-overlay.open .overlay-context-bar { opacity: 1 }` —
   the same (0,3,0) as this selector and 1200 lines further down, so it won on
   source order and the bar hid by `visibility` while its opacity stayed at 1:
   gone, but with no fade. Measured. Two classes outrank it wherever it sits,
   and the bar has nothing to say while the overlay is shut anyway. */
.aimy-overlay.open.is-scrolled .overlay-context-bar {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity var(--t-fast) var(--ease-out), visibility 0s linear var(--t-fast);
}
@media (prefers-reduced-motion: reduce) {
  .aimy-overlay .overlay-badge,
  .aimy-overlay .overlay-context-bar,
  .aimy-overlay.is-scrolled .overlay-badge,
  .aimy-overlay.is-scrolled .overlay-context-bar { transition: none; }
}

/* ═══════════════════════════════════════════════
   MODAL FOOTER — an action row, nothing else

   .modal-footer is `display: flex` with align-items defaulting to stretch, so
   its buttons grew to match the tallest child. With a 44px note panel in the
   row, .btn-sm rendered 56px instead of its designed 25px — the same class at
   two sizes in two modals, which reads as a rendering fault on the one surface
   that has to look trustworthy.

   Centred, and the footer now carries decisions only: the reversibility
   statement moved into the effect list where it belongs.
═══════════════════════════════════════════════ */
.modal-footer { align-items: center; }

/* ═══════════════════════════════════════════════
   POINTER TARGETS — WCAG 2.2 SC 2.5.8 (24×24 minimum)

   Three controls draw smaller than the minimum: .audit-revert at 52×16,
   .filter-chip at 22px tall, and the working-set checkbox at 18×18. The hit
   area is extended without changing how any of them looks — growing the
   visible control would break the density these surfaces depend on, and the
   success criterion is about the target, not the ink.
═══════════════════════════════════════════════ */
.audit-revert, .filter-chip { position: relative; }
.audit-revert::after, .filter-chip::after {
  content: ""; position: absolute;
  left: 0; right: 0; top: 50%; height: 1.5rem;
  transform: translateY(-50%);
}
/* The small-button variants land a pixel or two under once padding rounds.
   min-height rather than more padding, so the drawn size is unchanged. */
.btn-sm, .bcard-ack-btn, .wb-back, .ss-clear, .agg-more, .td-action, .cite-action,
.dv-notice-link, .mem-foot .btn, .aimy-toast-undo,
.v2-dropdown-btn, .dv-rel-item { min-height: 1.5rem; }

/* The text inputs draw 19-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. The input now fills its bar's height, and the bar
   forwards clicks to it. */
/* min-height only. align-self:stretch made a one-line field 36px tall against
   a 20px line-height — two lines' worth of box holding one line of text, which
   reads as a textarea waiting for a second line. The 24px minimum meets the
   pointer target on its own, and the bar forwards clicks for the rest. */
.aimy-float-input, .overlay-input { min-height: 1.5rem; }

/* ══ AND A WIDTH FLOOR IT DOES NOT DESERVE ════════════════════════════
   `min-width: 0`, which is the one declaration standing between the send
   button and the inside of its own bar on a phone.

   The library gives the field `flex: 1` and nothing else. A flex item's
   `min-width` defaults to `auto`, which for an <input> resolves to the
   control's INTRINSIC size — about 170px in Chrome, from the UA's default
   `size` — and `flex-shrink` is not allowed to go below it. So the field
   stopped shrinking at 170 while the bar kept narrowing, and the two items
   after it were pushed out of the end.

   Measured on console.html at a 360px viewport, bar 312px wide ending at 336:

     before   input 170 (r=249) · hint r=328 · send 338→374   38px outside
     after    input 123 (r=202) · hint r=281 · send 291→327   inside

   The send button — the bar's whole purpose — rendered past the right edge of
   the surface it belongs to, and `overflow` upstream hid it rather than
   scrolling to it, so nothing reported a defect.

   Both composers, because they are the same three-part bar: the float bar on
   the console and the canvas's own. Corrected here rather than in
   aimy-ds.css, which is a faithful copy of the shared library and stays one. */
.aimy-float-input, .overlay-input { min-width: 0; }

.aimy-float-bar, .overlay-input-bar { cursor: text; }

/* A staged prompt is AiMY-composed and waiting on you, so the bar says so
   rather than looking like text you typed and abandoned. Accent, because the
   send is the next step — matching the float bar’s own focus treatment. */
.overlay-input-bar.is-staged {
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 0.1875rem rgba(var(--accent-rgb), 0.12);
}
.aimy-float-bar > *:not(.aimy-float-input),
.overlay-input-bar > *:not(.overlay-input) { cursor: default; }

/* ═══════════════════════════════════════════════
   ACTIVE SCOPE — always visible once set

   .filter-tray sits at opacity 0 until the input takes focus, so scope-before-
   query (§7.2) was a core capability nobody could discover, and — worse — once
   set, scope was invisible while it silently narrowed every answer.

   An answer read without knowing its scope cannot be judged. So the moment
   scope exists it becomes permanent chrome via .afs, the library's own active-
   filter strip, and stays until cleared.
═══════════════════════════════════════════════ */
.k-scope-strip { margin-bottom: 0.75rem; }
.k-scope-strip .afs { width: 100%; }
.k-scope-strip .afs-chip { cursor: pointer; }

/* ═══════════════════════════════════════════════
   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.

   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 finishes answering. The
   scale has no press step; noted in ../GAPS.md.

   Scale is proportional to the surface. A 570px briefing card moving 3% is a
   lurch; a 32px button moving 3% is a press.
═══════════════════════════════════════════════ */
:root { --t-press: 90ms; }

@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, .dv-rel-item, .icon-btn,
  .topnav-bell, .ds-theme-toggle, .aimy-float-send, .overlay-send, .overlay-close,
  .agg-more, .ss-clear, .wb-back, .tab, .nav-item, .bcard-dismiss-reason {
    transition: transform var(--t-press) var(--ease-out),
                background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
  }
  .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, .dv-rel-item:active,
  .icon-btn:active, .topnav-bell:active, .ds-theme-toggle:active,
  .aimy-float-send:active, .overlay-send:active, .overlay-close:active,
  .agg-more:active, .ss-clear:active, .wb-back:active, .tab:active,
  .nav-item:active, .bcard-dismiss-reason:active { transform: scale(0.97); }

  /* Large surfaces move less — the same 3% reads as a lurch at card scale. */
  .rq-row, .src-row, .type-card, .list-row {
    transition: transform var(--t-press) var(--ease-out), background-color var(--t-fast) var(--ease-out);
  }
  .rq-row:active, .src-row:active, .type-card:active, .list-row:active { transform: scale(0.995); }
}

/* ═══════════════════════════════════════════════
   THE THINKING LABEL, CHANGING WHILE THE STATE HOLDS

   The mark keeps turning; the words under it resolve into the next thing that
   is true. Out is quick and up, in is slower and from below - the same
   asymmetry the token block argues for on opens and closes, for the same
   reason: what you are leaving should get out of the way, what is arriving is
   the part worth watching land.

   The blur is affordable here and almost nowhere else on this surface: it is
   ONE short line of text, three times per answer. A blur is a repaint per
   frame, and the rule for this codebase is that it never lands on a list.
═══════════════════════════════════════════════ */
.ai-thinking-label { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .ai-thinking-label.is-out {
    animation: think-label-out var(--t-fast) var(--ease-out) forwards;
  }
  .ai-thinking-label.is-in {
    animation: think-label-in var(--t-base) var(--ease-out) backwards;
  }
}
@keyframes think-label-out {
  to { opacity: 0; transform: translateY(-0.25rem); filter: blur(var(--blur-small)); }
}
@keyframes think-label-in {
  from { opacity: 0; transform: translateY(0.25rem); filter: blur(var(--blur-small)); }
}

/* ═══════════════════════════════════════════════
   MOVING BETWEEN PLACES

   The stage is replaced by writing innerHTML, so every navigation is a
   teardown and the eye reads a teardown as a blink. A view transition
   photographs the old state, lets the write happen, and dissolves between the
   two - which is the only way to get continuity out of this architecture
   without rewriting it into a diffing one.

   Only a PLACE gets this. Typing into a filter narrows the set you are already
   looking at, 260ms of debounce at a time, and dissolving the screen on each
   of those is the strobe rather than the cure - render() decides, and passes
   `quiet` for anything that is not a move. See AIMY_MOTION.swap.

   THE CHROME IS NOT IN IT. Naming the stage lifts it out of the root snapshot,
   so the topnav, the rail and the composer are never captured and never
   cross-faded against themselves - they simply stay put while the thing that
   actually changed is the thing that moves. The root keeps a short dissolve
   for the small diffs that ride along with a move: the chip bar emptying, the
   brief re-ordering.
═══════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  #wbStage { view-transition-name: wb-stage; }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: var(--t-fast);
    animation-timing-function: var(--ease-out);
  }

  /* Asymmetric on purpose, and the reason is the same one the token block
     gives for opens and closes: the state you are LEAVING should get out of
     the way quickly, and the state you are ARRIVING at is the one worth
     watching land. The outgoing stage is gone in 150ms; the incoming one takes
     250 and resolves out of a blur, which is the one place in a navigation
     where a blur is affordable - it is a single element, once. */
  ::view-transition-old(wb-stage) {
    animation: wb-stage-out var(--t-fast) var(--ease-out) both;
  }
  ::view-transition-new(wb-stage) {
    animation: wb-stage-in var(--t-base) var(--ease-out) both;
  }
  @keyframes wb-stage-out {
    to { opacity: 0; transform: scale(0.994); }
  }
  @keyframes wb-stage-in {
    from { opacity: 0; transform: scale(1.006); filter: blur(var(--blur-medium)); }
  }
}

/* ═══════════════════════════════════════════════
   ENTRY STAGGER

   The briefing arrives as one block of nine, 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) {
  /* Batch entry — a list that renders all at once. Index set in JS. */
  .k-stagger > * {
    opacity: 0;
    transform: translateY(6px);
    animation: kEnter var(--t-base) var(--ease-out) forwards;
    animation-delay: calc(min(var(--i, 0), 8) * var(--t-stagger));
  }
  /* Single entry — a block replacing its own skeleton. No delay: the
     resolution order already supplies the cascade, and adding a second one on
     top would double-count it. */
  .k-enter {
    opacity: 0; transform: translateY(6px);
    animation: kEnter var(--t-base) var(--ease-out) forwards,
               kEnterBlur var(--t-base) var(--ease-out);
  }
  /* 2px: below the threshold where anyone would name it, above the one where
     a row arrives with hard edges while it is still moving.

     TWO animations, because the blur is the one property that must NOT be
     held at the end. `forwards` freezes whatever the effect computed on its
     last frame, and a filter animated toward `none` does not land on `none` —
     it lands on `blur(0px)`. Measured, twice: naming the blur only in `from`
     did not help either, because the fill still covers the property. And
     `blur(0px)` is not free. It keeps the element on its own composited layer
     and its text on grayscale antialiasing for as long as the element lives,
     which for a menu or a panel is the rest of the session.

     So the blur runs as its own animation with NO fill mode. When it ends the
     property is simply not animated any more and the element goes back to the
     rule, which never mentioned a filter. opacity and transform keep their
     `forwards` — those two DO need holding, and they stay declared on the
     rule as start states, which is what makes a surface that never animates
     a surface that is simply there. */
  @keyframes kEnter { to { opacity: 1; transform: translateY(0); } }
  @keyframes kEnterBlur { from { filter: blur(var(--blur-small)); } }
}

/* ═══════════════════════════════════════════════
   SHELL — topnav → sidebar → scrollable main
═══════════════════════════════════════════════ */
body {
  /* 100vh is the viewport with the browser's toolbars RETRACTED, so on a
     tablet or phone showing its chrome the body is taller than the glass and
     everything anchored to its bottom — the input band, first of all — sits
     underneath the browser UI. That is the "the chat input is not visible"
     report, and it is not a width problem at all.
     100dvh is the live height. The 100vh line stays as the fallback. */
  height: 100vh;
  height: 100dvh;
  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;
}
/* Matched to AiMY/QA/my-profile.html. The design system's --grad-ellipse-*
   carry these exact colour stops but centre them at 30% 50% / 75% 70%, which
   is tuned for the documentation page's narrow column. QA re-centres both to
   `at center` and positions the pseudo-element instead. Same override here,
   so the two products sit on one ambient wash. */
:root {
  /* QA's dark body is #171a1c; the design system's token is #0f1215. Matching
     QA here, since the ellipse alphas above are calibrated against it — 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. QA's values. */
: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: 81.25rem; height: 65.625rem;
  background: var(--grad-ellipse-1);
}
#appEllipse::after {
  content: ""; position: absolute;
  bottom: -8%; right: -4%;
  width: 46.875rem; height: 40.625rem;
  background: var(--grad-ellipse-2);
}

/* ═══════════════════════════════════════════════
   TOPNAV — ported verbatim from AiMY/QA

   Values copied exactly from QA/data-ingestion.html so the two agents share
   one masthead. QA's own 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 QA is the point.

   Three deliberate departures, each stated rather than smuggled:

     · The calls button is removed, as asked.
     · 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.
     · The strip is centred on the window, not on the space left between the
       logo and the user pill — carried over from the earlier centring fix.
═══════════════════════════════════════════════ */
.app-topnav {
  height: var(--topbar-height);
  background: transparent;
  border-bottom: 0.0625rem solid rgba(255, 255, 255, 0.07);
  display: flex;
  align-items: center;
  padding: 0 1.25rem 0 1.5rem;
  gap: 0;
  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: 0.5rem;
  flex-shrink: 0;
  padding-right: 1.25rem;
  margin-right: 0.25rem;
}
.topnav-logo-mark { display: flex; align-items: center; flex-shrink: 0; }
.topnav-logo svg { display: block; height: 2rem; width: auto; }

.topnav-tabs {
  display: flex;
  align-items: center;
  gap: 0;
  flex: 1;
  justify-content: center;
}
.topnav-tabs-inner {
  display: flex;
  align-items: center;
  border: 0.046875rem solid #5b6771;
  border-radius: 1.5rem;
  padding: 0.25rem;
  gap: 0.125rem;
}
/* Some entries are <button> (agents this prototype does not contain) and the
   QA entry is a real <a> to the deployed product, so the class has to render
   identically on both. */
.topnav-tab {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  text-decoration: none;
  padding: 0.375rem 1rem;
  border-radius: 1.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--d400);
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
              transform var(--t-press) var(--ease-out);
  border: 0.0625rem solid transparent;
  white-space: nowrap;
  background: none;
  font-family: inherit;
}
/* The tab for the page you are ALREADY on is a <span>, not a button: a
   control that does nothing is worse than no control. Without these two rules
   it would keep the pointer cursor and the hover wash and go on looking like
   somewhere to go. */
span.topnav-tab { cursor: default; }
span.topnav-tab:hover { background: none; }
.topnav-tab:hover { background: rgba(255, 255, 255, 0.05); color: var(--d200); }
.topnav-tab.active { color: var(--ink-action); }
.topnav-tab svg { width: 0.8125rem; height: 0.875rem; flex-shrink: 0; }
.topnav-tab:active { transform: scale(0.97); }
.topnav-tab:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
: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); }

.topnav-right {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-shrink: 0;
}

.topnav-bell {
  width: 2.125rem;
  height: 2.125rem;
  border-radius: 0.5rem;
  background: rgba(255, 255, 255, 0.04);
  border: 0.0625rem solid rgba(255, 255, 255, 0.08);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), transform var(--t-press) var(--ease-out);
  color: var(--d400);
  position: relative;
}
.topnav-bell:hover { background: rgba(255, 255, 255, 0.08); color: var(--d200); }
.topnav-bell svg { width: 1rem; height: 1rem; stroke: currentColor; fill: none; stroke-width: 2; }
.bell-dot {
  position: absolute;
  top: 0.375rem;
  right: 0.375rem;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--err);
  border: 0.09375rem solid var(--body-bg);
}

.topnav-user {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.75rem 0.25rem 0.25rem;
  border-radius: 1.25rem;
  background: rgba(255, 255, 255, 0.04);
  border: 0.0625rem solid rgba(255, 255, 255, 0.08);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out), 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: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: linear-gradient(135deg, #7c3aed, #3369ff);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.625rem;
  font-weight: 700;
  flex-shrink: 0;
}
.topnav-user-info { display: flex; flex-direction: column; gap: 0.125rem; }
.topnav-user-name { font-size: 0.75rem; font-weight: 700; color: var(--d50); line-height: 1; }
.topnav-user-role { font-size: 0.625rem; color: var(--d400); line-height: 1; }
.topnav-user svg { width: 0.75rem; height: 0.75rem; color: var(--d400); }
:root[data-theme="light"] .topnav-bell,
:root[data-theme="light"] .topnav-user {
  background: rgba(16, 24, 40, 0.04); border-color: rgba(16, 24, 40, 0.10);
}

/* ═══════════════════════════════════════════════
   THE ACCOUNT MENU

   The pill has always drawn a chevron. Nothing was wired to it, so the chevron
   named a menu that did not exist, and `role="button" tabindex="0"` made the
   promise twice — focusable, announced as a button, inert on activation.

   Anchored exactly like the bell: a zero-width `position: relative` wrapper, so
   the panel hangs off the pill without becoming a flex item. The right cluster
   collides at 1114px and the topnav ladder above is arithmetic on measured
   widths; a new sibling here would have invalidated it.

   The panel is the library's .menu, but it is shown by [hidden] rather than by
   .menu-anchor.open. Two reasons. The design system's global closer removes
   .open from any .menu-anchor and knows nothing about the button that owns it,
   so aria-expanded would silently drift; and the bell next door already uses
   [hidden], so this is one dropdown language on this page rather than two.
   Overriding .menu's opacity/visibility needs (0,2,0) because the local
   visibility fix further down this file is (0,1,0) and lands later.
   ═══════════════════════════════════════════════ */
.user-anchor { position: relative; display: flex; align-items: center; flex-shrink: 0; }

/* The pill is a <button> now, and the UA gives buttons `text-align: center`.
   Its name and role are stretched flex items in a column, so the shorter line
   would have centred itself inside the wider one's box — a real change at the
   1536 reference, which is off-limits. The global button reset restores
   background, border, padding, font and colour, but not this one. */
.topnav-user { text-align: left; }
.topnav-user:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

.user-anchor .user-menu {
  left: auto; right: 0;
  min-width: 13.75rem;
  z-index: 940;
  opacity: 1; visibility: visible; transform: none; pointer-events: auto;
}
.user-anchor .user-menu[hidden] { display: none; }
.user-menu .menu-item { text-decoration: none; }

/* Equal flanks so the strip centres on the WINDOW, not on the space left
   between the logo and the user pill. Overrides QA's `flex: 1` on .topnav-tabs
   for the same reason as before: QA's version leans right by half the sidebar.

   The logo's padding-right: 20px and margin-right: 4px exist in QA to separate
   it from the CALLS BUTTON. With that button removed they are vestigial, and
   they unbalance the flanks by 28px — pushing the strip 14px right. Dropping
   them follows from removing the button rather than departing from QA. */
.topnav-logo { padding-right: 0; margin-right: 0; }
.topnav-logo, .topnav-right { flex: 1 1 0; min-width: 0; }
.topnav-tabs { flex: 0 1 auto; min-width: 0; }
.topnav-right { justify-content: flex-end; }
.topnav-right > * { flex-shrink: 0; }

/* THE TOPNAV LADDER -- measured, not estimated.

   The strip is centred on the WINDOW, which means both flanks are `flex: 1 1 0`
   and the strip is squeezed against whichever flank is wider. So the width it
   needs is not the sum of its parts, it is

       strip + 2 x (widest flank) + padding

   Measured live, with the strip at 474.6 and the topnav's padding at 44:

     right cluster        width    collides below
     everything shown     297.4        1114
     name dropped         223.9         967
     name and V1 dropped  158.0         835

   So the two rungs that were already here are correct, and this pass leaves
   them where they are. (An earlier draft moved the first one to 1110 on a
   strip width of 471.9 — a figure measured before the webfont had swapped in.
   1110 would have left a four-pixel window where the pill and the user cluster
   overlap. The number below is the one that survives measurement.)

   What was missing is the bottom of the ladder. Below 835 nothing was left to
   drop, and at 768 the three regions sat at EXACTLY 0px of gap, crushed
   against each other, with no rule to catch it:

     1120   drop the user's name      clears to  967
     1000   drop V1                   clears to  835
      900   stop centring; the strip takes what is left and scrolls.

   Nothing is deleted at the bottom of the ladder. Six products stay reachable
   on a tablet, which is the difference between adapting and dropping. */
@media (max-width: 1120px) {
  .topnav-user-info { display: none; }
  .topnav-user { padding: 0.25rem; }
}

.topnav-bell {
  width: 2.125rem; height: 2.125rem; border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 0.0625rem 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) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.topnav-bell:hover { background: rgba(255,255,255,0.08); color: var(--d200); }
.topnav-bell svg { width: 1rem; height: 1rem; fill: none; stroke: currentColor; stroke-width: 2; }
.bell-dot {
  position: absolute; top: 0.375rem; right: 0.375rem;
  width: 0.375rem; height: 0.375rem; border-radius: 50%;
  background: var(--err); border: 0.09375rem solid var(--body-bg);
}
:root[data-theme="light"] .topnav-bell {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}
:root[data-theme="light"] .topnav-bell:hover { background: rgba(16,24,40,0.07); }
:root[data-theme="light"] .user-pill {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}

/* ── 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.

   Same component as QA's, on this file's tokens — QA's --wash-6 is what
   --glass-border already is here, and the focus ring follows .topnav-tab. */
.ver-link {
  display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0;
  padding: 0.25rem 0.75rem; border-radius: var(--r-pill);
  font-family: var(--font-sans);
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); line-height: var(--lh-snug);
  color: var(--ink-faint); text-decoration: none;
  border: 0.0625rem solid var(--glass-border);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.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: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.ver-link-ico { width: 0.625rem; height: 0.625rem; opacity: 0.75; }
/* The flanks split what the six tabs leave, and the right one runs out first:
   below this the pill crosses the tabs strip rather than sitting beside it.
   V1 is the only thing in the topnav that goes somewhere else rather than
   carrying state, so it is the one that drops — the same call the user's name
   already makes 120px earlier. */
@media (max-width: 1000px) {
  .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. It is the same Lucide glyph the
   pill draws, written the same way — one chevron in this masthead, not two
   drawings of one.

   Quiet: no ground and no border, since it hangs off the brand mark and a
   boxed control there would read as a second logo.

   Ported from AiMY Sales' .prod-chev, which solved this first.
   ═══════════════════════════════════════════════ */

/* `display: none` ON THE ANCHOR, NOT ONLY ON THE CHEVRON, AND THAT IS THE
   WHOLE REFERENCE-WIDTH ARGUMENT.

   Sales hides the chevron and leaves its wrapper standing, which is free
   there because that masthead's logo region has no gap. This one is
   `.topnav-logo { gap: 0.5rem }` (:1557), so a wrapper that still generates a
   box is a flex item, and a flex item is 8px of gap — at 1536, where the
   rendering is off-limits. Measured with the anchor painted at base: the mark
   moved and every offset downstream of it moved with it.

   So the anchor itself goes, and comes back only in the query below.

   AND THIS WHOLE SECTION SITS ABOVE THAT QUERY, WHICH IS LOAD-BEARING. This
   rule and the `display: flex` that answers it are both (0,1,0), so whichever
   comes later in the file wins outright. Written underneath the query — which
   is where it started — the anchor stayed `display: none` at every width and
   the chevron never appeared at all; measured at 900, 768, 640, 480 and 400,
   on both shells. `.rail-scrim` and `.ov-chats-scrim` sit above their own
   queries for exactly this reason. Do not move it back down. */
/* NOT `position: relative`, and that is the whole of the rule under it. The
   library's `.menu` is `position: absolute; left: 0`, so it hangs off the
   nearest positioned ancestor — make that the anchor and the panel starts at
   the CHEVRON's left edge. Measured: 111.2px in on the gate, 161.2 on the
   console, a 200px card floating in the middle of a 400px masthead with the
   mark it belongs to nowhere near it. Left static, the nearest positioned
   ancestor is `.topnav-logo`, which is the edge it should share. */
.prod-anchor { display: none; position: static; align-items: center; flex-shrink: 0; }

.prod-chev {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; flex-shrink: 0;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--d400);
  transition: color var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
/* AGAINST `.topnav-logo svg { height: 2rem }` (:1472), which is a DESCENDANT
   selector and therefore reaches everything this anchor puts inside the logo
   region — the chevron and every mark in the menu under it. Left alone it
   drew a 32px chevron beside a 28px wordmark, and rendered the AiMY glyph on
   each menu row at four times the size the strip draws it. Both sizes are
   restated here rather than the library rule being narrowed, because that rule
   is what sizes the wordmark itself. */
.prod-chev svg { width: 0.875rem; height: 0.875rem; }
.prod-chev:hover { color: var(--d100); }
.prod-chev:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* Lit while its menu is open, which is what the account pill's own chevron
   does — one rotation language in this bar. */
.prod-anchor:has(.prod-menu:not([hidden])) .prod-chev { color: var(--d50); transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .prod-anchor:has(.prod-menu:not([hidden])) .prod-chev { transform: none; }
}

/* THE MENU HANGS FROM THE MARK, NOT FROM THE CHEVRON'S OWN 24px BOX.
   It is the AiMY mark that owns this list, and `left: 0` on the anchor is the
   chevron's left edge — 150px in from the masthead's own edge, which on a
   400px screen is a 220px card with nowhere to go but back over itself.
   Anchored on `.topnav-logo` instead, so the menu shares the mark's left edge.

   Shown by [hidden] rather than by `.menu-anchor.open`, which is the reason
   `.user-anchor .user-menu` gives at :1717 and holds here for the same two
   causes: the design system's global closer knows nothing about the button
   that owns the panel, so aria-expanded would drift, and this page already
   speaks [hidden] at the bell and the pill. (0,2,0) to outrank the local
   `.menu` visibility fix at :5531. */
.topnav-logo:has(.prod-anchor) { position: relative; }
.prod-anchor .prod-menu {
  left: 0; right: auto;
  min-width: 12.5rem;
  z-index: 940;
  opacity: 1; visibility: visible; transform: none; pointer-events: auto;
}
.prod-anchor .prod-menu[hidden] { display: none; }
/* AND NO UNDERLINES. Three of these five entries are real <a>s and two are
   not, so the user agent's anchor underline drew a line under exactly three
   rows of a list where every row is the same kind of thing. The strip has the
   same mix and underlines none of them. */
.prod-menu .menu-item { text-decoration: none; gap: 0.5rem; }
.prod-menu .menu-item svg { width: 0.8125rem; height: 0.875rem; flex-shrink: 0; }
/* THE ONE YOU ARE ON IS MARKED THE WAY THE STRIP MARKS IT — `.topnav-tab.active`
   is one declaration (:1611) and this is that same one. Not the menu's own
   hover 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. */
.prod-menu .menu-item.is-on,
.prod-menu .menu-item.is-on:hover { background: none; color: var(--ink-action); }
/* The ones with nowhere to go are quiet rather than struck through: a product
   that is not built yet is not an error. */
.prod-menu .menu-item[disabled]:not(.is-on) { color: var(--ink-quiet); cursor: default; }
.prod-menu .menu-item[disabled]:hover { background: none; }

/* ══ WHY THE MENU IS NOT KEYED TO THE STRIP'S OWN LIMIT ════════════
   The strip's limit was measured, and it is much lower than the rung this
   switches on at. Fonts settled, `scrollWidth - clientWidth` on
   `.topnav-tabs-inner` walking the width down:

       900   0        whole, 270px of clearance to the right cluster
       660   0        whole, 30.8px of clearance
       640   0        whole, 10.8px of clearance
       630   7        clipped
       400   237      119px of window onto 356px of tabs

   So the five tabs fit to 637, and an earlier draft of this fired at 640 for
   that reason — Sales' own rule is that the strip moves when it stops fitting,
   and Sales measured 720 for a SIX-tab strip 474.6px wide, which is 80px more
   than this one needs.

   IT IS KEYED TO THE SHELL INSTEAD, at Nour's call: 900 is where the rail and
   the canvas column both become drawers, so the products leave the bar at the
   same moment rather than 260px later. The cost is stated plainly — between
   900 and 640 five tabs that would still have fitted are behind a chevron —
   and the gain is one narrow mode instead of three shapes.

   IT MOVES, IT DOES NOT GO. The products hang from the mark as a menu, read
   back off this same strip so there is one of them. A row of its own was the
   obvious alternative and costs 44px of every phone screen for five links that
   are a chevron away; this costs 24.

   --topbar-height IS UNTOUCHED, and that is deliberate: the canvas, the rail
   drawer, its scrim and the notifications panel are all `position: fixed`
   against it, and a masthead that grew would have moved all four. */

/* ══ THE LAST RUNG, AND THE STRIP IS NOT ON IT ═════════════════════
   Below this a centred strip cannot be centred against anything, so it comes
   off the masthead and hangs from the mark as a menu instead — one chevron,
   the list behind it, the one you are on marked. The anatomy is at §THE
   CHEVRON ON THE MARK below; this is where it switches on.

   WHAT USED TO BE HERE was a strip that stopped centring, took the width the
   flanks left and scrolled inside its own pill behind a fade. That is gone
   with the rules that drew it — `overflow-x`, the scroll snapping and the mask
   are dead the moment the strip is `display: none`, and leaving them would be
   two answers to one question with only one of them reachable.

   900 IS THE SHELL'S OWN NARROW RUNG, not the strip's. On the measurement the
   strip alone is whole down to 637 — it is 356.4px wide here, five tabs, and
   the flanks clear it by 270px at this width. So this is not the strip running
   out of room; it is the width at which everything else in the shell changes
   state, and the products move with them:

       1040   the briefing rail becomes a drawer
        900   the canvas's conversation column becomes a drawer
        900   the products leave the bar for the mark   <- this

   One narrow mode, entered once. The alternative was a masthead that kept a
   left-aligned scrolling strip for another 260px after the two panels beside
   it had already become drawers, which is three shapes where the shell has
   two. Sales reaches the same arrangement at its own equivalent rung. */
@media (max-width: 900px) {
  .app-topnav { padding: 0 0.75rem; gap: 0.5rem; }
  .topnav-logo, .topnav-right { flex: 0 0 auto; }
  .topnav-logo svg { height: 1.75rem; }

  .topnav-tabs { display: none; }
  .prod-anchor { display: flex; }
  /* The strip was the middle of three regions and the logo's trailing space
     existed to separate it. With the strip gone that space is a gap before
     nothing. */
  .topnav-logo { padding-right: 0; margin-right: 0; }

  /* ══ AND SOMETHING HAS TO HOLD THE RIGHT EDGE ══════════════════════
     THE STRIP WAS DOING IT. It carried `flex: 1 1 auto` while both flanks are
     `0 0 auto`, so the strip — not the cluster — is what absorbed the leftover
     width and pushed the bell and the account pill out to the right edge.
     Taking it out of the row leaves no flexible item at all, and both flanks,
     being `0 0 auto`, pack against the start: measured, the theme control, the
     bell and the pill ended up against the chevron with the whole right half
     of the masthead empty behind them.

     `margin-left: auto` is that job done by the item that actually wants the
     edge, and it cannot be removed by a later breakpoint the way the strip
     was. Sales reaches the same layout by keeping one of its two
     `.topnav-spacer` divs; this masthead has no spacers, so the free space is
     claimed rather than occupied. */
  .topnav-right { margin-left: auto; }
}

/* ══ THE BOTTOM RUNG, AND ONLY THE CONSOLE REACHES IT ══════════════
   The masthead's parts do not shrink, so below some width they simply do not
   fit. Added up on the Console, which is the wider of the two shells because
   it alone carries the rail toggle:

       12  padding
      173  .topnav-logo   = toggle 34 + mark 91 + chevron 24 + two 8px gaps
        8  gap
      150  .topnav-right  = theme 34 + bell 34 + account 58 + two gaps
       12  padding
      ---
      355

   Measured against that: 0 over at 360, 3 at 340, 23 at 320 — the account
   pill's right edge simply leaves the screen. The gate needs 305 for want of
   the toggle, so it clears 320 on its own and nothing here changes it.

   TWO THINGS GIVE, AND NEITHER IS A FUNCTION. The masthead's own side padding
   drops from 12 to 8, and the account pill loses its CHEVRON — not its avatar,
   which is the target, and not the theme control or the bell, which do things.
   The chevron is the pill saying "this opens"; at 320 the room it costs is
   worth more than the promise it makes, and it is the same call the ladder
   already made 800px earlier when the user's NAME went. Measured after: 0 over
   at 360, 340 and 320.

   354.98, because 355 is the width it FITS at — the rule has to start one
   pixel below that, and the .98 clears a fractional viewport for the reason
   written out at the search's query. 320 is the floor this targets; below it
   the masthead is still 15px over and no current device is that narrow. */
@media (max-width: 354.98px) {
  .app-topnav { padding-left: 0.5rem; padding-right: 0.5rem; }
  .topnav-logo { gap: 0.375rem; }
  .topnav-user > svg { display: none; }
}

/* ── The notifications panel ───────────────────────────────────────
   The bell was an inert <div> carrying a dot nothing could clear. What it
   opens is the queue of 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 and none of them goes stale when a draft is published.

   Panel chrome follows .peek rather than the QA original's inlined literals:
   the radius, the shadow and the wash QA had to hard-code because six files
   disagreed about them are all declared here, and a second dropdown language
   on one page is one too many. */
.ntf-anchor { position: relative; display: flex; align-items: center; flex-shrink: 0; }
.topnav-bell:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.bell-dot[hidden] { display: none; }

.ntf-panel {
  position: absolute; top: calc(100% + 0.5rem); right: 0; z-index: 940;
  width: 22.5rem; max-width: calc(100dvw - 2rem);
  max-height: min(70vh, 32.5rem); overflow-y: auto;
  padding: 0.375rem; text-align: left;
  background: var(--panel-bg); border: 0.0625rem solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(0.75rem);
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent;
}
.ntf-panel[hidden] { display: none; }
.ntf-panel::-webkit-scrollbar { width: 0.25rem; }
.ntf-panel::-webkit-scrollbar-track { background: transparent; }
.ntf-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 0.125rem; }
: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); }

/* ═══════════════════════════════════════════════
   THE MASTHEAD PANELS ARRIVE AND LEAVE

   The notifications panel snapped on and snapped off. Everything else on this
   surface that opens over something else moves while it does it — the rail
   drawer, the canvas column, the peek, the canvas's own composer — and this
   was the one dropdown that did not.

   THE VALUES ARE THE LIBRARY'S, NOT NEW ONES. aimy-ds.css:2229 already states
   the house idiom for a menu: `opacity: 0` and `translateY(-4px)` closed,
   `opacity: 1` and `translateY(0)` open, and — in its own words — "a
   transition always belongs to the state being moved TO, so writing them on
   the two rules is what makes a dropdown leave faster than it arrives". So in
   on --t-base, out on --t-fast. The drawers in this file already lean the same
   way (out on --t-medium, in on --t-slow); this is that asymmetry at a
   dropdown's scale.

   ALL THREE PANELS, NOT JUST THE BELL. The bell, the account menu and the
   product menu are the same component shown the same way — by `[hidden]`, on
   the same 60px row, within eight pixels of each other. `.user-anchor` states
   the rule this follows in as many words: a second dropdown language in one
   topnav is one too many. Animating one and leaving its two neighbours to snap
   is exactly that second language. One rule, three selectors.

   WHY THIS NEEDS `allow-discrete`, WHICH IS THE WHOLE TRICK. These panels are
   hidden with the `hidden` ATTRIBUTE, which is `display: none`, and you cannot
   transition out of `display: none` — the element is simply not rendered, so
   there is no frame to animate and the close would still snap however long the
   duration said. `transition-behavior: allow-discrete` on `display` holds the
   panel displayed for the duration and flips it at the end, and
   `@starting-style` supplies the before-change value on the way in, which
   otherwise does not exist for an element that was not being rendered a frame
   ago. No JS changes: `bell.show()` and `bell.close()` still set one property.

   A browser without either feature ignores both and gets today's behaviour —
   the panel appears and disappears at once, which is what it does now. */
.ntf-panel,
.user-anchor .user-menu,
.prod-anchor .prod-menu {
  /* `none`, not `translateY(0)`: the open state has to stay byte-identical to
     what it renders today, and `none` is what `.user-anchor .user-menu`
     already declares. It still interpolates — a `none` transform is the
     identity, so the engine tweens it against the closed offset. */
  opacity: 1; transform: none;
  transition: opacity var(--t-base) var(--ease-out),
              transform var(--t-base) var(--ease-out),
              display var(--t-base) allow-discrete;
}
/* The closed state carries the LEAVE. `display: none` is restated here because
   these three selectors have to match the existing `[hidden]` rules exactly —
   at (0,2,0) and (0,3,0) they are not reachable by a weaker one, and this
   block sits after them so equal specificity resolves on order. */
.ntf-panel[hidden],
.user-anchor .user-menu[hidden],
.prod-anchor .prod-menu[hidden] {
  display: none;
  opacity: 0; transform: translateY(-0.25rem);
  /* For the 150ms it is fading out the panel is still `display: block`, which
     is what makes the fade possible and also leaves it hittable. The library's
     own closed `.menu` carries this for the same reason; without it a click
     aimed at whatever the panel was covering lands on a panel that is visually
     gone. The open rules restore `pointer-events: auto` where they need it. */
  pointer-events: none;
  transition: opacity var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out),
              display var(--t-fast) allow-discrete;
}
@starting-style {
  .ntf-panel:not([hidden]),
  .user-anchor .user-menu:not([hidden]),
  .prod-anchor .prod-menu:not([hidden]) {
    opacity: 0; transform: translateY(-0.25rem);
  }
}
/* A panel that never animates is a panel that is simply there — this file's
   own pattern for reduced motion, stated at .k-enter and at the drawers. The
   discrete `display` step is kept, because without it the close would leave a
   panel that has faded to nothing still occupying the tab order. */
@media (prefers-reduced-motion: reduce) {
  .ntf-panel,
  .user-anchor .user-menu,
  .prod-anchor .prod-menu,
  .ntf-panel[hidden],
  .user-anchor .user-menu[hidden],
  .prod-anchor .prod-menu[hidden] {
    transform: none;
    transition: none;
  }
}

.ntf-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem 0.75rem; }
.ntf-head b {
  font-family: var(--font-display); font-size: var(--ty-meta);
  font-weight: var(--fw-bold); color: var(--d50);
}
.ntf-count {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  padding: 0.125rem 0.5rem; border-radius: var(--r-pill);
  background: var(--accent-dim); color: var(--ink-action);
  font-variant-numeric: tabular-nums;
}
.ntf-count[hidden] { display: none; }
.ntf-clear {
  margin-left: auto; background: none; border: 0; padding: 0.125rem 0.25rem; cursor: pointer;
  font-family: inherit; font-size: var(--ty-micro); font-weight: var(--fw-semibold);
  color: var(--ink-faint); border-radius: var(--r-xs);
  transition: color var(--t-fast) var(--ease-out);
}
.ntf-clear:hover { color: var(--d100); }
.ntf-clear:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }

.ntf-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.125rem; }
.ntf-row {
  display: flex; gap: 0.5rem; padding: 0.75rem; border-radius: var(--r-md);
  transition: background-color var(--t-fast) var(--ease-out), opacity var(--t-fast) var(--ease-out);
}
.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; }
/* The bell's rows lost their dots with the band's, and for the same reason:
   needsYou() returns them worst-first, so the order already says it. */
.ntf-row-main { flex: 1; min-width: 0; }
.ntf-row-head { display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.25rem; }
.ntf-row-type { font-size: var(--ty-meta); font-weight: var(--fw-bold); color: var(--d50); }
.ntf-row-when { margin-left: auto; font-size: var(--ty-micro); color: var(--ink-faint); flex-shrink: 0; }
.ntf-row-body { margin: 0 0 0.5rem; font-size: var(--ty-meta); line-height: var(--lh-base); color: var(--ink-secondary); }
.ntf-row-cta {
  font-family: inherit; font-size: var(--ty-micro); font-weight: var(--fw-bold); cursor: pointer;
  padding: 0.25rem 0.75rem; border-radius: var(--r-pill);
  background: var(--accent-dim); color: var(--ink-action);
  border: 0.0625rem solid rgba(var(--accent-rgb), 0.26);
  transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.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: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

.ntf-foot { padding: 0.5rem 0.25rem 0.25rem; margin-top: 0.375rem; border-top: 0.0625rem solid var(--hairline); }
.ntf-foot-btn {
  width: 100%; padding: 0.5rem; cursor: pointer; font-family: inherit;
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
  border-radius: var(--r-md); color: var(--ink-muted);
  background: rgba(255,255,255,0.04); border: 0.0625rem solid var(--glass-border);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.ntf-foot-btn:hover { background: rgba(255,255,255,0.08); color: var(--d50); }
.ntf-foot-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
: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.
   It was unreachable while the dot was written into the markup. */
.ntf-empty { padding: 1.5rem 0.75rem; text-align: center; font-size: var(--ty-meta); color: var(--ink-quiet); }

/* 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 the width where the anchor still leaves room, pin the panel to the
   window instead. */
@media (max-width: 520px) {
  .ntf-panel {
    position: fixed;
    top: calc(var(--topbar-height) + 0.25rem);
    right: 0.75rem; left: 0.75rem;
    width: auto; max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ntf-row, .ntf-row-cta, .ntf-foot-btn, .ntf-clear { transition: none; }
}

/* Theme toggle — moved here from the design system's documentation chrome */
.ds-theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.125rem; height: 2.125rem; border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 0.0625rem solid rgba(255,255,255,0.08);
  color: var(--ink-muted); cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.ds-theme-toggle:hover { border-color: var(--card-border-hover); color: var(--d50); }
.ds-theme-toggle svg { width: 1rem; height: 1rem; display: block; }
.ds-theme-toggle .icon-moon { display: none; }
.ds-theme-toggle .icon-sun  { display: block; }
:root[data-theme="light"] .ds-theme-toggle {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}
:root[data-theme="light"] .ds-theme-toggle .icon-sun  { display: none; }
:root[data-theme="light"] .ds-theme-toggle .icon-moon { display: block; }

.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 and .nav-item { padding: 10px 14px;
   gap: 11px }. QA overrides all three, and QA's proportions are the ones we
   want: a 220px rail with roomier nav items reads as better-managed space
   than a wider rail holding tighter rows. Overridden here rather than in
   aimy-ds.css so the extracted stylesheet stays a faithful copy of the
   library and this divergence stays visible.

   It drives the rail and nothing else now: the canvas is fixed to the window
   and the float bar is centred on it, so neither reads this token — see THE
   CANVAS TAKES THE WINDOW. */
/* The library's default is 240 and this product had overridden it DOWN. The
   briefing carries 183px of content in 188px of usable width once its 16px
   gutters are taken, and two of its runs already overflowed. It also now holds
   the recent filters, which are labelled with the same chips the filter bar
   uses and are the widest thing in the column. */
/* Two things, and they used to be one. --rail-width is how wide the rail IS;
   --sidebar-width is how much room it TAKES OUT of the row. They are equal
   until the rail becomes a drawer, at which point it still has a width but
   occupies nothing — and every rule that centres against the main column
   reads --sidebar-width and recentres itself for free. */
:root { --rail-width: 16.75rem; --sidebar-width: var(--rail-width); }
.sidebar-nav .nav-item { padding: 0.75rem 1rem; gap: 0.75rem; }

/* No border and no background. The sidebar sits directly on the ambient
   layer, exactly as in QA — a rule here drew a hard seam down the page and
   made the sidebar read as a separate surface from the canvas it belongs to. */
.app-sidebar {
  width: var(--rail-width); flex-shrink: 0;
  display: flex; flex-direction: column;
  background: transparent;
}
.sidebar-nav {
  padding: 1.5rem 1rem; flex: 1; min-height: 0;
  display: flex; flex-direction: column; gap: 0.25rem;
  overflow-y: auto;
}
.sidebar-nav::-webkit-scrollbar { width: 0.25rem; }
.sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 0.125rem; }
:root[data-theme="light"] .sidebar-nav::-webkit-scrollbar-thumb { background: rgba(16,24,40,0.12); }
.sidebar-nav .nav-item { max-width: none; }
.sidebar-section-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: 0.12em; text-transform: uppercase;   /* QA's tracking */
  color: var(--ink-faint);
  /* 16px matches .nav-item's padding above, so "PRIMARY" and "CONFIGURE"
     line up with the nav icons beneath them. Moves with nav-item. */
  padding: 0.25rem 1rem 0.375rem; margin-top: 1rem;
}
.sidebar-section-label:first-child { margin-top: 0; }
.sidebar-nav .nav-item.is-last-primary { margin-bottom: 0; }

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; background: transparent; }

/* The scroll container. The float bar is absolutely positioned against
   .app-main, so it must not live inside this element or it scrolls away. */
.page-scroll {
  flex: 1; overflow-y: auto;
  padding: 1.5rem 2rem 6rem;   /* QA's .dashboard-scroll */
}

/* Measure cap. Without it a 1920px viewport gives two 800px columns, which
   is ~120 characters of 13px text per line — past the point where the eye
   reliably finds the start of the next line. Capped, columns land near 620px
   and the briefing stays readable on a large monitor. */
/* Measure cap. Without it a 1920px viewport gives two 800px columns, which
   is ~120 characters of 13px text per line — past the point where the eye
   reliably finds the start of the next line. Capped, columns land near 620px
   and the briefing stays readable on a large monitor.

   It is a rem, so it is 75.5% of the window at every width — the same fraction
   it is at the reference. An earlier pass in this work added a ramp here that
   drifted the column toward the window's centre on large screens, to soak up
   the 1100px of dead space a CAPPED scale left on its right. With the scale
   uncapped there is no dead space to soak up: the cap binds by the same 44
   reference pixels at 1536 and at 6144, and the column sits where it has
   always sat. The ramp was a fix for a problem that no longer exists, and it
   bought that fix by making large screens lay out differently from the
   reference — which is the one thing this whole pass is against. */
.page-inner { max-width: var(--measure-page); }

/* ═══════════════════════════════════════════════
   PAGE HEADER
═══════════════════════════════════════════════ */
.page-head { margin-bottom: 1.25rem; }
.page-head-row { display: flex; align-items: flex-start; gap: 1rem; }
.page-title {
  font-family: var(--font-display);
  font-size: var(--ty-display); font-weight: var(--fw-extrabold);
  letter-spacing: var(--ls-tight); color: var(--d50);
  line-height: var(--lh-tight);
}
.page-head-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.page-stamp {
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--ink-faint);
}

/* ═══════════════════════════════════════════════
   BRIEFING REGION — layout only, cards are .bcard
═══════════════════════════════════════════════ */
/* The design system's #bcard-extended anatomy names .bcard-body,
   .bcard-conclusion, .evidence-row, .bcard-action, .severity-dot and
   .priority-badge — none of which have any CSS (see ../GAPS.md; its
   specimens are inline-styled). Rather than shim six component classes
   locally and fork the system, briefing cards here compose from the
   primitives that ARE implemented: .bcard / .bcard-meta / .bcard-type /
   .bcard-title / .bcard-evidence / .evidence-pill / .status-dot / .tag /
   .work-state / .conf-badge / .entry-action / .bcard-ack-row.
   What remains below is layout, which the product legitimately owns. */
.briefing-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  align-items: start;
}
/* .bcard ships max-width:300px for its documentation specimen; every
   specimen in the design system overrides it inline. Grid owns width here. */
.briefing-grid .bcard { max-width: none; padding: 1rem; }
.briefing-grid .bcard-meta { flex-wrap: wrap; }
.briefing-grid .bcard-title { font-size: var(--ty-meta); line-height: var(--lh-base); }

/* Full-bleed rule above the action zone */
.k-rule { height: 1px; background: var(--hairline); margin: 0.75rem -16px; }

/* Action zone — exactly one primary .entry-action, per §10.3.
   The action keeps its natural pill width. Stretching it edge to edge turned
   a button into a banner at card widths above ~500px, and a banner reads as
   a state rather than as something you press. */
.k-action-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.k-action-note {
  flex: 1; min-width: 12.5rem;
  font-size: var(--ty-meta); color: var(--ink-faint);
  line-height: var(--lh-snug);
}
.briefing-grid > .is-wide { grid-column: 1 / -1; }
@media (max-width: 1180px) {
  .briefing-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Section rule between the ranked briefing and the trailing trend/confirmation
   blocks. §10.3 ranks 8 and 9 last: valuable, never urgent. */
.briefing-tail-label {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 0.5rem;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wider); text-transform: uppercase;
  color: var(--ink-faint); margin-top: 0.375rem;
}
.briefing-tail-label::after {
  content: ""; flex: 1; height: 1px; background: var(--hairline);
}

/* ═══════════════════════════════════════════════
   WORKBENCH — working set layout
═══════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   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 — and it broke here the moment the
   float bar was raised. AiMY QA resolves it at 200 / 500; Knowledge adopts
   the same numbers so the two agents stack identically. Filed in ../GAPS.md.
═══════════════════════════════════════════════ */
.aimy-float-wrap { z-index: 200; }
.aimy-overlay    { z-index: 500; }

/* 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 had to build it locally too, which is the argument for the system
   owning it (../GAPS.md). */
.overlay-context-bar {
  position: absolute; top: 4.125rem; left: 50%;
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 0.375rem;
  white-space: nowrap; max-width: calc(100% - 8.75rem);
  overflow: hidden; pointer-events: none;
  opacity: 0;
  /* The hold belongs to the ARRIVAL only. Written on the base rule it applied
     both ways, so closing the overlay left the bar sitting on a surface that
     was already going — 150ms of a label about a conversation you had just
     dismissed. A delay sequences an entrance; there is nothing to sequence on
     the way out, where the only correct answer is now. */
  transition: opacity var(--t-fast) var(--ease-out);
}
.aimy-overlay.open .overlay-context-bar {
  opacity: 1;
  /* 150ms was the literal here before, and it stays — as a token, and now
     tuned against the overlay it waits for: --t-fast then --t-base lands the
     bar at 400ms, which is the beat the overlay itself settles on. They
     finish together instead of the bar trailing into a surface already still. */
  transition: opacity var(--t-base) var(--t-fast) var(--ease-out);
}
/* .overlay-thread's 80px 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: 6.5rem; }
.overlay-context-label {
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--d500); flex-shrink: 0;
}
.overlay-context-tags { display: flex; align-items: center; gap: 0.25rem; 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: 0.0625rem solid rgba(255,255,255,0.08);
  border-radius: var(--r-pill);
  padding: 0.125rem 0.5rem; 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);
}

/* 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. QA gives the inner the
   same contract as the bar (width 100%, max-width 760px) so their edges line
   up and the chips start where the input starts. */
.filter-tray-inner { width: 100%; max-width: 47.5rem; }

/* Anchored to the bar, not to a fixed 70px offset that assumes a bar height. */
.filter-tray { bottom: calc(100% + 0.375rem); }

/* Empty working set: the float bar sits centred on the canvas (§5.2).
   Docks to the bottom the moment work exists. */
/* Fixed and centred on the WINDOW, not on the main column. Centring it inside
   .app-main put it half the rail's width right of the page's true centre —
   134px, and visible the moment you look for it. */
/* Centred on the WINDOW, and it stays that way. An earlier pass in this work
   re-anchored it to .app-main so it would sit under the content column; that
   moved the band 134px at the reference width, which is a change to a surface
   nobody asked to have changed. The note above is the standing decision and it
   holds.

   Two things do change, and neither moves it on a desktop:

   · The bottom offset adds env(safe-area-inset-bottom), which is 0 everywhere
     except under a device's home indicator — where it was the difference
     between a reachable input and one behind the system bar.
   · 100vw INCLUDES the scrollbar, so the band could overhang its own container
     by the scrollbar's width. 100% is the containing block and cannot. Only
     reachable below ~808px, where the min() switches arms. */
.aimy-float-wrap {
  position: fixed; top: auto;
  bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
  left: 50%; right: auto; transform: translateX(-50%);
  width: min(var(--measure-bar), calc(100% - 3rem));
}

.wb-blank {
  height: 100%;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; padding-bottom: 7.5rem;
}
.wb-blank-title {
  font-family: var(--font-display);
  font-size: var(--ty-title); font-weight: var(--fw-bold);
  color: var(--d200); letter-spacing: var(--ls-tight);
}
.wb-blank-body {
  font-size: var(--ty-meta); color: var(--ink-faint);
  margin-top: 0.5rem; max-width: 46ch; line-height: var(--lh-base);
}

/* Working-set header — count, scope, and the set-scope entry point */
.ws-head {
  display: flex; align-items: center; gap: 0.75rem;
  margin-bottom: 0.75rem; flex-wrap: wrap;
}
.ws-head-title {
  font-size: var(--ty-body); font-weight: var(--fw-bold);
  color: var(--d50); letter-spacing: var(--ls-tight);
}
.ws-head-count {
  font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--ink-faint);
}
.ws-head-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

.ws-grid {
  display: grid;
  /* Same guard as the live copy further down; see the note there. */
  grid-template-columns: repeat(auto-fill, minmax(min(20.625rem, 100%), 1fr));
  gap: 0.75rem;
  align-items: start;
}

/* Single-object mode: viewer / editor lay out at full canvas width, since
   no column is reserved for conversation (§5.4). */
.ws-single { max-width: 57.5rem; }

.wb-back {
  display: inline-flex; align-items: center; gap: 0.375rem;
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--ink-muted); background: none; border: 0; cursor: pointer;
  padding: 0.25rem 0; margin-bottom: 0.75rem;
  transition: color var(--t-fast) var(--ease-out);
}
.wb-back:hover { color: var(--d50); }
.wb-back svg { width: 0.8125rem; height: 0.8125rem; fill: none; stroke: currentColor; stroke-width: 2.2; }

/* Editor column + version panel. 360px, not 320: .ver-item's row needs 336px
   before it starts overflowing, and 360 is the split width the design system's
   own layout table already specifies for a list-beside-detail pane. */
.wb-editor-split {
  display: grid; grid-template-columns: minmax(0, 1fr) 22.5rem; gap: 1rem;
  align-items: start;
}
@media (max-width: 1240px) {
  .wb-editor-split { grid-template-columns: minmax(0, 1fr); }
}

/* Governance has no block of its own any more. It is stated on the axis panel
   and on the document, and changed through the same commit surface as
   everything else — so the only thing left from the old page is the
   customer-facing marker, which the commit form still needs. */
.gov-ext {
  display: block; font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); color: var(--ink-warn);
  text-transform: uppercase; margin-top: 0.125rem;
}

/* .gov-cr-audit-note is styled in the library only as
   `.gov-cr-actions .gov-cr-audit-note`, so outside that one parent it gets
   nothing — in a modal footer the icon jammed against the text at the wrong
   size. Same values, re-declared for the footer, and pushed left so it does
   not crowd the buttons. (../GAPS.md) */
.modal-footer .gov-cr-audit-note {
  margin-right: auto;
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: var(--ty-micro); color: var(--ink-faint);
}

/* ═══════════════════════════════════════════════
   RETRIEVAL — results-first result set (§7.1)
═══════════════════════════════════════════════ */
.rs-head {
  display: flex; align-items: baseline; gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.rs-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wider); text-transform: uppercase;
  color: var(--ink-faint);
}
.rs-note { font-size: var(--ty-meta); color: var(--ink-faint); }
.rs-list { display: flex; flex-direction: column; gap: 0.5rem; }

/* ═══════════════════════════════════════════════
   ANSWER SURFACE — must survive with no shell (§8.1)
   Layout only; every component inside comes from the design system and
   none of them may reference --accent, or the embedded case breaks.
═══════════════════════════════════════════════ */
/* ═══════════════════════════════════════════════
   SPECIMEN WIDTHS — released

   Nearly every component in the library carries a hard max-width tuned for
   the documentation page's narrow specimen column: .bcard 300 · .type-card
   420 · .agent-steps 420 · .source-list 460 · .trust-disclosure 460 ·
   .ai-suggestion 480 · .comment 480 · .ver-list 560 · .doc-view 620 ·
   .msg-bubble 480. Inside a real product shell they stop filling their
   containers and every panel ends up with a ragged right edge.

   Width is layout, and layout is the product's. Released per container below
   rather than globally, so a component dropped somewhere unplanned still gets
   its conservative default. Filed in ../GAPS.md — the constraint belongs on
   the documentation page, not on the component.
═══════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════
   SKILLS AND THE TRACE, IN THE CONVERSATION
═══════════════════════════════════════════════ */
/* The picker sits ABOVE the composer, not below it: it is opened by typing,
   so the eye is already at the input, and a list that pushed the input down
   would move the thing being typed into. */
.sk-pick {
  display: flex; flex-direction: column; gap: 0.125rem;
  margin-bottom: 0.5rem; padding: 0.375rem;
  border: 0.0625rem solid var(--card-border-hover);
  border-radius: var(--r-md);
  background: var(--panel-bg);
  box-shadow: var(--shadow-lg);
}
.sk-opt {
  display: flex; align-items: baseline; gap: 0.5rem;
  width: 100%; padding: 0.375rem 0.5rem;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer; text-align: left;
  font-family: inherit;
  animation: sk-in var(--t-base) var(--ease-out) both;
  /* Was an uncapped 30ms — a second stagger offset for the same idea the
     document list already spells 40ms, and the only one in the build with no
     ceiling, so a long skill list put its last row further out the more
     skills you had. Same token, same cap as .k-stagger: the offset is one
     decision, and past the eighth item the wait outlasts the reading either
     way. */
  animation-delay: calc(min(var(--i, 0), 8) * var(--t-stagger));
}
.sk-opt:hover:not(:disabled) { background: var(--card-bg-raised); }
.sk-opt:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -0.125rem; }
/* An automatic skill is listed but not offered. Hiding it would raise the
   question of where it went; disabling it answers it. */
.sk-opt:disabled { cursor: default; opacity: 0.55; }
.sk-opt-n {
  display: inline-flex; align-items: center; gap: 0.375rem;
  color: var(--ink-primary); font-size: var(--ty-meta); font-weight: var(--fw-medium);
}
.sk-opt-d { color: var(--ink-faint); font-size: var(--ty-micro); }
.sk-opt-t {
  margin-left: auto; flex-shrink: 0;
  color: var(--ink-faint); font-size: var(--ty-micro);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
}
.sk-none { padding: 0.75rem 0.5rem; color: var(--ink-faint); font-size: var(--ty-meta); }
.sk-manage {
  padding: 0.375rem 0.5rem; margin-top: 0.125rem;
  border-top: 0.0625rem solid var(--hairline);
  color: var(--ink-quiet); font-size: var(--ty-micro); text-decoration: none;
}
.sk-manage:hover { color: var(--ink-primary); }
@keyframes sk-in { from { opacity: 0; transform: translate3d(0, 0.25rem, 0); } to { opacity: 1; transform: none; } }

/* THE DISCLOSURE. If a skill shaped an answer, the answer says so — the same
   obligation the citation strip carries for sources, and for the same reason:
   what shaped a claim is part of the claim. */
.sk-used {
  display: flex; align-items: center; gap: 0.375rem;
  margin-bottom: 0.5rem; padding: 0.25rem 0.5rem;
  border-radius: var(--r-sm);
  background: var(--brand-dim);
  color: var(--ink-quiet); font-size: var(--ty-micro);
}
.sk-used b { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* THE TRACE. Closed by default: it is evidence, kept for the moment somebody
   doubts the answer, not narration to be read every time. */
.act-log { margin-top: 0.75rem; }
.act-sum {
  display: inline-flex; align-items: center; gap: 0.375rem;
  cursor: pointer; list-style: none;
  color: var(--ink-faint); font-size: var(--ty-micro);
}
.act-sum::-webkit-details-marker { display: none; }
.act-sum:hover { color: var(--ink-quiet); }
.act-log[open] .act-sum { color: var(--ink-quiet); margin-bottom: 0.375rem; }
.act-steps {
  margin: 0; padding: 0 0 0 0.75rem;
  border-left: 0.0625rem solid var(--hairline);
  list-style: none;
  display: flex; flex-direction: column; gap: 0.25rem;
}
.act-step { display: flex; gap: 0.5rem; align-items: baseline; }
.act-what { color: var(--ink-secondary); font-size: var(--ty-micro); font-weight: var(--fw-medium); }
.act-why { color: var(--ink-faint); font-size: var(--ty-micro); }

@media (prefers-reduced-motion: reduce) { .sk-opt { animation: none; } }


/* The way out of a question the corpus cannot answer. Beside the coverage-gap
   action, not instead of it: raising the gap fixes it for the next person,
   asking someone fixes it for you, and they are different jobs. */
.hand-off {
  display: inline-flex; align-items: center; gap: 0.375rem;
  margin-top: 0.5rem; padding: 0.3125rem 0.625rem;
  border: 0.0625rem solid var(--card-border-hover);
  border-radius: var(--r-sm);
  background: none; cursor: pointer;
  color: var(--ink-secondary);
  font-family: inherit; font-size: var(--ty-micro);
}
.hand-off:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.hand-off:disabled { opacity: 0.6; cursor: default; }
.hand-off:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* Attachments. The chip says "not uploaded" because it is not uploaded, and
   the moment that stops being true is the moment this line comes out. */
.clip-btn {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; width: 1.75rem; height: 1.75rem;
  border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink-faint); cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.clip-btn:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.clip-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.clip-chip {
  display: inline-flex; align-items: center; gap: 0.375rem;
  margin-bottom: 0.375rem; padding: 0.25rem 0.5rem;
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-sm);
  background: var(--card-bg);
  font-size: var(--ty-micro); color: var(--ink-secondary);
}
.clip-name { font-weight: var(--fw-medium); color: var(--ink-primary); }
.clip-note { color: var(--ink-faint); }
.clip-x {
  display: inline-flex; border: 0; padding: 0.125rem;
  border-radius: var(--r-xs); background: none;
  color: var(--ink-faint); cursor: pointer;
}
.clip-x:hover { color: var(--ink-primary); }
.clip-in { display: none; }

/* ══ AN ANSWER'S OWN CONTROLS ════════════════════════════════
   Quiet until the message is hovered or something inside it has focus. An
   action row at full strength under every answer competes with the answer,
   and these are all second thoughts — copy it, ask again, see what it stood
   on — not the reason the message is there.

   Focus-within, not just hover, or the row is unreachable from a keyboard. */


/* ══ THINKING HAS NO FRAME ═════════════════════════════════════════════
   No bubble and no avatar while there is nothing to put in them. The classes
   stay on the same elements throughout — only this rule comes off — so the
   frame appears around an answer that is already streaming rather than the
   message being rebuilt to gain one. */
.chat-msg.is-thinking .msg-avatar { display: none; }
/* ══ UNDRESSING THE BUBBLE TAKES TWO SELECTORS ═════════════════════
   The bubble is dressed TWICE in the library: once as `.chat-msg.aimy
   .msg-bubble` for the default dark theme, and again as `:root[data-theme=
   "light"] .chat-msg.aimy .msg-bubble` for light, which is five class-level
   selectors deep.

   The single rule here was three deep, so it beat the dark one and lost to
   the light one — and lost only PARTLY, which is why this was easy to miss.
   Padding and shadow are not set by the light rule, so those cleared; the
   background and border are, so a white pill with a hairline survived on a
   state that is meant to have no container at all. Dark mode was correct the
   whole time and light mode was not, from one rule that half-applied.

   So the light theme is answered on its own terms. Not `!important`, which
   would win here and then have to be fought later, and not a bare `:root`
   prefix to pad the count — the second selector says which rule it exists to
   override, which is the thing a reader needs to know. */
.chat-msg.is-thinking .msg-bubble,
:root[data-theme="light"] .chat-msg.aimy.is-thinking .msg-bubble {
  background: none;
  border-color: transparent;
  padding: 0;
  box-shadow: none;
}
/* The frame fades up rather than snapping on, so the first words are not
   interrupted by a box drawing itself around them.

   `:not(.is-thinking)` is doing real work here. A transition belongs to the
   state being entered, so declaring it on both states animates the frame OUT
   as well as in — and since `push` reads layout before the class is applied,
   the browser resolves the framed style first and then fades it away. The
   frame appeared for an instant and dissolved, which is the opposite of the
   intent. Declared only on the resting state, entering the thinking state is
   instant and leaving it fades in. */
.chat-msg.aimy:not(.is-thinking) .msg-bubble,
.chat-msg.aimy:not(.is-thinking) .msg-avatar {
  transition: background-color var(--t-base) var(--ease-out),
              border-color var(--t-base) var(--ease-out),
              opacity var(--t-base) var(--ease-out);
}
.chat-msg.is-thinking .msg-avatar { opacity: 0; }

/* Each block of the answer RESOLVES rather than appears: it rises, settles out
   of a hair of scale, and comes through a short blur. Transform and opacity
   carry the motion, so the stream never costs a layout on top of the text it
   is already reflowing.

   THE BLUR IS A SEPARATE ANIMATION AND HAS NO `forwards`, for the reason
   kEnterBlur states a few hundred lines up: a filter held by fill-mode leaves
   the element on its own compositor layer permanently, for a blur of zero.
   Running it unfilled lets the layer go the moment it lands.

   Affordable here because a block is ONE element and they arrive one at a
   time - see the beat in typeIn. The rule this codebase keeps is that a blur
   never lands on a list. */
.stream-in {
  animation: stream-in var(--t-slow) var(--ease-out) backwards,
             stream-in-blur var(--t-base) var(--ease-out);
}
@keyframes stream-in {
  from { opacity: 0; transform: translate3d(0, 0.5rem, 0) scale(0.994); }
  to   { opacity: 1; transform: none; }
}
@keyframes stream-in-blur { from { filter: blur(var(--blur-small)); } }
@media (prefers-reduced-motion: reduce) { .stream-in { animation: none; } }

/* ══ THE SOURCES ARRIVE AS A LIST, NOT AS A BLOCK ══════════════════
   Breadth of evidence is the thing a reader cannot get from the prose, and a
   source list that appears all at once reads as one object rather than as
   several sources. The cascade is the count, made visible.

   --i is set by nth-child rather than by the answer builder: these are
   generated strings and the stagger is a property of the ROW's position, which
   the stylesheet can already see. Capped at the eighth, past which a cascade
   stops reading as rhythm and starts reading as a queue - the same cap
   .k-stagger uses. */
@media (prefers-reduced-motion: no-preference) {
  .source-list > * {
    animation: stream-in var(--t-base) var(--ease-out) backwards;
    animation-delay: calc(min(var(--i, 8), 8) * var(--t-stagger));
  }
  .source-list > *:nth-child(1) { --i: 0; }
  .source-list > *:nth-child(2) { --i: 1; }
  .source-list > *:nth-child(3) { --i: 2; }
  .source-list > *:nth-child(4) { --i: 3; }
  .source-list > *:nth-child(5) { --i: 4; }
  .source-list > *:nth-child(6) { --i: 5; }
  .source-list > *:nth-child(7) { --i: 6; }
  .source-list > *:nth-child(8) { --i: 7; }

  /* ══ AND THE FOLLOW-UPS ARRIVE LAST ═════════════════════════
     followUpsHtml has been writing `--i` on every chip since it was built and
     nothing has ever read it. They are appended after the answer is complete -
     which is the existing decision, made for the right reason: offering
     somewhere to go next beside a paragraph nobody has read is asking the
     question for them. The cascade just makes the ordering legible. */
  .msg-follow .overlay-sugg-chip {
    animation: stream-in var(--t-base) var(--ease-out) backwards;
    animation-delay: calc(min(var(--i, 0), 8) * var(--t-stagger));
  }
}



/* ══ A CLOSED HOVERCARD TAKES NO ROOM ══════════════════════════════════
   `.cite-preview` is `position: absolute`, 320px wide, and closed with
   `opacity: 0; visibility: hidden`. Hidden that way it is still LAID OUT,
   so it still contributes to every ancestor's scroll width — a 315px card
   hanging off a 20px citation chip.

   On a desktop nothing shows for it: the thread is 720px and the card is
   centred over the chip with room on both sides. On a phone the thread is
   338px, so each closed card pushed the scroll width to 433 and the
   conversation scrolled sideways. Measured: with these cards taken out of
   layout the thread's scroll width is 338 — exactly its client width, with
   no other content overflowing at all.

   `display` rather than a width clamp, because the card is not merely too
   wide — it is in the wrong place entirely until it opens, at which point
   `openCite` reparents it to <body>. Until then it is a template, and a
   template should not occupy the layout it is a template for.

   OVERRIDDEN HERE, NOT PATCHED IN THE LIBRARY. `aimy-ds.css` is marked "not
   authored here"; this file loads after it and the selectors are matched at
   equal specificity, so later-wins does the work. */
.cite-preview { display: none; }
.cite-wrap:hover > .cite-preview,
.cite-wrap:focus-within > .cite-preview,
.cite-preview.is-open { display: block; }

/* ══ THE GREETING'S SUGGESTIONS ════════════════════════════════════════
   Offered questions, not results, so they sit apart from the answer above
   them with a rule rather than inside a card. The chips are the Console's
   own `.overlay-sugg-chip`, which means the existing delegated handler
   already submits them and nothing new has to be wired. */
.greet-next {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.75rem;
  border-top: 0.0625rem solid var(--hairline);
}
.greet-next-label {
  /* A FULL ROW OF ITS OWN. As an ordinary flex item the label sat on the first
     chip's line and only the overflow wrapped beneath it, so the label read as
     attached to one question rather than heading all three. A 100% basis in a
     wrapping row is the break — no extra wrapper element, and the chips stay
     direct children so `gap` keeps spacing them as one set. */
  flex: 0 0 100%;
  font: var(--ty-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* The thinking mark. Sized in rem so it tracks the type around it, and given
   a `color` because the canvas reads its fill from exactly that — the dots are
   the brand's blue without the paint being written twice. */
.think-mark {
  width: 1.625rem; height: 1.625rem;
  flex-shrink: 0;
  color: var(--ai-text);
  vertical-align: middle;
}

/* ══ A MESSAGE IS NEVER SQUEEZED, THE THREAD SCROLLS ═══════════════
   The thread is a flex COLUMN, so every child defaults to `flex-shrink: 1`
   and flexbox compresses them all once the content is taller than the pane.
   Most survive it because their text sets a minimum size. The memory panel
   does not: `overflow: hidden` drops its automatic minimum to zero, so it
   absorbs the ENTIRE overflow on its own and collapses to nothing but its 1px
   borders — a stray horizontal rule above the first message, with 167px of
   content clipped behind it.

   Turning shrink off is the general form of the fix rather than a patch on the
   one element that showed it: nothing in a transcript should ever be resized
   to make the transcript fit. That is what scrolling is for. */
.overlay-thread > * { flex-shrink: 0; }

/* ══ THESE SIT UNDER THE MESSAGE, NOT BESIDE IT ══════════════════
   `.chat-msg` is a flex ROW — avatar, then bubble. These three blocks were
   written with `grid-column: 2`, which did nothing at all because the parent
   is not a grid, so each one became another ITEM IN THAT ROW: the bubble was
   squeezed to a fraction of its width and clipped mid-word, the action row
   sat to its right, and three follow-up chips stacked into a narrow tower.

   Wrapping the row and giving each block the full line puts them back
   underneath. The indent is the avatar (30px) plus the row's own 10px gap, so
   they line up with the bubble's left edge rather than with the avatar. */
.chat-msg { flex-wrap: wrap; }
.msg-acts,
.msg-follow,
.msg-sources { flex: 1 1 100%; margin-left: 2.5rem; }

.msg-acts {
  display: flex; align-items: center; gap: 0.25rem;
  margin-top: 0.375rem;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease-out);
}
.chat-msg:hover .msg-acts,
.chat-msg:focus-within .msg-acts { opacity: 1; }
/* The placeholder is an assistant turn too. Hidden by what is INSIDE the
   bubble rather than by a branch in the template, so the row appears by
   itself when the answer swaps in — no second render, nothing to keep in
   step. */
.chat-msg.aimy:has(.ai-thinking) .msg-acts { display: none; }

.msg-act {
  display: inline-flex; align-items: center; gap: 0.25rem;
  padding: 0.1875rem 0.375rem;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer;
  color: var(--ink-faint);
  font-family: inherit; font-size: var(--ty-micro);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.msg-act:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.msg-act:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.msg-act[aria-expanded="true"] { background: var(--card-bg-raised); color: var(--ink-primary); }
.msg-acts-end { display: inline-flex; gap: 0.125rem; margin-left: auto; }
.msg-act.is-rate { padding: 0.1875rem; }
/* Rating is a stated opinion, so it stays lit once given rather than fading
   back with the rest of the row. */
.msg-act.is-rate[aria-pressed="true"] { color: var(--ink-action); opacity: 1; }
.chat-msg .msg-act.is-rate[aria-pressed="true"] { opacity: 1; }

/* The drawer under an answer. Opened from the row above it, and built from
   the same `sourceRow` the answer's own source list uses — one template, so a
   source is the same row wherever it is listed and opens the same document. */
.msg-sources {
  display: flex; flex-direction: column; gap: 0.25rem;
  margin-top: 0.5rem;
  padding: 0.75rem;
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-md);
  background: var(--card-bg);
}
.msg-sources-cap {
  font-size: var(--ty-micro);
  color: var(--ink-faint);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  margin-bottom: 0.25rem;
}

/* Follow-ups sit under the answer at the message's own indent, wrapping
   rather than scrolling: three short questions, and a horizontal scroller for
   three items is a control nobody asked for. */
.msg-follow {
  display: flex; flex-wrap: wrap; gap: 0.375rem;
  margin-top: 0.5rem;
}

/* An answer is not a prose bubble: it carries inline citations, a source list
   and a trust disclosure, and .msg-bubble's 480px was squeezing all three. */
.chat-msg.aimy:has(.answer-surface) { max-width: 100%; }
/* Fills the space LEFT BY THE AVATAR rather than claiming the whole row.
   At a flat 100% the bubble plus the avatar plus the gap is wider than the
   line, and now that the row wraps, the bubble dropped onto a line of its own
   with the avatar stranded above it. flex-basis 0 with grow takes whatever is
   left after the avatar, which is what "full width" meant here all along. */
.chat-msg.aimy:has(.answer-surface) .msg-bubble { max-width: 100%; flex: 1 1 0; min-width: 0; }
.answer-surface { display: flex; flex-direction: column; gap: 0.75rem; }
/* The surface owns the rhythm, once. Several of these blocks carry their own
   vertical margins for the places they appear outside an answer, and those
   margins land on top of the gap here — which is why the spacing between an
   answer's parts drifted between 12px and 24px. */
.answer-surface > * { margin-top: 0; margin-bottom: 0; }
.answer-surface .source-list,
.answer-surface .trust-disclosure,
.answer-surface .agent-steps { max-width: none; }

/* ── A citation says what it is citing ──

   The title was a bare text node, which as a flex child is an anonymous item
   nothing can style. With the type mark now sharing the row it needs to be the
   thing that gives way, so it takes the shrink and the ellipsis and the
   connector, status and type all keep their whole words. */
.source-item .source-title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.source-item .tc-kind { flex-shrink: 0; color: var(--ink-faint); font-weight: var(--fw-medium); }

/* ══ AND BELOW 520 IT TAKES A SECOND LINE RATHER THAN DELETE ITS SUBJECT ══
   The rule above makes the title the thing that gives way, which is right
   while there is something left to give. There is not, on a phone. Measured
   across the row's five cells — the number, the type, the title, the source
   and the state:

       768   title 290px
       560   title 130px
       480   title  50px      "Refu…"
       400   title   0px      gone, and the state 19px past the row
       360   title   0px      gone, and the state 59px past the row

   So the first thing to disappear is the only cell that says WHICH document is
   being cited, and once it has gone to zero there is nothing left to shrink:
   `Confluence` and `Out of date` are both nowrap at their min-content, so the
   badge simply runs out of the row. A citation reading "1 Article Confluence
   Out of date" names no document at all.

   Wrapping instead costs one line and keeps every word. Flex breaks a line
   BEFORE it shrinks, so the title arrives at its content width and the source
   and the state drop beneath it — which is also the right reading order: what
   it is, then where it came from and what state it is in. No reordering, no
   cell hidden, and the row cannot overflow because the line that would have
   overflowed is now the next one.

   520 is this file's own rung, and chat.css turns the gate chips into a
   carousel at the same width. */
@media (max-width: 520px) {
  .source-item { flex-wrap: wrap; }
  /* The state keeps its whole word. It is the reason the row is worth reading,
     and it is the one cell that must never be half a sentence. */
  .source-item .trust-state { flex-shrink: 0; }
}

.source-item .tc-kind svg { width: 0.75rem; height: 0.75rem; }
.source-item .source-domain { margin-left: 0; }
.cp-src .tc-kind { display: inline-flex; align-items: center; gap: 0.25rem; }
.cp-src .tc-kind svg { width: 0.625rem; height: 0.625rem; }

/* ═══════════════════════════════════════════════
   THE SETTINGS SHEET

   Same overlay as a document — it reuses .doc-overlay, .doc-sheet, .doc-bar and
   .doc-scroll rather than growing a second set of them. What is new here is the
   label/control row, which is the whole point: settings are edited in place,
   not described and then edited in a dialog.
═══════════════════════════════════════════════ */
.doc-sheet.is-settings { max-width: 38.75rem; }

.set-panel { display: flex; flex-direction: column; gap: 1rem; }

.set-lead { font-size: var(--ty-meta); color: var(--d200); line-height: var(--lh-base); margin: 0; }
.set-lead strong { color: var(--d50); }

.set-sub {
  font-size: var(--ty-micro); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: var(--ls-wide);
}

.set-group {
  display: flex; flex-direction: column; gap: 0.5rem;
  padding-top: 1rem; border-top: 0.0625rem solid var(--hairline);
}

/* Label left, control right, explanation under the control it explains — not
   under the row, where it reads as a footnote to both. */
.set-row { display: grid; grid-template-columns: 6.875rem minmax(0, 1fr); gap: 0.75rem; align-items: start; }
.set-label {
  font-size: var(--ty-meta); color: var(--ink-muted); padding-top: 0.5rem;
  overflow: hidden; text-overflow: ellipsis;
}
.set-control { display: flex; flex-direction: column; gap: 0.375rem; min-width: 0; }
.set-control .v2-dropdown { width: 100%; max-width: 16.25rem; }
.set-note { font-size: var(--ty-micro); color: var(--ink-faint); line-height: var(--lh-snug); }
.set-note strong { color: var(--ink-muted); }

.set-checks { display: flex; flex-direction: column; gap: 0.125rem; }
.set-check { padding: 0.375rem 0; }

.set-foot { padding-top: 1rem; border-top: 0.0625rem solid var(--hairline); }

/* A change the input asked for, sitting above the control it would move. It is
   a proposal, not a result — the same distinction the AiMY draft makes in the
   editor, and drawn the same way. */
.set-staged {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.75rem 0.75rem; border-radius: var(--r-md);
  background: var(--accent-dim); border: 0.0625rem solid rgba(139,79,244,0.28);
}
.set-staged-text { flex: 1; min-width: 11.25rem; font-size: var(--ty-meta); color: var(--d200); }
.set-staged-text strong { color: var(--d50); }
.set-staged-end { display: flex; align-items: center; gap: 0.375rem; flex-shrink: 0; }

/* The sheet's own facts row sits tighter than an answer's. */
.set-panel .conv-facts { padding: 0.75rem 0; }

@media (max-width: 720px) {
  .set-row { grid-template-columns: 1fr; gap: 0.25rem; }
  .set-label { padding-top: 0; }
}

/* ═══════════════════════════════════════════════
   SOURCES & DATA — the settings, where they can be found

   Not a destination. Each row opens the conversation that already carries the
   controls; this rail block exists because a setting you cannot find is a
   setting you do not have.
═══════════════════════════════════════════════ */
.rail-set { display: flex; flex-direction: column; gap: 0.125rem; }
.rail-set-row {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.375rem 0.5rem; border-radius: var(--r-sm);
  font-size: var(--ty-meta); color: var(--ink-secondary);   /* see § THE SIDEBAR IS NOT PROSE */
}
.rail-set-row:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .rail-set-row:hover { background: rgba(16,24,40,0.04); }
/* The name opens the conversation; the row's own control resolves it. */
.rail-set-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: inherit; color: inherit;
}
.rail-set-name:hover { color: var(--ink-primary); }
.rail-set-name:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
.rail-set-note { font-size: var(--ty-micro); color: var(--ink-faint); flex-shrink: 0; }
.rail-set-do {
  flex-shrink: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-micro); font-weight: var(--fw-bold);
  padding: 0.25rem 0.5rem; border-radius: var(--r-pill);
  background: var(--err-bg); border: 0.0625rem solid color-mix(in srgb, var(--err) 28%, transparent); color: var(--ink-err);
}
.rail-set-do:hover { background: color-mix(in srgb, var(--err) 20%, transparent); }
.rail-set-do:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* A formatting button that never shows what is already applied is telling you
   nothing about the text your caret is in. */
.toolbar .icon-btn.is-on {
  background: var(--accent-dim);
  color: var(--ink-action);
  border-color: rgba(139,79,244,0.35);
}

/* ═══════════════════════════════════════════════
   THE CHANGE MARKER

   Every action has to move the screen where you are looking. When a repaint is
   the whole of the feedback — a status badge quietly becoming a different word,
   a date catching up to its source — nothing takes the eye to it. This does.
═══════════════════════════════════════════════ */
.just-changed {
  /* Tint only. No transform, no outline: this lands on rows, badges, cards and
     whole answers, and anything geometric would reflow one of them. */
  animation: just-changed 1.5s var(--ease-out);
  border-radius: var(--r-sm);
}
@keyframes just-changed {
  0%   { background-color: var(--accent-dim); }
  60%  { background-color: var(--accent-dim); }
  100% { background-color: transparent; }
}
/* Held, not animated — the point is to be noticed, and a fade is not the only
   way to do that. */
@media (prefers-reduced-motion: reduce) {
  .just-changed { animation: none; background-color: var(--accent-dim); }
}

/* `display:flex` on .float-badge outranks the UA's `[hidden] { display:none }`,
   so the badge has been sitting there reading 0. Same shape of bug as the drop
   layer. */
.float-badge[hidden] { display: none; }

/* The mark on the input is the way back into the canvas, so it has to read
   as pressable. Everything else about it is the design system's. */
#canvasOpen { cursor: pointer; }
#canvasOpen:hover { border-color: rgba(139,79,244,0.6); }

/* The blanket `cursor: default` on the bar's children (§ FORM-CONTROL RESET)
   is right for its inert furniture — the "Enter to run" hint, the spinner —
   but it also caught the two things in the bar that do something when clicked.
   The unread badge is part of the opener's hit area: clicking it opens the
   canvas, so it must not be the one spot on that button where the cursor
   goes flat.

   The send needs the id to clear that blanket rule's specificity; the badge
   does not, because it sits inside the opener rather than beside it. */
.float-badge,
#floatSend { cursor: pointer; }

/* Working set — the viewer, editor and version panel own their columns. */
.ws-single .doc-view,
.wb-editor-split .doc-view,
.wb-editor-split .ai-suggestion,
.wb-editor-split .comment,
.wb-editor-split .comment-thread,
.wb-editor-split .ver-list,
.wb-editor-split .toolbar { max-width: none; }
/* .ver-item.is-current bleeds 8px each side (margin: 0 -8px) for its
   highlight, so the list needs that much room or it scrolls sideways. */
.wb-editor-split .ver-list { padding: 0 0.5rem; }
.ws-grid .type-card { max-width: none; }
.answer-body {
  font-size: var(--ty-body); color: var(--d100);
  line-height: var(--lh-base);
}
.answer-body p + p { margin-top: 0.5rem; }
.answer-scope {
  display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap;
  font-size: var(--ty-meta); color: var(--ink-faint);
}

/* ═══════════════════════════════════════════════
   PROTOTYPE-ONLY CONTROLS
   Deliberately styled as a dev tool, never as product chrome: mono type,
   dashed border, no accent. Direction §11 rejects an explicit role toggle
   in the product, so this must not be mistakable for one.
═══════════════════════════════════════════════ */
/* Out of the content flow entirely. It is dev chrome and should not be the
   first thing on the page it is there to test. Bottom-left, clear of the
   input's centred lane. */
/* ═══════════════════════════════════════════════
   THE DOCUMENT PAGE

   A page, not a panel. The paper is a surface distinct from the application
   behind it, the writing is the only thing on it, and everything ABOUT the
   document — its facts, its connections, its history, its comments — is in a
   rail beside it that can be folded away.

   The measure is the point: 66-ish characters, which is where the eye keeps
   the line it is on. Everything else here is in service of that column.
═══════════════════════════════════════════════ */
/* Reading one document is not the moment for a rail about everything else —
   nor for the controls that narrow a grid you cannot see. The URL keeps every
   value, so closing the document brings the row back exactly as it was.

   `.filter-row`, not `.filter-tray`. The tray is the floating bar at the foot
   of the window; the row is the thing on screen, and hiding the wrong one and
   then verifying against that same wrong name meant the check agreed with the
   fix while five live dropdowns went on standing over every document. */
body.is-doc .app-sidebar,
body.is-doc .filter-row { display: none; }

/* ══ SETTINGS AS A VIEW ═══════════════════════════════════════════════════
   The same deal the document above already has: the stage becomes something
   else, and the controls that describe a working set stop describing anything.

   One difference, and it is the point of the whole change: THE RAIL STAYS. A
   document is read and the rail is 268px of something else; settings is
   NAVIGATED, and the rail is what navigates it. */
body.is-settings #filterBar,
body.is-settings #chipBar { display: none; }
/* ── EXCEPT ON THE ARCHIVE ──
   The rule above holds because a settings module has no working set for those
   controls to describe. The Archive module is a set of documents drawn with
   the grid's own cards, so the controls describe it exactly as well as they
   describe the library, and hiding them would leave the one list you cannot
   narrow. */
body.is-settings.is-archive #filterBar,
body.is-settings.is-archive #chipBar { display: block; }

/* The two verbs on an archived card. A row, so they read as a pair of choices
   rather than a stack of two unrelated buttons — the same call `.rail-foot`
   makes about Report and Archive. Ruled off the facts above it, because they
   act on the document rather than describing it. */
/* Two classes deep, because `.tc-foot` is the insight footer and stacks its
   children in a column — equal specificity would have left the direction to
   source order, which is how these two came out as full-width bars. */
.tc-foot.tc-foot-arch {
  display: flex; flex-direction: row; align-items: center;
  /* Opposite ends, so the irreversible verb is never under the hand that has
     just pressed the other one. */
  justify-content: space-between; gap: 0.875rem;
  padding-top: 0.5rem; margin-top: 0.625rem;
  border-top: 0.0625rem solid var(--card-border);
}
/* A link, not a button. See typeCard: the card is already the object being
   read, so its two verbs are the smallest thing that is still a control. */
.tc-arch-lnk {
  appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 0.375rem;
  font-family: var(--font-sans); font-size: var(--ty-micro);
  font-weight: var(--fw-semibold); color: var(--ink-action);
}
.tc-arch-lnk svg { flex: none; }
/* The label underlines, not the glyph: an underlined icon is a box with a line
   under it. */
.tc-arch-lnk:hover { text-decoration: none; }
.tc-arch-lnk:hover span { text-decoration: underline; }
.tc-arch-lnk:hover { text-decoration: underline; }
.tc-arch-lnk:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: 0.125rem; }
/* The one place colour is spent here, and it is spent on the irreversible one. */
.tc-arch-lnk.is-danger { color: var(--err); }

/* ── The settings rows in the briefing rail ──
   `.rail-set-row` was a <div> holding two controls: a name that opened a sheet
   and a Reconnect button beside it. Each of these rows has ONE destination, so
   the ROW is the control — reading it and using it are the same act, which is
   the one thing worth taking from QA's rail wholesale. Hence the button resets:
   the class was written for a div. */
button.rail-set-row {
  border: 0; background: none; font-family: var(--font-sans); cursor: pointer;
  text-align: left;
}
button.rail-set-row:focus-visible {
  outline: 0.125rem solid var(--brand); outline-offset: -0.125rem;
}
/* Where a dot would be. Without it the names of the rows that have nothing to
   report hang 1.125rem left of the ones that do, and the column stops being a
   column. */
.rail-cfg-nodot { width: 0.5rem; flex-shrink: 0; }

/* A module with no pages of its own — Skills — is a leaf at the same level as
   the expandable ones, so its name starts where theirs do rather than 4px
   short of it. Its dot sits in the column the chevrons occupy. */
.rail-tree > .rail-cfg-row { padding-left: 0.375rem; gap: 0.375rem; }

/* The one you are standing in reads as settled rather than offered — QA's
   word for it. `aria-current="page"` carries the same fact for anything not
   reading the colour. */
.rail-cfg-row.is-on { background: var(--card-bg-raised); }
.rail-cfg-row.is-on .rail-set-name { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* The way back out of settings, at the head of the section rather than in the
   foot: you left from this rail, so this is where you look to return. */
.rail-cfg-back { color: var(--ink-faint); margin-bottom: 0.25rem; }
.rail-cfg-back svg { flex-shrink: 0; opacity: 0.8; }
.rail-cfg-back:hover .rail-set-name { color: var(--ink-primary); }

/* ═══════════════════════════════════════════════
   THE SETTINGS TREE

   One nav, not two. The in-page spine that used to map a module's sections
   from the left gutter is gone; its sections are pages, and they are listed
   here, under the module that holds them.

   Shape from the consoles that solved this first — Twilio's, and on Mobbin
   Supabase (the label-over-children contrast step) and Remote (expand to
   nested children). Three levels, three weights:

     group   micro, uppercase, --ink-faint     read once
     module  body,  --ink-secondary            where you are
     page    body,  --ink-primary when current  what you came for
     state   micro, --ink-faint                 what it is carrying

   The indent is a rule, not padding: a hairline down the children's left edge
   says "these belong to that" without spending 1rem of a 267px rail on it.
═══════════════════════════════════════════════ */
.rail-tree { gap: 0.0625rem; }
.rail-grp { display: flex; flex-direction: column; }

/* ── ONE CONTROL, ONE JOB ──
   The header was a chevron plus a title that navigated: two targets a
   millimetre apart doing different things, and the bigger one took you off the
   page you were reading just for pressing the name of the group you were in.
   A group has no page of its own — going "to" it only ever meant going to the
   first thing under it, which is listed directly beneath and one click away.
   So the whole header opens and closes, and the pages are the only
   destinations in the tree. */
.rail-grp-h {
  display: flex; align-items: center; gap: 0.375rem; width: 100%;
  padding: 0.375rem 0.5rem 0.375rem 0;
  border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  /* The group leads its own children on weight, since it cannot on size:
     both are at the meta step and the floor is one step below that. */
  font-family: var(--font-sans); font-size: var(--ty-meta);
  font-weight: var(--fw-semibold);
  color: var(--ink-secondary);
}
.rail-grp-h:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .rail-grp-h:hover { background: rgba(16,24,40,0.04); }
.rail-grp-h:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -0.125rem; }

/* ── THE LADDER, AND WHY IT WAS UPSIDE DOWN ──
   Measured from the rail's left edge, the tree read: module name at 40px, page
   name at 30. The children were indented LESS than the parent whose name they
   belonged under, which is the one thing an indent exists to prevent — the
   chevron and the status dot together were spending 40px before the module got
   to say what it was.

   The dot moved to the right, where the other status marks already are, and
   the chevron narrowed. What is left is a real ladder in half the space:
   group label 0, module 20, page 28. */
.rail-grp-x {
  flex-shrink: 0; display: grid; place-items: center;
  width: 0.875rem; height: 0.875rem;
  color: var(--ink-faint);
}
/* Status on the right, beside the count of what AiMY can do about it — the two
   marks that report on a folded-away group, together. */
.rail-grp-h .status-dot { order: 3; margin-left: auto; }
.rail-grp-h .rail-cfg-nodot { display: none; }
.rail-grp-h .rail-grp-acts { order: 4; }
.rail-grp-h .rail-set-name { flex: 0 1 auto; }
.rail-grp-x svg { transition: transform var(--t-fast) var(--ease-out); }
.rail-grp.is-open .rail-grp-x svg { transform: rotate(90deg); }
.rail-grp-h:hover .rail-grp-x { color: var(--ink-primary); }

/* The module you are in is a PLACE, not a selection: it comes up to full
   contrast and takes no fill, because the fill belongs to the page under it
   and two filled rows in one column read as two current things. */
.rail-grp-h.is-on .rail-set-name { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* ── WHAT A CLOSED GROUP STILL SAYS ──
   Folded away, every quick action under it goes with it, and a fix nobody can
   see is a fix nobody does. The dot says something is wrong; this says how
   many things AiMY can act on in there, in AiMY's own mark — so the reason to
   open the group is legible on the closed group. It is not on an open one:
   the actions themselves are showing, and a count of what is already in front
   of you is furniture. */
.rail-grp-acts {
  flex-shrink: 0; display: inline-flex; align-items: center; gap: 0.1875rem;
  padding: 0.0625rem 0.3125rem;
  border-radius: var(--r-pill);
  background: var(--accent-dim);
  color: var(--ink-action);
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}

/* ── The children ──
   Indented to sit under the module's NAME rather than under its chevron, so
   the column of page names lines up with the column of module names one level
   up. The rule is drawn on the container, not per row, or it would break at
   every gap. */
/* One step, and one that reads as one. Pulled to 7px first, which put the page
   names 3px right of the module name above them — an indent nobody can see is
   the same as none, and the tree flattened into a list. 8px is the smallest
   step that still says "under". */
.rail-pgs {
  display: flex; flex-direction: column; gap: 0.0625rem;
  margin: 0.125rem 0 0.375rem 0.625rem;
  padding-left: 0.6875rem;
  border-left: 0.0625rem solid var(--hairline);
}

/* ── STACKED, NOT SIDE BY SIDE ──
   Name and state shared a line first. The rail is 267px wide and the indent
   takes 21 of it, so "Data relevance" beside "2,695 queued to delete" left
   both truncated — and a page name you cannot read is worse than a state you
   have to look twice for. The state is meta and belongs under the thing it is
   about, which is also what the spine did with the same two facts. */
.rail-pg {
  display: flex; flex-direction: column; gap: 0.0625rem; width: 100%;
  padding: 0.3125rem 0.5rem 0.3125rem 0.4375rem; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-meta);
  color: var(--ink-secondary);
}
.rail-pg:hover { background: rgba(255,255,255,0.04); }
:root[data-theme="light"] .rail-pg:hover { background: rgba(16,24,40,0.04); }
.rail-pg:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -0.125rem; }
.rail-pg-n {
  min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  line-height: var(--lh-snug);
}
/* The state, in meta. It carries its severity as INK rather than as a dot:
   a dot plus a number is two marks for one fact, and at this indent there is
   no room to spend on the duplicate. */
.rail-pg-s {
  min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--ty-micro); color: var(--ink-faint); line-height: 1.3;
}
.rail-pg-s.sd-ok   { color: var(--ink-faint); }
.rail-pg-s.sd-warn { color: var(--ink-warn); }
.rail-pg-s.sd-err  { color: var(--ink-err); }

/* Where you are. A fill, because among the pages of one module this IS a
   selection — one of them is open and the others are not. */
.rail-pg.is-on { background: var(--card-bg-raised); }
.rail-pg.is-on .rail-pg-n { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* Unsaved on a page you are not looking at. See markDirtyStage. */
.rail-pg.is-dirty .rail-pg-n::after {
  content: ""; display: inline-block; vertical-align: middle;
  width: 0.3125rem; height: 0.3125rem; margin-left: 0.375rem;
  border-radius: 50%; background: var(--brand);
}

/* ── The quick action ──
   Under the row it belongs to, indented past the page name, and only present
   when something is actually wrong — so the rail is quiet on a healthy
   workspace and the one thing worth doing is the one thing that stands out.

   It is NOT a pill and NOT brand-filled. Its job is to be found without
   competing with the page names above it for the eye, and a filled chip in a
   267px column beats everything else in the rail whatever it says. The mark
   carries the attribution; the ink carries the urgency. */
.rail-fix {
  display: flex; align-items: center; gap: 0.375rem; width: 100%;
  margin: 0.0625rem 0 0.25rem;
  padding: 0.25rem 0.5rem 0.25rem 0.4375rem; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-micro);
  color: var(--ink-action);
}
.rail-fix:hover { background: var(--accent-dim); }
.rail-fix:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -0.125rem; }
.rail-fix-m { flex-shrink: 0; }
.rail-fix-l { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-fix-go { flex-shrink: 0; opacity: 0.55; transition: transform var(--t-fast) var(--ease-out); }
.rail-fix:hover .rail-fix-go { transform: translateX(0.125rem); opacity: 0.9; }

/* `.set2-save` is `position: sticky; bottom: 0`, written for a page whose
   scroll port ended at the window. Here the float bar owns that band, so the
   save bar stops above it. `.page-scroll` already reserves 6rem below, so
   nothing is clipped by moving it up. */
body.is-settings .set2-save { bottom: 4.75rem; }

/* A <button> wearing `.rail-back`. The class was written for an <a>, so the
   handful of user-agent styles a button brings have to come back off. */
button.rail-back {
  width: 100%; border: 0; background: none; font: inherit;
  text-align: left; cursor: pointer;
}
/* `max-width: none` let a document run to the full window: 2496px at 2560,
   which is ~180 characters a line. Capped at the width the reference viewport
   already gives it, so nothing changes at 1x and it stops growing after that —
   the line length then holds at roughly what it is today (~120 characters)
   instead of climbing with the monitor. */
body.is-doc .page-inner { max-width: var(--measure-doc-page); margin-inline: auto; }

.doc-page {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 0 1.5rem;
  height: 100%;
  min-height: 0;
}
.doc-page.rail-closed { grid-template-columns: minmax(0, 1fr) 0; gap: 0; }
.doc-page.rail-closed .doc-rail { display: none; }

/* No rule under it. The row holds the way back and the save state — chrome
   about the page, not a section of it — and a line across the full width read
   as the top of a card the document was inside. */
.doc-topbar {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 0.5rem;
  padding-bottom: 0.75rem; margin-bottom: 1rem;
}
.doc-back {
  display: inline-flex; align-items: center; gap: 0.375rem;
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--ink-muted); padding: 0.25rem 0;
}
.doc-back:hover { color: var(--d50); }
.doc-back:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
.doc-top-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }
.doc-rail-toggle {
  width: 1.75rem; height: 1.75rem; border-radius: var(--r-sm);
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-muted); cursor: pointer;
}
.doc-rail-toggle:hover { border-color: var(--card-border-hover); color: var(--d100); }
.doc-rail-toggle:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* ── The page ──

   There is no paper. A 760px column centred in an 867px region left 54px of
   dead space on each side and floated a bordered, shadowed card on the
   application background — a rectangle in the middle of a window, not a
   document. The region IS the document now: content starts at the left edge,
   the rail sits to its right, and nothing is drawn around it. */
.doc-canvas { overflow-y: auto; min-height: 0; padding-bottom: 2.5rem; }
.doc-paper { padding: 0.5rem 0 4rem; }

/* ── The writing tools, on the block being written ──

   They used to be a sticky row at the top of the paper that kept its 46px box
   even while hidden, so every document carried a strip of dead space and the
   tools were nowhere near the words they act on. Now they arrive above the
   active block and leave with it, which is the same rule the caret already
   follows — and above the block there is the full 762px measure, so the AiMY
   menu opens without clipping. */
.blk-tools {
  position: absolute; z-index: 6;
  flex-wrap: wrap;
  box-shadow: var(--shadow-lg);
}
@media (prefers-reduced-motion: no-preference) {
  .blk-tools { animation: blkIn var(--t-fast) var(--ease-out); }
  @keyframes blkIn { from { opacity: 0; transform: translateY(2px); } }
}
/* The assistant is the one control in here that writes rather than formats, so
   it is the only one carrying the mark and the accent. */
.blk-tools .blk-ai {
  display: inline-flex; align-items: center; gap: 0.375rem;
  height: 1.75rem; padding: 0 0.5rem; border-radius: var(--r-sm);
  background: none; border: 0; color: var(--ink-action); cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-micro); font-weight: var(--fw-semibold);
}
.blk-tools .blk-ai:hover { background: rgba(var(--accent-rgb), 0.14); }
.blk-tools .blk-ai:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.blk-tools .blk-ai svg { flex-shrink: 0; }
.blk-add { font-size: var(--ty-title); line-height: 1; }

/* ── The block list ──

   Reached from the + for the first time and from `/` for every time after.
   One list either way, so there is nothing to learn twice. */
.blk-menu {
  position: absolute; z-index: 7;
  min-width: 12.5rem; max-height: 20rem; overflow-y: auto;
  padding: 0.25rem;
  background: var(--panel-bg); border: 0.0625rem solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(0.75rem);
}
.blk-menu-item {
  display: flex; align-items: center; gap: 0.75rem; width: 100%;
  padding: 0.375rem 0.5rem; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--d100);
}
.blk-menu-item:hover { background: rgba(255,255,255,0.06); }
:root[data-theme="light"] .blk-menu-item:hover { background: rgba(16,24,40,0.05); }
.blk-menu-item:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.blk-menu-ico {
  flex-shrink: 0; width: 1.5rem; height: 1.5rem; border-radius: var(--r-xs);
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.05); color: var(--ink-muted);
  font-size: var(--ty-meta); font-weight: var(--fw-bold);
}
:root[data-theme="light"] .blk-menu-ico { background: rgba(16,24,40,0.05); }

/* ── The blocks themselves ── */
.doc-page #editBody > .doc-code {
  margin: 0 0 1rem; padding: 0.75rem 1rem; max-width: 80ch;
  background: rgba(255,255,255,0.04); border: 0.0625rem solid var(--hairline);
  border-radius: var(--r-md);
  font-family: var(--font-mono); font-size: var(--ty-meta); line-height: 1.6; color: var(--ink-secondary);
  overflow-x: auto; white-space: pre;
}
:root[data-theme="light"] .doc-page #editBody > .doc-code { background: rgba(16,24,40,0.04); }
.doc-code code:empty::before { content: "Type or paste code"; color: var(--ink-faint); }
.doc-page #editBody > .doc-figure { margin: 0 0 1rem; }
/* A button, not a hint. It used to read "Drop an image on the document to fill
   this", which asked for the gesture the workbench had already claimed — the
   drop layer turns any dropped file into a new document, so aiming at the
   figure made a document instead of an image. */
.doc-figure-ph {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  width: 100%; min-height: 7.5rem; padding: 1.5rem;
  background: rgba(255,255,255,0.03); border: 0.0625rem dashed var(--card-border);
  border-radius: var(--r-md); cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--ink-muted); text-align: center;
}
.doc-figure-ph:hover { border-color: var(--card-border-hover); color: var(--d100); }
.doc-figure-ph:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.doc-figure-img { display: block; max-width: 100%; border-radius: var(--r-md); }
:root[data-theme="light"] .doc-figure-ph { background: rgba(16,24,40,0.03); }
.doc-page #editBody > .doc-figure figcaption {
  margin-top: 0.5rem; font-size: var(--ty-meta); color: var(--ink-faint);
}
.doc-figure figcaption:empty::before { content: "Describe it"; opacity: 0.6; }
.doc-page #editBody > .doc-hr {
  margin: 1.5rem 0; border: 0; border-top: 0.0625rem solid var(--hairline);
}

.doc-title {
  font-family: var(--font-display);
  font-size: var(--ty-display); font-weight: var(--fw-extrabold);
  line-height: 1.14; letter-spacing: -0.022em; color: var(--d50);
  margin: 0 0 0.75rem; text-wrap: balance;
  border-radius: var(--r-sm); padding: 0.125rem 0.375rem; margin-left: -6px;
  outline: none; border: 0.0625rem solid transparent;
  transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
/* Nothing is armed. The affordance arrives under the pointer and leaves with
   it, so the thing reads as a document rather than as a form. The text cursor
   is the whole invitation. */
.doc-title, .doc-page #editBody { cursor: text; }
.doc-title:hover, #editBody:hover { border-color: var(--hairline); }
.doc-title:focus, #editBody:focus {
  border-color: var(--card-border-hover);
  background: rgba(255,255,255,0.02);
}
/* Focused but not yet armed — the keyboard's own state, and it has to be
   visible or Tab lands somewhere invisible. */
.doc-title:focus-visible:not([contenteditable]),
.doc-page #editBody:focus-visible:not([contenteditable]) {
  outline: 0.125rem solid var(--brand); outline-offset: 0.125rem;
}
:root[data-theme="light"] .doc-title:focus,
:root[data-theme="light"] #editBody:focus { background: rgba(16,24,40,0.02); }

/* ── The head, one rhythm ──

   Notice, byline and note each had their own margin — 20px, 24px, 16px — and
   they were interleaved with the title. As one block above the title they need
   one internal gap and one gap to the title beneath. */
.doc-head { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.doc-head .dv-notice { margin-bottom: 0; }
/* THE ICON SITS ON THE LINE IT MARKS. aimy-ds.css aligns the notice to
   flex-start and nudges the icon down 1px, which was measured for something
   — it lands the 14px glyph 3.2px ABOVE the middle of the 22.4px line box
   beside it. A fixed nudge cannot do this job, because the answer depends on
   the line-height, and this notice wraps to two lines on a narrow document.

   So the icon gets a box one line tall and lets the viewBox centre itself in
   it: preserveAspectRatio defaults to xMidYMid meet, so a 24x24 viewBox in a
   14 x 22.4 box draws 14x14 centred, and the glyph does not change size.
   Measured after: 0.00px from the first line’s middle, single-line and
   wrapped alike — an icon that marks a paragraph belongs on its first line,
   not floating between two of them.

   1lh is the honest unit and it is stated second so it wins where it is
   supported; 1.6em above it is the same number, restating the line-height
   the library sets three declarations up. */
.doc-head .dv-notice > svg {
  height: 1.6em;
  height: 1lh;
  margin-top: 0;
}
.doc-head .doc-note { margin: 0; }
/* ── A SEPARATOR THAT COULD NOT SURVIVE A WRAP ──

   The middots were flex items of their own, so a wrap could leave one
   stranded at the end of a line — which is what it did at 1180px: the row
   ended "… Came from Confluence ·" and put "Filed in Policies" underneath.
   Moving them into ::before or ::after only moves the stranding to the other
   end of the line; there is no CSS that knows where a line broke.

   So they go, and the gap separates instead. That works HERE because every
   item in this byline is a phrase rather than a value — "Owned by A.
   Mahfouz", "Updated 14 Jan 2026", "Came from Confluence", "Filed in
   Policies". Grammar is doing the separating already and the dot was
   repeating it. A middot earns its place in a run of bare values, which is
   what the card’s meta line is and why that one keeps them.

   It also fixes a second fault at the same time: .doc-versions is taller
   than the phrases around it and centres 4px higher, so the two dots
   bracketing it sat on a different baseline from the one after "Article". */
.doc-byline {
  display: flex; align-items: center; gap: 0.375rem 1rem; flex-wrap: wrap;
  font-size: var(--ty-meta); color: var(--ink-quiet);
  margin: 0;
}
/* The same problem the card's meta line had, on the line above the title:
   type, source, collection, owner and date all at one rank, so the two that
   can put the document in somebody's queue read no louder than the two that
   never change. The line is L4; the type glyph and the separators are L5. */
/* .doc-by-sep is gone — see the note at .doc-byline. */
.doc-by-kind { color: var(--ink-faint); }
/* Not a button: every other entity on this line opens a peek, and there is no
   peek for a type — the filter for it is the grid's own dropdown. So it reads
   as a fact rather than advertising a target that does not exist. */
.doc-by-kind { display: inline-flex; align-items: center; gap: 0.25rem; }
.doc-by-kind svg { width: 0.75rem; height: 0.75rem; }
.doc-by-ent {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--ink-quiet);
  border-bottom: 0.0625rem dotted var(--ink-faint);
}
.doc-by-ent:hover { color: var(--ink-action); border-bottom-color: var(--ink-action); }
.doc-by-ent:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* ── The run that leaves the product ──

   Every other entity on this line opens a peek — same dotted rule, same
   hover — so the one that opens Confluence has to differ before it is
   pressed, not after. It keeps the line's ink and its dotted rule, because it
   is still attribution and still L4, and it takes the arrow. One glyph at
   10px is the whole difference, which is the right size of difference: this
   is not a more important fact than "Filed in Policies", it is a differently
   shaped one. */
.doc-by-out { display: inline-flex; align-items: center; gap: 0.25rem; text-decoration: none; }
.doc-by-out:visited { color: var(--ink-quiet); }
.doc-by-out-ico { width: 0.625rem; height: 0.625rem; opacity: 0.7; flex-shrink: 0; }
.doc-by-out:hover .doc-by-out-ico { opacity: 1; }
.doc-note { font-size: var(--ty-meta); color: var(--ink-warn); margin: 0 0 1rem; }

/* The measure sits on the TEXT, not on the container. Capping the block would
   cage everything inside it; capping the paragraphs keeps the line at the
   length an eye can track back while a table, a quote or a drafted suggestion
   uses the width the page actually has. */
.doc-page #editBody {
  /* --ty-title, until the scale pass moved that step from 17 to 20 and the
     document's prose went with it. Two things broke at once: the paragraphs
     came within 8px of the sheet's own 28px title, and `h3` — which points at
     --ty-title too — became exactly the size of the text underneath it, a
     heading separated from its body by weight alone. Prose is what --ty-body
     is for; the token says so. The sheet reads 28 / 20 / 16 now, and h3 has a
     step of its own again. */
  /* 1.72 WAS PAYING FOR THE MEASURE. Long lines need more leading, because
     the eye has further to travel to find the next one; at 102 characters
     1.72 was the compensation. The line is 75 characters now and the
     compensation is no longer owed. --lh-relaxed is 1.6, which is what a
     column of this width takes on a dark ground. */
  font-size: var(--ty-body); line-height: var(--lh-relaxed); color: var(--ink-secondary);
  padding: 0.5rem; margin: 0 0 0 -8px;
  border-radius: var(--r-sm); border: 0.0625rem solid transparent;
  min-height: 10rem;
}

/* ── The block under the pointer is the block you would write in ──

   Second attempt. The first put a 2px rule in a 20px gutter beside every block,
   at rest — which answered "is this editable" and answered it in a vocabulary
   nothing else in the product speaks, in a column of its own, permanently.

   What every block editor does instead: the block itself responds. The
   paragraph under the pointer takes a soft background, so the surface reads as
   a set of things you can act on rather than as a page with a margin of marks
   beside it. Nothing at rest, and nothing in a gutter.

   The affordance is per BLOCK, and that is the whole difference from what was
   here before round two. `#editBody:hover` already drew a 1px border — around
   the entire body, one target the size of the page, which tells you the page is
   a thing and nothing about its parts. A paragraph that lights as you cross it
   tells you the parts are the things.

   Pure CSS, and it has to be. #editBody is contenteditable and writeBody writes
   its innerHTML back to o.html on EVERY KEYSTROKE, so anything added inside it
   becomes the document's stored content — an element, and equally a class on a
   paragraph, which would serialise as <p class="…"> and survive into a restored
   version. A :hover rule adds nothing to the DOM at all. */
/* The tint reaches past the glyphs by a box-shadow spread rather than by
   padding, and that is a deliberate third attempt at this one line.

   Padding on the block would have to be repaid with a negative margin, and the
   block rules below set `margin` as a shorthand — so the side margins would be
   silently overwritten further down the file, on paragraphs only. A pseudo-
   element inset outside the block needs `z-index: -1` to sit behind the text,
   which puts it behind any background its ancestors take — and `.is-writing`
   gives #editBody one, so the tint would vanish at exactly the moment the body
   is armed. A spread shadow has neither problem: it paints outside the border
   box, follows the border-radius, and touches no layout and no stacking.

   6px of spread against a 16px block gap, so two adjacent tints never meet. */
.doc-page #editBody > * {
  border-radius: var(--r-sm);
  transition: background-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out);
}
.doc-page #editBody > *:hover {
  background: rgba(255,255,255,0.04);
  box-shadow: 0 0 0 0.375rem rgba(255,255,255,0.04);
}
:root[data-theme="light"] .doc-page #editBody > *:hover {
  background: rgba(16,24,40,0.04);
  box-shadow: 0 0 0 0.375rem rgba(16,24,40,0.04);
}

/* ── Armed is not the same as hovered ──

   .is-writing is added to .doc-page by armEditable and removed by
   disarmEditable, and had no rule anywhere in either stylesheet until round
   two. Once the body is genuinely armed the whole surface says so, so the
   hover stops being the only thing that distinguishes "you could write here"
   from "you are writing here". */
.doc-page.is-writing #editBody { border-color: var(--card-border-hover); background: rgba(255,255,255,0.02); }
:root[data-theme="light"] .doc-page.is-writing #editBody { background: rgba(16,24,40,0.02); }

/* Reading history is not editing it. armEditable returns early while a
   .ver-preview is on the page, so the affordance has to agree with it — one
   that lies about what the surface will do is worse than none. */
.doc-paper:has(.ver-preview) #editBody > *:hover { background: none; box-shadow: none; }
.doc-page #editBody > p,
.doc-page #editBody > ul,
.doc-page #editBody > ol,
.doc-page #editBody > h3,
.doc-page #editBody > h4 { max-width: var(--measure-prose); }
.doc-page #editBody p { margin: 0 0 1rem; }
.doc-page #editBody h3 {
  font-family: var(--font-display); font-size: var(--ty-title); font-weight: var(--fw-bold);
  color: var(--d50); margin: 1.5rem 0 0.5rem; letter-spacing: -0.01em;
}
/* h4 shares a step with the prose under it and always did — at 15 when body
   was 15, at 16 now. One size is not a rank, so it takes the other two axes:
   the display face's bold and the identity ink, against 400 and secondary. */
.doc-page #editBody h4 {
  font-size: var(--ty-body); font-weight: var(--fw-bold); color: var(--ink-primary);
  margin: 1.25rem 0 0.375rem;
}
.doc-page #editBody ul, .doc-page #editBody ol { margin: 0 0 1rem; padding-left: 1.5rem; }
.doc-page #editBody li { margin-bottom: 0.375rem; }
.doc-page #editBody blockquote {
  margin: 0 0 1rem; padding-left: 1rem;
  color: var(--ink-muted); font-style: italic;
}
.doc-page .dv-notice { margin-bottom: 1.25rem; }
.doc-page .ai-suggestion { margin-top: 1.5rem; }

/* ══════════════════════════════════════════════════
   AIMY PUTS SOMETHING IN FRONT OF YOU

   A proposal is the one thing on this surface that arrives without being asked
   for, in the middle of a document somebody is already reading. Appearing
   between two frames is how a rendering fault looks. Arriving is how an offer
   looks, and the difference is most of what tells a reader which one it is.

   ITS PARTS RESOLVE IN ORDER, AND THE ORDER IS THE ARGUMENT: what AiMY is
   offering to do, then the words it would take out, then the words it would
   put there, and the buttons last. The foot is last on purpose - Accept should
   not be sitting under the pointer before the reader has seen the thing they
   would be accepting.

   `backwards`, never `both`: a part is hidden only while its own delay is
   genuinely pending, so an animation that is cancelled or never starts leaves
   the card fully readable rather than blank. knowledge.js strips the class on a
   timer as well, for the case where the clock freezes mid-delay.
   ══════════════════════════════════════════════════ */
/* ══ AND WHILE IT IS STILL BEING WRITTEN ════════════════════════
   The placeholder is the same box in the same place, holding a caption that
   says what AiMY is doing and three lines where the draft will be. It is not a
   spinner: a spinner says only that something is happening, and this surface
   can say WHAT, which is the difference between waiting and being told.

   The caption carries `.ai-thinking-label` so it is the same words in the same
   treatment as the chat's thinking line - one state, described one way.
   ════════════════════════════════════════════════════════════ */
.ai-suggestion.is-pending .ai-suggestion-body {
  display: flex; flex-direction: column; gap: 0.5rem;
}
.ai-suggestion-note { margin-bottom: 0.125rem; }

@media (prefers-reduced-motion: no-preference) {
  .ai-suggestion.is-proposing {
    animation: ai-propose var(--t-slow) var(--ease-out) backwards;
  }
  @keyframes ai-propose {
    from { opacity: 0; transform: translate3d(0, 0.75rem, 0) scale(0.985); }
  }

  /* `is-proposing` is the box arriving; `is-resolving` is the draft landing
     inside a box that is already there. They share the cascade and differ only
     in whether the card itself moves. */
  .ai-suggestion.is-proposing > *,
  .ai-suggestion.is-resolving > * {
    animation: ai-propose-part var(--t-base) var(--ease-out) backwards;
  }
  .ai-suggestion.is-proposing > :nth-child(1),
  .ai-suggestion.is-resolving > :nth-child(1) { animation-delay: calc(var(--t-stagger) * 2); }
  .ai-suggestion.is-proposing > :nth-child(2),
  .ai-suggestion.is-resolving > :nth-child(2) { animation-delay: calc(var(--t-stagger) * 4); }
  .ai-suggestion.is-proposing > :nth-child(3),
  .ai-suggestion.is-resolving > :nth-child(3) { animation-delay: calc(var(--t-stagger) * 8); }
  @keyframes ai-propose-part {
    from { opacity: 0; transform: translate3d(0, 0.25rem, 0); }
  }

  /* Out, then in. The strike-through is the current wording being set aside;
     the replacement follows it rather than landing beside it, so the swap
     reads as an edit being proposed and not as two paragraphs appearing. */
  .ai-suggestion.is-proposing .ai-suggestion-body del,
  .ai-suggestion.is-resolving .ai-suggestion-body del {
    animation: ai-propose-part var(--t-base) var(--ease-out) backwards;
    animation-delay: calc(var(--t-stagger) * 5);
  }
  .ai-suggestion.is-proposing .ai-suggestion-body ins,
  .ai-suggestion.is-resolving .ai-suggestion-body ins {
    animation: ai-propose-ins var(--t-base) var(--ease-out) backwards;
    animation-delay: calc(var(--t-stagger) * 7);
  }
  @keyframes ai-propose-ins {
    from { opacity: 0; transform: translate3d(0, 0.375rem, 0); filter: blur(var(--blur-small)); }
  }
}

/* ══ THE RAIL ══════════════════════════════════════

   Measured before this pass: 1612px in an 800px window — twice the viewport,
   with two of five blocks already collapsed — and **22 distinct type styles**
   in a 320px column, every one between 8.5px and 13px, in three purples.
   Nothing led, because nothing was meaningfully bigger than anything else.
   That is what "no hierarchy" is when you measure it.

   The five roles were right. Two of the five numbers were not: --d500 measures
   3.10:1 on a raised surface in the default theme, not the 4.69 GAPS §1.8
   reported, so *phrase* and *supporting* — the relationship and its author —
   were the two least readable runs in a column whose whole job is to be read.
   And 11px was a floor set against a scale that had 8.5px in it; against a
   product it is still small.

   Same five roles, on the density scale, at the rail's setting (--ty-title and
   --ty-body step down one notch here; the floor does not move):

     block title   13px / 700 / --ink-primary      L3 structure
     value         14px / 600 / --ink-primary      L2 what you are reading for
     lead-in       14px / 400 / --ink-secondary    L2
     phrase        13px / 600 / --ink-quiet        L4 the relationship
     supporting    12px / 400 / --ink-faint        L5 why, author, date, count

   Nothing below 12px. Cost of the increase, measured with all three blocks
   open: 926px → 1005px. They are <details> and closed by default, so nothing
   had to move to pay for it.

   One accent, spent only on actions, and it is --ink-action rather than
   --accent — the fill colour was 3.73:1 as text.
══════════════════════════════════════════════════ */
.doc-rail {
  overflow-y: auto; min-height: 0;
  display: flex; flex-direction: column; gap: 0.125rem;
  padding-bottom: 2.5rem;
}
.rail-block { border-bottom: 0.0625rem solid var(--hairline); }
.rail-block > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.75rem 0.25rem;
}
.rail-block > summary::-webkit-details-marker { display: none; }
.rail-block > summary:hover .rail-block-title { color: var(--ink-action); }
.rail-block > summary:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.rail-block[open] > summary .tree-chev { transform: rotate(90deg); }
.rail-block .tree-chev { color: var(--ink-faint); transition: transform var(--t-base) var(--ease-out); }
.rail-block-title { flex: 1; font-size: var(--ty-title); font-weight: var(--fw-bold); color: var(--ink-primary); }
.rail-block-n {
  font-size: var(--ty-micro); font-weight: var(--fw-medium); color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.rail-block-body { padding: 0 0.25rem 1rem; }

.rail-facts { display: flex; flex-direction: column; gap: 0.25rem; }
.rail-facts p { margin: 0; font-size: var(--ty-meta); line-height: 1.5; color: var(--ink-secondary); }
.rail-facts p.is-overdue { color: var(--ink-err); }

/* ── The ingestion date is the block's headline, not its first item ──

   Four facts set in one grey at one size, so "how old is the copy I am
   reading" — the question the block is opened to answer — ranked level with
   the three that qualify it. It leads them on three axes where the scale
   asks for two, because this one has to be readable from across the column.

   THE NUMBERS IN THIS NOTE HAVE BEEN WRONG TWICE. It said "15/14, 600/400"
   — true when --ty-title was 15, and the scale pass moved that step to 20
   and then the rail’s dense variant to 18, without the note following. It is
   16/500 over 14/400 now, and the note says so rather than saying a ratio
   that stops being true the next time a token moves.

   The duration sits beside the date rather than replacing it. A date alone
   makes the reader subtract; a duration alone loses the thing they would
   quote in a ticket. Both, and neither has to be looked up. */
.rail-facts p.rail-lead-fact {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 0.5rem;
  font-size: var(--ty-body); font-weight: var(--fw-semibold);
  color: var(--ink-primary); line-height: var(--lh-snug);
  margin-bottom: 0.25rem;
}
.rail-lead-age {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
/* ── The rail has no primary action ──

   `em-direct` renders filled in brand blue, so *Re-sync from Confluence* sat in
   a 320px column as a solid button beside *Find another source* outlined — two
   weights for two peers, and the loudest thing in the rail was a maintenance
   job. Everything here is quiet and equal; the page's one filled control is
   still New document, on the surface. */
.rail-act { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
.rail-act .entry-action {
  height: 1.75rem; padding: 0 0.75rem; border-radius: var(--r-md);
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-action);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
}
.rail-act .entry-action:hover { background: none; border-color: var(--card-border-hover); color: var(--ink-primary); }
/* An <a> in a row of <button>s. The geometry is `.entry-action`'s already; what
   an anchor brings and a button does not is an underline and a visited colour,
   and both of them would make one pill in the row look like a different
   component. */
.rail-act a.entry-action { text-decoration: none; }
.rail-act a.entry-action:visited { color: var(--ink-action); }
.rail-act a.entry-action:visited:hover { color: var(--ink-primary); }

/* ── em-out: the fifth affordance ──

   §3's four modes all describe something that happens HERE — direct completes
   in place, investigate and prompt open the canvas, review proposes a change.
   Opening the original in Confluence does none of that: it hands you to another
   product. So it gets its own mode rather than borrowing one that promises
   undo, and it is quiet — outlined, never filled — because leaving is a normal
   move and not the loudest thing in a 320px column. */
.em-out {
  background: none; border-color: var(--card-border); color: var(--ink-action);
  text-decoration: none;
}
.em-out:hover { background: none; border-color: var(--card-border-hover); color: var(--ink-primary); }
.rail-empty { margin: 0; font-size: var(--ty-body); color: var(--ink-secondary); line-height: 1.5; }
/* One row. Two short buttons stacked wasted a line each and read as a list of
   two unrelated things; side by side they read as what they are — the actions
   that belong to the whole document. Delete joins them when it exists. */
.rail-foot {
  display: flex; flex-direction: row; flex-wrap: wrap;
  /* The two gaps are not the same measure and were written as though they
     were. 4px of ROW gap is fine between controls that sit side by side and
     share a baseline; the moment the rail is narrow enough to wrap them onto
     their own lines — which is most of the time once Delete joins Report and
     Restore — 4px reads as two buttons stuck together rather than two
     choices. The column gap is untouched: side by side they were never the
     problem. */
  align-items: center; gap: 0.5rem 0.5rem; padding: 1rem 0.25rem 0;
}

/* Connections read as sentences: the relationship, the thing, and who said so. */
.rail-conn { display: flex; flex-direction: column; gap: 0.125rem; margin-bottom: 1rem; }
.rail-conn:last-child { margin-bottom: 0; }
/* A count and a relationship phrase were byte-identical — same step, same
   weight, same ink. That is the defect README's card pass named ("the two fact
   lines were byte-identical") reproduced in the rail one pass later. A count is
   ambient; a phrase names the group beneath it, so they separate on all three. */
.rail-conn-phrase { font-size: var(--ty-meta); font-weight: var(--fw-bold); color: var(--ink-quiet); margin-bottom: 0.25rem; }
/* ── The row, and the one control on it ──

   A connection is a claim, and a claim can be wrong, so the sentence carries
   the button that removes it rather than sending you back to the picker to
   find the same document among forty-one others.

   It is ALWAYS drawn. Revealing it on hover is the reflex here and it is two
   bugs: there is no hover on a touch screen, and an affordance you have to
   discover by waving at it is not quiet, it is hidden — which is the no-muted-
   text rule one step out from text. What keeps it from shouting is size and
   ink, not absence: a 14px glyph in secondary ink that only takes the error
   colour once you are on it. */
.rail-conn-row { display: flex; align-items: center; gap: 0.125rem; margin-left: -8px; }
.rail-conn-drop {
  flex: none; display: grid; place-items: center;
  width: 1.5rem; height: 1.5rem; padding: 0;
  border: 0; border-radius: var(--r-sm); background: none;
  color: var(--ink-secondary); cursor: pointer;
}
.rail-conn-drop:hover { background: var(--err-bg); color: var(--err); }
.rail-conn-drop:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
/* ── The attribution goes UNDER the title, not beside it ──

   Side by side, *A. Mahfouz, 20 Jun* is a 98px `flex-shrink: 0` block, so a
   320px rail spent a third of every row on the smallest type in the column and
   ellipsised the document's NAME to pay for it — the one thing on the row you
   are reading it for. Stacking costs ~17px of height per row and buys back
   every one of those pixels for the title.

   It is also the shape this product already uses one block up: the document's
   own byline is a title with who-and-when beneath it, not a title with
   who-and-when tucked against the right margin. */
.rail-conn-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.0625rem;
  flex: 1; min-width: 0;
  padding: 0.25rem 0.5rem; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-body); font-weight: var(--fw-semibold); color: var(--ink-primary);
}
.rail-conn-item:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .rail-conn-item:hover { background: rgba(16,24,40,0.04); }
.rail-conn-item:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
/* `width`, not `flex: 1`. In a column the main axis is vertical, so `flex: 1`
   would stretch the line's HEIGHT and leave it free to overflow sideways —
   which is the ellipsis silently not happening. */
.rail-conn-label { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Who asserted it, and when. An implicit edge has no author, so it shows none. */
.rail-conn-by {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--ty-micro); font-weight: var(--fw-medium); color: var(--ink-faint);
}

/* ── Version history, in the line that states the date ──

   It was in the topbar, at the far end of a row you read past on the way into
   the document, saying "3 versions" beside the save indicator. Nothing there
   said what it was for. The byline already claims *Updated 12 Jun* — which is
   the assertion a version history exists to qualify — so the two are one
   control, first line of the document, where the question gets asked.

   Still native <details>, so an open panel survives a repaint with no state of
   ours. */
.doc-versions { position: relative; }
.doc-versions > summary {
  list-style: none; cursor: pointer;
  padding: 0.25rem 0.5rem; border-radius: var(--r-sm);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--ink-quiet);
  white-space: nowrap;
}
/* In the byline it is one phrase among peers, so it carries their weight and
   colour, and the count is the part that is new. */
.doc-by-ver { margin: 0 -8px; }
.doc-by-ver > summary { font-weight: var(--fw-medium); }
/* ── The one control on this line that did not look like one ──

   Every other pressable run in the byline is a `.doc-by-ent` and carries a
   dotted underline at rest. This one carried a cursor and a chevron at half
   opacity, and the rule above had deliberately stripped its weight to make it
   match the plain FACTS beside it — so the only thing in the line that opens a
   panel was styled as the things that open nothing.

   On the phrase, not on the summary: the summary is padded and also holds the
   count pill and the chevron, so a border there would underline a block. */
.doc-by-vd { border-bottom: 0.0625rem dotted var(--ink-faint); }
.doc-by-ver > summary:hover .doc-by-vd { color: var(--ink-action); border-bottom-color: var(--ink-action); }
.doc-by-vn {
  margin-left: 0.25rem; padding: 0 0.375rem; border-radius: 1.25rem;
  background: rgba(255,255,255,0.07); color: var(--ink-secondary);
  font-size: var(--ty-micro); font-weight: var(--fw-semibold);
}
:root[data-theme="light"] .doc-by-vn { background: rgba(16,24,40,0.07); }
.doc-by-ver > summary:hover .doc-by-vn { color: var(--ink-action); }
.doc-by-ver .doc-versions-panel { right: auto; left: 0; }
.doc-versions > summary::-webkit-details-marker { display: none; }
/* No opacity. A rendered glyph at 0.5 is a run below the floor through a
   channel no colour token can see — the same finding this file already records
   about `.tag-token`. It is ambient, so it is coloured ambient. */
.doc-versions > summary::after {
  content: ""; display: inline-block; width: 0.75rem; height: 0.75rem;
  margin-left: 0.25rem; vertical-align: -0.125rem; background-color: var(--ink-faint);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* There was no [open] rule anywhere, so the one control in the byline with two
   states looked identical in both of them and the chevron never turned. */
.doc-versions[open] > summary::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
}
.doc-versions[open] > summary { background: rgba(255,255,255,0.05); }
.doc-versions[open] > summary .doc-by-vd { border-bottom-style: solid; border-bottom-color: var(--ink-action); }
:root[data-theme="light"] .doc-versions[open] > summary { background: rgba(16,24,40,0.05); }
.doc-versions > summary:hover { background: rgba(255,255,255,0.05); color: var(--d100); }
:root[data-theme="light"] .doc-versions > summary:hover { background: rgba(16,24,40,0.05); }
.doc-versions > summary:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.doc-versions-panel {
  position: absolute; top: calc(100% + 0.25rem); right: 0; z-index: 30;
  width: 18.75rem; max-height: 60vh; overflow-y: auto;
  padding: 0.75rem;
  background: var(--panel-bg); border: 0.0625rem solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(0.75rem);
}

/* ── Comments, under the writing ──

   You comment on a document while reading it, so the thread lives at the end of
   the document rather than in 141px of the rail's column. Full width, its own
   rule, and the count as its heading. */
/* ── The starting moves, in the writing ──

   Above the body and aligned to the text, so position says what they do: these
   write INTO the document. In the topbar they read as chrome about it, and the
   menu — which positions against #docCanvas — opened 426px away from its own
   button because the anchor had left that host. */
.doc-start {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
  margin-bottom: 1rem;
}
.doc-ai, .doc-start-act {
  display: inline-flex; align-items: center; gap: 0.375rem;
  height: 2rem; padding: 0 0.75rem; border-radius: var(--r-md);
  cursor: pointer; white-space: nowrap;
  font-family: var(--font-sans); font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
/* The assistant is the one thing here that is neither reading nor writing by
   hand, so it is the only one that takes the accent. */
.doc-ai {
  background: rgba(var(--accent-rgb), 0.12); border: 0.0625rem solid rgba(var(--accent-rgb), 0.35);
  color: var(--ink-action);
}
.doc-ai:hover { background: rgba(var(--accent-rgb), 0.2); border-color: var(--accent); }
.doc-start-act {
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-muted);
}
.doc-start-act:hover { border-color: var(--card-border-hover); color: var(--d100); }
.doc-ai:focus-visible, .doc-start-act:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.doc-ai svg, .doc-start-act svg { flex-shrink: 0; }
/* Naming the gesture and what it takes. The drop layer only ever appears once
   you are already dragging, which is no use to somebody wondering whether they
   can. */
.doc-start-fine { margin: -8px 0 1rem; font-size: var(--ty-meta); color: var(--ink-faint); }

.doc-comments { margin-top: 2.5rem; padding-top: 1.5rem; border-top: 0.0625rem solid var(--hairline); }
.doc-comments-head {
  margin: 0 0 0.75rem;
  font-family: var(--font-display); font-size: var(--ty-body); font-weight: var(--fw-bold);
  color: var(--d200);
}
.doc-page .comment-thread { margin: 0; padding: 0; border: 0; background: none; }
.doc-page .comment-thread-head { display: none; }
.doc-page .comment { max-width: var(--measure-prose); }
.doc-page .comment-text { font-size: var(--ty-meta); }
.doc-page .comment-compose { max-width: var(--measure-prose); }
.doc-page .props { gap: 0.75rem; }
/* `.prop-facts` stood here: the type's fields, read aloud as phrases above the
   rows. They became controls, and a value printed above the field that holds
   it is the same fact twice.

   `.prop-typed` stood here too: those controls, separated from the taxonomy
   every type shares by a hairline. They have left this panel entirely — the
   type's own fields are the record on the document now, where the type leads.
   What the rail keeps is the shared taxonomy, which is the split §6.4 draws.
   Both rules are removed rather than kept: a rule with no markup is invisible
   to the audit, which only ever looks the other way round. */

@media (max-width: 1100px) {
  .doc-page { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; }
  .doc-rail { border-top: 0.0625rem solid var(--hairline); padding-top: 0.5rem; }
}

/* ── A modal you can finish ──

   The library's `.modal` has no height contract: `max-height: none`,
   `overflow: visible`, and a backdrop that does not scroll. That is fine while
   the body is a sentence and fatal once it holds a list. Measured: the connect
   picker renders at 717px, so at a 640px window it was cut by 38px and **the
   confirm button could not be reached at all** — you could open the picker and
   never finish it. It only looked safe at 800px, where 42px happened to be
   spare.

   Same fix and same reason as the peek (GAPS §15): a surface whose content is
   data-dependent pins its actions and scrolls only its middle. */
.modal-backdrop { padding: 2.5rem 1.25rem; }
.modal {
  display: flex; flex-direction: column;
  max-height: calc(100dvh - 5rem); min-height: 0;
}
.modal-header, .modal-footer { flex-shrink: 0; }
.modal-body { min-height: 0; overflow-y: auto; }

/* ── The sheet ──

   Capping the box's height made it finishable but not comfortable: a list as
   long as the library inside 280px of scroll, inside a body that also scrolls.
   A surface where the list IS the decision wants the window's full height, so
   picking takes one scroll and the foot never moves off screen. The centred box
   stays for the case it was built for — a decision already made, arriving for
   confirmation. */
.modal-backdrop.is-sheet { padding: 0; justify-content: flex-end; align-items: stretch; }
.modal-sheet {
  height: 100%; max-height: 100%;
  border-radius: 0; border-top: 0; border-right: 0; border-bottom: 0;
}
/* Same shape as .k-enter: the start state lives inside the motion query and the
   keyframe only names the end, so a surface that never animates is a surface
   that is simply there. An opacity of 0 in a `from` would leave it invisible. */
@media (prefers-reduced-motion: no-preference) {
  .modal-sheet { transform: translateX(24px); animation: sheetIn var(--t-base) var(--ease-out) forwards; }
  @keyframes sheetIn { to { transform: none; } }
}
/* One scroll, not two: in a full-height sheet the list is the body. */
.modal-sheet .cx-list { max-height: none; overflow: visible; }

/* ═══════════════════════════════════════════════
   CONNECTING, AS ONE CHOICE

   This was a canvas answer: neighbour groups over a search, with a `+` per row
   opening its own confirmation. Three faults — the suggestions are noise on a
   new document, the rows navigated away from the document you were connecting,
   and one commit per connection is the wrong unit.

   One list inside the commit surface: the relationship chosen once, search over
   all of it, checkboxes rather than links, and the suggestion surviving as a
   reason on a row rather than as a section of its own.
═══════════════════════════════════════════════ */
.cx-pick { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem; }
/* The lead-in sits above the control rather than jammed against its left edge,
   so the three rows of this form share one left margin instead of three. */
.cx-as { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; }
.cx-as-lead { font-size: var(--ty-micro); font-weight: var(--fw-semibold); color: var(--ink-faint); }
.cx-seg { padding: 0.125rem; }
.cx-seg .seg-btn { height: 1.75rem; padding: 0 0.75rem; font-size: var(--ty-micro); }
.cx-q { height: 2rem; padding: 0 0.75rem; font-size: var(--ty-meta); }
/* Chrome draws its own clear button inside a `type=search` field: a grey ✕
   belonging to the browser, sitting in a field wearing the product's own
   class. Same defect as the number stepper, different chrome. Scoped to
   `.field-input` so it covers every search field this product ever puts on a
   surface, not just this one. */
.field-input::-webkit-search-cancel-button,
.field-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.cx-list {
  display: flex; flex-direction: column; gap: 0.125rem;
  max-height: 17.5rem; overflow-y: auto;
  padding: 0.25rem; margin: 0 -4px;
}
.cx-row {
  display: flex; align-items: flex-start; gap: 0.5rem;
  padding: 0.375rem 0.5rem; margin: 0 -8px; border-radius: var(--r-sm);
  cursor: pointer;
}
.cx-row:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .cx-row:hover { background: rgba(16,24,40,0.04); }
.cx-row[hidden] { display: none; }
.cx-row-text { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.cx-row-title { font-size: var(--ty-meta); color: var(--d100); }
/* Why AiMY would have suggested it — a caption, not a section. A guess and a
   claim still never render alike; the guess is just no longer given its own
   real estate. */
.cx-row-why { font-size: var(--ty-meta); color: var(--ink-faint); }
.cx-none { margin: 0.5rem 0 0; font-size: var(--ty-meta); color: var(--ink-faint); }
.cx-none[hidden] { display: none; }


/* ═══════════════════════════════════════════════
   THE PEEK

   Takes `.cite-preview`'s look — glass panel, 320px, head over body over foot —
   and none of its mechanism. The library's version is `position: absolute`
   against a `.cite-wrap` parent and opens on `:hover` of that parent, so it can
   only ever describe the thing it is nested inside. This one is anchored by
   script to whatever was clicked, anywhere on the page, including a row inside
   itself. See GAPS.md.
═══════════════════════════════════════════════ */
/* One box that scrolled put the actions under 978px of list, so the panel
   opened showing a list and no way out of it. Head, facts and actions are
   fixed; the body is the only thing that scrolls, and it never pushes anything
   off. What it is, what you can do, then what is connected. */
.peek {
  position: fixed; z-index: 940;
  width: 20rem; max-width: calc(100dvw - 1.25rem);
  max-height: min(70vh, 32.5rem);
  display: flex; flex-direction: column;
  background: var(--panel-bg); border: 0.0625rem solid var(--glass-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl);
  backdrop-filter: blur(0.75rem);
  padding: 0.75rem;
  opacity: 0; transform: translateY(4px);
}
.peek.is-open { opacity: 1; transform: translateY(0); }
.peek[hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .peek { transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out); }
}

.peek-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.375rem; }
.peek-back, .peek-close {
  width: 1.375rem; height: 1.375rem; flex-shrink: 0; border-radius: var(--r-sm);
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; color: var(--ink-muted); cursor: pointer;
}
.peek-close { margin-left: auto; }
.peek-back:hover, .peek-close:hover { background: rgba(255,255,255,0.08); color: var(--d50); }
:root[data-theme="light"] .peek-back:hover,
:root[data-theme="light"] .peek-close:hover { background: rgba(16,24,40,0.06); }
.peek-back:focus-visible, .peek-close:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.peek-title {
  font-family: var(--font-display); font-size: var(--ty-body); font-weight: var(--fw-bold);
  color: var(--d50); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* What kind of thing this is — the one place a bare noun earns its keep,
   because "Client" is the answer to a question the title cannot answer. */
.peek-kind {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-faint);
  flex-shrink: 0;
}
.peek-facts {
  margin: 0 0 0.75rem; font-size: var(--ty-meta); line-height: 1.55; color: var(--ink-muted);
}

.peek-head, .peek-facts, .peek-foot { flex-shrink: 0; }
/* The only scrolling part, and it is last, so nothing it holds can push an
   action out of reach. An entity has no body at all now, and an empty box
   must not leave its rule behind. */
.peek-body {
  display: flex; flex-direction: column; gap: 0.75rem;
  min-height: 0; overflow-y: auto;
  margin-top: 0.75rem; padding-top: 0.75rem; border-top: 0.0625rem solid var(--hairline);
}
.peek-body:empty { display: none; }
.peek-group { display: flex; flex-direction: column; gap: 0.125rem; }
.peek-phrase { font-size: var(--ty-meta); color: var(--ink-faint); }
.peek-items { display: flex; flex-direction: column; gap: 0.125rem; }
.peek-item {
  display: flex; align-items: baseline; gap: 0.5rem; width: 100%;
  padding: 0.25rem 0.375rem; margin-left: -6px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--d100);
}
.peek-item svg { color: var(--ink-faint); flex-shrink: 0; align-self: center; }
.peek-item:hover { background: rgba(255,255,255,0.06); }
:root[data-theme="light"] .peek-item:hover { background: rgba(16,24,40,0.05); }
.peek-item:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.peek-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.peek-by { font-size: var(--ty-micro); color: var(--ink-faint); flex-shrink: 0; }

/* Above the body, not below it — this is the "what you can do" zone and it is
   the one thing that must never need scrolling to find. */
.peek-foot { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.peek-foot:empty { display: none; }

.peek-act {
  cursor: pointer; font-family: var(--font-sans);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  padding: 0.25rem 0.75rem; border-radius: var(--r-pill);
  background: none; border: 0.0625rem solid var(--card-border); color: var(--d200);
}
.peek-act:hover { border-color: var(--accent); color: var(--ink-action); }
.peek-act:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* ═══════════════════════════════════════════════
   THE FOLDER VIEW

   Built on the library's `.tree` — native <details>, so open branches survive a
   re-render without any state of our own. What is added is what a folder view
   of *documents* needs and a generic tree does not: a count per branch, a
   flag for how many under it need a person, and a way to narrow the surface to
   a branch rather than merely expanding it.
═══════════════════════════════════════════════ */
.ws-tree {
  max-width: none;
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-lg);
  background: var(--card-bg);
  padding: 0.375rem;
}

.ws-tree summary { padding: 0.5rem 0.75rem; gap: 0.5rem; }
.ws-tree .ws-tree-col > summary { font-size: var(--ty-meta); color: var(--d100); }
.ws-tree .ws-tree-type > summary { font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--ink-muted); }
.ws-tree summary svg { color: var(--ink-faint); }
.ws-tree summary:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }

.ws-tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* How many, and how many of those want something from you. The count is
   neutral; the second number is not, so it does not pretend to be. */
.ws-tree-n {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
  background: rgba(255,255,255,0.05); border-radius: var(--r-pill);
  padding: 0.125rem 0.5rem; flex-shrink: 0;
}
:root[data-theme="light"] .ws-tree-n { background: rgba(16,24,40,0.05); }
.ws-tree-needs {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-warn); background: var(--warn-bg);
  border-radius: var(--r-pill); padding: 0.125rem 0.5rem; flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

/* Expanding a branch and filtering to it are different things, so they are
   different controls. Revealed on hover and focus, never hidden from the
   keyboard. */
.ws-tree-only {
  flex-shrink: 0; cursor: pointer; opacity: 0;
  font-family: var(--font-sans); font-size: var(--ty-micro); font-weight: var(--fw-bold);
  padding: 0.125rem 0.5rem; border-radius: var(--r-pill);
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-muted);
  transition: opacity var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.ws-tree summary:hover .ws-tree-only,
.ws-tree-only:focus-visible { opacity: 1; }
.ws-tree-only:hover { color: var(--ink-action); border-color: var(--accent); }
.ws-tree-only:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }

.ws-tree .tree-children { padding-left: 1.25rem; border-left: 0.0625rem solid var(--hairline); margin-left: 1rem; }

.ws-tree-doc {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.5rem 0.75rem; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--d200);
}
.ws-tree-doc:hover { background: rgba(255,255,255,0.045); color: var(--d50); }
:root[data-theme="light"] .ws-tree-doc:hover { background: rgba(16,24,40,0.04); }
.ws-tree-doc:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.ws-tree-doc-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-tree-doc-who {
  font-size: var(--ty-meta); color: var(--ink-faint); flex-shrink: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 7.5rem;
}
.ws-tree-doc .trust-state { flex-shrink: 0; }

/* ── The graph, inside the folders ──

   A document's claims on the line beneath it, so the connections are part of
   the structure rather than one click away inside each document. Only the
   asserted ones: the implicit edges ARE the tree's hierarchy, and printing
   "Filed in Policies" inside the Policies folder says nothing. */
.ws-tree-leaf { display: flex; flex-direction: column; }
.ws-tree-edges {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.125rem 0.5rem;
  margin: 0 0 0.25rem; padding: 0 0.75rem 0 2rem;
}
.ws-tree-edge {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--ink-faint);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The relationship word IS the row's content — "Answers", "Replaces". `--d600`
   is the separator grey, and at 2.49:1 the naming half of the phrase was
   quieter than the line it sat on. */
.ws-tree-edge em { font-style: normal; color: var(--ink-faint); }
.ws-tree-edge:hover, .ws-tree-edge:hover em { color: var(--ink-action); }
.ws-tree-edge:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }

/* A group header is a thing, not a heading — its name opens what else touches
   it. Where the axis has no entity behind it (a type), it stays plain text. */
.ws-tree-name.is-ent {
  background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
  font-family: inherit; font-size: inherit; font-weight: inherit; color: inherit;
  border-bottom: 0.0625rem dotted transparent;
}
.ws-tree-name.is-ent:hover { color: var(--ink-action); border-bottom-color: var(--accent); }
.ws-tree-name.is-ent:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* The documents an axis says nothing about are a finding, so they read as one. */
.ws-tree-name.is-none { color: var(--ink-faint); font-style: italic; }

.k-group .v2-dropdown-panel { min-width: 9.375rem; }

/* ── The utilities recede; one thing is an action ──

   Two passes were spent on this row. The first left heights 26/27/32, radii
   9999/8/8 and type 10/16/12 — dimensional chaos. Equalising all three fixed
   that and exposed what was actually wrong: three bordered boxes and three
   saturated elements in 418px, with the accent purple appearing TWICE meaning
   two different things — a sort state and a selected view — beside a solid blue
   button. And equal weight made three peers out of two utilities and one
   action.

   Needs attention, Cards/Folders and the grouping picker are all ways of
   LOOKING. They lose their boxes entirely: muted ink, nothing drawn at rest, a
   plain neutral fill when active — state by value, not by hue. New document is
   the only control that creates something, so it is the only filled control and
   the only saturated thing left in the row. */
/* ── Two utilities, two species ──

   They were made identical, which fixed the dimensional chaos and created a
   different error: a binary modifier and a mutually-exclusive view selection
   are not the same kind of control, and reading alike they invited you to
   expect one to behave like the other.

   Needs attention is a filter chip — a pill, outlined at rest, filled when on.
   Cards / Folders is a segmented switch — borderless text, one of two lit. The
   shape carries the difference; neither takes a hue, so New document is still
   the only saturated thing in the row. */
.rm-sort {
  display: inline-flex; align-items: center; gap: 0.375rem;
  height: 2rem; padding: 0 0.75rem; border-radius: var(--r-pill);
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-quiet);
  font-family: var(--font-sans); font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  cursor: pointer; white-space: nowrap;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.rm-sort svg { flex-shrink: 0; opacity: 0.6; }

/* The view switch loses the library's inset track with it — a track is a box,
   and this row has no boxes left. */
/* `border: 0`, not a transparent colour — a transparent 1px border is still
   1px, and it made the track 34px in a 32px row. */
.rm-views { background: none; border: 0; padding: 0; gap: 0.125rem; }
.rm-views .seg-btn {
  display: inline-flex; align-items: center; gap: 0.375rem;
  height: 2rem; padding: 0 0.5rem; border-radius: var(--r-md);
  background: none; box-shadow: none; color: var(--ink-muted);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
}
.rm-views .seg-btn svg { opacity: 0.6; }

/* `.k-filter .v2-dropdown-btn` sets 30px at equal specificity and later in the
   file, so this has to out-weigh it rather than tie with it. */
.rm-end .k-filter .v2-dropdown-btn {
  height: 2rem; padding: 0 0.5rem; border-radius: var(--r-md);
  background: none; border: 0; color: var(--ink-muted);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
}

/* One resting state, one hover, one active — shared by every utility here, so
   they cannot drift apart again. */
.rm-sort:hover,
.rm-views .seg-btn:hover,
.rm-end .k-filter .v2-dropdown-btn:hover { background: rgba(255,255,255,0.05); color: var(--d100); }
.rm-sort:hover { border-color: var(--card-border-hover); }
/* The chip fills and its outline goes with it; the segment only lights up. */
.rm-sort.is-on {
  background: rgba(255,255,255,0.1); border-color: transparent; color: var(--d50);
}
.rm-views .seg-btn.active { background: rgba(255,255,255,0.08); color: var(--d50); }
.rm-sort.is-on svg,
.rm-views .seg-btn.active svg { opacity: 1; }
:root[data-theme="light"] .rm-sort:hover,
:root[data-theme="light"] .rm-views .seg-btn:hover,
:root[data-theme="light"] .rm-end .k-filter .v2-dropdown-btn:hover { background: rgba(16,24,40,0.05); }
:root[data-theme="light"] .rm-sort.is-on { background: rgba(16,24,40,0.1); }
:root[data-theme="light"] .rm-views .seg-btn.active { background: rgba(16,24,40,0.07); }
.rm-sort:focus-visible,
.rm-views .seg-btn:focus-visible,
.rm-end .v2-dropdown-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }

.rm-end .entry-action { height: 2rem; padding: 0 1rem; border-radius: var(--r-md); font-size: var(--ty-meta); }

@media (max-width: 720px) {
  .ws-tree-doc-who { display: none; }
  .ws-tree-only { opacity: 1; }
}

/* ═══════════════════════════════════════════════
   THE DOCUMENT'S TYPE SCALE

   Measured before changing anything: the title was 17px against a 13px body —
   a 1.3× step — with labels at 10px and 8.5px. That is a compressed range, not
   a hierarchy: everything arrives at once and nothing leads. And the body and
   the comments were 13px vs 12.5px in two greys eight values apart, stacked in
   the same column with no rule between them, so there was no way to tell where
   the document ended. Both are the same defect — no contrast — in two places.

   The scale is the product's density scale now, so the document reads on the
   same ladder as the card and the rail rather than on a fourth private one:
   --ty-display / --ty-body / --ty-meta / --ty-micro. The body keeps its measure
   and its line height, which are what let it read as prose rather than as a
   field value.

   The 9px and 10px runs below are gone. They were the apparatus AROUND the
   document — governance labels, section heads — and stepping apparatus down is
   right, but 9px at --d500 is not a step down, it is a step out. They step down
   by weight, tracking and ink instead, at the floor.
═══════════════════════════════════════════════ */
.doc-scroll .dv-title {
  font-size: var(--ty-display);
  font-weight: var(--fw-extrabold);
  line-height: 1.18;
  letter-spacing: -0.02em;
  margin-bottom: 0.75rem;
  text-wrap: balance;
}

.doc-scroll .dv-body {
  font-size: var(--ty-body);
  line-height: var(--lh-relaxed);
  color: var(--ink-secondary);
  /* Prose wants a measure. At 620px and 13px the body ran to ~95 characters,
     which is where the eye starts losing the line it is on.

     AND 66ch WAS NOT 66 CHARACTERS — it renders 84 in Poppins. See the
     --measure-prose note beside the other measures. */
  max-width: var(--measure-prose);
}
.doc-scroll .dv-body p { margin: 0 0 1rem; }
.doc-scroll .dv-body p:first-child { color: var(--ink-primary); font-size: var(--ty-title); line-height: 1.5; }
.doc-scroll .dv-body h3 {
  font-family: var(--font-display);
  font-size: var(--ty-title); font-weight: var(--fw-bold); color: var(--ink-primary);
  letter-spacing: -0.008em;
  margin: 1.5rem 0 0.5rem;
}
.doc-scroll .dv-body h3:first-child { margin-top: 0; }
.doc-scroll .dv-body h4 { font-size: var(--ty-body); font-weight: var(--fw-bold); margin: 1.25rem 0 0.375rem; }
.doc-scroll .dv-body ul, .doc-scroll .dv-body ol { margin: 0 0 1rem; padding-left: 1.5rem; }
.doc-scroll .dv-body li { margin-bottom: 0.375rem; }
.doc-scroll .dv-body blockquote {
  margin: 0 0 1rem; padding-left: 1rem;
  border-left: 0.125rem solid var(--accent);
  color: var(--ink-secondary); font-style: italic;
}
.doc-scroll .dv-body strong { color: var(--ink-primary); font-weight: var(--fw-semibold); }

/* The governance strip and the detail block are apparatus around the document,
   so they step down rather than competing with it. */
.doc-scroll .dv-gov-label { font-size: var(--ty-micro); letter-spacing: var(--ls-wide); color: var(--ink-faint); }
.doc-scroll .dv-gov-val   { font-size: var(--ty-meta); color: var(--ink-quiet); }
.doc-scroll .dv-section-label {
  font-size: var(--ty-meta); letter-spacing: var(--ls-wide); color: var(--ink-quiet);
  padding-bottom: 0.5rem; border-bottom: 0.0625rem solid var(--hairline); margin-bottom: 0.75rem;
}
.doc-scroll .dv-section { margin-top: 1.5rem; }

/* ── Where the document ends ──

   A labelled rule, a step down in size, and a tint behind the thread. Any one
   of the three would answer "is this the document or a comment about it"; all
   three make it unmissable, which is what it was not. */
.doc-scroll .comment-thread {
  margin-top: 2rem;
  padding: 1rem 1.25rem 1rem;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,0.022);
  border: 0.0625rem solid var(--hairline);
  display: flex; flex-direction: column; gap: 0.75rem;
}
:root[data-theme="light"] .doc-scroll .comment-thread { background: rgba(16,24,40,0.022); }
/* A real head, not a ::before — it carries a count now, and generated content
   cannot hold two things or be read out in order by a screen reader. */
.comment-thread-head { display: flex; align-items: center; gap: 0.5rem; }
.comment-thread-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint);
}
.comment-thread-n {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  color: var(--ink-muted); font-variant-numeric: tabular-nums;
  background: rgba(255,255,255,0.06); border-radius: var(--r-pill); padding: 0.125rem 0.5rem;
}
:root[data-theme="light"] .comment-thread-n { background: rgba(16,24,40,0.06); }
.comment-empty {
  margin: 0; font-size: var(--ty-meta); line-height: 1.6; color: var(--ink-faint);
  max-width: 52ch;
}

/* ── A reported problem is not a remark ──

   Both live in this thread because both are things said ABOUT the document
   without changing it. They read differently because they ask different things
   of the owner: one is a conversation, the other is a job with a finish. */
.comment.is-problem {
  padding: 0.75rem; border-radius: var(--r-md);
  background: color-mix(in srgb, var(--err) 5%, transparent); border: 0.0625rem solid color-mix(in srgb, var(--err) 20%, transparent);
}
.comment-flag {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-err);
}
.comment-flag svg { flex-shrink: 0; }
.comment-resolve {
  margin-top: 0.5rem; padding: 0.25rem 0.75rem; border-radius: var(--r-md);
  background: none; border: 0.0625rem solid var(--card-border); color: var(--ink-muted);
  font-family: var(--font-sans); font-size: var(--ty-micro); cursor: pointer;
}
.comment-resolve:hover { border-color: var(--card-border-hover); color: var(--d50); }
.comment-resolve:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
/* Closed, not gone. The document keeps the record of what was raised and that
   somebody dealt with it — deleting it would lose the half that matters. */
.comment.is-problem.is-done {
  background: none; border-color: var(--hairline);
}
.comment.is-done .comment-flag { color: var(--ink-faint); }
.comment.is-done .comment-text { text-decoration: line-through; opacity: 0.7; }

/* The report's own form, in the commit surface. */
.rp-form { display: flex; flex-direction: column; gap: 0.375rem; margin-top: 1rem; }
.rp-what { min-height: 4.5rem; padding: 0.5rem 0.75rem; resize: vertical; line-height: var(--lh-base); }
.rp-hint { margin: 0; font-size: var(--ty-meta); color: var(--ink-faint); }
.doc-scroll .comment-text { font-size: var(--ty-meta); line-height: 1.6; color: var(--ink-muted); }
.doc-scroll .comment-author { font-size: var(--ty-meta); }
.doc-scroll .comment { max-width: none; }

/* The editor's body is the same document, so it reads the same. Its own
   affordances — the contenteditable outline, the placeholder — sit on top. */
.wb-editor-split .dv-body { font-size: var(--ty-body); line-height: var(--lh-relaxed); max-width: var(--measure-prose); }
.wb-editor-split .dv-body p:first-child { font-size: var(--ty-body); color: inherit; }
.wb-editor-split .dv-title { font-size: var(--ty-display); }
/* Editing needs to look like editing: the body carries a faint frame so its
   edge is visible before you click into it. */
#editBody {
  padding: 1rem 1rem; margin: 0 -16px;
  border-radius: var(--r-md);
  border: 0.0625rem solid transparent;
  transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
#editBody:hover { border-color: var(--hairline); }
#editBody:focus {
  outline: none;
  border-color: var(--card-border-hover);
  background: rgba(255,255,255,0.02);
}
:root[data-theme="light"] #editBody:focus { background: rgba(16,24,40,0.015); }

/* ═══════════════════════════════════════════════
   SEARCH INSIDE A FILTER

   The library's dropdown ships letter typeahead, which finds a value you can
   already spell. A client list is the case where you cannot, so this narrows
   the options instead of jumping between them. See GAPS.md.
═══════════════════════════════════════════════ */
.dd-search {
  display: flex; align-items: center; gap: 0.375rem;
  padding: 0.5rem 0.5rem; margin: -2px -2px 0.25rem;
  border-bottom: 0.0625rem solid var(--hairline);
  position: sticky; top: -2px; z-index: 1;
  background: var(--panel-bg, var(--card-bg));
}
.dd-search svg { color: var(--ink-faint); 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(--ty-meta); color: var(--d50);
}
.dd-search input::placeholder { color: var(--ink-faint); }
.dd-none {
  padding: 0.75rem;
  font-size: var(--ty-meta); color: var(--ink-faint); font-style: italic;
}

/* ═══════════════════════════════
   MULTI-SELECT FILTERS — Client and Product

   Borrows the dropdown's shape so the row does not have one control that looks
   foreign, and adds the three parts the library's select does not have: a
   checkbox, a group heading, and *Select all*.

   Not `.k-multi` — that name is taken by the rail's own multi-select, which is
   a 320px field over four options and cannot carry a searched, grouped, counted
   list of twenty-five. Both are re-implementations of one trigger the library
   does not expose; recorded in GAPS.md §45.
═══════════════════════════════ */
.k-facet { position: relative; display: flex; }

/* Mirrors `.k-filter .v2-dropdown-btn` from the same tokens. It cannot BE one:
   aimy-ds.js binds that class globally and would close the panel on choose. */
.k-facet-btn {
  display: flex; align-items: center; gap: 0.3125rem;
  height: 1.875rem; padding: 0 0.75rem; border-radius: var(--r-md);
  border: 0.0625rem solid var(--card-border); background: none;
  font-family: var(--font-sans); font-size: var(--ty-meta);
  font-weight: var(--fw-semibold); color: var(--ink-quiet);
  cursor: pointer; white-space: nowrap;
  transition: border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.k-facet-btn:hover { border-color: var(--card-border-hover); color: var(--ink-primary); }
.k-facet-btn svg { width: 0.5625rem; height: 0.5625rem; opacity: 0.7; }
.k-facet-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.k-facet-btn.active-filter { border-color: var(--brand); color: var(--ink-primary); }

/* Same glass as `.v2-dropdown-panel` and `.k-facet`'s sibling in the props
   panel, down to the light-mode flip — two panels open on one row with
   different backgrounds is the kind of thing that reads as a bug. */
.k-facet-panel {
  position: absolute; top: calc(100% + 0.25rem); left: 0; z-index: 200;
  min-width: 15rem; max-height: 22rem; overflow-y: auto;
  padding: 0.25rem; border-radius: var(--r-lg);
  background: var(--d800); border: 0.0625rem solid rgba(255,255,255,0.10);
  box-shadow: 0 0.5rem 2rem rgba(0,0,0,0.5), inset 0 0.0625rem 0 rgba(255,255,255,0.06);
}
:root[data-theme="light"] .k-facet-panel {
  background: #ffffff; border-color: rgba(16,24,40,0.12);
  box-shadow: 0 0.5rem 2rem rgba(16,24,40,0.16);
}

/* Which client a product belongs to, stated once above its rows rather than
   repeated into fourteen labels. */
/* A group with no heading is separated by a rule instead of a word — but only
   when there is something above it to be divided FROM. So the rule is hung off
   a preceding group rather than off `:first-child`, which never matches here:
   the search field and *Select all* are the panel's first children, and the
   client list's one untitled group would have drawn a line under nothing.

   `:not([hidden])` because the search hides whole groups, and a divider under a
   group that is no longer on screen is the same line under nothing again. */
.k-facet-group:not([hidden]) ~ .k-facet-group.is-untitled {
  margin-top: 0.25rem; padding-top: 0.25rem;
  border-top: 0.0625rem solid var(--hairline);
}

.k-facet-group-label {
  padding: 0.5rem 0.625rem 0.25rem;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-faint);
}

.k-facet-opt {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.375rem 0.625rem; border-radius: var(--r-sm);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--d200);
  cursor: pointer; white-space: nowrap;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.k-facet-opt:hover { background: rgba(255,255,255,0.06); color: var(--d50); }
.k-facet-opt:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
:root[data-theme="light"] .k-facet-opt:hover { background: rgba(16,24,40,0.05); }
.k-facet-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* A real box, because the screenshot's picker is one and because a tick with
   no frame around it gives an unchecked row nothing to be the absence of. */
.k-facet-check {
  width: 0.875rem; height: 0.875rem; flex-shrink: 0;
  border: 0.0938rem solid var(--card-border-hover); border-radius: var(--r-xs);
  position: relative; transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.k-facet-opt.is-on .k-facet-check { background: var(--brand); border-color: var(--brand); }
.k-facet-opt.is-on .k-facet-check::after {
  content: ""; position: absolute; left: 0.25rem; top: 0.0625rem;
  width: 0.25rem; height: 0.5rem; border: solid #fff;
  border-width: 0 0.125rem 0.125rem 0; transform: rotate(45deg);
}
.k-facet-opt.is-on { color: var(--ink-primary); }

.k-facet-all {
  width: 100%; text-align: left;
  padding: 0.375rem 0.625rem; margin-bottom: 0.125rem;
  border: 0; border-radius: var(--r-sm); background: none;
  font-family: var(--font-sans); font-size: var(--ty-micro);
  font-weight: var(--fw-bold); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-action); cursor: pointer;
}
.k-facet-all:hover { background: rgba(255,255,255,0.06); }
:root[data-theme="light"] .k-facet-all:hover { background: rgba(16,24,40,0.05); }


/* ═══════════════════════════════════════════════
   DATE RANGE FILTERS

   The panel holds the design system's `.cal` unchanged — head, nav, grid, dow,
   and the `.muted` / `.today` / `.selected` day states are all the library's.

   Three states are added, because `.cal` is a single-date picker and a range
   has ends: `.range-start`, `.range-end` and `.in-range`. Recorded in GAPS.md.
   They are built to sit on top of `.selected` rather than replace it, so a
   one-day range still looks like the library's selected day.
═══════════════════════════════════════════════ */
/* Which of the four date axes the range applies to. The segmented control is
   the design system's; it sits above the calendar because it changes what the
   calendar means. */
.k-date-field {
  display: flex; flex-direction: column; gap: 0.375rem;
  padding-bottom: 0.75rem; margin-bottom: 0.5rem;
  border-bottom: 0.0625rem solid var(--hairline);
}
.k-date-field-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-faint);
}
.k-date-field .seg { width: 100%; }
.k-date-field .seg-btn { flex: 1; padding: 0.375rem 0.5rem; font-size: var(--ty-meta); }
.k-date-body { display: flex; gap: 0.5rem; }

.k-date { position: relative; }
/* Mirrors .v2-dropdown-btn's visual rule from the same tokens. It cannot BE
   one: aimy-ds.js binds that class globally and assumes a .v2-dropdown parent. */
.k-date-btn {
  display: inline-flex; align-items: center; gap: 0.375rem;
  background: rgba(255,255,255,0.04);
  border: 0.0625rem solid rgba(255,255,255,0.09);
  border-radius: var(--r-sm); padding: 0.25rem 0.5rem 0.25rem 0.75rem;
  font-family: var(--font-sans); font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--d200); cursor: pointer; white-space: nowrap;
  transition: background-color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
/* The filter row's own treatment: chrome until it carries a value. */
.filter-row .k-date-btn {
  height: 1.875rem; padding: 0 0.75rem; border-radius: var(--r-md);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  background: none; border-color: var(--card-border); color: var(--ink-muted);
}
.filter-row .k-date-btn:hover { border-color: var(--card-border-hover); color: var(--d100); }
.k-date-btn:hover { background: rgba(255,255,255,0.07); border-color: var(--card-border-hover); }
.k-date-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }
.k-date-btn svg:last-child { width: 0.5rem; height: 0.5rem; flex-shrink: 0; opacity: 0.7; }
:root[data-theme="light"] .k-date-btn { background: rgba(16,24,40,0.03); border-color: rgba(16,24,40,0.1); }
:root[data-theme="light"] .k-date-btn:hover { background: rgba(16,24,40,0.06); }
.k-date-ico { color: var(--ink-faint); flex-shrink: 0; }
/* Same lit state the dropdowns get, from the same values — the DS scopes it to
   .v2-dropdown-btn.active-filter, which this deliberately is not. */
.k-date-btn.active-filter,
.filter-row .k-date-btn.active-filter {
  background: rgba(51,105,255,0.10); border-color: rgba(99,169,255,0.32); color: var(--ai-text);
}
:root[data-theme="light"] .k-date-btn.active-filter,
:root[data-theme="light"] .filter-row .k-date-btn.active-filter {
  background: rgba(51,105,255,0.10); border-color: rgba(51,105,255,0.32); color: #1f5fd0;
}
.k-date-btn.active-filter .k-date-ico { color: inherit; }
.k-date.is-open > .k-date-btn { border-color: var(--card-border-hover); }

.k-date-panel {
  position: absolute; top: calc(100% + 0.375rem); left: 0; z-index: 60;
  display: flex; flex-direction: column;
  padding: 0.75rem;
  background: var(--panel-bg, var(--card-bg));
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
}


.k-date-quick {
  display: flex; flex-direction: column; gap: 0.125rem;
  width: 8.25rem; flex-shrink: 0;
  padding-right: 0.75rem; border-right: 0.0625rem solid var(--hairline);
}
.k-date-q {
  text-align: left; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta);
  padding: 0.375rem 0.5rem; border-radius: var(--r-sm);
  background: none; border: 0; color: var(--ink-muted);
}
.k-date-q:hover { background: rgba(255,255,255,0.06); color: var(--d50); }
:root[data-theme="light"] .k-date-q:hover { background: rgba(16,24,40,0.05); }
.k-date-q.is-on { background: var(--accent-dim); color: var(--ink-action); font-weight: var(--fw-bold); }
.k-date-q:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }

/* The library's day is a <div>. A range needs to be operable, so it is a
   <button> here — same rule as everywhere else in this product. */
.k-date-panel .cal { box-shadow: none; border: 0; padding: 0; width: 14.875rem; }
.k-date-panel .cal-day {
  border: 0; font-family: var(--font-sans);
  background: none; padding: 0;
}
.k-date-panel .cal-day:disabled { opacity: 0.3; cursor: not-allowed; }
.k-date-panel .cal-day:disabled:hover { background: none; }
.k-date-panel .cal-day:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.k-date-panel .cal-day.today { border: 0.0625rem solid var(--card-border-hover); }

/* ── The three range states ── */
.cal-day.in-range {
  background: var(--accent-dim);
  color: var(--d50);
  border-radius: 0;
}
/* Square the inner edges so the run reads as one band rather than a row of
   separate pills. */
.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;  }
/* A band that breaks across a week should not look broken at the wrap. */
.cal-grid > .cal-day.in-range:nth-child(7n+1) { border-top-left-radius: var(--r-sm); border-bottom-left-radius: var(--r-sm); }
.cal-grid > .cal-day.in-range:nth-child(7n)   { border-top-right-radius: var(--r-sm); border-bottom-right-radius: var(--r-sm); }

.cal-hint {
  margin-top: 0.5rem; padding-top: 0.5rem;
  border-top: 0.0625rem solid var(--hairline);
  font-size: var(--ty-micro); color: var(--ink-faint);
}

@media (max-width: 720px) {
  .k-date-body { flex-direction: column; }
  .k-date-quick { width: auto; border-right: 0; border-bottom: 0.0625rem solid var(--hairline); padding: 0 0 0.5rem; }
}

/* ═══════════════════════════════════════════════
   PROTOTYPE CONTROLS — not product UI.

   Collapsed to an icon in the bottom-right corner, expanding upward. Fixed, so
   it cannot push anything — but "does not push" was never the whole problem:
   the earlier bar sat ON the input, because the input is centred at
   `min(760px, 100vw - 48px)` and its right edge reaches `vw/2 + 380`. A 32px
   icon at `right: 14px` starts at `vw - 46`, so the two clear each other only
   above ~850px. Below that the control lifts above the input band instead of
   overlapping it. The bottom-centre band belongs to the input and the toast.
═══════════════════════════════════════════════ */
.proto {
  position: fixed; right: 0.875rem; z-index: 960;
  /* Above the input band, not in it. The input is centred at
     min(760px, 100vw - 48px), so its right edge reaches vw/2 + 380 and the
     236px panel's left edge is vw - 250 — they clear each other only above
     ~1260px. Sitting the whole control just above the input's top edge is one
     rule that holds at every width, and it does not move when it opens. */
  bottom: 5.75rem;
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem;
}

.proto-toggle {
  width: 2rem; height: 2rem; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--card-bg-raised);
  border: 0.0625rem dashed var(--d600);
  color: var(--ink-muted); cursor: pointer;
  opacity: 0.45;
  transition: opacity var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.proto-toggle svg { width: 0.9375rem; height: 0.9375rem; }
.proto-toggle:hover { opacity: 1; color: var(--d200); }
.proto-toggle:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; opacity: 1; }
.proto.is-open .proto-toggle { opacity: 1; color: var(--d100); border-style: solid; }

/* Grows upward from the icon, so it never reaches down into the input. */
.proto-panel {
  order: -1;
  width: 14.75rem; max-height: min(62vh, 28.75rem); overflow-y: auto;
  display: flex; flex-direction: column; gap: 0.75rem;
  padding: 0.75rem; border-radius: var(--r-md);
  background: var(--card-bg-raised);
  border: 0.0625rem dashed var(--d600);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-mono); font-size: var(--ty-meta);
}
.proto-panel[hidden] { display: none; }

.proto-group { display: flex; flex-direction: column; gap: 0.25rem; }
.proto-group-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium); color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.proto-group-note { font-size: var(--ty-meta); color: var(--ink-muted); line-height: 1.35; }
.proto-links { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.proto-link {
  font-family: var(--font-mono); font-size: var(--ty-meta); line-height: 0.875rem;
  padding: 0.25rem 0.5rem; border-radius: var(--r-sm);
  background: rgba(255,255,255,0.05); border: 0; color: var(--ink-muted); cursor: pointer;
}
.proto-link:hover { background: rgba(255,255,255,0.1); color: var(--d50); }
.proto-link:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }
:root[data-theme="light"] .proto-link { background: rgba(16,24,40,0.05); }
:root[data-theme="light"] .proto-link:hover { background: rgba(16,24,40,0.1); }

/* ═══════════════════════════════════════════════
   THE SIDEBAR BRIEFING

   The rail is no longer navigation, so .sidebar-nav's metrics do not apply.
   What it holds is a composed briefing whose every item is a filter link.

   Measured before this pass: SIX type styles, and every one of them was 10px
   or 11px. A 1px range across a whole surface is not a compressed hierarchy,
   it is the absence of one — the panel whose only job is to say what to do
   first had no way to say which of six things came first. Three of the six
   also failed AA, the section label worst at 3.54:1.

   It gets the full five ranks now, at the panel's density setting. The rail is
   268px, so --ty-title and --ty-body step down with the rest of the panels. */
═══════════════════════════════════════════════ */
:root { --rail-width: 16.75rem; }

.app-sidebar { border-right: 0.0625rem solid var(--hairline); }
.brief {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 1.25rem 1rem 0.5rem;
  display: flex; flex-direction: column; gap: 0.5rem;
}

/* ══ THE WAY BACK TO THE CHAT ══════════════════════════════════════════
   Pinned to the foot of the briefing rail, opposite the gate's Console link,
   so the two surfaces point at each other from the same corner of the screen.
   Quiet on purpose: it is a way out, not the thing you came here to do.

   `brief-rail-foot`, not `rail-foot`: the document's rail has a foot of its
   own — Report a problem and Archive — and the two shared a name. This rule
   is the later of the two, so it won, and the document's foot was drawing a
   border-top directly under the last rail block's border-bottom. Two hairlines,
   two pixels apart, under a heading that needed no rule at all. The gate's
   equivalent was already `gate-rail-foot`; this follows it. */
.brief-rail-foot {
  flex-shrink: 0;
  padding: 0.5rem;
  border-top: 0.0625rem solid var(--hairline);
}
.rail-back {
  display: flex; align-items: center; gap: 0.625rem;
  padding: 0.5rem 0.625rem;
  border-radius: var(--r-sm);
  color: var(--ink-quiet); text-decoration: none;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.rail-back:hover { background: var(--card-bg-raised); color: var(--ink-primary); }
.rail-back:active { transform: translateY(0.0625rem); }
.rail-back:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.rail-back svg { flex-shrink: 0; opacity: 0.75; }
/* One line, not two — see the note at .gate-console-name in chat.css. The
   truncation moves onto the name, which is what is left to truncate. */
.rail-back-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
}
.brief::-webkit-scrollbar { width: 0.25rem; }
.brief::-webkit-scrollbar-track { background: transparent; }
.brief::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 0.125rem; }
:root[data-theme="light"] .brief::-webkit-scrollbar-thumb { background: rgba(16,24,40,0.12); }


.brief-scope {
  font-size: var(--ty-micro); color: var(--ink-faint); line-height: var(--lh-snug);
  padding-bottom: 0.75rem; border-bottom: 0.0625rem solid var(--hairline);
}
.brief-scope-more { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.25rem; flex-wrap: wrap; }
.brief-scope-link {
  background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit;
  font-size: inherit; color: var(--ink-quiet); border-bottom: 0.0625rem dotted var(--ink-faint);
}
.brief-scope-link:hover { color: var(--ink-action); }
.brief-withheld { color: var(--ink-warn); font-weight: var(--fw-semibold); }

/* ── THE QUIETEST OF THREE LEVELS ──
   It was `--ink-primary`, which was right while it labelled two sections of a
   flat list. The settings rail is a tree now: group label, page name, page
   state. The group is a signpost read once on the way past; the page name is
   what somebody came to find. A label at full contrast over children at less
   inverted that, so the eye landed on "CLIENT" before "Dynamic fields".

   Same rule as `.set2-sec-t` on the page itself, and deliberately so — the
   rail is a table of contents for that surface and the two should agree about
   what is loud. */
.brief-section-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint); margin-top: 1rem;
}
.rail-cfg-back + .brief-section-label { margin-top: 0.25rem; }
/* A bare shelf takes two ways in, and the second one needs naming: the file
   route existed as a drag gesture and nothing else, which is invisible until
   you are already doing it. */
.empty-state-fine {
  margin: 0.75rem 0 0; font-size: var(--ty-meta); color: var(--ink-quiet);
  max-width: 20rem; line-height: var(--lh-snug);
}
/* The rail's *Since Thursday* findings block and every rule it needed —
   .brief-list, .brief-quiet, .brief-entry, .brief-text and .brief-go — have
   been removed. The findings themselves were never this panel's: needsYou()
   computes them and `AiMY noticed` states them above the grid, against the set
   you filtered to rather than at the whole corpus. Two panels rendering one
   model on one screen is the same news twice, and this was the weaker telling
   in the more valuable column. .brief-section-label stays: the two sections
   that are only here still need naming. */
/* ── The last five sets you looked at ──

   What stood here was one button, "Resume your last filter", over a single
   slot in sessionStorage. Eleven filters left the row above on the argument
   that the input already speaks them; this is the other half of that trade —
   typing a combination once is cheap, and typing it every Monday is not.

   Rows rather than chips. A chip is a value you can remove, which is what the
   chip bar above the grid is for; these are whole sets you go back to, and a
   row that spans the column can carry three of their labels instead of
   clipping one. */
.brief-recents { display: flex; flex-direction: column; gap: 0.125rem; }
.brief-recent {
  display: flex; align-items: center; width: 100%;
  padding: 0.375rem 0.5rem; margin-left: -8px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-meta); color: var(--ink-quiet);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.brief-recent:hover { background: rgba(255,255,255,0.05); color: var(--ink-primary); }
:root[data-theme="light"] .brief-recent:hover { background: rgba(16,24,40,0.05); }
.brief-recent:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.brief-recent-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ═══════════════════════════════════════════════
   THE PAGE — greeting, filters, chips
═══════════════════════════════════════════════ */


.filter-row {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.filter-row.is-more { padding-top: 0.125rem; }
.filter-row-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

/* ══ AND WHEN THE ROW WRAPS, THE PUSHER HAS TO LET GO ══════════
   `margin-left: auto` separates the field from the facets while they share a
   line. It keeps doing it after they stop sharing one — an auto margin applies
   per FLEX LINE, so once the search drops to a line of its own it is shoved to
   the right end of that line with nothing beside it.

   Measured: the search stays on the facets' line down to 740 and wraps at 730.
   Below that the box sat hard right with dead space to its left — 331px of it
   at 640, 251 at 480, 131 at 360 — a lone 160px control on a ragged edge while
   every other thing on the page started at the same left margin.

   So below the wrap the margin goes, and the field takes the line it has been
   given rather than hovering at one end of it. A full-width search on a phone
   is also the bigger target, and it is what the box already says it wants:
   `max-width: 100%` was there for a line it never got.

   735 IS MEASURED, NOT CHOSEN, and it is a function of the five facet labels
   — change them and it moves. Walked a pixel at a time: the row still fits on
   one line at 736 and has wrapped by 735. A rung would be wrong either way —
   720 leaves a 15px band where the field has wrapped and is still pushed, and
   768 takes the push away where the row is intact and the separation is doing
   its job.

   AND THE .98 IS NOT DECORATION. Written `max-width: 735px` this rule MISSED at
   exactly the width it was written for: measured at a reported viewport of 730,
   `matchMedia('(max-width: 730px)')` came back FALSE while the field was
   already wrapped, because the real viewport is fractional — 730.x on a display
   at non-integer scaling — and `innerWidth` rounds where the media query does
   not. The fraction has to be cleared without reaching the next whole pixel.
   AiMY Sales carries the same convention on every one of its breakpoints. */
@media (max-width: 735.98px) {
  .filter-row-end { margin-left: 0; flex: 1 1 100%; }
  .k-search { width: 100%; }
}

/* ── The corpus search ──
   Same height, radius and border as the facet buttons beside it, because it
   belongs to the same row — but it is a FIELD, so it fills rather than hugs,
   and the gap plus `margin-left: auto` on its container is what says it is not
   a fifth facet. It shrinks before it wraps: on a narrow viewport the row is
   already carrying five controls, and a search box that drops to its own line
   reads as a second toolbar. */
.k-search {
  display: inline-flex; align-items: center; gap: 0.4375rem;
  height: 1.875rem; padding: 0 0.625rem;
  min-width: 8rem; width: 14rem; max-width: 100%;
  border: 0.0625rem solid var(--card-border); border-radius: var(--r-md);
  background: none; color: var(--ink-faint);
  transition: border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.k-search:hover { border-color: var(--card-border-hover); }
/* ── ONE FOCUS RING, ON THE WRAPPER ──
   The field is a bare `<input>` inside a bordered label, and the design system
   carries a global `input:focus-visible { outline: 2px solid var(--brand);
   outline-offset: 2px }`. That is right for an unstyled input and wrong here:
   it drew a second, tighter ring around the text INSIDE this one's border, so
   focusing the search produced two brand-blue rectangles nested in each other.

   `outline: none` on the element alone did not stop it — `.k-search-i` is
   0,1,0 and `input:focus-visible` is 0,1,1, so the design system won. The
   suppression has to carry the pseudo-class to outrank it.

   What replaces it is the design system's OWN treatment for a styled field,
   `.input:focus` — border to brand with a faint brand wash — applied to the
   thing that actually looks like the field. */
.k-search-i:focus,
.k-search-i:focus-visible { outline: none; box-shadow: none; }
/* Lit when it is doing something, from the same values every other active
   filter in this row uses. */
.k-search.is-on { border-color: rgba(99,169,255,0.32); color: var(--ai-text); }
:root[data-theme="light"] .k-search.is-on { border-color: rgba(51,105,255,0.32); color: #1f5fd0; }
/* AFTER `.is-on`, deliberately. Both are 0,2,0, so whichever is written last
   wins when the field is focused AND filtering — and that should be focus,
   which is where the reader is right now, not the tint, which is a fact about
   the set. It was resolving the other way purely on source order. */
.k-search:focus-within {
  border-color: var(--brand);
  background: rgba(51, 105, 255, 0.05);
  color: var(--ink-quiet);
}
.k-search svg { flex-shrink: 0; }
.k-search-i {
  flex: 1 1 auto; min-width: 0;
  appearance: none; border: 0; background: none; padding: 0;
  font-family: inherit; font-size: var(--ty-meta); color: var(--ink-primary);
  outline: none;
}
.k-search-i::placeholder { color: var(--ink-faint); }
/* The type="search" clear affordance is a second, differently-shaped way to do
   what Escape and the filter chip already do, and it only appears in one
   engine. */
/* The UA's own clear button is suppressed in favour of the explicit one: it
   only exists in one engine, sits at a different size, and cannot be reached
   by a screen reader with a name. */
.k-search-i::-webkit-search-cancel-button { display: none; }
.k-search { cursor: text; }
.k-search-x {
  flex-shrink: 0; appearance: none; border: 0; background: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.125rem; height: 1.125rem; margin-right: -0.1875rem;
  border-radius: var(--r-pill); color: var(--ink-faint);
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.k-search-x:hover { color: var(--ink-primary); background: rgba(255,255,255,0.08); }
:root[data-theme="light"] .k-search-x:hover { background: rgba(16,24,40,0.06); }
.k-search-x:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }
@media (max-width: 52rem) { .k-search { width: 10rem; } }

/* The dropdowns are chrome until they carry a value, so they sit quiet by
   default and the component's own .active-filter does the lighting up. */
.k-filter .v2-dropdown-btn {
  height: 1.875rem; padding: 0 0.75rem; border-radius: var(--r-md);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  background: none; border-color: var(--card-border); color: var(--ink-quiet);
}
.k-filter .v2-dropdown-btn:hover { border-color: var(--card-border-hover); color: var(--ink-primary); }
.k-filter .v2-dropdown-btn svg { width: 0.5625rem; height: 0.5625rem; opacity: 0.7; }
.k-filter .v2-dropdown-panel { min-width: 10.625rem; }

.k-toggle, .k-more, .k-clear {
  height: 1.875rem; padding: 0 0.75rem; border-radius: var(--r-md);
  border: 0.0625rem solid var(--card-border); background: none; cursor: pointer;
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  color: var(--ink-quiet);
  display: inline-flex; align-items: center; gap: 0.375rem;
  transition: border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out),
              transform var(--t-press) var(--ease-out);
}
.k-toggle:hover, .k-more:hover, .k-clear:hover { border-color: var(--card-border-hover); color: var(--ink-primary); }
.k-toggle:active, .k-more:active, .k-clear:active { transform: scale(0.97); }
.k-toggle:focus-visible, .k-more:focus-visible, .k-clear:focus-visible {
  outline: 0.125rem solid var(--brand); outline-offset: 0.125rem;
}
.k-toggle.is-on {
  background: var(--accent-dim); border-color: rgba(var(--accent-rgb), 0.4); color: var(--ink-action);
}
.k-more.is-open { color: var(--ink-primary); border-color: var(--card-border-hover); }
.k-more-n {
  font-size: var(--ty-micro); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  background: var(--accent-dim); color: var(--ink-action);
  border-radius: var(--r-pill); padding: 0.125rem 0.375rem;
}
.k-clear { border-color: transparent; color: var(--ink-quiet); }

/* ═══════════════════════════════════════════════
   THE CHIP BAR — only what a dropdown cannot say
═══════════════════════════════════════════════ */
.chip-bar {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  margin-bottom: 0.75rem;
}
.chip-bar .ctx-chips { flex: 1; min-width: 0; }
/* The library draws the chip at 11px, below this product's floor. It had never
   shown here at that size because a chip was suppressed while its dropdown was
   on screen, and eleven of those dropdowns have now left the row — so the axes
   that used to be shown by a control are shown by a chip instead, and the
   component came out from under the exemption it was living in.

   Eighth component re-stepped for the same reason (GAPS §27). */
.ctx-chip { font-size: var(--ty-micro); }

/* And the confidence badge on an ICP card, at 10px with `opacity: 0.75` on top
   — 3.61 dark and 3.06 light, which is the opacity channel defeating the ink
   scale exactly as `.tag-token` did. It renders on two of the nine types, so no
   sweep of the landing set had ever surfaced it; it took filtering to a set
   that happened to contain an ICP.

   Ninth component re-stepped (GAPS §27), and the second found this way. A
   component can be off-scale and invisible for as long as something else keeps
   it off screen, which is the argument for measuring rendered pages rather than
   auditing the stylesheet. */
.conf-badge { font-size: var(--ty-micro); letter-spacing: normal; }
.conf-badge .conf-val { font-size: var(--ty-micro); opacity: 1; }
/* The badge tints itself by confidence, and it does it with the FILL hues — the
   same conflation §1.8.1 records for every other status word in the product.
   On its own 12% tint --err measured 4.13 in dark. The tints, the meter bars
   and the borders keep the original hues; only the word moves. */
.conf-high   { color: var(--ink-ok); }
.conf-medium { color: var(--ink-warn); }
.conf-low    { color: var(--ink-err); }
.ctx-chip .ctx-key {
  font-size: var(--ty-micro); font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-faint); margin-right: 0.125rem;
}

/* ═══════════════════════════════════════════════
   RESULT META — count, scope, and what is excluded
═══════════════════════════════════════════════ */
.rm {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem;
  margin-bottom: 0.75rem; padding-bottom: 0.75rem;
  border-bottom: 0.0625rem solid var(--hairline);
}
/* The count owns its own line, and the controls own the next one. Crowding all
   of it onto one row put the utilities hard against the action and made the
   right edge a cluster; separated, the row below reads as "how you are looking"
   on the left and "what you can do" on the right. */
.rm-main { flex-basis: 100%; display: flex; align-items: baseline; gap: 0.375rem; flex-wrap: wrap; min-width: 0; }
/* AND THE COUNT IS METADATA. The banner four lines up says RESULT META in
   as many words, and extrabold is not allowed there. It is still the anchor
   of its row — 20px against the 16px word beside it and 600 against that
   word’s 500 — which is the two axes a lead figure needs. */
.rm-count {
  font-family: var(--font-display); font-size: var(--ty-title); font-weight: var(--fw-bold);
  color: var(--ink-primary); letter-spacing: var(--ls-tight); font-variant-numeric: tabular-nums;
}
.rm-word { font-size: var(--ty-body); font-weight: var(--fw-semibold); color: var(--ink-secondary); }
.rm-note { font-size: var(--ty-meta); color: var(--ink-quiet); }
/* -8px pulls the borderless utilities back to the count's left edge: their
   padding is now the only thing between the text and the margin. */
.rm-end {
  flex-basis: 100%; display: flex; align-items: center; flex-wrap: wrap;
  gap: 0.25rem; margin-left: -8px;
}
.rm-end .entry-action { margin-left: auto; }
/* ══ WHAT AiMY NOTICED ABOUT THIS SET ═══════════════════════════════

   AiMY QA/Sales' `Since your last visit` block (`.slv`), ported. Not restyled —
   ported, because the form is the argument: ONE first-person paragraph with the
   figures pressable inside it, under a marked head, over a ruled strip of what
   AiMY would do about it.

   The values are NOT Sales'. `sales.css` re-declares the whole type scale on
   :root and drops Poppins, so its `--fs-3xl` is 32px where this product's is
   28px and its `--fs-base` is 14px where this product's is 13px. Copying the
   tokens across would have rendered a different block. Every step is mapped
   onto this product's `--ty-*` scale instead, and the head is sized in em so
   the mark scales with the title the way Sales' does.

   AI provenance is five things and NO badge and NO wordmark: the mark first in
   the head, the accent wash on the panel, the first-person voice in the prose,
   the accent underline on every figure, and the `--aimy-mark` hook. A thing
   that has to write "AI" on itself is a thing that does not read as AI.

   Measured over --body-bg at both ends of the gradient, both themes: every ink
   role and every semantic text hue clears AA on it — worst is --ink-action at
   5.03 dark / 4.90 light. --brand measures 3.51 / 3.71 and is therefore the p3
   DOT and never text; 3:1 is the applicable threshold for a dot. */
.ins-band {
  background: var(--aimy-wash);
  border: 0.0625rem solid var(--aimy-wash-edge);
  border-radius: var(--r-lg);
  padding: 1.25rem;
  margin-bottom: 1.25rem;
}
/* The head sets the size and the mark takes it in em, so the two scale as one
   object. Pinning the mark to a px value is what makes a logo look pasted on. */
.ins-head {
  display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;
  font-size: var(--ty-title);
}
.ins-head svg { width: 0.9em; height: 1em; flex-shrink: 0; align-self: center; }
/* A BAND HEADING IS NOT THE PAGE NAME EITHER. This inherits --ty-title from
   .ins-head, so it was 20px at the weight reserved for the 28px display
   step. Bold leaves it the heaviest thing in its band and a clear step under
   the page title above it, which is the order those two are actually in. */
.ins-title {
  margin: 0;
  font-family: var(--font-display); font-size: inherit; font-weight: var(--fw-bold);
  color: var(--ink-primary); letter-spacing: var(--ls-tight); line-height: var(--lh-tight);
}
/* Named only when the set is one you chose. On the landing set the result line
   directly above already reads "12 documents of 36". */
.ins-scope { margin-left: auto; font-size: var(--ty-meta); color: var(--ink-quiet); }
.ins-body { min-width: 0; max-width: var(--measure-prose); }
/* Full ink. The figures were the only runs in the paragraph at --ink-primary,
   so the sentence read as grey packaging around three numbers — and the
   sentence is the insight. The figures keep their accent underline, which is
   what marks them as controls; they stop being the only thing you can see. */
.ins-line { margin: 0; font-size: var(--ty-body); line-height: var(--lh-base); color: var(--ink-primary); }
/* The hinge is DEMOTED, which is the part of Sales' design that is easy to get
   wrong. "Two things need you" is the only emphasis in the sentence that is not
   pressable, so at --ink-primary/800 it outweighed the several things that are.
   Weight only, and one rung down. */
.ins-line strong { color: var(--ink-secondary); font-weight: var(--fw-semibold); }
/* ── The clause is the button ──

   This was `display: inline` on a span wrapped around a pressable digit — one
   glyph, about 8px wide, carrying the only route from a finding to the
   documents it is about. Inline so that it WRAPS: a clause is a run of words
   inside a paragraph and has to break across lines like one, which is why this
   is not `inline-block`. It is also why it is a SPAN carrying the button role
   rather than a `<button>`: form controls ignore `display: inline` and stay
   atomic, which at 260px dropped the whole clause onto its own line instead of
   letting it continue the sentence. */
.ins-clause { display: inline; }
/* The clause that is NOT a control. It carries no affordance on purpose — the
   underline and the cursor below belong to `[role="button"]` — so this rule
   exists to say that the plain state is deliberate rather than unstyled. */
.ins-clause.is-flat { text-decoration: none; }
/* ── The underline is the size of the target ──

   It was on the FIGURE while the whole clause was the control, which is the
   worst of both: an affordance drawn around 8px of a run you can press 300px
   of. A reader can only conclude that the digit does something and the words
   do not, and the one thing a rest state has to get right is where the target
   ends.

   So the rule moves out to the clause. Same underline — accent-tinted, 1px,
   3px clear of the baseline — drawn under exactly what answers the mouse, and
   it breaks with the text across line boxes because the clause is an inline
   run rather than a block.

   It is not "heavy" to underline the whole thing: what is heavy is a paragraph
   claiming three targets and marking three digits. A sentence with pressable
   phrases in it is a sentence with underlined phrases in it, and that has been
   the convention since the first hypertext. The figure keeps its weight and
   its tabular numerals, which is what makes it scan as a figure — it just
   stops being the only underlined thing in a clause it does not own. */
.ins-clause[role="button"] {
  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) var(--ease-out), text-decoration-color var(--t-fast) var(--ease-out);
}
/* One gesture. The clause and its figure cannot light separately, because they
   were never two targets. */
.ins-clause[role="button"]:hover {
  color: var(--ink-action); text-decoration-color: rgba(var(--accent-rgb), 0.9);
}
.ins-clause[role="button"]:hover .ins-n { color: var(--ink-action); }
/* The outline follows the text across a line break, which is the other half of
   the argument for an inline box: a focus ring around an atomic block would
   have enclosed whitespace the clause does not occupy. */
.ins-clause[role="button"]:focus-visible {
  outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs);
}
/* ── No priority dots ──

   There was one per clause here, in err / warn / brand. Sales' block is
   deliberately single-tone and carries urgency by CLAUSE ORDER instead, and
   dropping the dots makes this a more faithful port rather than a looser one:
   needsYou() returns p1 → p2 → p3 by construction, so the worst thing is
   already the first thing you read.

   What the dots were adding on top of that was three hues inside one sentence,
   which is the alarm fatigue doctrine §5.6 warns about in the same breath as it
   asks for urgency to be distinguishable. Order distinguishes it. */
/* What you press is the SIZE OF THE SET YOU LAND ON, which is why the clause
   leads with a number and not an adjective: 3 lands on exactly those three.
   The figure carries weight and tabular numerals so it scans out of the
   sentence at a glance; the underline that marks the target belongs to the
   clause above, because the clause is the target.

   An underline rather than a colour, there and here, because five coloured
   phrases in one sentence is not a sentence. */
.ins-n {
  font-weight: var(--fw-bold); color: var(--ink-primary);
  font-variant-numeric: tabular-nums;
  transition: color var(--t-fast) var(--ease-out);
}
/* A clause with nothing to land on — a coverage gap counts questions, not
   documents — gets no underline at all, from the [role="button"] rule it does
   not match. The figure inside it keeps the tabular weight so the sentence
   still scans. Nothing further to say here: the absence IS the rule. */

/* The actions, inline, with nothing framing them. What stood here was a
   captioned strip of three ghost cards under an accent rule: 134px of a 279px
   block, so half the panel was chrome around three verbs.

   They are text, not buttons with edges, for the reason the rail states about
   itself — the page's one filled control is New document, and a block that
   reports is not a block that commands. */
.ins-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.25rem; margin-top: 0.75rem; }
.ins-act {
  background: none; border: 0; padding: 0; cursor: pointer; white-space: nowrap;
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-bold);
  color: var(--ink-action);
  transition: opacity var(--t-fast) var(--ease-out);
}
.ins-act:hover { opacity: 0.75; }
.ins-act:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }

/* Beside a document rather than above a set. The rail is 320px, so the block
   loses its wash and its action strip and keeps the marked rows — the panel it
   sits in already says whose findings these are. */
.doc-rail .ins-band {
  background: none; border: 0; border-radius: 0;
  padding: 0; margin-bottom: 1rem;
}
.ins-rail-lead {
  display: flex; align-items: center; gap: 0.375rem; margin: 0 0 0.5rem;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-quiet);
}
.ins-rail-lead svg { flex-shrink: 0; }
.ins-row { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem; margin-bottom: 0.75rem; }
.ins-row:last-child { margin-bottom: 0; }
.ins-row-text { margin: 0; font-size: var(--ty-body); line-height: var(--lh-snug); color: var(--ink-secondary); }
/* A text action, not a card: the rail's rule is that no filled control lives in
   it, and at 320px three ghost cards would be the column. */
.ins-row-act {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-bold);
  color: var(--ink-action);
  transition: opacity var(--t-fast) var(--ease-out);
}
.ins-row-act:hover { opacity: 0.75; }
.ins-row-act:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }

/* ── The kind menu on New document ──

   The design system's `.menu`, with two lines per item instead of one: the
   name, and what that kind is for. Nine labels alone would ask somebody to
   already know the difference between a Campaign and a Marketing Asset, which
   is exactly the knowledge a person creating their first one does not have.

   `.rm-end .entry-action { margin-left: auto }` used to push this button to
   the right end of the row. The button is inside an anchor now, so the rule
   fires on a child that is already alone in its box and the anchor stays where
   the flow put it — the pusher has to move out with it.

   The panel opens from the RIGHT edge for the same reason: anchored left at
   280px wide it would start at the row's right end and run off the region. */
.rm-end .menu-anchor { margin-left: auto; }
.rm-end .menu-anchor .entry-action { margin-left: 0; }

/* ══ AND WHEN IT WRAPS IT KEEPS THE RIGHT EDGE, WITH ROOM ABOVE IT ══
   Measured a pixel at a time, New document holds the utilities' line at 522
   and has wrapped by 521. `margin-left: auto` then puts it at the right end of
   a line it is alone on — which is where it belongs, and is Nour's call: it
   is the one control in this row that MAKES something, and a primary action
   sitting under the thumb on the right is the shape a phone expects.

   AN EARLIER PASS LEFT-ALIGNED IT, and that was solving the wrong half. What
   read as stranded was never the alignment — it was a 4px row gap, which put a
   filled blue button four pixels under a row of borderless text and made it
   look like an item that had fallen off the end of that row rather than its
   own line. The gap is the fix; the right edge was right all along.

   0.75rem, which is this file's standing vertical step and three times the
   4px the row uses BETWEEN its utilities. `row-gap` alone, so the horizontal
   spacing inside the utilities row is untouched — they are still one group,
   and only the line break gets the air.

   The `.98` clears a fractional viewport, for the reason written out at the
   search's own query above. */
@media (max-width: 521.98px) {
  .rm-end { row-gap: 0.75rem; }
}
.rm-end .new-menu { left: auto; right: 0; }
/* ── Four kinds, and enough of the fifth to know there are more ──

   Measured: nine items, 625px tall, bottom edge at 819px in an 800px window,
   `max-height: none` and `overflow-y: visible` — it ran off the screen and
   could not be scrolled to. Both sibling menus already cap: `.blk-menu` at
   320px and `.doc-versions-panel` at 60vh. This one was the exception.

   280px was also the reason the rows were not uniform: five of the nine
   descriptions wrapped to a second line, so the list was 55px and 74px rows
   mixed, and no cap could land anywhere predictable. 368px is the width the
   longest of them — "A customer outcome, with a quote cleared to use." — needs
   on one line, and a menu whose rows are two different heights scans worse
   anyway.

   Then 272 = 6 panel padding + 24 label + 4 × 55 + 22, and the 22 is the whole
   point: it is the fifth row's padding plus most of its name, so the cut falls
   THROUGH a row rather than between two. A list that ends flush reads as a list
   that ended; a name sliced through its x-height reads as a list that goes on.
   That is the only thing that makes the scroll discoverable before you try it. */
.new-menu {
  min-width: 23rem; max-height: 17rem; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent;
}
.new-menu::-webkit-scrollbar { width: 0.25rem; }
.new-menu::-webkit-scrollbar-track { background: transparent; }
.new-menu::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 0.125rem; }
:root[data-theme="light"] .new-menu { scrollbar-color: rgba(16,24,40,0.12) transparent; }
:root[data-theme="light"] .new-menu::-webkit-scrollbar-thumb { background: rgba(16,24,40,0.12); }
/* Pinned rather than scrolled away, so a panel that moves keeps saying what it
   is. -6px cancels the panel's own padding so it sticks flush, the same way
   `.dd-search` does inside the filter dropdowns. */
.new-menu .menu-label { position: sticky; top: -6px; z-index: 1; background: var(--panel-bg); }
/* Two lines again. The row went back to one when the nine notes were cut as
   things you learn once; it takes the second line back now that two of the
   eleven types do something a name cannot tell you — Product and Service put
   an entry in AiMY Sales. The glyph gets its nudge back with it: centred
   against a two-line block it floats between the lines. */
.new-menu .menu-item { padding: 0.5rem; width: 100%; text-align: left; align-items: flex-start; }
.new-menu .menu-item > svg { margin-top: 0.1875rem; flex: 0 0 auto; }
.new-menu-tx { display: flex; flex-direction: column; gap: 0.0625rem; min-width: 0; }
.new-menu-name { font-size: var(--ty-body); font-weight: var(--fw-semibold); color: var(--ink-primary); }
.new-menu-note {
  font-size: var(--ty-micro); line-height: var(--lh-snug); color: var(--ink-quiet);
}

/* ── SENT HERE FROM SOMEWHERE ELSE ──

   Arriving from *Create a product/service* in Sales settings opens this menu
   already scrolled to its last two rows. Scrolled and silent, that is
   indistinguishable from a menu you happened to open near the bottom, so the
   two rows say for one second that they are the ones you were sent for.

   The tint is painted by the class, NOT by an animation, so it is there for a
   reader with reduced motion too; the transition only governs how it leaves.
   That is this file's own pattern — the end state outside the motion query,
   the movement inside it. */
.new-menu .menu-item.is-flagged { background: var(--brand-dim); }
@media (prefers-reduced-motion: no-preference) {
  .new-menu .menu-item { transition: background var(--t-medium) var(--ease-out); }
}

/* ── Two library components that arrive below the floor ──

   `.tag` is 10px/700 uppercase and `.menu-label` is 10px/700 uppercase on
   --d500, which measured 3.83:1. Both are specimen sizes — the same finding as
   GAPS §27 for `.tc-*` and the version list, now for the third and fourth
   component. Re-stepped to the floor here rather than in the library, which is
   extracted, not authored, and given back their tracking: uppercase at 12px
   needs less letter-spacing than uppercase at 10px, not the same amount. */
.tag { font-size: var(--ty-micro); letter-spacing: var(--ls-wide); }
.menu-label { font-size: var(--ty-micro); letter-spacing: var(--ls-wide); color: var(--ink-quiet); }
/* Nine keyboard-reachable buttons with no focus ring anywhere in either
   stylesheet. `.blk-menu-item` has one; this was an omission, not a decision.
   Inset, because a positive offset is clipped by the panel's own padding the
   moment it scrolls.

   And they were reachable while the panel was INVISIBLE: the library's `.menu`
   is opacity:0 + pointer-events:none with no `visibility`, so Tab walked nine
   buttons inside an empty box and a ring on them would have been a ring in
   mid-air. The delayed visibility keeps the close transition the library
   wrote. */
.menu-item:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.menu { visibility: hidden; transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out), visibility 0s linear var(--t-fast); }
.menu-anchor.open .menu { visibility: visible; transition-delay: 0s; }
/* The rest of the same finding, measured on the page rather than guessed at:

     .v2-dropdown-option   11px    every option in every filter and field
     .trust-state           9px    the word that says a document is CONFLICTING
     .ver-mark              9px    which version you are looking at
     .ver-author          10.5px   who saved it and why
     .cite-action          10px    Report a problem · Archive · Delete

   Five components, and the two smallest carry the two most consequential
   facts in the product. This is the fourth and last re-step in this pass; all
   of them are recorded in GAPS §27, which is now a pattern rather than an
   incident: the library draws for a 320px specimen column and states no width
   it assumes, so every consumer discovers the mismatch by rendering it. */
.v2-dropdown-option { font-size: var(--ty-meta); }
.trust-state { font-size: var(--ty-micro); letter-spacing: var(--ls-wide); }
/* The library sets these from the raw hue, which is a fill colour. On its own
   12% tint --err measures 4.13 — the word CONFLICTING, at the smallest size in
   the byline, in the one state that most needs reading. */
.ts-expired { color: var(--ink-err); }
.ts-due     { color: var(--ink-warn); }
.ts-verified{ color: var(--ink-ok); }
:root[data-theme="light"] .ts-expired  { color: var(--ink-err); }
:root[data-theme="light"] .ts-due      { color: var(--ink-warn); }
:root[data-theme="light"] .ts-verified { color: var(--ink-ok); }

/* ── opacity is not a rank ──

   `.tag-token button` is --ink-faint at `opacity: 0.65`, which lands it at
   3.09:1 — below the floor by a channel no colour token can see and no audit
   of `color:` will ever catch. That is the whole argument for the ink scale
   having a bottom: if a rule wants something quieter than the quietest rank,
   the answer is to remove it, not to fade it. This one keeps its rank and
   loses the fade; it is already the smallest, lightest thing in the row. */
.props .tag-token button { opacity: 1; }
.ver-mark   { font-size: var(--ty-micro); }
.ver-author { font-size: var(--ty-micro); color: var(--ink-quiet); }
.cite-action { font-size: var(--ty-meta); }
/* .conv-* is the canvas's vocabulary and stays at its own scale there, but the
   settings sheet reuses it — two 10px labels on a surface that is entirely in
   scope. Scoped so the exclusion still holds where it was meant to. */
#setSheet .conv-label { font-size: var(--ty-micro); }

/* ═══════════════════════════════════════════════
   TYPE CARD — product-owned bits only
═══════════════════════════════════════════════ */
.tc-title-btn {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit;
  transition: color var(--t-fast) var(--ease-out);
}
.tc-title-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* No hover of its own: the whole card is the target, and a second highlight
   inside it advertises a second target that does not exist. Focus-visible
   stays — the keyboard still needs one thing to land on. */
.tc-tag {
  border: 0; cursor: pointer; font-family: inherit;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.tc-tag:hover { color: var(--ink-primary); }
.tc-tag:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.0625rem; }
.tc-mono { font-family: var(--font-mono); font-size: var(--ty-micro); }
.tc-gov { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.ws-grid .type-card { max-width: none; }

/* Thinned. Two labelled facts plus the review date, and the rest in the modal.

   ONE footer, not two. The library's card ends with a bordered, tinted .tc-gov
   strip and then a bordered .tc-action strip holding a full-width button —
   two rules and a banner, which is most of the height and all of the noise. */
/* ── The card's status, on its own border ──

   This was a 3px tinted LEFT edge, which was wrong twice. The library has no
   status edge on `.type-card` at all — the spec is eight templates over one
   fixed governance row, and status lives in `.trust-state` — and the left-border
   idiom I borrowed it from, `.finding.f-err`, sets `border-left-color` on a
   border the base rule has commented out. A dead pattern, copied.

   The live pattern for "a card whose severity tints its edge" is `.bcard.p1/
   .p2/.p3`: the WHOLE border, at low alpha, on the card's own 1px. Same
   information, no invented mechanism, no 2px content shift, and hover keeps
   its meaning because — exactly as `.bcard` does it — hover wins.

   Nothing wrong with the document means the default border. A screenful still
   shows you where the problems are before you have read a single title, and the
   word beside the dot carries the same fact in text, which is the rule: status
   is colour AND text, never colour alone. */
.type-card {
  --st: var(--d400);
  padding-bottom: 0; cursor: pointer;
}
.type-card[data-status="current"]     { --st: var(--ok); }
.type-card[data-status="unowned"]     { --st: var(--d300); }
.type-card[data-status="unused"],
.type-card[data-status="draft"]       { --st: var(--warn); border-color: color-mix(in srgb, var(--warn) 20%, transparent); }
.type-card[data-status="outdated"],
.type-card[data-status="conflicting"] { --st: var(--err);  border-color: color-mix(in srgb, var(--err) 20%, transparent); }
.type-card[data-status="superseded"]  { --st: var(--info); border-color: color-mix(in srgb, var(--info) 20%, transparent); }
/* The saturated tokens go dim on white; the badge already carries these. */
:root[data-theme="light"] .type-card[data-status="current"]     { --st: #0a7a45; }
:root[data-theme="light"] .type-card[data-status="unused"],
:root[data-theme="light"] .type-card[data-status="draft"]       { --st: #8a5200; }
:root[data-theme="light"] .type-card[data-status="outdated"],
:root[data-theme="light"] .type-card[data-status="conflicting"] { --st: #bf2f26; }
:root[data-theme="light"] .type-card[data-status="superseded"]  { --st: #036aa3; }
/* Anything interactive inside the card keeps its own cursor, so the card
   reading as clickable never claims a control that does something else. */
.type-card button { cursor: pointer; }
/* ── Three tiers ──

   Title, then the document's own words, then everything else on one dim line.
   The steps are 17 → 15 → 13, ink moving --ink-primary → --ink-secondary →
   --ink-quiet in step, so no two adjacent runs share both size and colour and
   the order they arrive in is the order they matter in. Nothing above the title.

   The previous ramp was 15 → 13 → 11 on --d50 → --d300 → --d500, and the
   bottom rung measured 3.51:1 — sixty runs of it on one screenful. The tiers
   were right; two of the three numbers were not.

   Rank now continues INSIDE the meta line rather than becoming a second row.
   A second row is what the three-tier pass removed, and re-adding one to get
   hierarchy would trade the same defect back. The line carries L4; the type
   glyph and the separators drop to L5 within it. */
/* ── The type, above the title, as a chip ──

   The library's own component (aimy-ds.css §TYPE CARD, .tc-head/.tc-type) at
   the library's own specimen values: 9.5px / 700 / uppercase / 0.07em. Those
   are the two uppercase runs a previous pass removed for outranking the title,
   and the diagnosis was right — but the remedy was to delete the fact rather
   than to fix its treatment, and the round after that deleted the word too and
   left a glyph at the end of a middot line. The type is what the thing IS. It
   goes back on top.

   It does not outrank .tc-title, because it differs from it on all three type
   channels at once: 12 against 17, 500 against 800, --ink-faint against
   --ink-primary. What carries identification instead is the SHELL — a hairline
   pill is an object, and an object at L5 reads as a label ON the card rather
   than as a line OF it — a non-type channel doing
   the work so the scale does not have to invent a sixth rank.

   No uppercase and no tracking. `.tag` was the other candidate and is wrong
   here for a reason that only shows on two of the nine types: axisTags already
   renders `.tag` for region and services, so an ICP card would carry a type
   chip and two axis chips in one identical pill and the type would be
   indistinguishable from a value.

   Not a button. .doc-by-kind states the reason for the same fact in the byline
   — "there is no peek for a type" — and the whole card is already one target.

   align-self is load-bearing: .type-card is a column flex container, inline-flex
   blockifies on a flex item, and the default `stretch` would run the pill's
   border the full width of the card. That is the banner-above-the-title failure
   arriving through a layout default instead of a type choice. */
.type-card .tc-type {
  align-self: flex-start;
  margin: 0.75rem 1rem 0; padding: 0.125rem 0.5rem; gap: 0.25rem;
  border: 0.0625rem solid var(--hairline); border-radius: var(--r-pill);
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: normal; text-transform: none;
  color: var(--ink-faint);
}
.type-card .tc-type svg { width: 0.75rem; height: 0.75rem; }
.tc-title-btn { padding: 0.375rem 1rem 0; }
/* NOT EXTRABOLD, BECAUSE IT IS A CARD. Eighteen of these render on the
   console at once, and extrabold on eighteen titles is not emphasis, it is
   the page’s new normal — the weight held for the ONE name at the top of a
   surface, spent eighteen times in the grid below it. It still leads its own
   card by a step of size and a step of weight over everything inside it. */
.tc-title {
  padding: 0;
  font-family: var(--font-display);
  font-size: var(--ty-title); font-weight: var(--fw-bold);
  line-height: 1.28; letter-spacing: -0.014em;
  color: var(--ink-primary);
  /* Two lines, then ellipsis: a long title must not push the facts off the
     bottom of a card whose height is shared with eleven others. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-wrap: pretty;
}
/* ── The body is the only zone that varies ──

   The library's spec is eight templates over one governance row, and the one
   thing it varies is `.tc-body`. This card kept the collapse of METADATA onto
   one dim line — that was the right move and it stays — but shrank the body to
   a single truncated fact, which is what left eight types looking alike.

   Nothing here changes what a library component looks like. It sets the
   product's own gutter (16px, matching .tc-meta) and the product's own type
   ramp (--ty-body / --ink-secondary — tier two, declared above), and it sets the height
   budget, which is a property of a stretched grid rather than of a component.
   Filed in GAPS.md: the library's .tc-* scale is a 320px specimen scale and
   this is the second time the product has had to re-step it. */
.type-card .tc-body { padding: 0 1rem; margin-top: 0.375rem; gap: 0.25rem; }
.type-card .tc-summary {
  padding: 0;
  font-size: var(--ty-body); line-height: 1.45; color: var(--ink-secondary);
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
  -webkit-line-clamp: 1;
}
/* Two lines when the prose IS the body, one when it shares the zone with a
   pill row or a quote. Same total either way, so a Ticket and an Article end
   at the same height in a row that stretches — and the rule is CSS rather than
   a string-length guess in JS, so it holds at every viewport. */
.type-card .tc-summary:only-child { -webkit-line-clamp: 2; }
.type-card .tc-quote {
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
  -webkit-line-clamp: 1;
}
.type-card .tc-mono {
  margin: 0; font-size: var(--ty-body); color: var(--ink-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tc-meta {
  margin: 0.75rem 0 0.75rem; padding: 0 1rem;
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.125rem 0.375rem;
  font-size: var(--ty-micro); font-weight: var(--fw-medium); line-height: 1.4;
  color: var(--ink-quiet);
}
/* ── The row is the floor, and the status is the exception ──

   It was --ty-meta, which the scale pass moved from 13 to 14. That put six
   runs — status, glyph, owner, source, and two dates — one step under a 16px
   summary and a weight ABOVE it (500 against 400), so the card's least
   actionable line was its second loudest and the summary had to compete with
   the facts about it. Two grey blocks of the same texture, which is what
   "nothing leads" looks like when the sizes are two pixels apart.

   At the floor the summary clears it on all three axes — step, weight and ink
   — and the card reads 20 / 16 / 12 rather than 20 / 16 / 14 / 14 / 14.

   The status does not go with it. It is the one run on the row that puts a
   document in a queue and the one carrying a hue, so it keeps --ty-meta and
   leads the line it sits in. */
.type-card .tc-st { font-size: var(--ty-meta); }
/* Punctuation, so it sits a rank below the facts it divides — but it is still
   a rendered glyph and --d600 gave it 2.49:1, which is below even the non-text
   floor. Ambient is not invisible. */
.tc-sep { font-style: normal; color: var(--ink-faint); }
/* ── The type is a glyph, not a fact ──

   It was an icon PLUS its label, in the same treatment as the status, the
   owner, the source and both dates — six runs, one rank, so the type competed
   with the one thing on the line that can put the document in a queue. The
   type is the least actionable fact the card carries: it never changes, it is
   already legible from the card's own body shape, and nine of them are the
   whole corpus. The glyph keeps it available; the name goes to the tooltip and
   to assistive tech, and the line gets seven characters shorter. */
.tc-kind { display: inline-flex; align-items: center; gap: 0.25rem; }
/* Scoped to the card: typeMark() reuses .tc-kind inside canvas citations, where
   the label is still the point and the run is not competing with five others. */
.type-card .tc-kind {
  font-size: var(--ty-micro); font-weight: var(--fw-medium); color: var(--ink-faint);
}
/* No opacity. The glyph is the strongest type cue the meta line has, and it
   was dimmed for no stated reason on top of an 11px --d500 run that already
   measures 3.51:1. Dropping the label is the de-emphasis instead, which costs
   the glyph nothing — status is still the only COLOURED run on the card, and
   colour is what wins here. */
.tc-kind svg { width: 0.6875rem; height: 0.6875rem; }
.type-card .tc-kind svg { width: 0.75rem; height: 0.75rem; }
/* The one coloured thing on the card, and it is four characters wide. */
.tc-st {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-weight: var(--fw-semibold); color: var(--st);
}
.tc-dot { width: 0.375rem; height: 0.375rem; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.tc-who {
  max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: inherit; font-weight: inherit; color: inherit;
}
/* `--d600` is the separator grey. *no owner* is not punctuation — it is the
   reason the card is in a needs-a-person set — and at 2.49:1 on a dark card it
   was quieter than the dot between the facts beside it. The italic already
   carries "this is an absence"; the colour does not have to as well. */
.tc-who.is-none { max-width: none; font-style: italic; color: var(--ink-quiet); }
.tc-who:hover { color: var(--ink-action); }
.tc-who:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* Same entity treatment as the owner beside it — both open a peek, so both
   have to look like they do. No max-width: a connector name is two words and
   clipping "Manual upload" to 12ch would invent an ambiguity. */
.tc-src {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: inherit; font-weight: inherit; color: inherit;
  white-space: nowrap;
}
.tc-src:hover { color: var(--ink-action); }
.tc-src:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }
/* Compact cards live inside answers and connection groups, where the row is
   the point and the action belongs to the panel around it. */
.type-card.is-compact .tc-title-btn { padding-top: 0.5rem; }
.type-card.is-compact .tc-meta { margin-bottom: 0.5rem; }

/* margin-top:auto pins the footer to the bottom of a stretched card, so a row
   of cards ends on one line whatever each of them had to say. */
.tc-foot {
  display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;
  margin-top: auto; padding: 0.75rem 1rem;
  font-size: var(--ty-micro); color: var(--ink-faint);
}
/* ── The footer's LAST BOX ends 12px above the card, and that is the rule ──

   The contract is that a row of cards ends together. It used to be stated of
   the action, because the action was the last thing in the footer and it sat on
   the card's own ground: a bare control, so its own bottom edge WAS the
   footer's. That stopped being true the moment the action moved inside a
   bordered panel, and following the old wording cost the card its gutter — the
   footer gave back 11 of its 12px so the button inside the panel could reach
   the old line, and the panel then ended 3px from a card edge whose sides it
   clears by 16. A pixel-aligned button in a box that looks like it fell out of
   the card.

   So the rule is stated of the box, not of what is in it: whatever the footer
   ends on — a panel on the cards with a finding, a bare action on the seven
   without — ends 12px above the card. Both cases get it from one declaration,
   which is why there is no `:has()` override here any more. A row of cards
   still ends together; the actions inside the panels simply sit a panel's
   padding higher, which is what being inside something means. */
/* A flex container, not a block holding an inline button — a block would give
   the button a line box, and the leading under it is dead height inside the
   panel. The line-height is stated rather than inherited for the same kind of
   reason: `.entry-action` declares none, so its height is whatever leading the
   container hands it — the panel's snug 13px prose in one case, the footer's
   11px chrome in the other — and the same control would render 2.3px taller
   depending on which box it was in. */
.tc-foot-act {
  display: flex; justify-content: flex-end; margin-left: auto; flex-shrink: 0;
  line-height: var(--lh-snug);
}
/* ── What AiMY noticed about THIS one ──

   It was a run of footer chrome: the same 12px, the same --ink-faint, the same
   weight as the timestamp beside it, told apart from it by a dot. The one thing
   on the card the document did not say about itself was styled as the thing it
   sits in.

   THE MARK, NOT A DOT. Severity does not go missing with it — .type-card
   already tints its own border per status, and the status word is in the meta
   line, so on a card the dot was a fact's third statement. What the mark says
   instead is whose sentence this is, which nothing else on the card was saying.

   One step and one ink rung up from the footer it sits in, and the same accent
   wash the band upstairs uses, so the three surfaces that carry a finding — the
   band, the card, the rail — read as one voice in three places rather than as
   three components that happen to agree.

   Full width and two lines. At 115px, which is what the footer's left half
   comes to on a 322px card once the action has taken its share, every one of
   the nine findings ellipsised, and a truncated insight costs a line to deliver
   half a fact.

   AND ITS ACTION, INSIDE IT. The panel said what was wrong and the button that
   answers it stood outside on the card's ground — one exchange in two boxes,
   with only proximity claiming the second was the reply to the first. The band
   over the grid keeps its verbs inside its own wash and the rail keeps them on
   the row that states the finding; the card was the odd one out. So: the
   sentence, then the verb, in the panel that is speaking. */
.tc-foot .tc-ins {
  display: flex; flex-direction: column; gap: 0.5rem; margin: 0; min-width: 0;
  padding: 0.5rem 0.75rem; border-radius: var(--r-md);
  background: var(--aimy-wash);
  border: 0.0625rem solid rgba(var(--accent-rgb), 0.18);
  font-size: var(--ty-meta); font-weight: var(--fw-medium); line-height: var(--lh-snug);
  color: var(--ink-secondary);
}
/* The mark and the sentence. Scoped to the sentence's own row and to a DIRECT
   child, because the action below it now brings an svg of its own into the
   panel and a 2px nudge meant for the mark would land on the button's glyph. */
.tc-ins-say { display: flex; align-items: flex-start; gap: 0.5rem; min-width: 0; margin: 0; }
.tc-ins-say > svg { flex-shrink: 0; margin-top: 0.125rem; }
.tc-ins-t {
  min-width: 0;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* A text action, not a filled bar — the card is a result, not a form. The
   entry-mode icon stays: with it hidden, colour alone would be carrying which
   mode the action is, which §13 forbids. */
/* ── Take a variant apart and you take its colour with it ──

   `em-direct` is white text ON a brand fill. Removing the fill here left the
   white text sitting on the card, which reads fine on a dark card and is
   **invisible on a white one** — measured at a contrast ratio of 1.00 in light
   mode, the Open button on every card. Dark mode made a missing declaration
   look like a design. Same defect as borrowing `.finding.f-err`'s left border
   from a rule whose border is commented out: a variant's parts are not
   separable, so a rule that drops one of them owns the rest. */
.tc-foot-act .entry-action {
  background: none; border-color: transparent; padding: 0;
  font-size: var(--ty-meta); color: var(--ink-action);
}
.tc-foot-act .entry-action:hover { background: none; opacity: 0.75; }
/* ── One height per row ──

   `stretch` rather than the design system's `start`, which this is the only
   place to override. A row of cards is scanned across, and cards that end on
   six different lines make the reader's eye do the work of a grid.

   The trailing space on a card with less to say is not a cost of that override
   and never was. Content length varies — a two-line title against a one-line
   title, a body of forty words against twelve — so the tallest card in a row
   has always set the height and the shorter ones have always had slack. Before
   the footer became conditional the slack sat BETWEEN the meta line and the
   action, because `margin-top: auto` parked the action at the bottom of it; a
   quiet card now carries the same slack after the meta line instead, with
   nothing standing at the end of it. Same space, one fewer button in it.

   Which is also why `margin-top: auto` stays on `.tc-foot`: a card with a
   finding still puts its panel on the row's bottom line, where a reader
   scanning for what AiMY said finds all of them together. */
.ws-grid {
  /* ══ `min()` INSIDE THE `minmax()`, AND IT IS NOT BELT AND BRACES ══════
     `minmax(18.75rem, 1fr)` reads as "at least 300px", and a grid track
     honours that floor even when its CONTAINER is narrower — so the column
     stays 300px and the card hangs out of the page instead of fitting it.
     Measured on `.page-inner`, which is what the grid has to live inside:

         390   container 322   track 322   fits
         360   container 291   track 300   9px out
         320   container 251   track 300   49px out

     `min(18.75rem, 100%)` makes the floor "300px, or the whole container,
     whichever is smaller", so the track can give up the minimum exactly when
     the minimum is the thing that does not fit. Inert wherever the container
     clears 300px — which is every width at and above 390, the 1536 reference
     included, where `min()` resolves to the same 18.75rem it always did. */
  grid-template-columns: repeat(auto-fill, minmax(min(18.75rem, 100%), 1fr));
  gap: 0.75rem;
  align-items: stretch;
}

/* ══ THE SHAPE OF WHAT IS COMING ══════════════════════════════
   A placeholder is only worth showing if it is the same shape as the thing it
   is standing in for. The title and the scope line are identical on every
   settings module, so those two land exactly where the real ones will and do
   not move when the page arrives; the rows are an approximation, which is what
   the cross-blur on the reveal is for.

   `--h` is the row's height in quarter-rem steps, so one class covers a
   settings row, a paragraph of a document and the gap between them without a
   second class per shape.
═══════════════════════════════════════════════════════════════════════ */
.sk-title { height: 2.125rem; width: 13rem; border-radius: var(--r-sm); }
.sk-bar   { height: 1.625rem; width: 20rem; margin-top: 0.875rem; border-radius: var(--r-sm); }
.sk-rows  { margin-top: 1.75rem; display: flex; flex-direction: column; gap: 0.75rem; }
.sk-row   { height: calc(var(--h, 3) * 0.25rem * 4); border-radius: var(--r-md); }
.sk-doc   { max-width: 46rem; margin: 0 auto; padding: 2.25rem 2rem 6rem;
            display: flex; flex-direction: column; gap: 0.75rem; }
.sk-doc .sk-row:first-of-type { margin-top: 1.25rem; }

/* ══ AND THE ORDER IT RESOLVES IN ══════════════════════════════
   `backwards` on the blur, and it matters. Each block waits its turn, and
   without the backwards fill it would sit sharp through that wait and then
   snap to blurred the instant its own animation began - a flash per block,
   in sequence, which is a worse artefact than no motion at all.

   No `forwards` on it either, for the reason kEnterBlur gives: a filter held
   by fill-mode keeps the element on its own compositor layer for a blur of
   zero. It runs, and then the element has no filter again. */
@media (prefers-reduced-motion: no-preference) {
  .k-resolve {
    animation: k-resolve var(--t-base) var(--ease-out) backwards,
               k-resolve-blur var(--t-fast) var(--ease-out) backwards;
    animation-delay: calc(min(var(--i, 0), 8) * var(--t-stagger));
  }
  @keyframes k-resolve {
    from { opacity: 0; transform: translate3d(0, 0.375rem, 0); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes k-resolve-blur { from { filter: blur(var(--blur-small)); } }
}

/* Skeletons resolve in place of cards, at card proportions, so the grid does
   not reflow when the real content lands. */
.skeleton-card {
  border: 0.0625rem solid var(--card-border); border-radius: var(--r-lg);
  background: var(--card-bg); padding: 1rem;
  display: flex; flex-direction: column; gap: 0.5rem; min-height: 11.875rem;
}

/* ═══════════════════════════════════════════════
   DOCUMENT VIEWER — provenance and tags
═══════════════════════════════════════════════ */
.dv-prov {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(8.75rem, 1fr));
  gap: 0.75rem 1rem; padding: 1rem 0; margin-top: 0.25rem;
  border-top: 0.0625rem solid var(--hairline);
}
.dv-prov-item { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.dv-prov-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-faint);
}
.dv-prov-val {
  font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--d200);
  font-variant-numeric: tabular-nums;
}
.dv-prov-val.is-overdue { color: var(--ink-warn); }
.dv-tags { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; padding-top: 0.75rem; }
.dv-tags-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-faint);
  margin-right: 0.125rem;
}
/* ── The document's type slot ──

   Same shapes as the card's body, unclamped: the document has the width the
   card does not, and the rows the card drops are the reason to open it.
   Replaces `.dv-body .tc-fields`, which was this idea written into the
   stylesheet and never wired to any markup.

   It sits OUTSIDE #editBody. See renderDoc: the body is contenteditable and
   its innerHTML is written back on the first edit, so a derived block rendered
   inside it would become the document's own stored content. */
.dv-typed { display: flex; flex-direction: column; gap: 0.5rem; margin: 1rem 0 1.5rem; }
/* `.doc-head .dv-typed` stood here, for when this block was a read-only replay
   of the card's body in the document's head. It is the record's derived
   stratum now and the head no longer carries it, so the override went with the
   markup it was written for. */
.dv-typed .tc-body { padding: 0; margin: 0; gap: 0.5rem; }
/* Tier two at the document's scale, matching .dv-body's own 15px prose rather
   than the card's 13px — one ramp per surface, not one ramp everywhere. */
.dv-typed .tc-summary { margin: 0; padding: 0; font-size: var(--ty-body); line-height: 1.6; color: var(--d200); }
.dv-typed .tc-mono { margin: 0; font-size: var(--ty-meta); color: var(--ink-muted); word-break: break-all; }
.dv-typed .tc-tags { gap: 0.375rem; }
/* `--d600` measures 2.49:1 on a dark card — the marker would be quieter than
   the criterion it marks. `--d500` is the step this ramp uses for punctuation. */
.tc-list li::before { color: var(--ink-faint); }

/* ═══════════════════════════════════════════════
   THE EIGHT VIEWS — layout only

   Every shape below belongs to the design system; these rules place it in a
   760px column instead of a 320px rail, and nothing more. Per-type variation
   is selected on [data-type] over a static class — a data- attribute is not
   scanned by css-audit.js's class regex, so all eight variants stay audited
   where an interpolated class would leave seven of them unchecked. And what
   varies is column count and order, never colour: colour is status's.
═══════════════════════════════════════════════ */

/* ── A secondary run, named ──

   Prose that is not the point still has to say what it is for. An <h2> is the
   right rung — the title is the page's only h1 and #editBody emits h3/h4, so
   this sits between them, the same level `.doc-comments-head` already uses.
   It reads as a label rather than a heading, because the document's own
   headings are the ones inside the body. */
.dv-run-label {
  margin: 0 0 0.5rem;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-faint);
}

/* ── The type's record, in the document ──

   The rail's rows were built for 320px: one column, the lead above or beside
   its value, the value taking whatever is left. At the document's measure that
   leaves a twelve-character phrase against 600px of value and the left edge
   stops being scannable. Same rows, two columns, and the measure capped at
   80ch for the same reason the paragraphs are — a line you cannot track back
   to is not more readable for being wider. */
.dv-record { display: flex; flex-direction: column; gap: 0.75rem; max-width: var(--measure-prose); margin: 0 0 2rem; }
.dv-record .dv-typed { margin: 0; }
.dv-record .prop-rows { gap: 0.25rem; }
.dv-record .prop-kv-read {
  display: grid; grid-template-columns: 10rem minmax(0, 1fr);
  align-items: baseline; gap: 0.5rem;
}
/* The rail truncates because it has 320px. The document does not have to. */
.dv-record .prop-kv-val { white-space: normal; font-size: var(--ty-body); }
.dv-record .prop-why { margin: 0; }

/* A case record is a register: state, who raised it, how it closed. A tighter
   left column than the default, because none of its leads is long. */
.dv-record[data-type="ticket"] .prop-kv-read { grid-template-columns: 8.75rem minmax(0, 1fr); }

/* A profile is read as two lists side by side — what qualifies and what rules
   out — because that comparison IS the profile. Nothing here sets a colour;
   what varies per type is the column count, which is shape. */
.dv-record[data-type="icp"] .prop-rows {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.5rem 1.5rem;
}
.dv-record[data-type="icp"] .prop-rows > .prop-kv { grid-column: 1 / -1; }

/* Somebody else's words, at the document's scale rather than the card's. */
.dv-record[data-type="story"] .prop-kv-val .tc-quote { margin: 0; font-size: var(--ty-body); }

/* ── The subject: the file, or the live page ── */
.dv-subject { display: flex; flex-direction: column; gap: 0.5rem; max-width: var(--measure-prose); margin: 0 0 1.5rem; }
.dv-subject .tc-mono { margin: 0; font-size: var(--ty-body); word-break: break-all; }
.dv-subject-state { margin: 0; font-size: var(--ty-meta); line-height: 1.5; color: var(--ink-muted); }
.dv-subject .rail-act { margin-top: 0; }

/* `.dv-links` was here — the featured connection phrase rendered as content
   inside `.dv-record`. It read as one more editable property row and was the
   only thing in that column you could not type into. It is one block in the
   rail again; see connectionsBlock for why it was ever two. */

@media (max-width: 900px) {
  /* Two lists side by side stop comparing and start wrapping. */
  .dv-record[data-type="icp"] .prop-rows { grid-template-columns: minmax(0, 1fr); }
  .dv-record .prop-kv-read,
  .dv-record[data-type="ticket"] .prop-kv-read { grid-template-columns: minmax(0, 1fr); }
}

/* ═══════════════════════════════════════════════
   ANSWER → SURFACE
═══════════════════════════════════════════════ */
.answer-apply {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding-top: 0.75rem; border-top: 0.0625rem solid var(--hairline);
}
/* .answer-apply-note is gone. The button said "Show these 3 on the surface"
   and the note beside it said "The grid becomes exactly these documents" — a
   gloss on a label that was already a sentence. Six other .answer-apply
   blocks in this build carry no note at all, so this was the odd one out
   rather than the pattern. */

/* ═══════════════════════════════════════════════
   THE INPUT — degraded state

   AI-unavailable does not disable the product. Filters, grid, viewer and
   editor are state reads and keep working; only the parse and the canvas go.
═══════════════════════════════════════════════ */
.aimy-float-bar.is-degraded { opacity: 0.72; }
.aimy-float-bar.is-degraded .aimy-float-send { pointer-events: none; opacity: 0.4; }
.filter-tray.is-forced { opacity: 1; visibility: visible; transform: none; pointer-events: auto; }

/* The toggle. Hidden above the drawer breakpoint, where the rail is simply
   there and a button offering to reveal it would be describing a state that is
   already true. */
/* ══ AND THE ICON IS THE SAME GREY AS THE BELL BESIDE IT ══════════════
   `--ink-quiet` is `--d300` (#a8b5c5); `.topnav-bell`, sitting in this same
   row four controls over, is `--d400` (#93a2b4). One rung apart, on two
   buttons the same size with the same ground and the same border — which
   reads as one of them being lit and the other not, rather than as a
   hierarchy, because there is no hierarchy between them to state.

   `--d400` / `--d200`, written the way `.topnav-bell` writes it rather than
   through `--ink-faint` / `--ink-secondary`, which resolve to the identical
   values: these three buttons are one family and the point is lost if they
   are spelled three different ways. Sales settles it the same way and in one
   rule — `.topnav-bell:hover, .rail-toggle:hover { color: var(--d200) }` —
   so this is the arrangement both products were already meant to have. */
.rail-toggle {
  display: none;
  width: 2.125rem; height: 2.125rem; flex-shrink: 0;
  align-items: center; justify-content: center;
  margin-right: 0.5rem; border-radius: var(--r-md);
  background: rgba(255,255,255,0.04);
  border: 0.0625rem solid rgba(255,255,255,0.08);
  color: var(--d400); cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
:root[data-theme="light"] .rail-toggle {
  background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10);
}
.rail-toggle:hover { background: rgba(255,255,255,0.08); color: var(--d200); }
:root[data-theme="light"] .rail-toggle:hover { background: rgba(16,24,40,0.07); }
.rail-toggle:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }
.rail-toggle svg { width: 1.0625rem; height: 1.0625rem; fill: none; stroke: currentColor; stroke-width: 2; }

/* Present at every width, painted at none of them until the drawer exists.
   `display` rather than the `hidden` attribute, 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. */
.rail-scrim { display: none; }

/* ---------------------------------------------------------------
   THE BRIEFING, ON A NARROW SCREEN

   This was `display: none`. The rail is the product's whole IA -- the comment
   in index.html says so in as many words: "The IA is one item, so there is
   nothing to navigate to. What the rail carries instead is the briefing."
   Deleting it below 1040px deleted who you are, what is yours, what needs you,
   and where you left off, and left a grid with no way back to any of it.

   It becomes a drawer instead. Same markup, same filter links, same language
   as the filter bar. It stops taking room in the row and starts opening over
   it. --sidebar-width goes to 0 here and --rail-width does not, which is what
   lets the main column and the input band recentre with no second rule.
   --------------------------------------------------------------- */
@media (max-width: 1040px) {
  :root { --sidebar-width: 0px; }

  .rail-toggle { display: inline-flex; }

  .app-sidebar {
    position: fixed; z-index: 700;
    top: var(--topbar-height); bottom: 0; left: 0;
    width: min(var(--rail-width), calc(100dvw - 3.5rem));
    background: var(--panel-bg);
    border-right: 0.0625rem solid var(--glass-border);
    box-shadow: var(--shadow-xl);
    backdrop-filter: blur(1.125rem);
    -webkit-backdrop-filter: blur(1.125rem);
    /* 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-sidebar.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; }

  /* Reading a document already hides the rail at every width. The button that
     opens it should not outlive the thing it opens. */
  body.is-doc .rail-toggle { display: none; }
}
: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 (.k-enter, .modal-sheet): a surface that
   never animates is a surface that is simply there. */
@media (prefers-reduced-motion: no-preference) {
  .app-sidebar { transition: transform var(--t-medium) var(--ease-out), visibility var(--t-medium) var(--ease-out); }
  .app-sidebar.is-open { transition: transform var(--t-slow) var(--ease-out), visibility 0s; }
  /* The scrim leaves with the rail, not on its own clock — a dimmed shell
     behind a rail that has gone is a room still darkened for nothing. */
  .rail-scrim { transition: opacity var(--t-medium) var(--ease-out); }
  .rail-scrim.is-open { transition: opacity var(--t-slow) var(--ease-out); }
}



/* ═══════════════════════════════════════════════
   THE DOCUMENT OVERLAY

   Same frame as the canvas: absolute inside .app-main, blurred backdrop, the
   shell stays interactive. One overlay language for the conversation and for
   the document.

   Stacking. The input sits ABOVE the document, because a bar that stops
   working when you open something is not the surface's one input. The canvas
   sits above both, so you can ask about what you are reading.
═══════════════════════════════════════════════ */
.doc-overlay  { z-index: 450; }
.aimy-float-wrap { z-index: 550; }

/* ══ THE CANVAS'S OWN CONTROLS SIT ABOVE ITS THREAD ════════════════
   `.overlay-close`, `.overlay-badge` and `.overlay-thread` were all
   `z-index: auto`, which puts them on one level where the LAST one painted
   wins. The thread is painted after, so the moment a conversation is long
   enough to scroll, a message bubble passing under the header takes the tap:
   the × is drawn, fully visible, and does nothing. `.overlay-thread` already
   reserves 5rem of top padding, so at the top of a thread nothing is under
   them — which is why this only shows up once you have scrolled, and why a
   short conversation never reproduces it.

   AiMY Sales carries the same fix for the same reason, measured there rather
   than here: "at 894 and at 459 the element under the cursor is `.msg-bubble`
   — the control is drawn, fully visible, and cannot be pressed."

   1, not Sales' 10, because this build's conversation drawer sits at 2 and its
   scrim at 2 as well: a drawer standing over the canvas should still cover the
   canvas's own controls, which is the arrangement verified when that scrim was
   added. 1 clears the thread and nothing else. */
.overlay-close,
.overlay-badge { z-index: 1; }
.aimy-overlay { z-index: 600; }

.doc-overlay {
  position: absolute; inset: 0;
  display: flex; align-items: stretch; justify-content: center;
  padding: 1.25rem 1.25rem 6rem;
  background: rgba(20, 27, 34, 0.34);
  backdrop-filter: blur(1.375rem) saturate(1.3) brightness(0.88);
  -webkit-backdrop-filter: blur(1.375rem) saturate(1.3) brightness(0.88);
  opacity: 0; pointer-events: none;
}
:root[data-theme="light"] .doc-overlay { background: rgba(238, 242, 246, 0.62); }
.doc-overlay.open { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: no-preference) {
  .doc-overlay { transition: opacity var(--t-base) var(--ease-out); }
  .doc-overlay .doc-sheet { transform: translateY(8px); transition: transform var(--t-base) var(--ease-out); }
  .doc-overlay.open .doc-sheet { transform: translateY(0); }
}

.doc-sheet {
  width: 100%; max-width: 58.75rem;
  display: flex; flex-direction: column; min-height: 0;
  background: var(--card-bg-raised);
  border: 0.0625rem solid var(--card-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg, 0 1.5rem 3.75rem rgba(0,0,0,0.4));
  overflow-y: auto; position: relative;
}
/* The editor needs its version panel beside the body, so it takes the room. */
.doc-sheet:has(.wb-editor-split) { max-width: 73.75rem; }

.doc-bar {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.75rem 1rem;
  background: var(--card-bg-raised);
  border-bottom: 0.0625rem solid var(--hairline);
}
.doc-bar-crumb {
  display: inline-flex; align-items: center; gap: 0.375rem;
  font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--ink-muted);
}
.doc-bar-crumb svg { flex-shrink: 0; }
.doc-bar-sep { color: var(--ink-faint); }
.doc-bar-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }
.doc-close {
  width: 1.875rem; height: 1.875rem; border-radius: var(--r-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.05); border: 0.0625rem solid rgba(255,255,255,0.09);
  color: var(--ink-muted); cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.doc-close:hover { background: rgba(255,255,255,0.1); color: var(--d50); }
:root[data-theme="light"] .doc-close { background: rgba(16,24,40,0.04); border-color: rgba(16,24,40,0.10); }
.doc-scroll { padding: 1.25rem 1.5rem 2rem; }
.doc-scroll .doc-view { max-width: none; border: 0; background: none; padding: 0; }
.doc-scroll .toolbar,
.doc-scroll .ver-list,
.doc-scroll .ai-suggestion,
.doc-scroll .comment,
.doc-scroll .comment-thread { max-width: none; }
.doc-scroll .ver-list { padding: 0 0.5rem; }
.doc-scroll > .toolbar { position: sticky; top: 3.375rem; z-index: 2; margin-bottom: 0.75rem; }
.doc-scroll .ai-suggestion { margin-top: 0.75rem; }
/* margin-top lives with the rest of the thread's treatment, above. */

.dv-title { font-family: var(--font-display); }
.dv-section { margin-top: 1rem; }
.dv-section-label,
.ver-panel-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  color: var(--ink-quiet); margin-bottom: 0.5rem;
}
.dv-aud {
  background: none; border: 0; padding: 0 0.375rem 0 0; cursor: pointer; font-family: inherit;
  font-size: inherit; font-weight: inherit; color: inherit;
  border-bottom: 0.0625rem dotted var(--ink-faint);
}
.dv-aud:hover { color: var(--ink-action); }

/* Grounding — governance, stated on the document it governs */
.dv-grounding {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.75rem 0; margin-top: 0.125rem;
  border-top: 0.0625rem solid var(--hairline); border-bottom: 0.0625rem solid var(--hairline);
}
.dv-ground-list { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.dv-ground {
  display: inline-flex; align-items: center; gap: 0.25rem;
  padding: 0.25rem 0.5rem; border-radius: var(--r-pill);
  font-size: var(--ty-micro); font-weight: var(--fw-semibold);
  border: 0.0625rem solid var(--card-border); color: var(--ink-faint);
}
.dv-ground.is-on { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok) 28%, transparent); color: var(--ink-ok); }
.dv-ground.is-on.is-external { background: var(--warn-bg); border-color: color-mix(in srgb, var(--warn) 30%, transparent); color: var(--ink-warn); }
.dv-ground-edit {
  margin-left: auto; background: none; border: 0; cursor: pointer; font-family: inherit;
  font-size: var(--ty-micro); font-weight: var(--fw-semibold); color: var(--ink-muted);
  transition: color var(--t-fast) var(--ease-out);
}
.dv-ground-edit:hover { color: var(--ink-action); }

.dv-prov-act { grid-column: 1 / -1; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; padding-top: 0.25rem; }
.dv-prov-note { font-size: var(--ty-micro); color: var(--ink-warn); }
.dv-prov-val .status-dot { margin-right: 0.25rem; }

.dv-props { padding: 0.75rem 0; border-top: 0.0625rem solid var(--hairline); }
.dv-prop-list { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; margin-top: 0.375rem; }
.dv-prop {
  display: inline-flex; align-items: stretch; overflow: hidden;
  border: 0.0625rem solid var(--card-border); border-radius: var(--r-sm);
  background: none; cursor: pointer; font-family: var(--font-mono);
  transition: border-color var(--t-fast) var(--ease-out);
}
.dv-prop:hover { border-color: var(--card-border-hover); }
.dv-prop-k { padding: 0.25rem 0.5rem; font-size: var(--ty-micro); color: var(--ink-faint); background: rgba(255,255,255,0.04); }
.dv-prop-v { padding: 0.25rem 0.5rem; font-size: var(--ty-micro); color: var(--d200); }
:root[data-theme="light"] .dv-prop-k { background: rgba(16,24,40,0.04); }

.dv-versions { margin-top: 1rem; border-top: 0.0625rem solid var(--hairline); padding-top: 0.75rem; }
.dv-versions-head {
  cursor: pointer; list-style: none;
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wider); text-transform: uppercase; color: var(--ink-faint);
}
.dv-versions-head::-webkit-details-marker { display: none; }
.dv-versions-head::before {
  content: ""; display: inline-block; width: 0.625rem; height: 0.625rem;
  margin-right: 0.25rem; vertical-align: -0.0625rem; background-color: var(--ink-faint);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.dv-versions[open] .dv-versions-head::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
.dv-versions-n {
  margin-left: 0.25rem; padding: 0.125rem 0.375rem; border-radius: var(--r-pill);
  background: rgba(255,255,255,0.06); color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
:root[data-theme="light"] .dv-versions-n { background: rgba(16,24,40,0.06); }
.dv-versions .ver-list { margin-top: 0.5rem; }

/* The send glyph sits inside the field, which is what every message box does
   and what Enter already did. A word-button beside it was a second control for
   one action. */
.comment-compose { position: relative; display: flex; align-items: center; margin-top: 0.75rem; }
.comment-compose .field-input { flex: 1; padding-right: 2.5rem; }
.comment-send {
  position: absolute; right: 0.25rem;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border-radius: var(--r-sm);
  background: none; border: 0; color: var(--ink-faint); cursor: pointer;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.comment-send:hover { color: var(--ink-action); background: rgba(255,255,255,0.06); }
:root[data-theme="light"] .comment-send:hover { background: rgba(16,24,40,0.05); }
.comment-send:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.icon-btn.is-ai svg { display: block; }

/* ── The AiMY menu, anchored to whatever opened it ──

   `aiMenu` and `wireSelectionMenu` both append to #docCanvas and set inline
   top/left from the anchor's rect. Those styles were dead: the only rule making
   the menu absolute was scoped to `.doc-sheet`, which was deleted when the
   document stopped being a modal, so the menu computed `position: static` and
   simply flowed to the end of the canvas — 246px below its own button, and the
   same for every selection menu. A positioned menu also needs a positioned
   host, or it resolves against `.app-main` instead. */
.doc-canvas { position: relative; }
.doc-page .ai-menu { position: absolute; z-index: 5; }
/* Formatting, in the menu a selection opens. The library's `.ai-menu button` is
   a labelled action — 10px of side padding for a word — and these are single
   glyphs, so they take the square the icon wants and keep everything else. */
.ai-menu .ai-menu-fmt {
  justify-content: center;
  min-width: 1.5rem; padding-left: 0; padding-right: 0;
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
}
.ai-menu .ai-menu-fmt.is-on { background: var(--card-bg-raised); color: var(--ink-action); }

/* ═══════════════════════════════════════════════
   THE EDITOR — properties, versions, placeholder

   Restored. These rules were destroyed by a bulk deletion that cut a range
   between two section markers without checking what was inside it. The audit
   in ../README.md — every class the JS renders must resolve to a rule — is the
   check that catches this, and it is a script rather than an eyeball.
═══════════════════════════════════════════════ */
.editor-side .ds-tabs { margin-bottom: 0.75rem; }
.editor-side .ds-tabpanel { padding: 0; }

.props { display: flex; flex-direction: column; gap: 0.75rem; }
/* ── The details, one fact per row ──

   No label column, and no flowing sentence either. Prose around six inline
   controls cannot wrap without orphaning its punctuation, and the open panel
   landed on the words that followed it. A row holds the phrase and its value
   together, and the whole row is the target. */
.prop-rows { display: flex; flex-direction: column; gap: 0.125rem; }
.prop-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.125rem 0.375rem;
  padding: 0.375rem 0.5rem; margin-left: -8px;
  font-size: var(--ty-body); line-height: 1.5; color: var(--ink-secondary);
}
/* The half of the fact the control cannot say for itself. Quiet, because the
   value is what you are reading for — but quiet is --ink-secondary, not the
   3.10:1 the old --d400/--d500 pairing collapsed to in a raised panel. */
.prop-lead { color: var(--ink-secondary); flex-shrink: 0; }
/* ── The colon belongs to the pattern, not to the copy ──

   The panel names its fields now — *Client: Nordwind GmbH* — rather than
   narrating them, and a label that runs straight into its value reads as two
   values. Set here and not in nine strings, because it is punctuation the
   pattern owns: every row gets it, including a custom fact whose name is
   whatever somebody typed, and none of them can forget it.

   Scoped to .props deliberately. The document's own record uses .prop-lead for
   the type's fields, where the leads are still phrases — *Raised by*, *Closed
   with*, *Fits when* — and a colon after a phrase is a colon in the wrong
   place. Same primitive, two readings, one selector between them. */
.props .prop-lead::after { content: ":"; }
.prop-also { margin: 0 0 0.25rem; font-size: var(--ty-body); color: var(--ink-secondary); }
/* The control and its explanation stack, so the sentence reads as belonging to
   the field above it rather than to the row beside it. */
.prop-status { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.prop-why { font-size: var(--ty-micro); color: var(--ink-faint); line-height: 1.45; }
/* A remove control should not be the brightest ink in the column. The library's
   token inherits the accent at 13px, which made every × louder than the value
   it removes. */
/* ── A value is not an action ──

   The library's `.tag-token` is an accent fill, an accent border and accent
   text. Two tag fields put up to ten filled purple pills into a 320px column —
   the loudest thing in a rail whose own rule is "no filled control lives in it"
   and "the accent is spent only on actions". They are tags. The chip shape
   stays, because it is what says these are separable values; the colour goes
   back to the ink scale. */
.props .tag-token {
  background: var(--card-bg-raised); border-color: var(--card-border);
  color: var(--ink-primary); font-size: var(--ty-meta);
}
.props .tag-token button { font-size: var(--ty-micro); color: var(--ink-faint); }
.props .tag-token button:hover { color: var(--ink-err); }

/* Property dropdowns read as fields, not as the filter row's buttons. */
.k-prop { width: 100%; }
.k-prop .v2-dropdown-btn {
  width: 100%; justify-content: space-between;
  height: 1.875rem; padding: 0 0.5rem; border-radius: var(--r-sm);
  font-size: var(--ty-meta); font-weight: var(--fw-semibold);
  background: var(--card-bg-raised); border-color: var(--card-border); color: var(--d100);
}
.k-prop .v2-dropdown-btn:hover { border-color: var(--card-border-hover); }
.k-prop .v2-dropdown-btn svg { width: 0.5625rem; height: 0.5625rem; opacity: 0.6; }
.k-prop .v2-dropdown-panel { min-width: 100%; }

/* ── A control that is a row ──

   The lead lives inside the trigger, so the phrase and its value are one hit
   target and the panel opens under its own row. At rest there is no box: the
   value's weight is the only thing separating it from the words around it, and
   the chevron and the background arrive under the pointer. */
/* ── One left edge for every lead in the panel ──

   Three row shapes share this column — a dropdown, a click-to-edit field and a
   plain row — and all three use the same trick to bleed their hover left while
   keeping their text on the block edge: `margin-left: -8px` against
   `padding-left: 8px`. Only the dropdown did not land on it. Its lead sat 5px
   right of the other twelve, which down a 320px rail is a ragged column of
   leads and the kind of thing you see before you can name it.

   Two causes, both here. The 1px transparent border was one pixel the other
   two rows do not carry. The other four came from the negative margin itself:
   `.v2-dropdown` is a FLEX container, so a 100%-wide item pulled 8px left
   leaves 8px of free space in the line, and the item does not simply start at
   -8. Widening it by exactly the margin removes the free space, and
   `flex: 0 0 auto` stops the shrink that would put it back. */
.k-prop.k-row .v2-dropdown-btn {
  width: calc(100% + 0.5rem); flex: 0 0 auto;
  height: auto; justify-content: flex-start; gap: 0.375rem;
  padding: 0.375rem 0.5rem; margin-left: -8px; border-radius: var(--r-sm);
  background: none; border: 0;
  font-size: var(--ty-meta); font-weight: var(--fw-medium); line-height: 1.5; text-align: left;
  color: var(--ink-muted);
}
.k-prop.k-row .v2-dropdown-btn .dd-label-text {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: var(--fw-semibold); color: var(--d100);
}
.k-prop.k-row .v2-dropdown-btn:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .k-prop.k-row .v2-dropdown-btn:hover { background: rgba(16,24,40,0.04); }
.k-prop.k-row .v2-dropdown-btn:hover .dd-label-text { color: var(--ink-action); }
.k-prop.k-row .v2-dropdown-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.k-prop.k-row .v2-dropdown-btn svg {
  width: 0.5625rem; height: 0.5625rem; flex-shrink: 0; opacity: 0; transition: opacity var(--t-fast) var(--ease-out);
}
.k-prop.k-row .v2-dropdown-btn:hover svg,
.k-prop.k-row .v2-dropdown-btn:focus-visible svg,
.k-prop.k-row.is-open .v2-dropdown-btn svg { opacity: 0.6; }
.k-prop.k-row .v2-dropdown-panel { min-width: 100%; }

/* Tags stay inline after their lead — the tokens ARE the words after "Tagged". */
.props .tag-input {
  display: inline-flex; flex-wrap: wrap; max-width: none; width: auto;
  vertical-align: baseline; padding: 0; gap: 0.25rem;
  background: none; border: 0;
}
.props .tag-input input { font-size: var(--ty-meta); width: 6ch; min-width: 6ch; }
/* Empty, the placeholder is the answer to the sentence, so it needs the room
   to say it: "It touches nothing yet". */
.props .tag-input.is-empty input { width: 12ch; min-width: 12ch; }
/* ── A list of sentences, not of slugs ──

   An ICP's fit criteria are prose — "Existing QA function with a named owner"
   — so the tokens stack one per line instead of flowing inline, and the field
   takes the full width because 6ch is a control for typing `emea` into. */
.props .tag-input.is-prose { display: flex; flex-direction: column; align-items: stretch; width: 100%; gap: 0.25rem; }
.props .tag-input.is-prose .tag-token {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.375rem;
  width: 100%; white-space: normal; text-align: left; line-height: 1.45;
}
.props .tag-input.is-prose input,
.props .tag-input.is-prose.is-empty input { width: 100%; min-width: 0; }
/* ── A set, behind the same trigger as a choice ──

   Groups and Audience stood open as seven checkboxes in the column, which is a
   third of a nine-fact panel spent on two fields and two rows that looked
   nothing like the seven above them. They are `.k-multi` now: the trigger and
   the panel geometry of `.k-prop.k-row`, with checkboxes inside.

   Every rule below is the dropdown's rule, restated rather than inherited. It
   cannot BE a `.v2-dropdown`: aimy-ds.js binds that class and its `choose()`
   is single-select, so a second tick would clear the first. Same conclusion
   `.k-date` reached, same remedy, recorded in GAPS.md.

   The 8px bleed is load-bearing. Three row shapes share this column and all
   three hang their hover 8px left of the text with `margin-left: -8px` against
   `padding-left: 8px`; a flex item pulled left that way leaves free space in
   the line unless it is widened by exactly the margin, which is what the
   `calc` and the `flex: 0 0 auto` are for. Getting it wrong here would put one
   lead 8px off a column of nine. */
.k-multi { position: relative; width: 100%; }
.k-multi-btn {
  display: flex; align-items: center; gap: 0.375rem;
  width: calc(100% + 0.5rem); flex: 0 0 auto;
  padding: 0.375rem 0.5rem; margin-left: -8px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--ty-meta); font-weight: var(--fw-medium);
  line-height: 1.5; text-align: left; color: var(--ink-muted);
}
.k-multi-btn .dd-label-text {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: var(--fw-semibold); color: var(--d100);
}
.k-multi-btn:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .k-multi-btn:hover { background: rgba(16,24,40,0.04); }
.k-multi-btn:hover .dd-label-text { color: var(--ink-action); }
.k-multi-btn:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
/* The chevron arrives under the pointer, like the dropdowns' — at rest the
   row is a fact, not a control with furniture on it. */
.k-multi-btn svg {
  width: 0.5625rem; height: 0.5625rem; flex-shrink: 0; opacity: 0;
  transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.k-multi-btn:hover svg,
.k-multi-btn:focus-visible svg,
.k-multi.is-open .k-multi-btn svg { opacity: 0.6; }
.k-multi.is-open .k-multi-btn svg { transform: rotate(180deg); }

/* `.v2-dropdown-panel`'s geometry and glass, from the same values. */
.k-multi-panel {
  position: absolute; top: calc(100% + 0.25rem); left: 0; z-index: 200;
  min-width: 100%; padding: 0.25rem;
  background: var(--d800); border: 0.0625rem solid rgba(255,255,255,0.10);
  border-radius: var(--r-lg);
  box-shadow: 0 0.5rem 2rem rgba(0,0,0,0.5), inset 0 0.0625rem 0 rgba(255,255,255,0.06);
  max-height: 16.25rem; overflow-y: auto;
}
:root[data-theme="light"] .k-multi-panel {
  background: #ffffff; border-color: rgba(16,24,40,0.12);
  box-shadow: 0 0.5rem 2rem rgba(16,24,40,0.16);
}
/* A row, not a chip. The whole row is the label, so the pointer never has to
   find an 18px box — and the box is a real checkbox, so Tab reaches it and
   Space ticks it without a keyboard model of our own. */
.k-multi-opt {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.375rem 0.5rem; border-radius: var(--r-sm);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.k-multi-opt:hover { background: rgba(255,255,255,0.06); }
:root[data-theme="light"] .k-multi-opt:hover { background: rgba(16,24,40,0.05); }
.k-multi-opt input { width: 0.9375rem; height: 0.9375rem; }
.k-multi-opt input[type="checkbox"]:checked::after {
  left: 50%; top: 50%; width: 0.25rem; height: 0.46875rem;
  transform: translate(-50%, calc(-50% - 1.2px)) rotate(45deg);
}
.k-multi-text {
  font-size: var(--ty-meta); font-weight: var(--fw-medium); color: var(--d100);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.k-multi-opt input:checked ~ .k-multi-text { font-weight: var(--fw-semibold); }

/* ── The tick, centred in its box ──

   The library positions it with two fixed offsets — `left: 5px; top: 1.5px` on an
   18px box — and neither survives contact with the box's own border. Measured on
   the connect sheet: `box-sizing` is `border-box`, so the ::after's containing
   block is the PADDING box, which is 18px minus two borders. The tick's own box
   centre landed at (7.5, 6.0) where the padding box centre is (8.2, 8.2) — 2.2px
   high and 0.7px left, and it moves again the moment a border width changes.

   Two separate corrections, because there are two separate offsets:

   FIRST, centre the box on the box. `left`/`top` at 50% with a -50% translate is
   the version that cannot drift: it reads the container at render time, so a
   border change moves the tick with it instead of leaving it behind.

   SECOND, and this is the part a plain -50% gets wrong: the glyph is not centred
   INSIDE its own element. It is drawn from the right and bottom borders of a 5×9
   rectangle, so the ink sits in the bottom-right corner and the rest is empty.
   Rotating 45° about the element's centre swings that ink DOWN — 1.4px at these
   dimensions, computed from the two border strips' area centroid — so centring
   the element leaves the visible tick low by exactly that much.

   Hence the calc. It is an optical correction, which this file already keeps
   apart from rhythm: it is a judgment about one glyph at one size, not a value
   off a scale, and it is written as the number it is rather than folded into a
   percentage that hides what it is compensating for. */
.ds-choice input[type="checkbox"]:checked::after {
  left: 50%; top: 50%;
  transform: translate(-50%, calc(-50% - 1.4px)) rotate(45deg);
}

/* ── Anything the fixed fields cannot hold ──

   Two permanent text boxes named only by `aria-label` were the least legible
   thing in the rail. Closed, a property is a fact in the same shape as every
   row above it — "tier is enterprise". Open, it is the two inputs, and the
   placeholders do the naming the invisible labels never did. */
.prop-custom { display: flex; flex-direction: column; gap: 0.125rem; }
.prop-also { margin: 0 0 0.125rem; }
.prop-kv { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 1.5rem; gap: 0.25rem; align-items: center; }
.prop-kv .field-input { height: 1.75rem; padding: 0 0.5rem; font-size: var(--ty-meta); }
/* Closed: the read row spans the whole grid and the inputs are not rendered. */
.prop-kv:not(.is-open) { grid-template-columns: minmax(0, 1fr) 1.5rem; }
/* A type field has no delete control and no key to rename — the key belongs to
   the type, and removing it would be changing what kind of thing this is. So
   one column open or closed, where a custom fact needs two and a bin. */
.prop-kv.is-fixed,
.prop-kv.is-fixed:not(.is-open) { grid-template-columns: minmax(0, 1fr); }

/* ── The long-text surface ──
   Which document you are editing, because the modal covers the title that
   would otherwise say so. */
.xm-of { margin: 0 0 0.75rem; font-size: var(--ty-meta); color: var(--ink-muted); }
.xm-of strong { color: var(--d100); font-weight: var(--fw-semibold); }
/* A sentence needs the measure and the wrap a rail input cannot give it.
   Resize stays vertical: horizontal would drag it out of the modal. */
.xm-text {
  width: 100%; min-height: 6rem; resize: vertical;
  padding: 0.5rem; font-family: var(--font-sans); font-size: var(--ty-meta); line-height: 1.6;
}
.prop-kv:not(.is-open) .field-input { display: none; }
/* ── The number field's editing control ──

   A number rendered as a bare `input type=number`, so the browser drew its own
   stepper on it: two grey chevrons belonging to Chrome, on a page where every
   other control is the library's. The library ships `.stepper` and it was
   never used here.

   These rules do not draw it — that is the library's. They fold it away with
   the row it belongs to, which the rule above only knew how to do for
   `.field-input`, and bring it down to the 28px this panel's fields use from
   the 34px a standalone stepper is. */
.prop-kv:not(.is-open) .stepper { display: none; }
.prop-kv .stepper button { width: 1.75rem; height: 1.75rem; font-size: var(--ty-body); }
.prop-kv .stepper input { width: 2.75rem; height: 1.75rem; font-size: var(--ty-meta); }
.prop-kv .stepper button:focus-visible,
.prop-kv .stepper input:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.prop-kv.is-open .prop-kv-read { display: none; }
.prop-kv-read {
  display: flex; align-items: baseline; gap: 0.375rem; min-width: 0;
  padding: 0.375rem 0.5rem; margin-left: -8px; border-radius: var(--r-sm);
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--ty-body); line-height: 1.5; color: var(--ink-secondary);
}
.prop-kv-read:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .prop-kv-read:hover { background: rgba(16,24,40,0.04); }
.prop-kv-read:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.prop-kv-val {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: var(--fw-semibold); color: var(--ink-primary);
}
.prop-kv-read:hover .prop-kv-val { color: var(--ink-action); }
.prop-kv-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink-faint); cursor: pointer;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.prop-kv-x:hover { color: var(--ink-err); background: var(--err-bg); }
.prop-add {
  align-self: flex-start; background: none; border: 0; padding: 0.25rem 0; cursor: pointer;
  font-family: inherit; font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--ink-quiet);
}
.prop-add:hover { opacity: 1; color: var(--ink-action); }
.prop-add:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; border-radius: var(--r-xs); }

/* Versions are pressable in the editor, and the one you opened says so. */
.ver-item.is-pickable {
  width: 100%; text-align: left; border: 0; background: none;
  font-family: inherit; cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out);
}
.ver-item.is-pickable:hover { background: rgba(255,255,255,0.05); }
:root[data-theme="light"] .ver-item.is-pickable:hover { background: rgba(16,24,40,0.05); }
.ver-item.is-pickable:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: -2px; }
.ver-item.is-viewing { background: var(--accent-dim); }
.ver-hint { font-size: var(--ty-micro); color: var(--ink-faint); margin-top: 0.5rem; line-height: var(--lh-snug); }
/* ── The library's version list is drawn at specimen sizes ──

   `.ver-time` is 10px and `.ver-tag` is 8.5px — the smallest run in the whole
   system, on a stamp that says which version you are looking at. Measured at
   3.83:1 and 4.08:1 respectively. Same finding as GAPS §27 for `.tc-*`: these
   are specimen steps, and a consumer at application scale has to re-step them.
   Re-stepped here rather than in the library, which is extracted, not authored. */
.ver-time { font-size: var(--ty-micro); color: var(--ink-quiet); }
.ver-tag {
  font-size: var(--ty-micro); letter-spacing: var(--ls-wide);
  color: var(--ink-action); border-color: rgba(var(--accent-rgb), 0.32);
}

.ver-preview {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.5rem 0.75rem; margin-bottom: 0.75rem;
  border-radius: var(--r-md);
  background: var(--accent-dim); border: 0.0625rem solid rgba(var(--accent-rgb), 0.3);
  font-size: var(--ty-meta); color: var(--d100);
}
.ver-preview svg { flex-shrink: 0; color: var(--ink-action); }
.ver-preview-end { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

/* A placeholder, not content. It was a <p> inside the contenteditable, which
   made it real text: it looked publishable because it was. */
.dv-body.is-blank { min-height: 7.5rem; }
/* Keyed off .is-blank, not :empty — the element carries whitespace text nodes
   from the template, and :empty counts those as content. */
.dv-body.is-blank[data-placeholder]::before {
  content: attr(data-placeholder);
  color: var(--ink-faint); font-style: italic; pointer-events: none;
}

/* Overridden status carries a marker wherever it shows. */
.trust-state.is-pinned { position: relative; }
.pin-dot {
  width: 0.3125rem; height: 0.3125rem; border-radius: 50%;
  background: currentColor; opacity: 0.75; margin-left: 0.125rem; flex-shrink: 0;
}

.commit-typed { margin-top: 0.75rem; }
.cite-action.is-danger { color: var(--ink-err); }
.cite-action.is-danger:hover { color: var(--ink-err); opacity: 0.8; }
.skeleton-line { height: 0.6875rem; border-radius: var(--r-xs); }

/* ═══════════════════════════════════════════════
   CONFLICT — two documents, one choice
═══════════════════════════════════════════════ */
.conflict-pair {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(14.375rem, 1fr));
  gap: 0.5rem;
}
.conflict-side {
  display: flex; flex-direction: column; gap: 0.375rem;
  padding: 0.75rem; border-radius: var(--r-md);
  border: 0.0625rem solid var(--card-border); background: var(--card-bg);
}
.conflict-head { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.conflict-title {
  font-size: var(--ty-meta); font-weight: var(--fw-bold); color: var(--d50);
  line-height: var(--lh-snug);
}
.conflict-body { font-size: var(--ty-meta); color: var(--ink-muted); line-height: var(--lh-base); }
.conflict-meta { font-size: var(--ty-micro); color: var(--ink-faint); margin-top: auto; }
.conflict-side .entry-action { align-self: flex-start; }

/* What you did with a proposal, written into the message it belongs to. */
.ai-outcome {
  display: flex; align-items: center; gap: 0.375rem; margin-top: 0.5rem;
  padding-top: 0.5rem; border-top: 0.0625rem solid var(--hairline);
  font-size: var(--ty-micro); font-weight: var(--fw-semibold);
}
.ai-outcome.is-accept { color: var(--ink-ok); }
.ai-outcome.is-reject { color: var(--ink-faint); }
.ai-suggestion ins.is-editing {
  outline: 0.125rem solid var(--brand); outline-offset: 0.125rem;
  border-radius: var(--r-xs);
}

/* ═══════════════════════════════════════════════
   DROP LAYER
═══════════════════════════════════════════════ */
.drop-layer {
  position: fixed; inset: 0; z-index: 930;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 27, 34, 0.55);
  backdrop-filter: blur(0.375rem); -webkit-backdrop-filter: blur(0.375rem);
  opacity: 0; pointer-events: none;
}
:root[data-theme="light"] .drop-layer { background: rgba(238, 242, 246, 0.7); }
.drop-layer.open { opacity: 1; }
/* display:flex above outranks the UA's [hidden] rule, so state it here too. */
.drop-layer[hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .drop-layer { transition: opacity var(--t-fast) var(--ease-out); }
}
.drop-card {
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  padding: 2rem 3rem; border-radius: var(--r-xl);
  background: var(--card-bg-raised);
  border: 0.125rem dashed rgba(var(--accent-rgb), 0.5);
  color: var(--ink-action); text-align: center;
}
.drop-title { font-size: var(--ty-body); font-weight: var(--fw-bold); color: var(--d50); }
.drop-sub { font-size: var(--ty-meta); color: var(--ink-faint); max-width: 34ch; }
.drop-hint { color: var(--ink-faint); font-style: italic; }

/* ═══════════════════════════════════════════════
   CONVERSATION BLOCKS

   Governance and source health used to be a panel above the grid. They are
   answers now, so they are built from the answer surface's own parts and only
   add the shapes an answer did not already have.
═══════════════════════════════════════════════ */
.conv-facts {
  display: flex; align-items: baseline; gap: 1.25rem; flex-wrap: wrap;
  padding: 0.75rem 0; border-top: 0.0625rem solid var(--hairline); border-bottom: 0.0625rem solid var(--hairline);
}
.conv-fact { font-size: var(--fs-xs); color: var(--ink-muted); }
.conv-fact strong { color: var(--d50); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.conv-fact-v {
  font-size: var(--fs-sm); font-weight: var(--fw-bold); color: var(--d100);
  font-variant-numeric: tabular-nums;
}
.conv-fact-l { font-size: var(--ty-meta); color: var(--ink-muted); }
.conv-code { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--ink-err); }

.conv-label {
  font-size: var(--ty-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--ink-muted);
}
.conv-ground { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }

.conv-log { display: flex; flex-direction: column; gap: 0.25rem; }
.conv-log-row { display: flex; align-items: center; gap: 0.5rem; font-size: var(--fs-xs); }
.conv-log-when { width: 3.75rem; flex-shrink: 0; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.conv-log-note { color: var(--ink-muted); }

.answer-apply { flex-wrap: wrap; }

/* The chip's label opens the conversation about that axis; its × still just
   removes the filter. Two targets in one chip, so both need to read as one. */
.ctx-ask {
  display: inline-flex; align-items: center; gap: 0.25rem;
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: inherit; font-size: inherit; font-weight: inherit; color: inherit;
  border-bottom: 0.0625rem dotted currentColor;
}
.ctx-ask:hover { color: var(--ink-action); }
.ctx-ask:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* The way into a collection's or a source's conversation, on the one line that
   is already saying what you filtered to. */
.rm-ask {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: inherit; font-size: inherit; font-weight: var(--fw-semibold);
  color: var(--ink-action); border-bottom: 0.0625rem dotted currentColor;
}
.rm-ask:hover { opacity: 0.75; }
.rm-ask:focus-visible { outline: 0.125rem solid var(--brand); outline-offset: 0.125rem; }

/* The choice forms inside a commit surface. They were named for the axis panel
   that used to open them; the panel is gone and the forms are not. */
.commit-form { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.75rem; }
.commit-choice { display: flex; align-items: center; gap: 0.5rem; }
.commit-choice-label { font-size: var(--ty-meta); font-weight: var(--fw-semibold); color: var(--d100); }
.commit-choice-label .gov-ext { display: inline; margin-left: 0.375rem; }
.retain-note { font-size: var(--ty-meta); color: var(--ink-faint); margin-top: 0.75rem; line-height: var(--lh-base); }

/* Nothing to save, so this is a state and not a button. */
.doc-saved {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: var(--ty-micro); font-weight: var(--fw-semibold); color: var(--ink-faint);
  margin-right: 0.125rem;
}
.doc-saved svg { color: var(--ink-ok); }

/* And its opposite. Same slot, same weight, said in the colour the rest of the
   product uses for "this is not settled yet" — loud enough to be the reason
   you look at the two buttons beside it, quiet enough not to read as an error.
   It is a label and not a button: the buttons are what you do about it. */
.doc-unsaved {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: var(--ty-micro); font-weight: var(--fw-semibold); color: var(--ink-warn);
  margin-right: 0.125rem;
}
.doc-unsaved svg { color: var(--ink-warn); }

/* ═══════════════════════════════════════════════
   UTILITIES
═══════════════════════════════════════════════ */
.k-stack { display: flex; flex-direction: column; }
.k-gap-2 { gap: 0.5rem; }  .k-gap-3 { gap: 0.75rem; }  .k-gap-4 { gap: 1rem; }
.k-row { display: flex; align-items: center; }
.k-hidden { display: none !important; }
.k-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ═══════════════════════════════════════════════
   THE PATTERNS — where they sit

   halaska-mounts.js renders them; this only gives each one a place to land.
   The patterns carry their own styling (inline, through the token bridge), so
   nothing here paints them — these are hosts, not skins.
═══════════════════════════════════════════════ */

/* In the document rail, above `AiMY noticed` — the reply comes before the
   observation, because a reader who reported a problem is looking for the
   answer to that, not for a restatement of the state. */
.agent-repair { margin-bottom: 0.875rem; }
.agent-repair:empty { display: none; }

/* The run and its receipt. Same corner and the same z-layer as the toast they
   replace for bulk writes, so a write reports itself in the place this product
   has always reported writes. `hidden` is honoured by the library's reset, so
   the host takes no space until something is in it. */
#agentHost {
  position: fixed; right: 1.5rem;
  /* Clear of the floating composer, which owns the bottom of this shell. At
     1.5rem the run sat ON the input — the two things a write involves, the
     sentence and its result, stacked on the same pixels. */
  bottom: 5.75rem;
  z-index: 60; max-width: min(28rem, calc(100vw - 3rem));
  display: flex; justify-content: flex-end;
}
@media (max-width: 720px) {
  #agentHost { left: 1rem; right: 1rem; max-width: none; }
}

/* ═══════════════════════════════════════════════
   A TARGET BIG ENOUGH TO HIT, EVERYWHERE ELSE IT MATTERS

   chat.css states this argument for the gate's two rail buttons, and it is
   keyed to `pointer: coarse` rather than to a width because the question is
   what is doing the pointing, not how wide the window is. This is that rule
   applied to every control that was still cursor-sized.

   MEASURED at a 390px viewport across both shells and the settings modules.
   Forty-one controls came in under 44px; these are the ones that failed WCAG
   2.5.8's 24px floor outright, plus the icon-only buttons that are the only
   way into something:

       .tc-who / .tc-src            60x17   x34 instances
       .ins-act                     96x17
       .set2-tip-b                  18x18   (in settings.css)
       .entry-action.em-review      73x21
       .entry-action.em-investigate 82x21
       .clip-btn                    28x28
       .prod-chev                   24x24
       .ds-theme-toggle / bell      34x34
       .ov-chats-toggle             32x32

   NOTHING MOVES, AND THAT IS THE POINT. These sit in rows whose rhythm is the
   design — 34 card bylines each growing to 44px would re-lay-out every card on
   the surface. Verified rather than asserted: with a noise floor of 0 measured
   across two identical snapshots, applying this block moves the treated
   controls and NOTHING ELSE.

   ── TWO TECHNIQUES, BECAUSE ONE OF THEM DOES NOT ALWAYS WORK ──────────────
   The tidy way to grow a hit area without touching layout is a transparent
   `::after` stretched past the box. It fails silently on `.tc-who`, which
   carries `overflow: hidden` for its ellipsis — an absolutely positioned
   pseudo inside a clipping box is clipped to the box, so the target stays 17px
   and nothing says so. Measured: `.tc-src` answered on all four edges and
   `.tc-who`, with identical rules, on one.

   So the text buttons take PADDING instead, with a negative margin of the same
   size cancelling it. The padding grows the border box — which is the hit area
   — while the margin keeps the outer size the row lays out against, and both
   parents here are `display: flex; align-items: center`, where that cancels
   exactly. `z-index: 1` because the grown box now reaches over the line of
   text beneath it, and without it that text, painting later, takes the tap.
   Checked: no other control's centre resolves to one of these afterwards.

   THE ONE THAT CANNOT HAVE 44. `.ins-act` sits in a wrapping row of actions
   with 15px above and 8 below it — measured — so a 44px box would reach into
   the next row of the same group and start taking its taps, which is worse
   than a small target. It takes 32: past the 24px floor, clear of both
   neighbours. 0.46875rem of padding each side is exactly that.

   IT LIVES AT THE END OF THIS FILE, AND THAT IS LOAD-BEARING. `.tc-who`,
   `.tc-src` and `.ins-act` each declare `padding: 0` in their own component
   block, and a `padding` SHORTHAND beats a `padding-block` longhand of equal
   specificity by source order alone. Written where these controls are
   described, the margin applied and the padding did not — measured on a real
   coarse pointer: `margin-block: -13.5px` with `padding: 0px`, which is the
   box 27px SMALLER than it was rather than 27px bigger. Anything added here
   has to come after every component it corrects.

   Inside `pointer: coarse`, so a mouse never sees any of it and the 1536
   reference cannot reach this block at all. */
@media (pointer: coarse) {
  /* Text buttons: padding grows the target, the negative margin hides it from
     the layout. See the note above for why these cannot use a pseudo. */
  .tc-who, .tc-src {
    padding-block: 0.84375rem; margin-block: -0.84375rem;
    position: relative; z-index: 1;
  }
  .ins-act {
    padding-block: 0.46875rem; margin-block: -0.46875rem;
    position: relative; z-index: 1;
  }

  /* Box and icon buttons: none of these clips, so the pseudo is free and
     leaves the border box — which some of them draw — exactly as it was. */
  .entry-action.em-review, .entry-action.em-investigate,
  .clip-btn, .prod-chev, .ds-theme-toggle { position: relative; }
  /* `.topnav-bell` is already `position: relative` (it carries `.bell-dot`),
     and `.ov-chats-toggle` is `position: absolute` — restating `relative` on
     that one tore it out of its own placement and moved it 179px across the
     canvas. Measured. Neither gets a position here; both keep their pseudo. */
  .entry-action.em-review::after, .entry-action.em-investigate::after,
  .clip-btn::after, .prod-chev::after,
  .ds-theme-toggle::after, .topnav-bell::after,
  .ov-chats-toggle::after {
    content: '';
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: max(100%, 2.75rem);
    height: max(100%, 2.75rem);
  }
}
