/* ============================================================================
   crm-shell2.css — the new CRM shell. DELIBERATELY EMPTY.

   Created 11 Aug 2026, Track B slice 3. This file is the VAN, and it ships
   before anything is loaded into it: the delivery mechanism is proved to work
   — new file on disk, linked from the page, reachable behind the switch, seen
   by the gates, byte-identical screenshots on both sides — while it still
   contains nothing that could have changed a pixel. A mechanism proved after
   it carries cargo cannot be told apart from the cargo.

   ── WHY A SECOND FILE AT ALL ─────────────────────────────────────────────────
   There is ONE stylesheet for both pages, and 49 classes appear in both pages'
   markup. Redesigning the CRM by editing that file edits the quote portal in
   the same breath — and the quote portal is what the BD team quotes real
   customers with, on production, today. A file that only crm.html loads cannot
   reach index.html. That is a guarantee made by a file boundary rather than by
   being careful, and this codebase has a written record of being careful
   failing silently.

   ── WHAT MAY GO IN HERE ──────────────────────────────────────────────────────
   The new shell's layout, and nothing else. Specifically NOT:

   · Colour literals. The raw-hex ratchet stands at 302 with hexOutside at
     exactly 302 — ZERO headroom — and D17 says the ratchet may fall, never
     rise. Use var(--token) against the 36 tokens styles.css already declares.
     If this file ever needs its own :root, uicheck now sums every :root block
     rather than only the first (fixed in the same commit as this file), but
     the ratchet still has no slack: adding a colour is a deliberate act that
     goes to Shaf, not a side effect of a layout change.

   · Re-declarations of a class styles.css already defines, until the D20
     question is settled. uicheck's rule-conflict check keys on
     selector|property across the CONCATENATION of every stylesheet, so a
     deliberate cross-file override reads to it as the same class defined
     twice — a hard error with no legal way to silence it, because
     KNOWN_CONFLICTS may shrink and never grow. Deciding what D20 means across
     a file boundary is a doctrine change, and doctrine is Shaf's.
     ⚠ The way to sidestep it entirely is to give the new shell NEW class
     names. A new shell is not a re-skin of the old classes; it is a different
     shell that happens to sit on the same page. Prefer that.

   ── AND THE TRAP THAT IS ALREADY PROVEN ──────────────────────────────────────
   styles.css:2552 is `.crm-page .main-nav .nav-btn` — specificity (0,3,0).
   Every scoping scheme proposed for this file so far is weaker and LOSES in
   every file order, no matter which stylesheet is linked second: `.crm-shell2
   .nav-btn` is (0,2,0) and `body.shell-v2 .nav-btn` is (0,2,1). A rule that
   cannot WIN is a lie (D33), and the sidebar would have shipped with the old
   nav padding silently winning while every gate stayed green.
   Measure computed style on the real element. Never infer it from this file.
   ============================================================================ */

/* ============================================================================
   SLICE 4 — the deal list, read on a phone. The first cargo in the van.

   Shaf named this surface in his own words on 9 Aug — "ye senarai deal nak baca
   di phone" — which is what makes it the fourth screen held to 390px as a build
   gate (N7). It was named that day and gated NOWHERE until 11 Aug.

   Nine columns of a table do not fit in 390px and never will: measured, the
   panel scrolled sideways by 493px. Sideways scrolling to read one deal row is
   not reading, so below 768px each row becomes a CARD and each cell becomes a
   labelled field.

   ⚠ NOTHING IS DROPPED, SEN INCLUDED. Not a design flourish — Shaf's reason,
   recorded: a number that vanishes on a phone is a number somebody will assume
   is zero. The easy version of this pattern hides the columns that do not fit;
   that version is forbidden here, and the gate counts the labels against the
   table's own headings so a quietly dropped column fails the build.

   ── Why every selector is this long ──────────────────────────────────────────
   `.crm-table { min-width: 460px }` at styles.css:3280 is what forces the
   sideways scroll, and `.browse-table tbody td` (0,1,2) sets the cell padding.
   A rule that cannot WIN is a lie (D33), so each rule below is deliberately
   more specific than the one it must beat rather than relying on this file
   being linked second — file order is not a specificity strategy, and the day
   somebody reorders the <link> tags it would fail silently.
   ⚠ And a media query adds NO specificity of its own. The @media around these
   rules buys nothing in the cascade; only the selectors do.

   ── Scoped to NAMED SURFACES, never to the class ─────────────────────────────
   `.crm-table` is worn by several tables, and a screen joins the phone standard
   when somebody NAMES the job it does there — not by inheriting a class.
   Widening this to `.crm-table` would silently enrol every other table in a
   standard nobody asked for. So the selector list names the surfaces, one by one.

   ⚠ UPDATED 13 Aug 2026 — this comment said "only the DEAL LIST has had a phone
   job named for it", which stopped being true the moment Shaf said *"The Leads
   table is not responsive on phone"*. **#crmLeads is the FIFTH phone-gated
   surface** (Approvals · the shell · My work · the deal list · Leads) and joins
   the SAME rules rather than getting a copy of them: two blocks saying one thing
   drift within a week (D20), and this file has already paid for a note that
   stated a fact instead of pointing at one. **The next surface to be named is
   added to these selector lists — it does not get its own block.**
   ⚠ Adding a surface here means adding it to `nfr.md` §N7 in the SAME change.
   The standard is N7's to state; this file only implements it.
   ============================================================================ */

@media (max-width: 767px) {

  /* The panel stops being a scroll container. Its frame goes too: each card
     carries its own, and a border around a stack of bordered cards reads as a
     box somebody forgot to remove. */
  [data-shell="2"] #crmList .browse-tablewrap,
  [data-shell="2"] #crmLeads .browse-tablewrap,
  [data-shell="2"] #crmAccount .browse-tablewrap {
    overflow: visible;
    max-height: none;
    border: 0;
    background: transparent;
  }

  /* The floor that made it scroll. */
  [data-shell="2"] #crmList .crm-table,
  [data-shell="2"] #crmLeads .crm-table,
  [data-shell="2"] #crmAccount .crm-table {
    min-width: 0;
    display: block;
  }

  /* The headings move into the cells as labels, so the row of them is redundant
     — but it is REMOVED FROM VIEW, not from the document: it is still what the
     labels are read from, and screen readers still need it. `display: none`
     would take it from both. */
  [data-shell="2"] #crmList .crm-table thead,
  [data-shell="2"] #crmLeads .crm-table thead,
  [data-shell="2"] #crmAccount .crm-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  [data-shell="2"] #crmList .crm-table tbody,
  [data-shell="2"] #crmLeads .crm-table tbody,
  [data-shell="2"] #crmAccount .crm-table tbody {
    display: block;
  }

  /* The card. */
  [data-shell="2"] #crmList .crm-table tbody tr,
  [data-shell="2"] #crmLeads .crm-table tbody tr,
  [data-shell="2"] #crmAccount .crm-table tbody tr {
    display: block;
    margin: 0 0 10px;
    padding: 4px 2px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
  }

  /* THE MARK MOVES TO THE CARD, because on a phone the CARD is the row.
     ⚠ Found by LOOKING at it, which is the only way it could have been (D14):
     every measurement passed and the computed style was correct. The shared rule
     puts the bar on the first CELL — right on a desktop table, where the first
     cell spans the row's full height — but a card stacks its cells, so the bar
     became a 3px stub beside CODE and read as a rendering artifact rather than a
     deliberate mark. The band was doing all the work and the bar was noise.
     Same shape as the slice-5 finding where a status badge stretched to the full
     width of its value column: right colour, right position, WRONG SHAPE. */
  [data-shell="2"] #crmList .crm-table tbody tr.row-peeked {
    box-shadow: inset 3px 0 0 var(--primary-text);
  }
  [data-shell="2"] #crmList .crm-table tbody tr.row-peeked > td:first-child {
    box-shadow: none;
  }

  /* Label left, value right, on one line each. `minmax(0, …)` on BOTH tracks
     because a grid track's default minimum is min-content (D21): without it the
     long client name — "Perbadanan Pengurusan Sisa Pepejal dan Pembersihan Awam
     Wilayah Persekutuan Labuan" — would refuse to shrink and push the card back
     off the screen edge, which is the defect this whole rule exists to remove.

     ⚠ The label column is a TOKEN, not a number typed twice, because the two
     surfaces have different longest headings and one width cannot serve both:
     the deal list tops out at "Weighted" (8 characters) while Leads carries
     "Organisation" (12). Hard-coding 8.5ch here would wrap every Leads label
     onto two lines; hard-coding 12ch would steal three characters of value
     column from every deal card. Each surface states its own below, and the
     shared rule reads the token — one definition, two widths (D20). */
  [data-shell="2"] #crmList .crm-table tbody td,
  [data-shell="2"] #crmLeads .crm-table tbody td,
  [data-shell="2"] #crmAccount .crm-table tbody td {
    display: grid;
    grid-template-columns: minmax(0, var(--card-label-w, 8.5ch)) minmax(0, 1fr);
    gap: 12px;
    align-items: baseline;
    padding: 6px 12px;
    border-bottom: 0;
    text-align: left;
    overflow-wrap: anywhere;
    /* ⚠ A grid item's justify-self defaults to STRETCH, so a badge inside a cell
       grows to the full width of the value column and a one-word status renders
       as a bar across the card. Found by LOOKING at the Leads cards (D14) — every
       measurement passed, because a stretched pill has exactly the right text,
       the right colour and the right position, and is simply the wrong shape.
       `start` makes each value hug its own content; text still wraps inside the
       track, because the track is what caps it. */
    justify-items: start;
  }

  /* "Organisation" is the longest label on this screen. Measured, not guessed. */
  [data-shell="2"] #crmLeads { --card-label-w: 12ch; }
  /* ⚠ Its own width, because its longest heading is longer than either sibling's:
     the deal list tops out at "Weighted" (8), Leads at "Organisation" (12), and this
     screen carries "Last contacted" (14). One number cannot serve three surfaces —
     which is exactly why the width is a token each surface sets rather than a
     figure typed into the shared rule (D20). */
  [data-shell="2"] #crmAccount { --card-label-w: 14ch; }

  /* ⚠ THE EMPTY-STATE ROW IS A SENTENCE, NOT A FIELD, and it was rendering
     broken on both card surfaces until 13 Aug 2026.
     `<td colspan="8" class="muted">No leads are visible to your role.</td>`
     carries no data-label, so its ::before resolves to "" — but an empty
     pseudo-element is still a GRID ITEM, and it held the label column open.
     Measured at 390px: the message started 126px in and wrapped inside a
     176px column, with a third of the card left blank beside it.
     ⚠ NO GATE HAD EVER SEEN THIS, and could not have: the fixture always
     draws rows, so the zero-rows extreme (D22) was never rendered on a phone.
     It was found by deliberately building the empty case and looking at it.
     ⚠ It was live on the DEAL LIST too, from slice 4 (11 Aug) — inherited, not
     introduced, by Leads. Both are fixed here, which is the point of the two
     surfaces sharing one rule rather than owning copies (D20). */
  [data-shell="2"] #crmList .crm-table tbody td:not([data-label]),
  [data-shell="2"] #crmLeads .crm-table tbody td:not([data-label]),
  [data-shell="2"] #crmAccount .crm-table tbody td:not([data-label]) {
    display: block;
  }

  /* ⚠ `justify-items: start` above is right for a VALUE and wrong for a CONTROL,
     and this pair is the correction — found by a review of the commit that added
     it, 13 Aug 2026.
     A value should hug its text, which is why the rule exists (a one-word status
     was rendering as a bar across the card). But the slice-9 value EDITOR is a
     control, and `start` let it take its intrinsic 216px inside a 201px track:
     measured, its right edge landed exactly on the card's 1px border and spilled
     3px past the cell's content box. Under the previous `stretch` it was 201px
     and sat 15px clear. So the editor opts back IN to stretching, and is told it
     may shrink — `min-width: 0`, because a flex container's default minimum is
     its own min-content and that is what pushed it out in the first place. */
  [data-shell="2"] #crmList .crm-table tbody td .dl-editbox {
    justify-self: stretch;
    width: 100%;
    min-width: 0;
  }

  /* ⚠ AND THE BUTTONS INSIDE IT WERE 29px WIDE — "Cancel" stacked as six
     one-character lines. PRE-EXISTING, not introduced here, but it lives in the
     rule this work touched and it breaks the phone standard the same media block
     declares: the 44px minimum is honoured on HEIGHT and silently missed on
     WIDTH. The cause is the card's own `overflow-wrap: anywhere`, which exists so
     a long client name cannot push the card off-screen — correct for prose,
     wrong for a two-word button, and it inherits into everything in the cell.
     Turned off for the controls only. */
  [data-shell="2"] #crmList .crm-table tbody td .dl-editbox button,
  [data-shell="2"] #crmList .crm-table tbody td .dl-editbox .dl-input {
    overflow-wrap: normal;
    word-break: normal;
    white-space: nowrap;
  }

  [data-shell="2"] #crmList .crm-table tbody td::before,
  [data-shell="2"] #crmLeads .crm-table tbody td::before,
  [data-shell="2"] #crmAccount .crm-table tbody td::before {
    content: attr(data-label);
    color: var(--text-3);
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    text-align: left;
  }

  /* A card whose every field is empty is still a card, and an empty VALUE must
     look empty rather than looking like a missing label. */
  [data-shell="2"] #crmList .crm-table tbody td:empty::after,
  [data-shell="2"] #crmLeads .crm-table tbody td:empty::after,
  [data-shell="2"] #crmAccount .crm-table tbody td:empty::after {
    content: "—";
    color: var(--text-3);
  }

  /* Money keeps the mono/tabular treatment it has everywhere else, but stops
     being right-aligned: against a left-aligned label column, a right-aligned
     value leaves a gap in the middle of a 390px card and the eye loses the pair.
     The figures still line up with each other, because they share a column. */
  [data-shell="2"] #crmList .crm-table tbody td.ta-r,
  [data-shell="2"] #crmLeads .crm-table tbody td.ta-r,
  [data-shell="2"] #crmAccount .crm-table tbody td.ta-r {
    text-align: left;
  }

  /* The whole row is the tap target — it always was, but at 44px of finger the
     affordance has to be obvious rather than inferred from a cursor nobody has. */
  [data-shell="2"] #crmList .crm-table tbody tr.row-open,
  [data-shell="2"] #crmLeads .crm-table tbody tr.row-open,
  [data-shell="2"] #crmAccount .crm-table tbody tr.row-open {
    cursor: pointer;
  }
}

/* ============================================================================
   SLICE 5 — the left sidebar. The shell itself.

   Shaf's direction: the CRM moves from a top bar to a left sidebar, and the
   Portal follows for consistency. This is that sidebar, behind ?shell=2, with
   the OLD shell left completely intact beside it — the team quotes on the old
   one every day for however many weeks this is under review.

   ── Every class here is NEW, and that is a decision, not a naming accident ───
   The plan recorded a trap: `.crm-page .main-nav .nav-btn` is (0,3,0), and every
   scoping scheme proposed for re-styling the old classes is weaker and loses in
   every file order (D33). Rather than fight that cascade, the new shell does not
   use those classes at all. `sb-*` names were checked against styles.css before
   a line was written: all twelve appear ZERO times.

   ⚠ That also side-steps the D20 question rather than answering it: uicheck's
   duplicate-rule check reads only BARE single-class selectors, so a new class
   nothing else declares cannot collide, and the scoped rules below are invisible
   to it either way (the plan's trap #2). **The doctrine question is still open.**
   It is dodged here because a new shell genuinely is new, not because the gate
   was satisfied — and those are different things worth not confusing.
   ============================================================================ */

/* ⚠ `[data-shell="2"] .topbar, .nav2 { display: none }` stood here until slice 11.
   There is nothing left to hide — the markup is gone from crm.html — so the rule
   went with it rather than being left as a rule about an element that does not
   exist (D33's neighbour: a declaration that can never MATCH is as dead as one
   that can never win).

   ⚠ `[data-shell="2"]` STAYS on every other selector in this file, deliberately,
   and it is not laziness. Removing it lowers each rule's specificity by one
   class, and several of them exist precisely because they must BEAT a rule in
   styles.css. Stripping the hook in the same commit as an irreversible deletion
   would change the cascade silently, everywhere, with no way back — the exact
   D33 trap this redesign was warned about on day one. shell.js now sets the
   attribute unconditionally, so the hook always matches and costs nothing.
   Tidying it is a separate, reversible change. */

/* ---- the sidebar ---------------------------------------------------------- */
/* ⚠ THE WIDTH IS A TOKEN BECAUSE TWO PLACES HAVE TO AGREE. The sidebar sets its
   own width and the body pads by the same amount; when those two disagree the
   content sits under the menu or floats away from it, and nothing fails — it just
   looks wrong. The rail changes ONE number and both follow. */
[data-shell="2"] { --sb-w: 232px; }
[data-shell="2"][data-rail="1"] { --sb-w: 60px; }

[data-shell="2"] .sb-nav {
  position: fixed;
  /* Cleared of the staging banner, which is fixed at z-index 300 and would
     otherwise paint over the brand. The value is MEASURED and set by crm.js —
     see the comment there — and falls back to 0 on production, where there is
     no banner at all. */
  top: var(--sb-top, 0px);
  left: 0;
  width: var(--sb-w, 232px);
  height: calc(100vh - var(--sb-top, 0px));
  overflow-y: auto;
  padding: 14px 10px 24px;
  background: var(--primary-dark);
  color: var(--on-primary);
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 40;
}

[data-shell="2"] .sb-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 2px 8px 16px;
}

/* The logo is dark artwork on transparency, so on this dark sidebar it needs a
   light chip behind it exactly as the old shell gives it (styles.css:1292) —
   without one it is technically rendered and practically invisible, which a
   screenshot shows and no assertion does.
   ⚠ The old rule uses a raw `#fff`. This one must not: the hex ratchet stands at
   302 with ZERO headroom and D17 says it may fall, never rise. `--panel` is the
   light surface token and is the right semantic anyway — this is a surface the
   logo sits on, not a foreground colour.
   Width-capped at 104px because N7 caps it there for the phone: it was once
   303px of a 390px window. */
[data-shell="2"] .sb-logo {
  width: 104px;
  height: auto;
  background: var(--logo-chip);
  padding: 4px 10px;
  border-radius: 6px;
  box-sizing: content-box;
}

[data-shell="2"] .sb-app {
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 12.5px;
  opacity: 0.85;
}

/* A section heading, not a control — it must not read as something to tap. */
[data-shell="2"] .sb-group {
  padding: 14px 10px 5px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.55;
}

/* The icon is sized here rather than left to inherit: the sprite draws on a 24x24
   box, and at that size it would dwarf a 13px label. It takes currentColor, so it
   costs the hex ratchet nothing (D17). */
[data-shell="2"] .sb-ico { width: 17px; height: 17px; flex: none; opacity: 0.9; }

[data-shell="2"] .sb-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 14px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

[data-shell="2"] .sb-item:hover { background: rgba(255, 255, 255, 0.09); }

/* The current screen. It carries BOTH a background and a left edge: on this
   dark ground a background alone is a difference somebody has to look for. */
[data-shell="2"] .sb-item.active {
  background: rgba(255, 255, 255, 0.14);
  box-shadow: inset 3px 0 0 var(--accent-grn);
  font-weight: 600;
}

[data-shell="2"] .sb-mark { margin-left: auto; opacity: 0.6; font-size: 12px; }
[data-shell="2"] .sb-count { margin-left: auto; }

/* ---- the bar above the content ------------------------------------------- */
/* ⚠ The sidebar is FIXED, so it is out of flow and reserves no space. The room
   for it is made by padding the BODY, not by putting a margin on each child.
   The first attempt did the latter and the page scrolled sideways by exactly
   232px: `.crm-layout` carries `width: 100%` (styles.css:2556), so a 232px
   margin plus a full-width box is 232px too wide. Padding the body narrows the
   containing block instead, and every `100%` inside it resolves correctly
   without a single child rule fighting one it cannot see. Measured, not
   reasoned about — the overflow was found by reading computed geometry (D33). */
[data-shell="2"] body { padding-left: var(--sb-w, 232px); }

[data-shell="2"] .sb-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}

/* The title carries the job the permanently visible menu used to do (D3), so it
   is the loudest thing in this bar rather than a caption on it. */
[data-shell="2"] .sb-title {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
}

/* No drawer to open on a desktop, so no handle. */
[data-shell="2"] .sb-burger { display: none; }

[data-shell="2"] .sb-bar .umenu { margin-left: auto; }

/* ⚠ THE ACCOUNT BUTTON WAS INVISIBLE IN LIGHT MODE, and had been since the flip.
   `.umenu-btn` in styles.css is `color: var(--on-primary)` on a 10% WHITE wash —
   correct, and written for the OLD shell, whose top bar was --primary dark green.
   Slice 5 MOVED the button into the new bar, which is --panel. In light that is
   white: white text on a white wash over white. **Measured 1.00:1**, the worst
   ratio arithmetic allows.

   ⚠ IT LOOKED FINE IN DARK — 11.99:1 — which is why it survived the theme work,
   and it is the reason Shaf found it and no gate did. Nothing we own measures
   whether text can be read against what is behind it: uicheck reads files,
   layoutcheck reads geometry, the suites read values. A colour that renders
   perfectly and cannot be seen passes all three.

   ⚠ AND LOOKING DID NOT CATCH IT EITHER, which is the part worth keeping. The
   light-mode screenshot taken this morning CONTAINED this defect and it was read
   as an empty corner. **Invisible and absent are the same picture.** D14 says look
   at the surface; this is the case where looking is not enough and the answer is
   to measure the two colours (the `.btn-ghost` scar, in a second costume).

   The button belongs to the bar it sits in, so it is scoped to that bar and takes
   the bar's own text colour: 14.04:1 in light, 12.27:1 in dark. */
[data-shell="2"] .sb-bar .umenu-btn {
  background: var(--row-band);
  color: var(--text);
  border: 1px solid var(--border);
}
[data-shell="2"] .sb-bar .umenu-btn:hover { background: var(--accent-soft); }

[data-shell="2"] .sb-scrim { display: none; }

/* ---- below 1200px the sidebar becomes a drawer (D3, Shaf's ruling) ---------
   ⚠ THE THRESHOLD MOVED FROM 900px TO 1200px IN SLICE 11, and it is a fix for a
   class of defect rather than for one screen. A fixed 232px sidebar takes that
   width away from every screen behind it, and by the time the full width sweep
   ran against the new shell it had broken FOUR: the costing board at 1280 (99px
   over), the capabilities band at 1024 (6px), and the Clients and Settings
   screens at 1024 (167px each — a 220px action column with nowhere to go).

   Four patches would have been four scroll containers, and each one clips
   anything absolutely positioned inside it. The arithmetic says the sidebar is
   simply not affordable below about 1200px: those rows need ~935px, and 232px
   of sidebar plus gutters leaves 768px at 1024.

   So below 1200px it overlays instead of displacing — the content gets the full
   width back, exactly as it had before the sidebar existed, and the whole class
   goes away including the next one.
   ⚠ The trade, stated: on a 1024px laptop the menu is now behind a tap rather
   than always visible. That is Shaf's D3 arrangement applied at a wider
   threshold than "phone", and it is the arrangement he chose — the header title
   still says where you are. If he wants it visible at 1024 the answer is a
   narrower icons-only rail, which is a design change and not a patch. */
@media (max-width: 1200px) {
  /* Off-canvas, not display:none — it slides, and a keyboard user tabbing into
     a menu that is off screen would otherwise scroll the page sideways to reach
     it, which is why it is also made inert until opened. */
  [data-shell="2"] .sb-nav {
    transform: translateX(-100%);
    transition: transform 0.18s ease-out;
    visibility: hidden;
    width: 264px;
  }

  [data-shell="2"] .sb-nav.sb-open {
    transform: translateX(0);
    visibility: visible;
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.001);
  }

  [data-shell="2"] .sb-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 9px;
    background: var(--panel);
    color: var(--text);
    cursor: pointer;
  }

  /* The drawer overlays rather than displaces, so no room is reserved for it. */
  [data-shell="2"] body { padding-left: 0; }

  [data-shell="2"] .sb-nav.sb-open ~ .sb-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 35;
    background: var(--scrim);
  }
}

/* The Catalogue genuinely LEAVES this page for the quote portal. The old shell
   says so with an arrow and a title, and pretending otherwise is what made Shaf
   ask twice why "the old portal" opens. It is dimmed and set apart from the
   items that stay inside the CRM, so the difference is visible before the tap
   rather than discovered after it. */
[data-shell="2"] .sb-away {
  margin-top: 6px;
  opacity: 0.72;
  font-size: 13.5px;
}

[data-shell="2"] .sb-away:hover { opacity: 1; }

/* ---- what a 232px sidebar costs the widest screen -------------------------
   The sidebar is not free: it takes 232px out of the content box, and one screen
   could not absorb it. `.cost-row` is an eight-track grid whose fixed minimums
   come to 1100px (110 + 150 + 4×120 + 90 + 200, plus 7 × 10px of gaps). At
   1280px the old shell leaves it 1240px and it fits; the new shell leaves 1008px
   and the PAGE scrolled sideways by 99px. layoutcheck caught it as an error on
   the first run of the new walk — which is precisely why that walk had to exist
   before this sidebar shipped.

   The fix is the rule N7 already sets for wide tables: content too wide for the
   screen scrolls INSIDE its own container, and the page body never scrolls. Not
   a narrower grid — those eight columns are money and their widths are already
   the minimum that keeps the figures readable, and squeezing them is how a sen
   column becomes unreadable on the screen where costs are approved. */
[data-shell="2"] #costingList {
  overflow-x: auto;
}

/* ---- slice 6: where the sixteen screens live ------------------------------
   Shaf's D2 ruling executed. Two things need styling that the old shell never
   had a use for.

   1. A group NOTE. The "Retiring" group would otherwise read as an insult to
      three working screens — it has to say what is actually happening to them,
      on the screen, not in a commit message (D10: say what the system did).

   2. Settings now lives in Management, and it was already behind the avatar.
      Two doors to one screen is the shape that drifts (D26), so the avatar's
      copy is hidden IN THIS SHELL ONLY — shell 1 keeps it exactly where the
      team expects it, because that menu is shared markup and this rule is
      scoped rather than a deletion. */
[data-shell="2"] .sb-note {
  margin: 0 10px 6px;
  font-size: 11px;
  line-height: 1.35;
  opacity: 0.5;
}

/* Going somewhere else soon. Dimmed rather than struck through: they still work
   and are still the only place that work lives today, so they must not look
   broken — only unfinished. */
[data-shell="2"] .sb-going { opacity: 0.66; }
[data-shell="2"] .sb-going:hover { opacity: 1; }

[data-shell="2"] #umenuPop [data-crmview="admin"] { display: none; }

/* ============================================================================
   SLICE 8 — the new table view. READ-ONLY.

   The first time the new shell shows real money in a new layout, and the first
   surface born under slice 7's gate: every Value and Weighted figure below is
   read back off the screen and compared to what the system computed, on every
   run, in both shells.

   READ-ONLY is the whole point of shipping it separately. Editing is slice 9 and
   arrives one field per shipment, each with the confirm-first behaviour Shaf
   chose (D5). Nothing here writes anything; clicking a row still only OPENS the
   deal, which is navigation, not editing.

   ⚠ The columns and the data are UNCHANGED — same nine, same figures, row for
   row against today's. That is deliberate. Restructuring columns would change
   what the phone cards show, and "nothing dropped, sen included" is Shaf's rule
   about those cards; a table restyle is not the place to quietly renegotiate it.

   The language is the one he reviewed in the prototype: a panel with a quiet
   banded header, uppercase micro-caps, hairline rules, and money in the mono
   tabular face so figures line up down the column. Colour is tokens only — the
   hex ratchet is 302 against a baseline of 302.
   ============================================================================ */

@media (min-width: 768px) {

  /* The wrapper stops being the frame; the table becomes the panel, so the
     header band and the rounded corner belong to the same object. */
  [data-shell="2"] #crmList .browse-tablewrap {
    border: 0;
    background: transparent;
    border-radius: 0;
  }

  [data-shell="2"] #crmList .crm-table {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 9px;
    border-collapse: separate;
    border-spacing: 0;
    overflow: hidden;
    font-size: 13.5px;
  }

  /* Micro-caps on a band: a heading has to be findable without competing with
     the figures underneath it, which are the reason anyone opened the screen.
     ⚠ TRIMMED 13 Aug 2026 to the ONE declaration that still differs. This block
     used to carry the whole treatment — band, micro-caps, colour, weight — and
     on that date the same treatment moved onto `.browse-table` in styles.css so
     all eight tables share it (D20: a shared class is defined once). Everything
     except this padding became a duplicate of the base rule the moment it did,
     and a duplicate is what drifts. The padding stays because slice 8 shipped
     the deal list 1px tighter than the browse tables and density is settled —
     unifying it would be a density change dressed up as a tidy-up. */
  [data-shell="2"] #crmList .crm-table thead th { padding: 9px 11px; }

  [data-shell="2"] #crmList .crm-table tbody td {
    padding: 9px 11px;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
  }

  /* A rule under the last row draws a line under a border that is already
     there — two lines where the design has one. */
  [data-shell="2"] #crmList .crm-table tbody tr:last-child td { border-bottom: 0; }

  /* The row is the tap target and always was; hover is what says so. */
  [data-shell="2"] #crmList .crm-table tbody tr:hover { background: var(--row-band); }
}

/* The stage, as a pill. Read at a glance down the column instead of being read
   as a word — and it carries meaning rather than decoration: a won deal is the
   accent green, a lost or dropped one is muted so a dead deal does not shout as
   loudly as a live one.
   ⚠ Scoped to shell 2, so in the shell the team uses today this span is
   unstyled and renders as the plain text it has always been. Nothing about the
   live CRM moves.

   ⚠ THE `#crmList` SCOPE CAME OFF ON 14 AUG 2026 (slice 5), and it had to: the
   peek draws the same stage on the same deal, and a rule scoped to the list would
   have rendered NOTHING inside the panel — a declaration that cannot win is a lie
   (D33), and it is the failure mode that looks like a styling oversight rather
   than a bug. One stage pill, two surfaces, one definition (D20) — the selector
   gains a surface rather than a second block, exactly as the phone card rules did
   when Leads joined them. Nothing else on either page emits `.dl-pill`, verified
   by grep before widening, so the list is unchanged BY IDENTITY. */
[data-shell="2"] .dl-pill {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--primary);
  color: var(--on-primary);
  font-size: 11.5px;
  font-weight: 600;
  white-space: nowrap;
}

/* ⚠ THE WON PILL WAS THE WORST-MEASURED TEXT SITE ON THE PLATFORM: white on
   `--accent-grn`, 4.54:1 in light and **2.54:1 in dark**, against a 4.5 floor —
   the top row of `contrastcheck`'s list since the walk first reached the CRM.
   It is repaired here rather than carried onto a new surface, because slice 5's
   own gate is "contrast on every new text site in both themes" and the peek shows
   the won stage on 11 of the 74 deals. Shipping it unchanged would have painted
   the platform's worst defect onto a screen built this week.
   The replacement is not a new colour and not a judgement call: it is the pair
   Shaf's own badge contract already names for the won end — `--ok-bg`/`--ok-text`,
   which flip with the theme and measure **6.97:1 light / 8.25:1 dark**.
   ⚠ THE DEAL LIST VISIBLY CHANGES: a Won pill goes from solid mid-green with white
   text to a pale green chip with dark green text. That is the only visual change
   this slice makes outside the peek, and it is stated rather than buried. */
[data-shell="2"] .dl-won { background: var(--ok-bg); color: var(--ok-text); }

/* ⚠ Lost and dropped deliberately DEPART from the contract's "lost = --bad-bg /
   --bad-text". The reason is already written above and predates this slice: a dead
   deal must not shout as loudly as a live one, and 18 of 74 deals are lost or
   dropped. Measured, so the departure is not also a defect: --muted on --panel is
   4.96:1 light / 7.11:1 dark, and the border clears the 3:1 non-text floor. */
[data-shell="2"] .dl-off {
  background: transparent;
  border: 1px solid var(--text-3);
  color: var(--muted);
}

@media (min-width: 768px) {

  /* ⚠ MONEY MUST NOT WRAP, and this is a D14 catch: every assertion passed
     while the screen read "RM" on one line and "1,250,000.50" on the next.
     Slice 7's gate reads textContent, which a line break does not change, so
     the number was correct and unreadable at the same time — the two are not
     the same property and only a screenshot tells them apart.
     The deal code broke across three lines for the same reason.
     Nowrap raises the table's minimum width; if that outgrows the panel the
     panel scrolls, which is N7's own rule and is the right trade — a figure
     split down the middle is worse than one you scroll to. */
  [data-shell="2"] #crmList .crm-table tbody td.ta-r,
  [data-shell="2"] #crmList .crm-table tbody td.mono,
  [data-shell="2"] #crmList .crm-table thead th { white-space: nowrap; }

  /* The panel takes the scroll when the columns genuinely need more room than
     the sidebar leaves them. */
  [data-shell="2"] #crmList .browse-tablewrap { overflow-x: auto; }
}

/* ---- slice 9: the confirm dialog ------------------------------------------
   Built on the shared modal contract — `.modal-overlay`, `.modal`,
   `.modal-header`, `.modal-footer` all already exist in styles.css and are what
   the quote portal uses (D13: use the classes this app already has). The CRM
   has never had a modal of its own, so only the two parts the contract does not
   cover need a rule: the body's padding and the footer's button row.
   The overlay is created by JS and removed on close, so the old shell ships no
   markup for a dialog it does not use. */
[data-shell="2"] .mv-body {
  padding: 14px 18px 4px;
  font-size: 14px;
  line-height: 1.5;
}

[data-shell="2"] .mv-body p { margin: 0 0 10px; }

/* The destructive-consequence line has to outweigh the polite one above it, or
   a person reads "move this deal" and stops. */
[data-shell="2"] .mv-body .warn { color: var(--alert-text); }

[data-shell="2"] .mv-foot {
  gap: 8px;
  justify-content: flex-end;
  padding: 10px 18px 16px;
}

/* ---- slice 9 shipment 3: the value editor --------------------------------
   Two cells, two different promises, and they must not look the same.
   `dl-edit` can be changed here; `dl-fixed` shows a Final TCV and cannot,
   because the only door writes the budget figure and 51 of 74 deals have a TCV
   that would win the COALESCE. D5: a control that offers a change must be able
   to deliver it — so only one of these offers anything. */
[data-shell="2"] #crmList .dl-edit { cursor: text; }

/* A dotted underline rather than a button: it says "this is changeable" without
   turning a table of figures into a wall of controls. */
[data-shell="2"] #crmList .dl-edit:hover {
  box-shadow: inset 0 -1px 0 var(--muted);
}

/* Deliberately NOT styled as interactive. It still answers a click — with the
   sentence explaining where the number comes from — but it must not look like
   something that will let you type (D5: two visible states, no no-ops). */
[data-shell="2"] #crmList .dl-fixed { cursor: help; }

[data-shell="2"] #crmList .dl-editbox {
  display: inline-flex;
  gap: 6px;
  align-items: center;
  justify-content: flex-end;
}

[data-shell="2"] #crmList .dl-input {
  width: 130px;
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  padding: 3px 6px;
}

/* ---- the sidebar's cost, and the general answer to it --------------------
   232px out of the content box overflows bands whose columns have fixed
   minimums. Costing was the first (99px at 1280px). The capabilities band was
   the second (6px at 1024px), and two occurrences in two slices says the next
   one is coming rather than that this one is special.

   ⚠ The first attempt here was `overflow-x: auto` on the band, and it did NOT
   work — the assertion asks whether the band overflows, and making it scrollable
   does not change scrollWidth. Scrolling is the right answer where content is
   genuinely wider than any screen (the deal table, the costing grid); it is the
   wrong answer to being SIX PIXELS short.

   So: give the content back some of what the sidebar took. Below 1200px the
   page gutter drops from 20px to 12px — 16px recovered on every screen at
   exactly the widths where the sidebar bites, without touching a single band's
   internals. The gutter is the app's own slack; the old shell already narrows
   it at 720px for the same reason. */
@media (max-width: 1200px) {
  [data-shell="2"] .crm-layout { padding-left: 12px; padding-right: 12px; }
}

/* ============================================================================
   THE RAIL — Shaf's ask, 12 Aug 2026: "enable the menu to be minimized to show
   the icon only instead of full item name."

   ⚠ THE LABELS ARE HIDDEN, NEVER REMOVED. icons.js states the house rule — "an
   icon accompanies a label; it never replaces one" — and a deliberately collapsed
   rail is the case that rule did not anticipate. It is honoured rather than
   dodged: `.sb-label` is taken out of the PICTURE and left in the DOM, so a screen
   reader still reads every item; each button also carries an `aria-label` and a
   `title` set unconditionally, so the accessible name never depends on whether the
   rail happens to be collapsed. Widening that rule is a skill edit, made with this
   change rather than left for somebody to discover.

   ⚠ NOT `display: none` on the label. That would take it from the accessibility
   tree as well as the screen, which is exactly the difference between collapsing
   a menu and deleting half of it.

   ⚠ AND THIS IS WHY THE RAIL WAS WANTED. Slice 11 moved the drawer threshold to
   1200px because a fixed 232px sidebar left the costing and client rows too little
   room at 1024px, and the note recorded then said: "if he wants it visible at 1024
   the answer is a narrower icons-only rail — a design change, not a patch." This
   is that design change. Whether the threshold now comes back down is a SEPARATE
   decision with its own measurement, and it is not taken here.
   ============================================================================ */
[data-shell="2"][data-rail="1"] .sb-label,
[data-shell="2"][data-rail="1"] .sb-group,
[data-shell="2"][data-rail="1"] .sb-note,
[data-shell="2"][data-rail="1"] .sb-mark {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

[data-shell="2"][data-rail="1"] .sb-item {
  justify-content: center;
  padding: 10px 0;
  gap: 0;
}

/* The brand shrinks to the mark rather than being hidden: a rail with nothing at
   the top reads as a page that failed to load its header. */
[data-shell="2"][data-rail="1"] .sb-brand { padding: 2px 6px 12px; justify-content: center; }
[data-shell="2"][data-rail="1"] .sb-brand .sb-app { display: none; }

/* ⚠ THE RAIL SHOWS THE "EMT" MARK ONLY — Shaf, 12 Aug 2026. Shrinking the whole
   lockup to 44px was the first attempt and it is unreadable: `emt-logo.png` is
   2000x300, an aspect of 6.67, so the wordmark "ENVIROMET / From Data to
   Intelligence" collapses to a smear at rail width. A logo nobody can read is
   decoration taking up the one place a rail has none to spare.

   CROPPED, not replaced, so there is no second asset to keep in step with the
   first (D26 — one logo, one file). The arithmetic, written down because the
   numbers look arbitrary otherwise: `object-fit: cover` scales the image to fill
   the box, and since the image is far wider than the box it scales by HEIGHT —
   18px tall renders 18 x 6.67 = 120px wide, of which the box shows the leftmost
   42px. That is 35% of the lockup, which is exactly the EMT badge.
   `object-position: left` is what keeps the crop on the badge rather than the
   middle of the word.

   ⚠ Change the box and you change the crop. If the image is ever re-exported at a
   different aspect these numbers move with it — which is why the ratio is stated
   here rather than left to be re-derived from a screenshot. */
[data-shell="2"][data-rail="1"] .sb-logo {
  width: 42px;
  height: 18px;
  object-fit: cover;
  object-position: left center;
  padding: 3px 4px;
}

/* A count cannot be read at 60px, so it becomes a presence rather than a number —
   and the number is still on the button's accessible name. A badge that says
   nothing is worse than a dot that says "something is here". */
[data-shell="2"][data-rail="1"] .sb-count {
  min-width: 8px;
  width: 8px;
  height: 8px;
  padding: 0;
  font-size: 0;
  line-height: 0;
  position: absolute;
  top: 6px;
  right: 10px;
}
[data-shell="2"][data-rail="1"] .sb-item { position: relative; }

/* The toggle itself. ⚠ MOVED TO THE TOP OF THE MENU — Shaf, 24 Aug 2026, it used
   to be the last thing in the list, after "Retiring", so reaching it meant
   scrolling past every group. `shell-nav.js` now emits it first, right after the
   brand; the rule beneath it moved from `border-top`/`margin-top` to
   `border-bottom`/`margin-bottom` for the same reason — the divider still marks
   the toggle off from what follows, just on the other side of it now. */
[data-shell="2"] .sb-railtoggle {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  margin-bottom: 6px;
  padding: 9px 10px;
  border: 0;
  border-bottom: 1px solid var(--sb-rule, rgba(255,255,255,0.10));
  border-radius: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12.5px;
  opacity: 0.75;
  cursor: pointer;
  text-align: left;
}
[data-shell="2"] .sb-railtoggle:hover { opacity: 1; }
[data-shell="2"][data-rail="1"] .sb-railtoggle { justify-content: center; padding: 10px 0; gap: 0; }
[data-shell="2"][data-rail="1"] .sb-railtoggle .ico { transform: rotate(180deg); }

/* The Retiring group's heading doubles as a native <details> toggle now (24 Aug
   2026), so it needs to read as tappable — the plain `.sb-group` opacity (0.55)
   was deliberately too quiet for that; a summary gets a little more weight and a
   hover state, same idiom as every other clickable row in this sidebar. */
[data-shell="2"] .sb-group-details > summary {
  cursor: pointer;
  opacity: 0.75;
}
[data-shell="2"] .sb-group-details > summary:hover { opacity: 1; }

/* ⚠ Below the drawer threshold the sidebar is already an overlay you open and
   dismiss, so "make it narrower" would offer a change the layout cannot deliver
   (D5). The control is hidden there, and the rail width is not applied either —
   a 60px drawer would be a worse drawer, not a narrower menu. */
@media (max-width: 1200px) {
  [data-shell="2"] .sb-railtoggle { display: none; }
  [data-shell="2"][data-rail="1"] { --sb-w: 232px; }
  [data-shell="2"][data-rail="1"] .sb-label,
  [data-shell="2"][data-rail="1"] .sb-group,
  [data-shell="2"][data-rail="1"] .sb-note,
  [data-shell="2"][data-rail="1"] .sb-mark {
    position: static; width: auto; height: auto; overflow: visible; clip-path: none;
  }
  [data-shell="2"][data-rail="1"] .sb-item { justify-content: flex-start; padding: 9px 10px; gap: 9px; }
}

/* ============================================================================
   THE PEEK — slice 4, 14 Aug 2026
   ============================================================================
   Shaf's brief: "Deal detail (slide-over panel from the right, opens on row/card
   click)". It REUSES the quote portal's `.drawer` contract rather than declaring
   a second panel (D20) — so the slide, Escape, the focus trap and focus-return
   all arrive from `overlay.js` and cannot drift from the portal's.

   ⚠ Nothing here redefines `.drawer` or `.drawer-overlay`. It adds `.peek-overlay`
   beside them, which is why this file can carry the CRM's variant without the
   cross-file conflict the D20 question is still open about. */

/* 620px is arithmetic, not taste: the peek carries a row of number tiles, and at
   the portal's 560px a figure like "RM 1,240,000" does not fit its column. The
   portal keeps 560px BY IDENTITY — it never reads this token. */
[data-shell="2"] .peek-overlay { --drawer-w: 620px; }

/* ⚠ ON A PHONE IT IS A BOTTOM SHEET, NOT A NARROW RIGHT PANEL, and the reason is
   behavioural rather than aesthetic. At 390px a right panel at 94vw is 367px —
   indistinguishable from a page navigation — so it teaches people to press Back
   to dismiss it. A sheet that leaves a strip of the list showing above it says
   "you are still on the list, and it kept your place", which is the literal truth.
   ⚠ The slide direction changes with it: `overlay.js` animates whatever transform
   the panel declares, so the axis lives HERE and the mechanism does not care. */
@media (max-width: 768px) {
  [data-shell="2"] .peek-overlay,
  [data-shell="2"] #newClientOverlay { justify-content: stretch; align-items: flex-end; }
  [data-shell="2"] .peek-overlay .peek-panel,
  [data-shell="2"] #newClientOverlay .newclient-panel {
    width: 100%;
    height: 92%;
    border-radius: 14px 14px 0 0;
    transform: translateY(100%);
    box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.25);
  }
  [data-shell="2"] .peek-overlay.is-open .peek-panel,
  [data-shell="2"] #newClientOverlay.is-open .newclient-panel { transform: translateY(0); }
}

/* THE NEW-CLIENT DRAWER (slice 3, 18 Aug 2026).
   ⚠ The sheet treatment above NAMES this overlay rather than getting a copy of the
   block — the same reason the phone-card rules list their surfaces one by one (D20).
   It matters more here than for the peek: the sheet is what stops a 94vw right panel
   reading as a page change, and Back on this panel does not merely close it, it
   rebuilds the approve form underneath and destroys everything typed into it.

   🛑 `.if-grid` CANNOT BE LEFT ALONE IN A 560px DRAWER, and the breakpoint is why.
   It is 12 columns with `.field { grid-column: span 3 }`, dropping to 6 columns at
   `max-width: 900px` — of the VIEWPORT, not of the container. On a 1512px desktop that
   never fires, so eight controls get ~130px each inside 560px, and a `<select>` is
   sized by its longest option ("Government-linked company (federal)"). `.drawer-body`
   is `overflow: auto`, so it would read as sideways scrolling rather than a visible
   break — measured nowhere and photographed by nothing. Fields go full width here. */
[data-shell="2"] #newClientOverlay .if-grid > .field,
[data-shell="2"] #newClientOverlay .if-grid > .field-guide { grid-column: 1 / -1; }
[data-shell="2"] #newClientOverlay .panel-subtitle { margin-top: 0; }

/* ⚠ `.peek-empty` LIVED HERE AND IS DELETED IN THE SAME COMMIT THAT STOPPED
   EMITTING IT (slice 5). Slice 4 shipped the peek empty on purpose so the gates
   would be watching before the first line of content; that sentence is now
   replaced by real content, and a rule left behind for a class nothing emits is
   a declaration that can never win (D33) — the shape that has cost this codebase
   two investigations. Removed rather than kept "in case". */
[data-shell="2"] .peek-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* ---------------------------------------------------------------------------
   THE PEEK'S CONTENT — slice 5, 14 Aug 2026
   ---------------------------------------------------------------------------
   A short list, and it is meant to stay short: the stage, the value, the weighted
   value, the COSTED GP, the idle clock, the next action and the flags. ⚠ This line
   said "Six things, and no more" and was wrong the moment Costed GP was added on
   16 Aug 2026 — so it no longer states a total at all, because a count nothing
   re-derives goes stale silently and this file has already paid for that twice.
   **The CONSTRAINT is what matters and it is unchanged:** what the peek does NOT
   carry is the point of the 620px cap — a peek that can hold a full record becomes
   a second detail screen and the two drift (D26). Adding to this list is a decision,
   not a convenience. */

/* The deal's name. ⚠ IT IS NEVER TRUNCATED AND NEVER ELLIPSISED. Measured on the
   live database, not guessed: the longest `shortname` is 103 characters. A name
   half-shown is a deal somebody misidentifies, and there is no width pressure in
   a panel that scrolls vertically. `overflow-wrap` catches the one case wrapping
   at spaces cannot — a single unbroken 60-character token. */
[data-shell="2"] .pk-name,
[data-shell="2"] .acc-name {
  margin: 0 0 3px;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--text);
  overflow-wrap: anywhere;
}
[data-shell="2"] .pk-who { margin: 0 0 12px; font-size: 12.5px; color: var(--muted); }

/* The state line: exactly ONE solid pill (the lifecycle) and any number of
   outline chips beside it. The one-pill rule exists because three solid pills
   side by side leave the eye unable to tell which one is the status. */
[data-shell="2"] .pk-state {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin: 0 0 14px;
}

/* THE TILES — the arithmetic that decided 620px. `minmax(0, 1fr)` rather than a
   bare `1fr` because a grid track's default minimum is min-content, and these
   hold money whose width is not ours to choose (D21): `RM 1,250,000.50` would
   otherwise set the floor and push the panel sideways. */
/* ⚠ TWO COLUMNS AT EVERY WIDTH SINCE 16 Aug 2026, WHEN THE ROW GAINED A FOURTH
   TILE (Costed GP, on Shaf's ask). It was three-up on desktop and two-up under
   768px; four-up was MEASURED before it was rejected, not guessed:
     row 584px · gap 10px · tile padding 22px
     3 tracks -> 188px, 166px usable   FITS
     4 tracks -> 139px, 117px usable   BREAKS
     2 tracks -> 287px, 265px usable   FITS
   The widest money figure this app has broken a row with is `RM 5,610,845.75` at
   **126px** in this face, so a fourth column would have re-created the exact defect
   the phone rule below was written to stop — a money figure with nowhere to go but
   a second line. Two tracks clear it at 390px too (172px track, 150px usable), so
   the breakpoint that used to live here is GONE rather than kept as a second copy
   of one decision (D20). */
[data-shell="2"] .pk-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 0 0 16px;
}
/* ⚠ NO FILL, AND THAT IS A CONTRAST DECISION RATHER THAN A LOOK. The obvious
   tile is a `--row-band` box, the way every inset strip in this app is drawn —
   but `--muted` on `--row-band` measures **4.47:1**, a hair under the 4.5 floor,
   and it is already one of the nine near-misses sitting on the contrast list.
   Filling the tile would have put its label and its note there too, i.e. grown
   the ratchet on a surface built this week to add three text sites. Left on
   `--panel` (the drawer's own ground) the same token reads **4.96:1 light /
   7.11:1 dark**, and the border does the separating. `.ap-card` already draws a
   card exactly this way, so this is the app's existing shape, not a new one. */
[data-shell="2"] .pk-tile,
[data-shell="2"] .acc-tile {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 11px;
  min-width: 0;
}
/* ⚠ THE LABEL IS `--muted`, NOT `--text-3`. --text-3 is the token whose job is
   exactly this — labels and units — and as TEXT on `--panel` it measures 3.84:1,
   under the 4.5 floor. Using it would have added a new rule to the contrast
   ratchet on a surface built the same week. It is fine as a BORDER, where the
   floor is 3:1, which is why the chips use it there and not here. */
[data-shell="2"] .pk-t-label,
[data-shell="2"] .acc-t-label {
  display: block; font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 3px;
}
/* ⚠ `white-space: nowrap`, AND IT IS THE SECOND TIME THIS FILE HAS PAID FOR IT.
   The rule a few hundred lines above says it plainly — money must not wrap — and
   this tile broke it anyway at 390px, drawing "RM" on one line and "300,000.00"
   on the next. Every assertion passed: `textContent` is identical either way, so
   the number was correct and unreadable at the same time. Caught by looking at
   the phone screenshot (D14), which is the only thing that has ever caught it. */
[data-shell="2"] .pk-t-value,
[data-shell="2"] .acc-t-value {
  display: block;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 14px; font-weight: 700; color: var(--text);
  white-space: nowrap;
}
[data-shell="2"] .pk-t-note,
[data-shell="2"] .acc-t-note { display: block; margin-top: 3px; font-size: 11px; color: var(--muted); }

/* ⚠ THE STALLED TILE IS MARKED BY ITS BORDER, NOT BY ITS TEXT COLOUR, and that is
   a measurement rather than a preference. `--warn-text` — the amber Shaf chose on
   13 Aug — is 3.99:1 on `--warn-bg` and 4.34:1 on white: it cannot carry small
   text at AA anywhere on this palette, and painting the note with it would have
   grown the ratchet. As a BORDER it is a non-text component at 4.34:1 light /
   9.73:1 dark, well clear of the 3:1 floor. The words stay `--muted`. */
[data-shell="2"] .pk-tile.pk-stalled { border-color: var(--warn-text); }

[data-shell="2"] .pk-label {
  margin: 0 0 4px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--muted);
}
/* The derived playbook sentence. Longest in the live data is 146 characters, so
   it wraps and is given room rather than being clipped to one line. */
[data-shell="2"] .pk-action {
  margin: 0 0 16px; font-size: 13.5px; line-height: 1.5; color: var(--text);
}

/* ⚠ THE PHONE BREAKPOINT THAT STOOD HERE IS DELETED, 16 Aug 2026 — not dropped.
   It said two columns under 768px while the base said three; the base now says two
   at every width, so the query was a second copy of the same decision and D20 says
   a shared decision is made once. **Its arithmetic is preserved in the base rule's
   comment**, because that sum is why the number is two and a rule whose reason is
   deleted gets "tidied" back to three by the next person.
   ⚠ The FRAME still survives, and now it is FOUR tiles rather than three: all four,
   still in order, wrapping to 2×2. Dropping one on a phone would be the mistake this
   codebase has a rule against — a number that vanishes is a number somebody assumes
   is zero. */

/* ---------------------------------------------------------------------------
   THE CONTACT BLOCK — slice 6, 14 Aug 2026
   ---------------------------------------------------------------------------
   ONE block, drawn by ONE renderer, on TWO surfaces: inside the peek's drawer body
   and inside `#dealContact` on the deal screen. The rules are therefore written
   against the block itself and never against either host, so the two cannot drift
   (D20) — and so that a third surface, the account screen's people band, inherits
   them rather than growing a copy.

   ⚠ The block deliberately has NO panel chrome of its own. On the deal screen its
   host `#dealContact` is already a `.panel`; inside the peek it sits on the drawer's
   ground. A border here would draw a box inside a box on one surface and a lone box
   on the other — the same shape as "a border around a stack of bordered cards" that
   the phone card rules already avoid. */
[data-shell="2"] .ct-row > p { margin: 0 0 4px; }
[data-shell="2"] .ct-row > p:last-child { margin-bottom: 0; }

/* ⚠ NOT TRUNCATED, AND THAT IS MEASURED. The longest name recorded is 38 characters
   and the longest job title is 50 — in a 620px panel that is two lines, and on a
   390px sheet three. A person half-named is a person somebody misidentifies, and
   there is no width pressure in a panel that scrolls vertically. This is the inverse
   of the usual instinct and it is deliberate: when narrowing, you MOVE information,
   you never hide it. */
[data-shell="2"] .ct-name {
  font-size: 14px; font-weight: 700; color: var(--text); overflow-wrap: anywhere;
}
[data-shell="2"] .ct-title { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }

/* ⚠ 44px OF TAP TARGET ON A PHONE, AND IT IS THE WHOLE POINT OF THIS SLICE. Shaf's
   own words for what the peek is for: reaching a deal in a car park. A phone number
   you cannot hit with a thumb is a phone number you retype. The rule lives with the
   links rather than in the phone block below, because the floor applies to the touch
   device and this is the only control here.
   ⚠ Padding, not height: `min-height` on an inline element does nothing, which is
   how a 29px "Cancel" button shipped under a 44px floor the same stylesheet declared. */
[data-shell="2"] .ct-reach { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13px; }
[data-shell="2"] .ct-reach a { display: inline-block; padding: 4px 0; overflow-wrap: anywhere; }
[data-shell="2"] .ct-read { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; }
[data-shell="2"] .ct-none { margin: 0; font-size: 12.5px; }

/* ══ THE CONTACT IS A CARD IN THE PEOPLE STRIP — Shaf took Option B, 22 Aug 2026 ══════
   The three-column layout that lived here is GONE with the full-width band it was written
   for: inside a card of roughly 240–290px the four groups stack, which is what the shared
   renderer already does for the peek. Only two things are still worth saying here.
   ⚠ Phone and email one under the other rather than side by side — in a card this narrow an
   email is the longest unbreakable string and would decide the card's width for everyone. */
[data-shell="2"] #dealContact .ct-reach { flex-direction: column; gap: 2px; }
/* The button is the card's last line, full width of the card rather than floated beside
   content it no longer sits next to. */
[data-shell="2"] #dealContact .ct-pick { margin-top: 2px; }

@media (max-width: 768px) {
  /* The 44px floor. `line-height` plus symmetric padding gets there without a
     min-height that an inline box would ignore. */
  [data-shell="2"] .ct-reach a { padding: 12px 0; line-height: 20px; }
  [data-shell="2"] .ct-reach { gap: 0 18px; }
  /* ⚠ BACK IS A LINK ON A PHONE, NOT A BUTTON. Shaf: "The back button also ugly." It
     is `.btn .btn-ghost .btn-sm`, so at 390px it drew as a full outlined box alone on
     its own line above the breadcrumb — roughly 90px of vertical space at the top of
     every record, spent on the one control the phone's own back gesture already does.
     The 44px target stays; only the box goes. */
  [data-shell="2"] #dealBack {
    border: 0; background: none; padding: 0 10px 0 0;
    min-height: 44px; font-weight: 600; color: var(--primary-text);
  }
}

/* ⚠ THE PEEK SPACES ITS OWN BANDS, and this is a D14 catch: with the contact block's
   last line carrying `margin-bottom: 0` (right, inside a panel) the sentiment chip sat
   flush against the "NEXT ACTION" label below it, so two unrelated bands read as one.
   Every assertion passed — spacing is not something any gate we own measures.
   The rule lives on the HOST rather than on the block: a block that knows which panel
   it is in is a block that cannot be reused, and this one already serves two surfaces
   and is meant to serve a third. */
[data-shell="2"] #peekBody .ct-row,
[data-shell="2"] #peekBody .ct-none { margin-bottom: 16px; }

/* ==================== ONE ACCOUNT — the screen, slice 8 ====================
   Every rule here lives in the NEW stylesheet, scoped to the new shell, which is
   the direct answer to Shaf's complaint that tapping a row on the new list lands
   you on old styling. The prefix is `.acc-`; ⚠ `.cl-` is NOT free (the quote
   portal owns nine) and `.cli-` was the design's choice before he renamed the
   screen to Accounts on 14 Aug.

   ⚠ WHAT IS *NOT* HERE, AND THAT IS THE POINT: the number tile, its label, its
   value, its note and the record name. Those are the PEEK's rules with this
   screen's class added to their selector list — one rule, two surfaces, the same
   mechanism the deal-list and Leads phone rules already use (D20). A tile is one
   object; two copies of it would agree the day they were written and drift after. */

/* The four-slot frame. Four rather than the peek's three because this is a full
   page, not a 620px drawer — the GRID is the per-surface part, the tile is not. */
[data-shell="2"] .acc-tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
/* THE DEAL SCREEN'S SEVEN TILES — TWELVE TRACKS, NOT FOUR, and the number is doing
   real work. The tiles split into four money figures and three judgement ones, and
   those two groups want different widths: 4 x span-3 fills one row exactly, 3 x
   span-4 fills the next. A `repeat(4, ...)` frame like `.acc-tiles` above cannot do
   that — it would leave the seventh tile alone in a four-wide row with three empty
   tracks beside it, which reads as a missing tile rather than a deliberate group.
   ⚠ The TILE itself is the shared `.dsc-tile` rule far above, joined to
   `.pk-tile`/`.acc-tile` (D20). Only the FRAME is per-surface, exactly as the
   comment on `.acc-tiles` says. */
/* `.dsc-tiles` (the deal screen's six-tile numbers grid) left on 21 Aug 2026 — slice 1 of the
   redesign moved the figures into the head; the rules that laid the grid out went with it. */

/* "2 of 3" — the denominator is quieter than the number, but NOT `--muted` on a
   smaller size, because it sits inside `.acc-t-value` whose job is the figure. */
[data-shell="2"] .acc-t-of { font-weight: 400; color: var(--muted); font-size: 12px; }

/* ⚠ THE NAME IS NEVER TRUNCATED — no ellipsis, no clamp. The longest on the
   register is 47 characters and it wraps at spaces, so "Perbadanan" cannot split
   down the middle. An account name half-shown is an account somebody
   misidentifies. `.acc-name` shares the peek's rule, which already carries
   `overflow-wrap: anywhere`; this only gives it page-sized type. */
[data-shell="2"] .acc-name { font-size: 20px; margin-bottom: 6px; }
[data-shell="2"] .acc-sub {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 0 0 10px; font-size: 12.5px; color: var(--muted);
}
[data-shell="2"] .acc-owner { margin: 0; font-size: 13.5px; color: var(--text); }

/* 🛑 THE BLOCKED LINE — the loudest thing on the screen, and it earns that by
   being a business rule rather than a tidiness complaint: under BR-27 no deal may
   be raised against an account nobody owns, so the record is BLOCKED, not merely
   incomplete. Three accounts are in this state.
   ⚠ IT WARNS, IT DOES NOT DISABLE (D3) — a warning never blocks, and red for
   something that is not blocked teaches people to ignore red. So it takes the
   WARN pair Shaf chose on 13 Aug, not the alert red.
   ⚠ The text is `--text`, NOT `--warn-text`: that token measures 3.99:1 on
   `--warn-bg` and cannot carry small text at AA anywhere on this palette (the
   peek's stalled tile made the same call). The BORDER carries the warning colour,
   where the floor is 3:1 and it clears comfortably. */
[data-shell="2"] .acc-blocked {
  margin: 10px 0 0;
  padding: 9px 11px;
  border: 1px solid var(--warn-text);
  border-left-width: 3px;
  border-radius: 8px;
  background: var(--warn-bg);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}
/* An absence that is a FACT rather than a fault — "nobody is recorded here" — so
   it is quiet, and it is a paragraph rather than a bordered box. */
[data-shell="2"] .acc-empty { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text); }

/* The person cell: name, and the job title beneath it. ⚠ THE 50-CHARACTER TITLE
   IS NEVER HIDDEN — measured, not imagined — it wraps to two or three lines here
   and to full width on a phone card. When you narrow, you MOVE information; you
   do not drop it. */
[data-shell="2"] .acc-p-name { display: block; font-weight: 600; color: var(--text); }
[data-shell="2"] .acc-p-title {
  display: block; margin-top: 2px; font-size: 12px; line-height: 1.4;
  color: var(--muted); overflow-wrap: anywhere;
}

/* How it is filed. Last on the screen deliberately: reading an account's people
   and deals is the daily job; editing its sector happens twice a year. Putting the
   filing at the top would optimise this screen for data hygiene, which is not the
   job Shaf named. */
[data-shell="2"] .acc-filed {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px 18px;
}
[data-shell="2"] .acc-f-row { min-width: 0; }
[data-shell="2"] .acc-f-k {
  display: block; font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 3px;
}
[data-shell="2"] .acc-f-v { display: block; font-size: 13.5px; color: var(--text); overflow-wrap: anywhere; }

/* ⚠ THE PHONE HALF. The account screen is NOT registered against the full phone
   standard — Shaf has not named the job it does on a phone, and the design says so
   explicitly. What it must be is NARROW-SAFE: no sideways page scroll at 390px,
   nothing unreachable, 44px touch targets. Both its tables carry `data-label` on
   every cell, so they become cards through the rules the deal list already owns. */
/* 🛑 44px OF TAP TARGET, AND IT WAS MEASURED RATHER THAN ASSUMED — the links came
   out at **16px**, less than half the floor this screen's own scope commits to.
   Nothing caught it: `layoutcheck` reads overflow, `contrastcheck` reads colour, and
   the assertions read text, so a phone number no thumb can hit passes every gate we
   own. The peek's contact block already solved exactly this and its comment names
   the trap — **padding, not `min-height`, because `min-height` on an inline element
   does nothing**, which is how a 29px "Cancel" once shipped under a 44px floor
   declared in this same file.
   ⚠ Applied at EVERY width rather than only on a phone: a tel: link is a touch
   control wherever it is, and this screen has no phone-only variant to hide behind. */
[data-shell="2"] #crmAccount .acc-p-name { padding: 2px 0; }
[data-shell="2"] #accPeopleTable a.ext-link {
  display: inline-block;
  padding: 12px 0;
  line-height: 20px;
  overflow-wrap: anywhere;
}

/* ================= EDITING, slice 10 =====================================
   Inline, one field at a time, no modal and no "edit mode". The affordance has to
   say "this can be changed" without turning a reading screen into a form: seven
   boxed inputs stacked down the filing band would make the daily job (reading the
   people and the deals) look like data entry, which is the opposite of the order
   these bands are in.

   ⚠ SO THE RESTING STATE IS A DOTTED UNDERLINE, NOT A BOX — and the reason is
   measured rather than aesthetic: `--text-3` is the quietest token that clears the
   3:1 floor a non-text component needs (3.84 light / 5.05 dark, the same measurement
   that rescued the outline chip in slice 5). A border colour carrying meaning is a
   family `contrastcheck` cannot see at all — it only ever reads the words inside —
   so this number is a hand measurement and is written down to be re-derived. */
[data-shell="2"] #crmAccount .acc-ed {
  cursor: pointer;
  border-bottom: 1px dotted var(--text-3);
  /* 🛑 PADDING, NEVER `min-height` — these are inline elements and `min-height` does
     nothing at all on one. That exact mistake shipped a 29px "Cancel" under a 44px
     floor declared in this same file, and shipped this screen's own phone links at
     16px two slices ago.
     ⚠ AND THIS NUMBER WAS WRONG WHEN IT WAS WRITTEN AS ARITHMETIC. It said 11px
     "and the row's own spacing carries it over the floor" — measured on the real
     element, that is **43px against a 44px floor**. One pixel, in a rule whose
     comment claimed it cleared. The fourth time this codebase has put a tap target
     under 44 and the second time the cause was a sum done in a comment rather than
     a measurement: **do not compute a tap target, measure it.** 12px + 20px + 12px
     = 44px, and it is measured at 44 in both themes at 1280px and 390px. */
  padding: 12px 2px;
  line-height: 20px;
  display: inline-block;
  /* 🛑 44px ON THE **WIDTH** AXIS TOO, AND THIS IS THE THIRD TIME THE SAME MISS HAS
     BEEN MADE HERE. A tap floor is a floor on BOTH axes; "Cancel" once shipped 29px
     wide as six one-character lines, honoured on height, missed on width. The cell
     that made it visible is Division, whose value is an em dash on 64 of 66 people —
     padding around one character is a **12px** control, and it reads as a fleck
     rather than something you can press. Found by looking at the phone cards (D14),
     measured, not guessed. */
  min-width: 44px;
  max-width: 100%;
}
/* 🛑 `overflow-wrap: anywhere` BELONGS TO THE FILING BAND ONLY, and putting it on
   the shared class shipped a defect that every measurement passed. It is INHERITED,
   so wrapping the people table's cells in this class handed it to the sentiment
   chips inside them — and `anywhere` collapses an element's min-content width to a
   single character, so the table stopped reserving room for the word and then broke
   it: **"Champion" rendered as "Champ / ion", "Blocker" as "Blocke / r".** Right
   colour, right position, right text — `textContent` is identical either way, so
   the assertions, the layout gate and the contrast gate all passed. **Only looking
   at it showed it** (D14), which is the third time on this screen.
   The filing band genuinely needs it: "NGOs and international organisations" in a
   quarter-width column has nowhere else to go. */
[data-shell="2"] #accFiling .acc-ed { overflow-wrap: anywhere; }

/* While an editor is OPEN the host stops drawing its own affordance — otherwise the
   underline hangs out to the right of the control it belongs to, and the padding
   that makes the closed state a 44px tap target pads the open one as well. */
[data-shell="2"] #crmAccount .acc-ed.acc-editing {
  border-bottom-color: transparent;
  padding: 0;
  display: block;
  /* The OPEN control must be free to shrink inside a grid track again — the 44px
     floor above is about a thing you press, and this is a thing you are already in. */
  min-width: 0;
}
[data-shell="2"] #crmAccount .acc-ed:hover,
[data-shell="2"] #crmAccount .acc-ed:focus-visible {
  border-bottom-style: solid;
  border-bottom-color: var(--link);
  outline: none;
}
/* The head pill is already a filled shape, so it must not also grow an underline —
   it says "editable" by lifting instead. */
/* ⚠ AND THE PILL PAYS THE SAME PRICE AS EVERYTHING ELSE THAT BECAME PRESSABLE. It
   measured **56×28** the moment it became a control — the badge padding it had as a
   label is not a tap target. Sized to the floor here rather than exempted: this
   screen commits to 44px touch targets in the design's own words, and the one solid
   pill on the record is exactly the control somebody will reach for on a phone.
   ⚠ Scoped to `#crmAccount`, so the deal list's `.dl-pill` — which is a LABEL and
   not a control — keeps its own size. A shared class was not widened to serve one
   screen's new job. */
[data-shell="2"] #crmAccount .dl-pill.acc-ed {
  border-bottom: 0;
  padding: 12px 14px;
  line-height: 20px;
  min-width: 44px;
  text-align: center;
  /* ⚠ AND THE RADIUS COMES DOWN WITH IT. `.dl-pill` is `border-radius: 999px`,
     which is right for a 22px label and turns a 44px one into a lozenge — the shape
     stops reading as a status and starts reading as a stretched blob. At control
     height it should read as a control. Found by looking at it, not by measuring:
     every number was already correct. */
  border-radius: 10px;
}
[data-shell="2"] #crmAccount .dl-pill.acc-ed:hover,
[data-shell="2"] #crmAccount .dl-pill.acc-ed:focus-visible {
  box-shadow: 0 0 0 2px var(--link);
}

/* The control itself. ⚠ `max-width: 100%` and `min-width: 0` are load-bearing, not
   defensive: on a phone these cells are GRID items and a `<select>` holding
   "NGOs & International Organizations" refuses to shrink below its longest option
   otherwise — which is how the slice-9 value editor ended up 216px inside a 201px
   track, sitting on the card border. */
[data-shell="2"] #crmAccount .acc-edin {
  font: inherit;
  font-size: 16px;          /* 16px or Safari zooms the page on focus (B2). */
  color: var(--text);
  background: var(--panel);
  /* 🛑 NOT `--border`, AND THIS WAS CAUGHT BY MEASURING RATHER THAN BY READING.
     `--border` is the hairline that separates a row from a row: against `--panel`
     it is **1.27:1 light / 1.38:1 dark**, and this control sits ON `--panel` with
     `--panel` inside it — so the border is the ONLY thing that says a box is
     there, and at 1.27 there is no box, just text with an arrow. Against the 3:1
     floor a non-text component must clear, that is a fail.
     ⚠ **1.27:1 is the SAME NUMBER slice 5 measured** when the badge contract
     prescribed `--accent-soft` for the outline chip and the shape carrying the
     whole distinction turned out to be invisible. Same mistake, same figure, one
     week apart — a separator token reused as a component edge.
     `--text-3` is the quietest token that clears the floor, which is the answer
     slice 5 landed on: **3.84:1 light · 5.05:1 dark**, measured on this element in
     the rendered page, not inferred from the stylesheet (D33).
     ⚠ No gate we own judges either number — `contrastcheck` says in its own header
     that it does not read non-text contrast, and it never opens an editor, so
     these colours are painted by nothing it walks. Hand-measured, written here to
     be re-derived. */
  border: 1px solid var(--text-3);
  border-radius: 6px;
  padding: 8px 8px;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
  min-height: 44px;         /* a real block element, so this one does work */
}

/* The owner controls. Two of them share this block: naming a FIRST owner, and MOVING
   one (23 Aug 2026). `.acc-own-why` JOINS `.acc-own-pick`'s selector list rather than
   taking a copy of it — one class, one place (D20); a second rule with the same values
   drifts within a week.
   ⚠ The count that stood here — "(3 of 56)" — was wrong twice over and is removed rather
   than corrected: it is 0 of 64 today, and a hand-written count in a comment is exactly
   what nothing re-derives (D35). `accounts_auto_owner` fills any null owner from the
   segment default, so the first-owner half cannot draw at all; the MOVE half is the one
   that runs. */
[data-shell="2"] .acc-own {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 10px;
}
[data-shell="2"] .acc-own label { font-size: 12px; color: var(--muted); }
[data-shell="2"] .acc-own-pick,
[data-shell="2"] .acc-own-why {
  font: inherit; font-size: 16px; color: var(--text); background: var(--panel);
  /* Same measurement, same reason as `.acc-edin` above — a control's edge is not a
     row separator. */
  border: 1px solid var(--text-3); border-radius: 6px; padding: 8px;
  min-height: 44px; min-width: 0; max-width: 100%; box-sizing: border-box;
}

@media (max-width: 767px) {
  [data-shell="2"] .acc-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Two across on a phone. Both groups take the same span so the money row and the
     judgement row stack identically — 12 tracks / span 6 = 2, which keeps the money
     figure on one line (the `white-space: nowrap` rule this file has paid for twice). */
  [data-shell="2"] .acc-filed { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  [data-shell="2"] .acc-name { font-size: 17px; }
  /* ⚠ The editor opts BACK IN to stretching. The card rules set `justify-items` on
     the value column, and an editor that keeps `start` is sized by its longest
     option rather than by the track it sits in. */
  [data-shell="2"] #crmAccount .acc-edin { justify-self: stretch; }
  [data-shell="2"] .acc-own { flex-direction: column; align-items: stretch; }
}

/* ============================================================================
   ONE DEAL AND ONE LEAD — the screens, 15 Aug 2026, on Shaf's word "restyle it
   then".

   🛑 THIS IS THE COMPLAINT ITSELF, and it outlived the thirteen slices that were
   supposed to answer it: *tapping a row on the new list lands you on old
   styling.* Every class on both detail screens came from `styles.css` — this
   file named `#crmDeal` and `#crmLead` ZERO times — so the deal screen was
   reorganised (slice 11), given a peek (12) and documented (13) while still
   wearing the language of the shell that was deleted on 11 Aug.

   ⚠ THE GATE THAT WATCHED IT WAS LYING. `detail-screens-restyled` answered 0
   when the truth was 1, because it looked for `#crmClient` — renamed
   `#crmAccount` on 14 Aug — so 23 real rules counted for nothing and its
   `done: n >= 3` could never have gone true. Corrected first, then this built.

   ⚠ WHAT IS NOT HERE, AND THAT IS THE POINT: the tile, the pill, the chip, the
   contact block, the peek. Those are shared objects and they already carry this
   language; a second copy scoped to this screen would agree the day it was
   written and drift after (D20/D26). Only the things that are genuinely THIS
   screen's are below.

   ⚠ SCOPED TO BOTH IDS TOGETHER wherever the shape is the same. The lead screen
   emits the identical head — `.panel-title`, `.panel-subtitle`, `.deal-kv` — so
   splitting it into two blocks would be two copies of one decision.
   ============================================================================ */

/* THE RECORD NAME. Page-sized, and it shares its measurement with the peek's:
   the longest `shortname` on the live database is 103 characters, so it is NEVER
   truncated and never ellipsised — a name half-shown is a record somebody
   misidentifies. `overflow-wrap` catches the one case wrapping at spaces cannot.
   ⚠ IT MOVES FROM `--primary-text` TO `--text`, AND THAT IS THE VISIBLE CHANGE.
   The old stylesheet painted every `.panel-title` in the brand green. The new
   language spends colour on STATE — the one solid pill — and leaves the name as
   ink, exactly as `.acc-name` does. A green heading above a green pill is two
   things competing to be the thing you look at first.
   ⚠ Scoped to these two screens, never to `.panel-title`: the quote portal wears
   that class on nine panels and this is not a decision about them. */
[data-shell="2"] #crmDeal .panel-title,
[data-shell="2"] #crmLead .panel-title {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--text);
  margin: 0 0 5px;
  overflow-wrap: anywhere;
}

/* The line under it — what this is and who it is with. Quiet, because the name
   above and the pill beside it are what the eye should reach first. */
[data-shell="2"] #crmDeal .panel-subtitle,
[data-shell="2"] #crmLead .panel-subtitle {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
  margin: 0 0 12px;
}

/* THE FACT GRID takes the TILE language — the same micro-caps label and figure
   the peek and the account record already use, so one object reads one way on
   all three surfaces.
   ⚠ THE LABEL IS `--muted`, NOT `--text-3`, and the reason is written twice in
   this file already: `--text-3` is the token whose job is labels and it measures
   3.84:1 on `--panel`, under the 4.5 floor. It is safe as a BORDER, where the
   floor is 3:1, and not as a LABEL. */
[data-shell="2"] #crmDeal .kv-k,
[data-shell="2"] #crmLead .kv-k {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 3px;
}
/* ⚠ `tabular-nums` AND NOT `nowrap`. The tiles above may refuse to wrap because
   they hold money and nothing else; these cells hold money AND a 85-character
   division name, so forcing one line would push the grid off the screen edge —
   which is the defect this screen spent slice 11 closing. Tabular figures line
   the money up down the column; wrapping stays available to the words. */
[data-shell="2"] #crmDeal .kv-v,
[data-shell="2"] #crmLead .kv-v {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* TRACK C-1 — THE RECORD IS INCOMPLETE, AND IT SAYS WHICH FIELDS.
   ⚠ It takes the SAME contract as `.acc-blocked` rather than a new one, because it
   is the same kind of sentence: a business rule, warning, never blocking (D3). So
   the decisions carry across unchanged and are not re-argued — the border carries
   the warning colour (3:1 floor, cleared comfortably) and the WORDS are `--text`,
   because `--warn-text` on `--warn-bg` measures 3.99:1 and cannot hold small text
   at AA anywhere on this palette. The peek's stalled tile and the blocked-account
   line both made this call already.
   ⚠ It is deliberately NOT the alert red: an incomplete record is not a blocked
   one, and red for something that is not blocked teaches people to ignore red. */
[data-shell="2"] #crmDeal .deal-gaps {
  margin: 10px 0 0;
  padding: 9px 11px;
  border: 1px solid var(--warn-text);
  border-left-width: 3px;
  border-radius: 8px;
  background: var(--warn-bg);
  color: var(--text);
}
[data-shell="2"] #crmDeal .dg-head {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.5;
}
/* The list is the ANSWER to the heading, so it is close to it and reads as one
   block. `list-style: none` with the count already in the heading — a bulleted
   list of four items under "4 required fields" counts them twice. */
[data-shell="2"] #crmDeal .dg-list {
  margin: 5px 0 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  line-height: 1.6;
}
[data-shell="2"] #crmDeal .dg-list li { font-weight: 600; }
/* ══ A GAP NAMES ITSELF, EXPLAINS ITSELF, AND CARRIES ITS OWN WAY IN — 22 Aug 2026 ══
   The row is label + reason on the left and one control on the right, wrapping to two
   lines when the reason is long rather than squeezing the button.
   ⚠ THE REASON IS `--text`, NOT `--muted`, and that is a measurement rather than a
   taste: this band fills itself with `--warn-bg`, and the block comment above already
   records that `--warn-text` on `--warn-bg` is 3.99:1 and cannot hold small text at AA.
   `--muted` on a filled band is the same family of mistake — `--muted` on `--row-band`
   measures 4.47:1, a hair under, which is why the peek's tiles were left on `--panel`.
   The reason is ordinary body colour at a slightly smaller size; the WEIGHT is what
   separates it from the label, not the colour.
   ⚠ `flex: 1 1 240px` rather than `1 1 auto`: a bare `auto` lets the text's preferred
   width count toward the row's max-content and push the button off the end, which is
   the same min-content trap the stylesheet already carries three scars for. */
[data-shell="2"] #crmDeal .dg-row {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 6px 10px;
  padding: 3px 0;
}
[data-shell="2"] #crmDeal .dg-what { flex: 1 1 240px; min-width: 0; }
[data-shell="2"] #crmDeal .dg-reason { font-weight: 400; font-size: 12.5px; }
[data-shell="2"] #crmDeal .dg-go { flex: 0 0 auto; }
[data-shell="2"] #crmDeal .dg-where {
  flex: 0 0 auto;
  font-weight: 400;
  font-size: 12.5px;
}
[data-shell="2"] #crmDeal .dg-why {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
}

/* The band heading gains the new rhythm. It was already micro-caps and muted —
   the one thing the old stylesheet had in common with this one — so this only
   settles the spacing against the panels the groups now put around it. */
[data-shell="2"] #crmDeal .crm-band-title,
[data-shell="2"] #crmLead .crm-band-title {
  font-size: 11px;
  letter-spacing: 0.07em;
  margin: 0 0 9px;
}

/* ENGAGEMENTS — the one list on the deal screen that is neither a table nor a
   form. It gets the row language the rest of the new shell uses: a hairline
   between rows rather than a box around each, the date in tabular figures so a
   column of them lines up, and the summary carrying the weight.
   ⚠ `minmax(0, …)` on the text tracks because a grid track's default minimum is
   min-content (D21) and neither the summary nor the person's name is a width we
   choose. */
[data-shell="2"] #crmDeal .act-row {
  display: grid;
  grid-template-columns: 92px 120px minmax(0, 1fr) minmax(0, 140px);
  gap: 4px 12px;
  align-items: baseline;
  padding: 8px 2px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
[data-shell="2"] #crmDeal .act-row:last-child { border-bottom: 0; }
[data-shell="2"] #crmDeal .act-when {
  color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums;
}
[data-shell="2"] #crmDeal .act-type {
  color: var(--muted); font-size: 11px; font-weight: 600;
  letter-spacing: 0.03em; text-transform: uppercase;
}
[data-shell="2"] #crmDeal .act-body { color: var(--text); line-height: 1.5; }
[data-shell="2"] #crmDeal .act-who { color: var(--muted); font-size: 12px; }

/* Below 768px the four tracks become one stack — an engagement is a sentence
   with a date on it, and four columns in 390px is four columns of two words. */
@media (max-width: 767px) {
  [data-shell="2"] #crmDeal .act-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

/* ============================================================================
   MY WORK — the screen, 15 Aug 2026, and Shaf found it the only way it could be
   found: by clicking a row.

   🛑 THE GATE WATCHING THE RESTYLE ASKED ABOUT THREE SCREENS OUT OF SEVENTEEN
   and answered "3 — may be done" while TWELVE screens had zero rules in this
   file. My work, the dashboard, the flow board, the accounts register, Wins,
   Costing, Activity, the scorecard, Settings, the brief, Profile and
   Notifications were all still wearing the shell deleted on 11 August. **A gate
   that enumerates three of seventeen reports the other fourteen clean by never
   asking** (D28). It reads every screen off the markup now.

   ⚠ MY WORK IS ON THE PHONE STANDARD — N7 names it, alongside Approvals, the
   shell, the deal list and Leads — so every rule here is checked at 390px, and
   the existing single-column stack below 768px is kept rather than replaced.
   ⚠ IT IS THE INBOX A BDM OPENS FIRST. Nothing here may make a row harder to
   scan: the code stays in tabular figures so a column of them lines up, and the
   action stays on the right where it already was.
   ============================================================================ */

/* The group heading takes the micro-caps the bands and tiles use, so "Wins with
   nothing to prove them" reads as a SECTION rather than as a sentence competing
   with the rows under it. The count beside it is quieter still.
   ⚠ `--muted`, not `--text-3`: 3.84:1 on `--panel` is under the 4.5 text floor,
   and this file has now paid for that distinction three times. */
[data-shell="2"] #crmWork .work-title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
[data-shell="2"] #crmWork .work-count { font-size: 11px; color: var(--muted); }
[data-shell="2"] #crmWork .work-head {
  padding-bottom: 7px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
[data-shell="2"] #crmWork .work-group { margin-bottom: 26px; }
[data-shell="2"] #crmWork .work-group:last-child { margin-bottom: 0; }

/* ⚠ THE URGENCY COLOURS STAY ON THE HEADING AND ARE NOT OVERRIDDEN. `Lead
   waiting` in alert red and `Stalled` in amber are how the attention band says
   which of its groups is shouting, and they are Shaf's colour decisions from
   13 Aug. Micro-caps changes the SHAPE of those headings, not their meaning. */
[data-shell="2"] #crmWork .att-head-urgent .work-title { color: var(--alert-text); }
[data-shell="2"] #crmWork .att-head-warn .work-title { color: var(--warn-text); }

/* The rows. A hairline between them rather than a box around each — the same
   choice the engagements list on the deal screen makes, and for the same reason:
   nine boxes stacked reads as nine things to deal with, nine lines reads as one
   list. */
[data-shell="2"] #crmWork .work-row,
[data-shell="2"] #crmWork .att-row {
  padding: 9px 2px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.5;
}
[data-shell="2"] #crmWork .work-row:last-child,
[data-shell="2"] #crmWork .att-row:last-child { border-bottom: 0; }
/* The reference column: tabular so a column of codes and dates lines up, and
   quiet, because it is how you FIND the row rather than what the row says. */
[data-shell="2"] #crmWork .work-row .mono {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--muted);
}
[data-shell="2"] #crmWork .work-what,
[data-shell="2"] #crmWork .att-what { font-weight: 600; color: var(--text); }
[data-shell="2"] #crmWork .work-sub { margin-top: 2px; line-height: 1.45; }

/* THE ACTION. It is the thing you came to do, so it stops being grey text and
   becomes the word you reach for — `--primary-text`, the token that exists
   because the brand green is a FILL and cannot be read on a dark surface.
   ⚠ It is NOT a button: nine buttons down a page is a wall, and the whole ROW is
   already the target (`.row-open`). This only stops the action reading as an
   afterthought in the same grey as the sub-line. */
[data-shell="2"] #crmWork .work-do {
  color: var(--primary-text);
  font-weight: 600;
  font-size: 12.5px;
  white-space: nowrap;
}

/* On a phone the row is already one column (the shared rule below 768px). The
   action drops under the text and needs to look tappable there rather than like
   a caption — and it gets the 44px it needs from the ROW, which is the target. */
@media (max-width: 767px) {
  [data-shell="2"] #crmWork .work-row,
  [data-shell="2"] #crmWork .att-row { padding: 12px 2px; }
  [data-shell="2"] #crmWork .work-do { white-space: normal; }
}

/* ⚠ THE "NEEDS A LOOK" BAND IS PART OF THIS SCREEN AND WAS READING AS A DIFFERENT
   ONE — found by looking at the restyled screenshot, not by a gate (D14). Its
   sub-groups (`Lead waiting`, `Stalled`, `Incomplete`, `No owner`) carry
   `.work-title` so they took the micro-caps above, but not the rule under them,
   so four headings sat flush against the rows they head while every other group
   on the page had a hairline. One screen, one rhythm. */
[data-shell="2"] #crmWork .att-head {
  margin-top: 22px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
[data-shell="2"] #crmWork .att-head:first-child { margin-top: 0; }
/* The sentence explaining what the whole group means — one line, quiet, and it
   belongs to the heading rather than to the first row under it. */
[data-shell="2"] #crmWork .att-why {
  margin: 6px 0 2px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}
/* The band itself needs air above it: it is the last section on the page and it
   followed the costing rows with nothing between them. */
[data-shell="2"] #crmWork #workAttBand { margin-top: 30px; }

/* ============================================================================
   THE REMAINING ELEVEN SCREENS — 15 Aug 2026, Shaf's "ok go".

   🛑 THE GATE ASKED ABOUT THREE SCREENS OUT OF SEVENTEEN and reported the other
   fourteen clean by never asking (D28). Once it read the markup instead, the
   honest answer was 6 of 17 — and My work's own rows land on Costing and
   Settings, so the complaint that started this ("my work list when click is
   still old screen") was only half answered.

   ⚠ ONE BLOCK, NOT ELEVEN. What these screens have in common is exactly the
   shared furniture — band titles, panel heads, the browse tables, the row
   families the JS renders — so the language arrives once and cannot drift
   between them (D20). Each id is NAMED rather than the selector being widened
   to `.crm-view`, for the same reason the phone-card list names its surfaces:
   widening a selector enrols screens nobody decided about.

   ⚠ WHAT IS NOT HERE: the flow board's columns and the dashboard's tiles. Those
   are components with their own measured rules — the board scrolls sideways BY
   DESIGN and the tiles carry money that must not wrap — and rewriting them from
   a sweep is how a measured decision gets undone by tidying (the merge that two
   judges called the largest gratuitous risk in slice 11's set). They keep their
   own rules; this only gives their SCREEN the shared language.
   ============================================================================ */

/* THE BAND HEADING, everywhere it appears. This is the single most visible part
   of the old language: 13px bold in `--text` reads as a sentence competing with
   the rows under it, where the new shell's is a quiet micro-caps label with a
   rule under it. The deal and lead screens got this in the restyle; these are
   the rest. */
[data-shell="2"] #crmDash .crm-band-title,
[data-shell="2"] #crmAccounts .crm-band-title,
[data-shell="2"] #crmPerf .crm-band-title,
[data-shell="2"] #crmAdmin .crm-band-title {
  font-size: 11px;
  letter-spacing: 0.07em;
  color: var(--muted);
  margin: 0 0 9px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--border);
}

/* THE PANEL HEAD on the screens that have one. Same decision as the detail
   screens: the name is page-sized INK, not brand green — colour is spent on
   STATE, and a green heading above a green pill is two things competing to be
   looked at first. */
[data-shell="2"] #crmActivity .panel-title,
[data-shell="2"] #crmProfile .panel-title {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--text);
  margin: 0 0 5px;
  overflow-wrap: anywhere;
}
[data-shell="2"] #crmActivity .panel-subtitle,
[data-shell="2"] #crmProfile .panel-subtitle,
[data-shell="2"] #crmWins .panel-subtitle {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
  margin: 0 0 12px;
}

/* THE ROW FAMILIES the JS renders on these screens — Settings' orphan accounts
   and capability board (`.adm-row`), Costing's rows (`.cost-row`), Wins'
   (`.win-row`). A hairline between rows rather than a box around each: the same
   choice My work and the deal screen's engagements make, and the reason is the
   same — a stack of boxes reads as a stack of problems. */
[data-shell="2"] #crmAdmin .adm-row,
[data-shell="2"] #crmCosting .cost-row,
[data-shell="2"] #crmWins .win-row {
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.5;
}
[data-shell="2"] #crmAdmin .adm-row:last-child,
[data-shell="2"] #crmCosting .cost-row:last-child,
[data-shell="2"] #crmWins .win-row:last-child { border-bottom: 0; }
/* The reference column, tabular so a column of codes and dates lines up. */
[data-shell="2"] #crmAdmin .adm-key,
[data-shell="2"] #crmCosting .mono,
[data-shell="2"] #crmWins .mono {
  font-variant-numeric: tabular-nums;
}

/* 🛑 MONEY MUST NOT WRAP, AND #crmWins WAS NEVER ENROLLED IN THE RULE THAT SAYS SO.
   18 Aug 2026: the Final TCV column broke `RM 1,250,000.50` across two lines on the two
   largest rows, in both themes. `.ta-r` (styles.css) sets alignment and tabular figures and
   declares no `white-space`, so an auto-layout table squeezed the column to its min-content.
   ⚠ THE OBVIOUS FIX IS HALF-BROKEN AND WAS MEASURED BEFORE BEING REJECTED: adding `#crmWins`
   to the existing nowrap list further up this file puts it inside `@media (min-width: 768px)`.
   That query is right for `#crmList`, which becomes CARDS below 768px so its cell rules stop
   applying — but the Wins table stays a TABLE on a phone, which is exactly where its column is
   narrowest (118px) and where it needs the rule most. Unmediated on purpose.
   ⚠ And NOT on the bare `.ta-r`: that class is worn by seven other surfaces, including the
   phone cards which deliberately carry `overflow-wrap: anywhere` so a long value cannot push a
   390px card off-screen. A global nowrap would fight a decision already signed off.
   ⚠ AND `.mono` IS IN THE SELECTOR BECAUSE THE PROBE FOUND A SECOND DEFECT THE DIAGNOSIS HAD
   NOT NAMED: the deal CODES wrapped too — `DQ-2026-0001` broken across two lines on three
   rows. It was invisible for the same reason the money was, and it was caught only because
   the new check copied the deals-list probe wholesale (which measures both columns) instead
   of narrowing to the one column that had been reported. A gate scoped to the reported
   symptom would have shipped the other half. */
[data-shell="2"] #crmWins .crm-table td.ta-r,
[data-shell="2"] #crmWins .crm-table td.mono { white-space: nowrap; }
/* The sub-line under a row's subject: quiet, and it belongs to the row above it
   rather than floating between two. */
/* ⚠ `.cost-sub` STOOD HERE AND NOTHING EMITS IT. Written from the shape the other
   screens have rather than from what this one renders — a rule whose scope no
   page can satisfy is a lie (D33), and it is the exact defect this file warns
   about three times over. Caught by `dead-css` moving 43 → 44 at the debrief,
   which is the ratchet doing its job on the person who added the rule. */
[data-shell="2"] #crmAdmin .adm-sub {
  line-height: 1.45;
  margin-top: 2px;
}

/* THE EMPTY STATES on the two screens that are nothing else — Notifications and
   Profile both draw one and only one thing when there is nothing to show. They
   are the screens most likely to be somebody's first impression of a section, so
   an empty state that looks like a failed load is the worst version of them. */
[data-shell="2"] #crmNotif .empty-what,
[data-shell="2"] #crmProfile .empty-what {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}
[data-shell="2"] #crmNotif .empty-why,
[data-shell="2"] #crmProfile .empty-why {
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 62ch;
}

/* THE BRIEF is prose, and prose has a measure. A generated paragraph running the
   full width of a 1512px window is a paragraph nobody finishes. */
[data-shell="2"] #crmBrief .brief-body {
  font-size: 13.5px;
  line-height: 1.65;
  max-width: 78ch;
}

/* THE HINT — the sentence a screen says when it has nothing else to say. It was
   the old stylesheet's, so it is the last thing that would have kept these screens
   looking old after everything above.

   🛑 THIS WAS A LIST OF NINE SCREEN IDS AND THE LIST WENT STALE, WHICH IS THE WHOLE
   REASON IT IS GONE (18 Aug 2026). Naming the screens meant every screen built after
   the list was written silently opted out, and three had: measured on the real
   elements (D33), **8 paragraphs across `#crmDeal`, `#crmList` and `#crmAccount` were
   still at 14px with no measure** — including "That account is no longer in view",
   which is the entire content of that screen when it fires. A rule keyed to a list of
   places is a count nothing re-derives (D35), and it fails silently and invisibly.

   ⚠ `p.hint`, NOT `.hint`, AND THE ELEMENT IS THE DISCRIMINATOR ON PURPOSE — this is
   the same seam the `span.hint` rule in `styles.css` was cut on earlier today, after
   ONE CLASS SERVING TWO COMPONENTS painted wrong on the Dashboard. `.hint` is both the
   app's muted explanatory paragraph AND the info-button widget (a `<span>` wrapping
   `.hint-btn`/`.hint-pop`). **`#crmList` contains one of those spans**, so restoring
   this rule as a bare `.hint` scoped to that screen would have styled the widget as a
   paragraph — re-creating the exact collision that was just fixed, from the other end.
   Type-selecting `<p>` cannot touch it.

   ⚠ Specificity DROPS from (1,2,0) to (0,2,1) — no id any more. Verified by measuring
   every `.hint` on every CRM screen before and after: nothing else in either stylesheet
   wins in between. `.adhoc-form .field .hint` is (0,3,0) and would now outrank this, but
   it lives on the quote portal, which is not `[data-shell="2"]`. */
[data-shell="2"] p.hint {
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 72ch;
}

/* TRACK C-2 — A PLAN IS THE SAME ROW SHAPE AS A LOG, AND THAT IS DELIBERATE.
   It is the same object at a different point in its life (D26), so it takes the
   `.act-row` grid unchanged and is separated by being in its own PANEL with its
   own heading — not by being a differently-shaped row. Two row shapes for one
   object is how a person starts believing they are two kinds of thing.
   ⚠ The left border is the only difference, and it is 2px so it reads as a marker
   rather than as the bordered box the whole new shell moved away from. */
[data-shell="2"] #crmActivity .act-plan {
  border-left: 2px solid var(--link);
  padding-left: 9px;
}
/* TRACK C-2 — THE MONTH AHEAD. ⚠ `minmax(0, 1fr)` on all seven columns, not `1fr`:
   a grid track's default minimum is min-content (D21), and a cell holding
   `DQ-2026-0061` would otherwise make that day's column as wide as the code and
   push the week past the panel. The deal codes are not a width we choose. */
[data-shell="2"] #crmActivity .cal-head,
[data-shell="2"] #crmList .cal-head {
  display: flex; align-items: center; gap: 10px; margin: 0 0 10px;
}
[data-shell="2"] #crmActivity .cal-title,
[data-shell="2"] #crmList .cal-title {
  font-size: 13px; font-weight: 700; color: var(--text); min-width: 12ch;
}
[data-shell="2"] #crmActivity .cal-grid,
[data-shell="2"] #crmList .cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
[data-shell="2"] #crmActivity .cal-dow,
[data-shell="2"] #crmList .cal-dow {
  background: var(--panel);
  padding: 6px 7px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
[data-shell="2"] #crmActivity .cal-cell,
[data-shell="2"] #crmList .cal-cell {
  background: var(--panel);
  min-height: 62px;
  padding: 5px 6px;
  display: block;
}
/* An empty leading cell is STRUCTURE, not a day — it carries no number and no border
   treatment, so the month reads as starting on its real weekday. */
[data-shell="2"] #crmActivity .cal-pad,
[data-shell="2"] #crmList .cal-pad { background: var(--bg); min-height: 62px; }
[data-shell="2"] #crmActivity .cal-num,
[data-shell="2"] #crmList .cal-num {
  display: block; font-size: 11px; font-weight: 700;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
[data-shell="2"] #crmActivity .cal-today .cal-num,
[data-shell="2"] #crmList .cal-today .cal-num { color: var(--link); }
[data-shell="2"] #crmActivity .cal-has,
[data-shell="2"] #crmList .cal-has { background: var(--panel); }
/* ⚠ `overflow-wrap: anywhere` rather than truncation: a day with two site visits must
   show both. When you narrow, you MOVE information; you do not drop it. */
[data-shell="2"] #crmActivity .cal-item,
[data-shell="2"] #crmList .cal-item {
  display: block;
  margin-top: 3px;
  padding: 2px 4px;
  border-left: 2px solid var(--link);
  border-radius: 3px;
  background: var(--bg);
  font-size: 10.5px;
  line-height: 1.35;
  color: var(--text);
  overflow-wrap: anywhere;
}
/* A day already gone that still holds a plan. Same WARN reasoning as `.badge-late`. */
[data-shell="2"] #crmActivity .cal-late,
[data-shell="2"] #crmList .cal-late { border-left-color: var(--warn-text); }

/* The one divider on the deal's timeline, where the future meets the past. It is a
   LABEL, not a band heading — the band already has one, and a second title-weight
   line inside it would read as a second section. */
[data-shell="2"] #crmDeal .tl-split {
  margin: 10px 0 6px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid var(--border);
  padding-top: 8px;
}
[data-shell="2"] #crmDeal .act-plan { border-left: 2px solid var(--link); padding-left: 9px; }
[data-shell="2"] #crmDeal .badge-late {
  background: var(--warn-bg); border: 1px solid var(--warn-text); color: var(--text);
  border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 700; white-space: nowrap;
}

/* A DEAL THAT WENT BACKWARDS — the stage history, 19 Aug 2026. Its own class rather
   than `.badge-late`: that name means "a date has passed" everywhere else on the
   platform, and a stage moving back is neither late nor a date. A class name is not a
   place to hide a second meaning (D20). The treatment is deliberately identical — this
   is the same WARN pair, for the same reason `.badge-late` takes it rather than the
   alert red: a deal going back a stage is ordinary business, not a broken record, and
   red for something that is not broken teaches people to ignore red.
   ⚠ The WORDS are `--text`, never `--warn-text`: that pair measures 3.99:1 on
   `--warn-bg` and cannot carry text this small at AA. Same constraint, same answer. */
[data-shell="2"] #crmDeal .act-back {
  background: var(--warn-bg); border: 1px solid var(--warn-text); color: var(--text);
  border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 700; white-space: nowrap;
}

/* LATE — the plan whose date has gone past. ⚠ It takes the WARN pair, not the alert
   red: a plan you have not got to yet is not a broken record, and red for something
   that is not broken teaches people to ignore red (same call as `.acc-blocked` and
   the peek's stalled tile). The WORDS are `--text`, because `--warn-text` on
   `--warn-bg` measures 3.99:1 and cannot carry text this small at AA. */
[data-shell="2"] #crmActivity .badge-late {
  background: var(--warn-bg);
  border: 1px solid var(--warn-text);
  color: var(--text);
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* ⚠ THE PEEKED-ROW MARK ON MY WORK — 15 Aug 2026, and without it the peek from
   this screen loses the thing that makes stepping usable. The shared mark is
   `tr.row-peeked > td`, a TABLE rule, and My work's rows are DIVs: the class was
   being applied correctly and painting nothing. Exactly the shape slice 7 found
   when the mark became a 3px stub on a phone card — right class, right position,
   no shape — and the same lesson: the mark has to be re-drawn for the SHAPE the
   row actually is, not inherited from the shape it usually has.
   ⚠ The bar is `--primary-text`, the same token and the same 3px the table rows
   use, so the two surfaces mark a row identically. Zero new colours. */
[data-shell="2"] #crmWork .work-row.row-peeked {
  background: var(--bg);
  box-shadow: inset 3px 0 0 var(--primary-text);
}

/* ============================================================================
   THE DEALS LIST AT DESKTOP WIDTHS — c94, 16 Aug 2026.

   🛑 WHY THIS RULE EXISTS, AND IT WAS FOUND BY LOOKING AT THE SHOT, NOT BY A GATE
   (D14). The Close date and Last activity columns took this table from nine
   columns to eleven, and at 1280px the sidebar leaves it about 1070px. An HTML
   table with no declared widths distributes what it has, so the two TEXT columns
   gave way first: the Deal column collapsed to roughly one word per line and the
   longest deal name rendered as a THIRTEEN-line cell 250px tall, with the rest of
   the row's data stranded beside a wall of text.

   ⚠ `layoutcheck` reported this as a 155px WARNING, which is true and badly
   understates it — the gate measures whether content overflows its container, and
   a column that wraps rather than spills has not overflowed anything. The row was
   unreadable and every number in it was correct. Only the screenshot showed it.

   ⚠ THE FIX IS A FLOOR, NOT AN ELLIPSIS. Truncating the name would hide
   information, and this list is where somebody decides WHICH deal to open — the
   name is the thing they are reading. A minimum width keeps every character
   readable and pays for it in horizontal scroll inside `.browse-tablewrap`, which
   is a container this table already has and which the page's own scroll is
   deliberately kept out of (0 sideways page scroll is the gate that matters).

   ⚠ Scoped to `#crmList` deliberately, not to `.crm-table`: the other five tables
   sharing that class have their own column counts and none of them has this
   problem. Widening the shared class would enrol screens nobody decided about,
   which is the same argument the restyle made for naming ids one by one.

   ⚠ NOT INSIDE A MEDIA QUERY, and the 767px block above overrides it there — below
   768px this table is CARDS, where a column minimum is meaningless and would fight
   the stacking. Order is load-bearing: this sits after that block, so it must not
   claim widths a card layout has to undo. It does not: cards set `display: block`
   on the cells, and `min-width` on a block in a stacked card costs nothing.
   ============================================================================ */

/* ⚠ THESE FLOORS WERE `:nth-child(1)` AND `:nth-child(2)` UNTIL 17 Aug 2026, AND
   ADDING THE SELECT COLUMN AT POSITION 0 SILENTLY MOVED BOTH ONTO THE WRONG
   COLUMNS — the 200px Deal floor landed on a 34px checkbox and the 130px Client
   floor landed on Deal. Nothing would have gone red: no overflow, no missing class,
   no colour below floor. It is the third time in ONE change that a position-based
   selector broke when a column was inserted (the other two were ten harness
   selectors and the phone-card labels), which is what earned the rule now written
   in `emt-ui`: on a table whose columns can move, address a cell by what it IS.
   Keyed on the class, these cannot break again whichever end a column is added at. */
[data-shell="2"] #crmList .crm-table td.dl-name { min-width: 200px; }
[data-shell="2"] #crmList .crm-table td.dl-client { min-width: 130px; }

/* The close-date cell must not wrap its own two halves apart — "04 Aug 2026 · 12d
   late" reading as four lines is the money-wrapping defect in another costume. */
[data-shell="2"] #crmList .crm-table td.dl-close { white-space: nowrap; }

/* ============================================================================
   PIPELINE BY STAGE — the bar band on the deals list. c96, 17 Aug 2026.

   ⚠ ONE FILL, NOT TWELVE. The design log settled this on 9 Aug 2026: stage is an
   ORDERED registry set whose endpoints are the reserved status colours, and the
   series palette is four slots for named things rather than magnitudes. A bar is
   already separated from its neighbours by POSITION and carries its own label and
   its own figure, so hue would be decoration that runs out — silently — the day
   somebody adds a thirteenth stage. Won and lost differ in KIND rather than in
   degree, so those two keep their semantic fills.

   ⚠ The track and the fill both need a visible edge against --panel. `--border` is
   1.27:1 there and is the separator token, not a component edge — so the track is
   --row-band (a fill, which is what it is) and the bar itself carries the colour.
   ============================================================================ */

[data-shell="2"] #crmList .stage-bars { display: grid; gap: 8px; }

/* One grid, shared by every row, so the labels and the figures line up down the
   band without any row knowing about any other (D12). `minmax(0, 1fr)` on the
   track because its width is data we do not control. */
[data-shell="2"] #crmList .sb-row {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr) 132px 76px;
  align-items: center;
  gap: 12px;
}
[data-shell="2"] #crmList .sb-label { font-size: 13px; color: var(--text); }
[data-shell="2"] #crmList .sb-track {
  background: var(--row-band);
  border-radius: 999px;
  height: 20px;
  overflow: hidden;
}
[data-shell="2"] #crmList .sb-fill {
  display: block;
  height: 100%;
  background: var(--primary-text);
  border-radius: 999px;
}
/* Six categorical slots. A stage takes one by its REGISTRY position, so a filter
   that changes how many stages are drawn never repaints the ones that survive, and
   a stage keeps the same colour on every screen that ever draws it. */
[data-shell="2"] #crmList .sb-fill.sb-s1 { background: var(--stage-1); }
[data-shell="2"] #crmList .sb-fill.sb-s2 { background: var(--stage-2); }
[data-shell="2"] #crmList .sb-fill.sb-s3 { background: var(--stage-3); }
[data-shell="2"] #crmList .sb-fill.sb-s4 { background: var(--stage-4); }
[data-shell="2"] #crmList .sb-fill.sb-s5 { background: var(--stage-5); }
[data-shell="2"] #crmList .sb-fill.sb-s6 { background: var(--stage-6); }
/* Won and lost keep the RESERVED status colours. A status colour is never reused
   as a series step — these two differ in KIND from the stages above them. */
/* Won, lost and dropped take RESERVED status colours and never a categorical slot.
   ⚠ Lost and dropped were BOTH grey until 17 Aug — two different outcomes rendering
   identically, which the desktop fixture could not show because it only ever draws
   one of them. */
[data-shell="2"] #crmList .sb-fill.sb-won { background: var(--ok-text); }
[data-shell="2"] #crmList .sb-fill.sb-lost { background: var(--alert-text); }
[data-shell="2"] #crmList .sb-fill.sb-off { background: var(--text-3); }
/* Money is mono and tabular so the column can be scanned down, and never wraps —
   a six-figure figure breaking onto two lines is the defect this rule exists for. */
[data-shell="2"] #crmList .sb-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  text-align: right;
  white-space: nowrap;
  color: var(--text);
}
[data-shell="2"] #crmList .sb-count { font-size: 12px; color: var(--muted); white-space: nowrap; }

/* On a phone the label and the figures stack above a full-width bar: four tracks
   in 390px leaves the money ~70px, which is where it wraps. */
/* ⚠ On a phone the row becomes: name + money on one line, the deal count under the
   MONEY it belongs to, then the bar full width. The first version let the count land
   in its own grid cell between the two, which put it floating in the middle of the
   row belonging to neither — visible on a real device and on nothing else. It is a
   caption on the figure, so it sits under the figure. */
@media (max-width: 767px) {
  [data-shell="2"] #crmList .sb-row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 1px 10px;
  }
  [data-shell="2"] #crmList .sb-label { grid-column: 1; grid-row: 1; }
  [data-shell="2"] #crmList .sb-value { grid-column: 2; grid-row: 1; }
  [data-shell="2"] #crmList .sb-count { grid-column: 2; grid-row: 2; text-align: right; }
  [data-shell="2"] #crmList .sb-track { grid-column: 1 / -1; grid-row: 3; margin-top: 5px; }
}

/* ============================================================================
   SCORECARD — the Enviromet Design System's KPI surface. c99, 17 Aug 2026.

   Read off its own specimen card rather than guessed: a micro-caps letterspaced
   label, a LARGE MONO figure with its unit set small and quiet beside it, and a
   context line under it whose colour carries MEANING rather than direction.

   ⚠ It is a NEW class, not a restyle of `.tile`. The Dashboard's tiles are a
   different object with a different job (standing company totals) and widening
   `.tile` would have dragged them along — the same reason the restyle named ids
   one by one instead of widening `.crm-view`.
   ============================================================================ */

[data-shell="2"] #crmList .tile-row { gap: 12px; }

[data-shell="2"] #crmList .sc {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;                 /* the system's `lg 14` radius step */
  padding: 18px 20px;
  min-width: 0;                        /* the money is data whose width is not ours */
}
/* The letterspaced micro-caps label is a core brand motif — it mirrors the
   wordmark. 12px/600/0.12em, on the quietest token that still clears the floor. */
[data-shell="2"] #crmList .sc-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Mono + tabular, because this is a READING. Never wraps: a six-figure ringgit
   figure breaking onto two lines is the defect the phone rules already name. */
[data-shell="2"] #crmList .sc-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 30px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--text);
  margin: 8px 0 6px;
  white-space: nowrap;
  display: flex;
  align-items: baseline;
  gap: 5px;
}
/* The unit rides the value's BASELINE at a smaller size — "RM" and "%" are units
   here exactly as °C and mm are on the system's own card. */
/* ⚠ `--muted`, NOT `--text-3`, AND THE GATE CAUGHT ME TAKING THE OBVIOUS ONE.
   `--text-3` is the token whose stated job is "labels, units, em-dash placeholders",
   which is exactly what this is — and as TEXT on `--panel` it measures 3.84:1,
   under the 4.5 floor. The emt-ui skill documents this precisely ("may be a border
   and may not be a label") and I reached for it anyway because the token's own
   description names units. `--muted` is 4.96:1 and reads just as quiet. */
[data-shell="2"] #crmList .sc-unit {
  font-size: 14px;
  font-weight: 500;
  color: var(--muted);
}
[data-shell="2"] #crmList .sc-note { font-size: 12.5px; font-weight: 600; color: var(--muted); }

/* 🛑 THE PHONE SIZE IS ARITHMETIC, NOT TASTE — AND THE FIRST VERSION SHIPPED A
   WRONG NUMBER. At 390px each tile leaves 147px of content, and `RM 7,155,213.62`
   at 22px mono needs 153px. Because `.sc-value` is `white-space: nowrap`, it did
   not wrap — it CLIPPED, and the screen read "RM 7,155,213.6". A money figure that
   silently drops its last digit is not an ugly figure, it is a WRONG one, and it
   looks entirely deliberate.
   ⚠ NO GATE WE OWN CATCHES THIS. contrastcheck reads colour, layoutcheck reads
   whether content overflows its CONTAINER — and this overflowed the text's own box
   inside a container that never moved, so the page still measured 0px of sideways
   scroll. Only `scrollWidth > clientWidth` on the value itself sees it, and nothing
   was asking. A check now does (c103).
   THE SIZE IS DERIVED FROM THE WORST CASE, not from this month's biggest deal: the
   widest money string this app has ever broken a row with is `RM 5,610,845.75` —
   15 characters. In this mono face a character is ~0.6em, so 15 × 0.6 × F ≤ 147
   gives F ≤ 16.3px. 16px it is, which is also the phone base size, so it is the
   smallest this platform will set anything anyway. */
@media (max-width: 767px) {
  [data-shell="2"] #crmList .sc { padding: 12px 13px; }
  [data-shell="2"] #crmList .sc-value { font-size: 16px; }
  [data-shell="2"] #crmList .sc-unit { font-size: 11px; }
  [data-shell="2"] #crmList .sc-label { font-size: 11px; letter-spacing: 0.08em; }
}

/* ============================================================================
   RANGESLIDER + DATERANGEPICKER — the Enviromet Design System's form components,
   read off its own Forms specimen card. c99, 17 Aug 2026.
   ============================================================================ */

/* ---- RangeSlider: two handles on ONE track ------------------------------- */
/* ⚠ TWO STACKED <input type="range"> IS THE MECHANISM, and the trap is that the
   TOP one swallows every click across the whole track — including clicks meant
   for the lower handle. `pointer-events: none` on the input with `auto` on the
   THUMB only is what makes both handles reachable. Without it the lower handle
   is unmovable and the control looks broken in a way no assertion sees, because
   both inputs still hold correct values. */
[data-shell="2"] #crmList .rs { position: relative; width: 190px; height: 22px; }
[data-shell="2"] #crmList .rs-track {
  position: absolute; top: 9px; left: 0; right: 0; height: 4px;
  background: var(--row-band); border-radius: 999px;
}
[data-shell="2"] #crmList .rs-fill {
  position: absolute; top: 0; height: 100%;
  background: var(--primary); border-radius: 999px;
}
[data-shell="2"] #crmList .rs input[type="range"] {
  position: absolute; top: 0; left: 0; width: 100%; height: 22px; margin: 0;
  -webkit-appearance: none; appearance: none;
  background: transparent; pointer-events: none;
}
[data-shell="2"] #crmList .rs input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--primary);
  cursor: pointer; pointer-events: auto;
}
[data-shell="2"] #crmList .rs input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--primary);
  cursor: pointer; pointer-events: auto;
}
/* The readout is a MEASUREMENT, so it is mono and tabular like every other
   figure this platform prints. */
[data-shell="2"] #crmList .rs-read {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 12px; color: var(--muted); white-space: nowrap;
}
/* ---- DateRangePicker: ONE calendar, both ends ---------------------------- */
[data-shell="2"] #crmList .drp { position: relative; display: inline-block; }
[data-shell="2"] #crmList .drp-btn {
  font: inherit; font-size: 13px;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 12px; cursor: pointer; white-space: nowrap;
}
[data-shell="2"] #crmList .drp-btn:hover { background: var(--row-band); }
/* ⚠ z-index 60 puts it over the tiles and the stage band but UNDER the staging
   banner (300) and the drawer — the banner exists so nobody mistakes staging for
   production, so nothing on this screen may cover it. */
[data-shell="2"] #crmList .drp-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
  display: block; width: 268px; padding: 14px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
[data-shell="2"] #crmList .drp-pop[hidden] { display: none; }
[data-shell="2"] #crmList .drp-head {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;
}
[data-shell="2"] #crmList .drp-head strong { font-size: 14px; color: var(--text); }
[data-shell="2"] #crmList .drp-nav {
  border: 0; background: transparent; cursor: pointer;
  font-size: 17px; line-height: 1; color: var(--muted); padding: 4px 8px; border-radius: 6px;
}
[data-shell="2"] #crmList .drp-nav:hover { background: var(--row-band); color: var(--text); }
[data-shell="2"] #crmList .drp-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
}
[data-shell="2"] #crmList .drp-dow {
  font-size: 11px; color: var(--text-3); text-align: center; padding-bottom: 4px;
}
[data-shell="2"] #crmList .drp-cell {
  display: flex; align-items: center; justify-content: center;
  height: 30px; font-size: 12.5px; border-radius: 6px;
}
[data-shell="2"] #crmList .drp-day {
  border: 0; background: transparent; color: var(--text); cursor: pointer; font: inherit; font-size: 12.5px;
}
[data-shell="2"] #crmList .drp-day:hover { background: var(--row-band); }
/* The two ENDS are solid; the days between them take a quiet wash, so a range
   reads as one span rather than as two unrelated selected days. */
[data-shell="2"] #crmList .drp-day.drp-on { background: var(--primary); color: var(--on-primary); font-weight: 600; }
[data-shell="2"] #crmList .drp-day.drp-in { background: var(--accent-soft); }
[data-shell="2"] #crmList .drp-foot {
  display: flex; align-items: center; gap: 8px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
}
[data-shell="2"] #crmList .drp-end { flex: 1 1 0; min-width: 0; }
[data-shell="2"] #crmList .drp-arrow { color: var(--text-3); }
/* Leading AND trailing pad cells. The trailing ones are not optional: without
   them the row after the last day collapses and the grid's own gap shows through
   as an undivided block — found by looking at the Activity calendar, by no gate. */
[data-shell="2"] #crmList .drp-pad { background: transparent; }
/* A PREFIX unit (RM) sits tight against the figure and slightly raised; a SUFFIX
   unit (%, mm) rides the baseline after it. Same token, same size, opposite side. */
[data-shell="2"] #crmList .sc-value.sc-pre { gap: 4px; }
[data-shell="2"] #crmList .sc-value.sc-pre .sc-unit { align-self: flex-start; margin-top: 4px; }
[data-shell="2"] #crmList .sc-value:not(.sc-pre) .sc-unit { margin-left: -1px; }

/* ============================================================================
   THE FILTER BAR — one line at rest. c100, 17 Aug 2026.

   🛑 WHAT ACTUALLY FORCED THE OLD THREE-ROW WRAP WAS NOT THE NUMBER OF CONTROLS.
   It was the PIC `<select>`: a select sizes itself to its LONGEST OPTION, and the
   longest option is a full Malaysian name — so it grew to roughly 550px and shoved
   everything after it onto new lines. A control whose width is set by somebody's
   name is a layout nothing can hold. Every select in this bar is capped and
   ellipsises; the fix belongs on the control, not on the row.
   ============================================================================ */

[data-shell="2"] #crmList .fb-main { align-items: center; gap: 10px; row-gap: 8px; }
[data-shell="2"] #crmList .fb-main input[type="search"] { flex: 1 1 240px; min-width: 180px; max-width: 340px; }
/* The one thing here that CREATES rather than narrows, so it sits apart from
   everything else rather than in the queue with it. */
[data-shell="2"] #crmList .fb-new { margin-left: auto; }

[data-shell="2"] #crmList .fb { position: relative; display: inline-block; }
[data-shell="2"] #crmList .fb-btn {
  font: inherit; font-size: 13px;
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 12px; cursor: pointer; white-space: nowrap;
}
[data-shell="2"] #crmList .fb-btn:hover { background: var(--row-band); }
/* The count is the honest half of a fold-away panel: it says how much is on
   without being opened. Filled, because a number nobody notices is no count. */
[data-shell="2"] #crmList .fb-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  background: var(--primary); color: var(--on-primary);
  border-radius: 999px; font-size: 11px; font-weight: 700;
}
[data-shell="2"] #crmList .fb-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 70;
  display: grid; gap: 10px; width: 320px; padding: 16px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
[data-shell="2"] #crmList .fb-pop[hidden] { display: none; }
/* One grid for every row, so the labels line up down the panel without any row
   knowing about any other (D12). */
[data-shell="2"] #crmList .fb-row {
  display: grid; grid-template-columns: 54px minmax(0, 1fr);
  align-items: center; gap: 10px;
}
[data-shell="2"] #crmList .fb-lab { font-size: 12px; color: var(--muted); }
/* ⚠ minmax(0, 1fr) AND max-width TOGETHER. The grid track alone does not stop a
   select from reporting a huge min-content width off its longest option. */
[data-shell="2"] #crmList .fb-row select { width: 100%; max-width: 100%; }
[data-shell="2"] #crmList .fb-readout { min-height: 16px; }
[data-shell="2"] #crmList .fb-foot { display: flex; justify-content: flex-end; border-top: 1px solid var(--border); padding-top: 10px; }

/* ---- active-filter chips ------------------------------------------------- */
[data-shell="2"] #crmList .fb-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
[data-shell="2"] #crmList .fb-chips[hidden] { display: none; }
[data-shell="2"] #crmList .fb-chip { display: inline-flex; align-items: center; gap: 6px; }
[data-shell="2"] #crmList .fb-x {
  border: 0; background: transparent; cursor: pointer;
  color: var(--muted); font-size: 15px; line-height: 1; padding: 0 1px;
}
[data-shell="2"] #crmList .fb-x:hover { color: var(--text); }

/* On a phone the bar stacks and the create button returns to the flow — a
   right-pinned button beside a wrapped row lands in the middle of nowhere. */
@media (max-width: 767px) {
  [data-shell="2"] #crmList .fb-main input[type="search"] { flex: 1 1 100%; max-width: none; }
  [data-shell="2"] #crmList .fb-new { margin-left: 0; }
  [data-shell="2"] #crmList .fb-pop { width: min(320px, calc(100vw - 40px)); }
}

/* ---- PIC avatar: MOVED to styles.css, 25 Aug 2026 --------------------------
   The `.pcell`/`.pav`/`.pav-1..6`/`.pnm` rules that stood here (c101) now live in
   `styles.css`, UN-SCOPED, because the All Quotes register on the portal wears the
   same avatar (Shaf's ask) and only crm.html loads this file. Values moved
   byte-for-byte; the helpers moved the same way, `crm-list.js` → the shared
   `person-badge.js`. One definition, both pages (D20). */

/* ================================================================= MULTI-SELECT ==
   THE ENVIROMET DESIGN SYSTEM'S `Checkbox`, ADOPTED EXACTLY — and this time read
   from `components/forms/Checkbox.jsx` through DesignSync, not sampled off a
   specimen card. Its numbers, verbatim: 18x18 box, 4px radius (`--radius-xs`),
   1.5px border, tick at 3.5 stroke in bone, 10px gap to the label, 120ms fade on
   `cubic-bezier(.2,.6,.2,1)`.

   ⚠ ONE DELIBERATE DEPARTURE, AND IT IS A RECORDED DECISION NOT AN OVERSIGHT. The
   system fills a ticked box with its brand anchor `--emt-forest-800` `#1E3B2B`.
   This platform's brand is `--primary` `#1E3F3C` and doctrine says it never moves;
   whether the platform adopts the system's anchor is an open question waiting on
   Shaf. Using `--primary` keeps the two greens from being decided by a checkbox.

   ⚠ AND THE ONE THE SYSTEM CANNOT DECIDE FOR US: 18px is well under the 44px tap
   floor, and the deal list is on the phone standard (N7). It is not a conflict once
   you read what the component IS — a <label> wrapping the box AND its text, so the
   LABEL is the hit area. The box keeps 18px because that is the visual spec; the
   label gets 44px on a phone below. Copying the intent, not the measurement — the
   lesson the Scorecard's `RM` prefix earned the day before this.

   ⚠ SCOPED TO THE CLASS, NOT TO `#crmList`, AND THAT IS DELIBERATE. It was written
   `#crmList .dsx` first — and a component whose rules name one screen is one the
   next screen re-invents slightly differently, which is how this platform ended up
   with three background washes for one idea. It is also unrenderable in
   `patterns.html` without faking its context, so the library could only DESCRIBE it.
   Widening cost nothing today: `.dsx` is a brand-new class name and no other screen
   emits it, so the blast radius is identical and the next surface gets it free.
   ⚠ The rules that ARE list-specific — the 34px column, the picked-row tint, the
   action bar — stay on `#crmList`, because they are about this table rather than
   about a checkbox. */
[data-shell="2"] .dsx {
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; user-select: none;
  min-width: 22px; min-height: 22px;   /* desktop: the row is already 44px+ tall */
}
[data-shell="2"] .dsx-in {
  position: absolute; opacity: 0; width: 0; height: 0; margin: 0;
}
[data-shell="2"] .dsx-box {
  width: 18px; height: 18px; flex: none;
  border-radius: 4px;
  border: 1.5px solid var(--text-3);
  background: var(--panel);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background 120ms cubic-bezier(.2, .6, .2, 1),
              border-color 120ms cubic-bezier(.2, .6, .2, 1);
}
/* ⚠ THE BORDER IS `--text-3`, NOT `--border`. `--border` is the hairline BETWEEN
   rows and measures 1.27:1 against the panel — an unticked box drawn with it is a
   control with no visible edge at all, under the 3:1 floor a non-text component
   carrying meaning has to clear. That exact mistake has now been made twice in this
   stylesheet (the outline chip, then the inline editor), which is why it is written
   here beside the rule rather than trusted to memory. `--text-3` is 3.84:1 light /
   5.05:1 dark — the quietest token that clears it. */
[data-shell="2"] .dsx-in:checked + .dsx-box {
  background: var(--primary);
  border-color: var(--primary);
}
/* The tick is drawn, not an image: a background-image cannot follow the theme, and
   `currentColor` here is the bone the design system specifies. */
[data-shell="2"] .dsx-in:checked + .dsx-box::after {
  content: '';
  width: 10px; height: 6px;
  border-left: 2.5px solid var(--brand-cream);
  border-bottom: 2.5px solid var(--brand-cream);
  transform: rotate(-45deg) translate(1px, -1px);
}
/* The middle state the header box needs: some rows on screen ticked, not all. A bar
   rather than a tick, because a half-ticked box showing a tick is a control lying
   about what pressing it will do. */
[data-shell="2"] .dsx-in:indeterminate + .dsx-box {
  background: var(--primary); border-color: var(--primary);
}
[data-shell="2"] .dsx-in:indeterminate + .dsx-box::after {
  content: '';
  width: 9px; height: 0;
  border-bottom: 2.5px solid var(--brand-cream);
  transform: none;
}
[data-shell="2"] .dsx-in:focus-visible + .dsx-box {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
[data-shell="2"] #crmList .dl-selcol { width: 34px; text-align: center; padding-right: 0; }

/* A ticked row is marked, because a tick 34px away at the far left of a nine-column
   row is easy to lose track of when you are scanning the other end. Same tint the
   peeked row uses, deliberately: one list, one way of saying "this one". */
[data-shell="2"] #crmList tr.row-picked > td { background: var(--row-band); }

/* -------------------------------------------------------------- the action bar --
   Drawn only when something is ticked; `hidden` does the rest. */
/* 🛑 STICKY, AND THAT CAME FROM LOOKING AT THE SHOT RATHER THAN FROM ANY GATE
   (D14). The bar sits after the table, so on the six-row fixture it is plainly
   visible two inches under the rows — and on the REAL list of 79 deals it would be
   roughly two screens below the fold. You would tick three rows at the top and have
   no way of knowing a bar had appeared at all, which reads as the checkboxes doing
   nothing. Every gate was green over that: the element exists, it is not hidden, it
   overflows nothing, and its colours pass. D32's shape in the layout dimension —
   the fixture is too SHORT to show the failure.
   ⚠ `bottom: 0` and not a fixed overlay: fixed would cover the last row and would
   also sit under the staging banner's family of problems. Sticky keeps it in the
   panel it belongs to and lifts it only while there is something to say. */
[data-shell="2"] #crmList .ls-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  background: var(--row-band);
  position: sticky; bottom: 0; z-index: 2;
  box-shadow: 0 -2px 8px rgba(23, 45, 33, .10);
}
[data-shell="2"] #crmList .ls-said { color: var(--text); font-size: 13px; }
[data-shell="2"] #crmList .ls-acts {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto;
}
/* ⚠ CAPPED, NOT CONTENT-SIZED. A <select> sizes itself to its longest option, and
   that is what pushed the filter bar onto three rows yesterday — a control whose
   width is decided by the longest stage label is a layout nothing can hold. */
[data-shell="2"] #crmList .ls-stage { max-width: 170px; }
[data-shell="2"] #crmList .ls-date { max-width: 150px; }

@media (max-width: 768px) {
  /* THE 44px FLOOR, ON THE LABEL RATHER THAN THE BOX — see the header above. The
     box stays 18px; what a thumb has to hit is the label around it. */
  [data-shell="2"] .dsx { min-width: 44px; min-height: 44px; }
  /* ⚠ BOTH AXES, MEASURED — the deal record's phone sweep asserts min(width,height) over
     every control on the screen and its list has reached EMPTY, so a new button under the
     floor on EITHER axis reddens the build by name. `.btn-sm` is comfortably under 44 tall
     on its own, and "Fill it in" is short enough to be under 44 wide at the smallest step. */
  [data-shell="2"] #crmDeal .dg-go { min-height: 44px; min-width: 44px; }
  /* Below 768px the rows are stacked CARDS, so the select cell is not a labelled
     field — it is the card's own corner control, and it must not draw a "—" label
     the way an empty data cell does. */
  [data-shell="2"] #crmList .dl-selcol { width: auto; text-align: left; }
  [data-shell="2"] #crmList .ls-acts { margin-left: 0; width: 100%; }
}

/* ================================================================ MULTI-SELECT ==
   The control the platform did not have. SHAPE adopted from the Enviromet Design
   System's `Combobox` (read through DesignSync, not sampled): 38px minimum field,
   6px inner padding, 6px gap, chips at 12.5px/600 in a pill, a menu 4px below
   capped at 200px, 13.5px options, a 14px chevron that rotates, 120ms on
   cubic-bezier(.2, .6, .2, 1).

   ⚠ SCOPED TO THE CLASS, NOT TO A SCREEN. `.msel` is a brand-new class name that no
   other surface emits, so widening costs nothing today and the next screen gets it
   free — the same reasoning as the checkbox. A component whose rules name one screen
   is one the next screen re-invents slightly differently.

   ⚠ THREE DEPARTURES FROM THE SYSTEM, EACH MEASURED:
   1. The field edge is `--text-3` (3.84:1 light / 5.05:1 dark), NOT `--border`
      (1.27:1) — a separator token reused as a component edge is a control with no
      visible box, under the 3:1 floor. This file has paid for that twice already.
   2. The system's fixed `width: 280` is dropped. A constant-width control cannot sit
      in `.if-grid`, and a control sized by its longest option is what pushed the
      filter bar onto three rows on 17 Aug.
   3. 44px on a phone. The design system has no phone story; N7 does. */
[data-shell="2"] .msel { position: relative; display: block; }

[data-shell="2"] .msel-field {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  min-height: 38px; padding: 6px 32px 6px 10px;
  background: var(--panel);
  border: 1px solid var(--text-3);
  border-radius: 6px;
  cursor: text;
  transition: border-color 120ms cubic-bezier(.2, .6, .2, 1),
              box-shadow 120ms cubic-bezier(.2, .6, .2, 1);
}
[data-shell="2"] .msel-field.msel-open {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 28%, transparent);
}

/* The chip. `--accent-soft` is the ONE soft fill this platform has; inventing a
   second tint for a new component is how three washes for one idea happened. */
[data-shell="2"] .msel-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px; border-radius: 999px;
  background: var(--accent-soft); color: var(--primary);
  font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
[data-shell="2"] .msel-x {
  border: 0; background: none; padding: 0; cursor: pointer;
  color: inherit; font-size: 15px; line-height: 1;
  min-width: 16px;
}
[data-shell="2"] .msel-x:hover { color: var(--bad-text); }

/* ⚠ `min-width: 0` and `flex: 1 1 0`, both load-bearing: an input floors at roughly
   min-content, so `flex-basis` alone is ignored and the field grows past its grid
   track the moment somebody types. The recorded fix for exactly this. */
[data-shell="2"] .msel-in {
  flex: 1 1 0; min-width: 60px; width: 0;
  border: 0; outline: 0; background: none; padding: 2px 0;
  font: inherit; font-size: 14px; color: var(--text);
}

/* 🛑 `--muted`, NOT `--text-3`, AND THIS IS THE THIRD TIME THIS PLATFORM HAS PAID FOR IT.
   The design system paints its Combobox chevron `--emt-ink-3`, whose platform counterpart is
   `--text-3` — and `--text-3` AS TEXT on `--panel` measures 3.84:1, under the 4.5 floor. It
   is a legal BORDER token and an illegal LABEL one, which is written in `emt-ui` in those
   words, and I reached for it anyway because the system's own chevron uses its equivalent.
   The Scorecard's unit made the identical mistake on 16 Aug for the identical reason.
   ⚠ THE GATE CAUGHT IT: contrastcheck went 13 → 14 and named `span.msel-chev`. NOT
   re-baselined — the number went back to 13. `--muted` is 4.96 light / 7.11 dark.
   ⚠ And the chevron IS text (a ▾ glyph), not a border, even though it is decorative and
   aria-hidden. What a gate measures is what it is, not what it is for. */
[data-shell="2"] .msel-chev {
  position: absolute; right: 11px; top: 10px;
  color: var(--muted); font-size: 11px; pointer-events: none;
  transition: transform 120ms cubic-bezier(.2, .6, .2, 1);
}
[data-shell="2"] .msel-chev.msel-up { transform: rotate(180deg); }

[data-shell="2"] .msel-menu {
  position: absolute; top: 100%; left: 0; right: 0; margin-top: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(23, 45, 33, .10), 0 1px 2px rgba(23, 45, 33, .06);
  max-height: 200px; overflow-y: auto;
  /* Above the rows it covers, below the staging banner (300) and any overlay. */
  z-index: 40;
}
[data-shell="2"] .msel-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 12px; font-size: 13.5px; color: var(--text); cursor: pointer;
}
[data-shell="2"] .msel-opt:hover { background: var(--row-band); }
[data-shell="2"] .msel-opt.on { background: var(--accent-soft); color: var(--primary); font-weight: 600; }

/* Drawn, not an image: a background-image cannot flip with the theme. */
[data-shell="2"] .msel-tick {
  flex: 0 0 auto; width: 9px; height: 5px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg) translate(1px, -1px);
}

/* ⚠ It says what would HELP, not merely that nothing matched. The design system's own
   empty state is the bare words "No matches", which tells nobody what to do next. */
[data-shell="2"] .msel-none {
  padding: 10px 12px; font-size: 13px; color: var(--muted);
}

@media (max-width: 768px) {
  /* N7's tap floor, on the field a thumb actually hits. */
  [data-shell="2"] .msel-field { min-height: 44px; }
  /* And on the chip's remove button, which is the smallest thing here — 16px of
     target inside a 22px chip is the collapse the phone rules already warn about. */
  [data-shell="2"] .msel-x { min-width: 28px; min-height: 28px; }
  /* A 200px menu on a phone is most of the screen; slightly shorter keeps the field
     it belongs to visible above it, so the chips stay readable while choosing. */
  [data-shell="2"] .msel-menu { max-height: 168px; }
}

/* ---- The quote portal's status + account controls, inside the shell bar --------
   Added 22 Aug 2026 with "one shell and we call it CRM". These controls used to sit
   in the portal's own top bar; `shell-nav.js` moves the block into `.sb-bar` at mount,
   which makes it a FLEX ITEM for the first time — and a flex item's default minimum is
   its own min-content, so at 390px it refused to shrink and pushed 77px past the right
   edge with `Log out` hanging off it. Measured, not guessed (D14/D21).
   `min-width: 0` is the whole fix; the wrap rules `.topbar-meta` already carries then
   do their job. */
[data-shell="2"] .sb-bar { flex-wrap: wrap; }
[data-shell="2"] .sb-bar > .topbar-meta {
  margin-left: auto;
  /* ⚠ BOTH OF THESE, AND NEITHER IS ENOUGH ALONE — measured, not reasoned (D33).
     `min-width: 0` lifts the flex item's default min-content floor; `flex-shrink: 1`
     overrides the `flex: 0 0 auto` a narrow-width rule in styles.css sets on this
     class, which pins it at max-content. With only the first, the block still measured
     377px inside a 358px bar at 390px and pushed the page 3px sideways — small enough
     to look like rounding and real enough to fail the gate. */
  min-width: 0;
  flex: 1 1 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}
