/* NDP MP Portal — shared styles. Brand palette matches the guides index.

   ⚠⚠ THE GROUND IS A COOL NEUTRAL, NOT CREAM (Eric, 2026-08-11: "there's too
   much cream happening... the background behind the white bubbles is not
   cream"). Taken from the Zoho client record he shared, then pulled back once
   because the first pass "leaned a little too blue".

   ⚠ CREAM IS NOT DELETED — IT IS DEMOTED. `--cream` still names the same two
   roles it always did: the page ground AND the ink on navy. Re-pointing the one
   token moved both correctly, which is why this is a palette change and not a
   token split — every existing `var(--cream)` site keeps working in whichever
   role it was playing. The topbar, the rail and the orange/teal brand stripe
   are untouched.

   ⚠ THE NEUTRALS HAD TO MOVE WITH IT, and they were the real problem. The
   ground was only 20 points of red-over-blue; `--line` was 23 and the muted
   greys up to 24. Warm hairlines and warm grey text on a cool ground read as a
   colour mistake rather than a choice. Every near-neutral in portal chrome was
   moved by the same rule (mirror the channel spread, at 0.3 of its original
   width) so the whole family stays coherent.

   ⚠⚠ SEMANTIC TINTS DID NOT MOVE, and the rule that spared them is HUE, not
   saturation: the error background #fdecec has LESS chroma than cream #f4f1e0,
   so a saturation threshold would have greyed out the error state and kept the
   cream. What separates them is hue — creams and olive greys sit at 40-110
   degrees, every red / amber / orange / pink tint sits outside that. Those
   carry meaning and stayed exactly as they were.
   ⚠ One consequence worth knowing: a light warm tint whose hue lands inside
   40-110 was moved with the neutrals. If a pale-yellow status chip ever looks
   wrong, that is where it went. */
:root {
  --navy: #0D1529;
  --cream: #EEF1F4;
  --orange: #E16C35;
  --teal: #5BB6BC;
  --ink: #0D1529;
  --muted: #686A6C;
  --line: #DCE0E4;
  --white: #fff;
  /* aliases used by the ported dashboard components */
  --blue-black: #0D1529;
  --border: #DCE0E4;
  --bg: #e7eaed;
  --green: #2e7d4f;
  --shadow: 0 12px 40px rgba(13,21,41,.12);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--cream); color: var(--ink); line-height: 1.55;
  min-height: 100vh;
}

/* Top bar */
.topbar { background: var(--navy); position: relative; }
.topbar::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 4px;
  background: linear-gradient(to right, var(--orange) 50%, var(--teal) 50%); }
.topbar-inner { max-width: 760px; margin: 0 auto; padding: 16px 24px; display: flex;
  align-items: center; justify-content: space-between; gap: 16px; }
.brand { color: var(--cream); font-weight: 900; letter-spacing: -.02em; font-size: 18px; }
.brand .accent { color: var(--teal); }
a.brand { text-decoration: none; cursor: pointer; }
.topbar a.brand { color: var(--cream); }            /* keep brand cream even as a link */
.topbar a.brand:hover { color: var(--cream); opacity: .92; }
.topbar a { color: rgba(238,241,244,.78); text-decoration: none; font-size: 13px; font-weight: 600; }
.topbar a:hover { color: var(--cream); }
.topbar .sep { color: rgba(238,241,244,.3); margin: 0 10px; }

/* Layout */
.wrap { max-width: 760px; margin: 0 auto; padding: 32px 24px 64px; }
.label { color: var(--orange); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; font-weight: 600; margin-bottom: 8px; }
h1 { font-size: 28px; font-weight: 900; letter-spacing: -.02em; }
h2 { font-size: 18px; font-weight: 800; margin: 28px 0 12px; }
.sub { color: var(--muted); font-size: 15px; margin-top: 6px; }

/* Auth card (login / set-password) */
.center { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.auth-card { background: var(--white); border-radius: 14px; padding: 32px; width: 100%; max-width: 400px;
  box-shadow: 0 12px 40px rgba(13,21,41,.12); }
.auth-card .label { text-align: center; }
.auth-card h1 { font-size: 24px; text-align: center; }
.auth-card .sub { text-align: center; margin-bottom: 22px; }

/* Cards / grid */
.card { display: block; background: var(--white); border-radius: 12px; padding: 20px 22px; margin-bottom: 14px;
  text-decoration: none; color: inherit; box-shadow: 0 8px 28px rgba(13,21,41,.07);
  transition: transform .15s, box-shadow .15s; border: 1px solid transparent; }
a.card:hover { transform: translateY(-2px); box-shadow: 0 12px 34px rgba(13,21,41,.12); }
.card-title { display: block; font-size: 17px; font-weight: 700; }
.card-blurb { display: block; color: var(--muted); font-size: 14px; margin-top: 4px; }
.card-go { display: inline-block; color: var(--orange); font-weight: 600; font-size: 14px; margin-top: 10px; }
.card-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.gstatus { font-size: 11px; font-weight: 700; padding: 3px 11px; border-radius: 999px; white-space: nowrap; }
.gstatus.complete { background: #eaf6f0; color: #1e7a4d; }       /* green */
.gstatus.notstarted { background: #e9ebed; color: #838689; }     /* gray */

/* Forms */
label.field { display: block; font-size: 13px; font-weight: 600; color: var(--ink); margin: 14px 0 6px; }
input, select { width: 100%; font: inherit; padding: 11px 13px; border: 1px solid var(--line);
  border-radius: 9px; background: #f9fafb; color: var(--ink); }
input:focus, select:focus { outline: none; border-color: var(--teal); box-shadow: 0 0 0 3px rgba(91,182,188,.18); }

/* ⚠⚠ A TICK BOX IS NOT A TEXT FIELD, and the bare `input` rule above hits it.
   `width: 100%` + `padding: 11px 13px` + `border-radius: 9px` + a grey background turn a native
   checkbox into a small grey CIRCLE — squashed by the padding, rounded to nothing by the radius,
   and with the tick invisible against #f9fafb. It renders exactly like a control that failed to
   load, which is what /admin/raw-media's Issue Log selection column had become.
   ⚠ The fix is here, ONCE, and not per page: `.fd-pick input` in the filter drawer had already
   patched around it locally with its own width/height, and every new checkbox anywhere in the
   portal was going to have to rediscover the same three-property workaround.
   ⚠ Everything is reset to the browser default rather than restyled — `accent-color` is the ONE
   opinion, so the tick is NDP orange instead of Chrome blue. No `appearance: none`: an
   appearance-less box has to be redrawn by hand (indeterminate state, focus ring, the tick
   itself), and half-redrawing it is how it ended up looking broken in the first place. */
input[type="checkbox"], input[type="radio"] {
  width: 16px; height: 16px; flex: none; padding: 0; border: 0; border-radius: 0;
  background: none; accent-color: var(--orange); vertical-align: middle; }
.hint { font-size: 12px; color: var(--muted); margin-top: 5px; }

.btn { display: inline-block; width: 100%; font: inherit; font-weight: 700; cursor: pointer;
  background: var(--orange); color: #fff; border: 0; border-radius: 9px; padding: 12px 16px; margin-top: 20px;
  transition: filter .15s; }
.btn:hover { filter: brightness(1.05); }
.btn:disabled { opacity: .55; cursor: default; }
.btn.secondary { background: var(--navy); }
.btn.ghost { background: transparent; color: var(--navy); border: 1px solid var(--line); }
.btn.inline { width: auto; margin-top: 0; padding: 9px 16px; font-size: 14px; }

/* Messages */
.msg { padding: 11px 14px; border-radius: 9px; font-size: 14px; margin-top: 16px; display: none; }
.msg.show { display: block; }
.msg.error { background: #fdecec; color: #9b2c2c; border: 1px solid #f5c6c6; }
.msg.ok { background: #eaf6f0; color: #1e7a4d; border: 1px solid #bfe6d2; }
.msg.info { background: #eef6f7; color: #2a6f74; border: 1px solid #cfe7e9; }

/* Copyable invite link */
.copybox { display: flex; gap: 8px; margin-top: 10px; }
.copybox input { background: #f9fafb; font-size: 13px; }

/* Users table */
table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 14px; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); }
th { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
.pill { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 9px; border-radius: 999px; }
.pill.admin { background: #2a2f45; color: #fff; }
/* ⚠ Every role in shared/types/accounts.ts needs a rule here. A role with none falls back to
   the bare .pill — no background at all — which reads as the label failing to load rather than
   as a role nobody has styled yet. Operations sits in the Blue Black family: it is the closest
   role to admin, and the teal/orange pair is already spoken for by the two media-pro roles. */
.pill.operations { background: #e2e8f5; color: #35507f; }
.pill.media_pro { background: #e7f1f2; color: #2a6f74; }
.pill.lead_mp { background: #fbe3d5; color: #a2451a; }
.pill.pending { background: #fbeede; color: #a85a1f; }
.pill.active { background: #eaf6f0; color: #1e7a4d; }
/* ⚠ NOT a role — `profiles.platform_admin` is a second axis and renders BESIDE a role pill, so it
   must not look like one more member of the set above. Outlined rather than filled: two solid
   pills side by side read as two competing roles, and this one is a qualifier on the first. */
.pill.platform { background: transparent; color: #2a2f45; box-shadow: inset 0 0 0 1.5px #2a2f45; }

/* Performance view — ported from the dashboard's MP components */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px; }
.stat { padding: 16px 18px; }
.stat .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; font-weight: 700; }
.stat .value { font-size: 26px; font-weight: 800; letter-spacing: -.01em; margin-top: 4px; }
.stat .hint { font-size: 12px; color: var(--muted); margin-top: 3px; }
.stat-link { position: relative; }
.stat-arrow { position: absolute; top: 14px; right: 16px; color: var(--muted); opacity: .45; font-weight: 700; }

.section-title { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; margin-bottom: 10px; }
.backlink { display: inline-block; color: var(--teal); text-decoration: none; font-weight: 600; font-size: 14px; margin-bottom: 14px; }
.backlink:hover { text-decoration: underline; }
.note { background: #eef6f7; color: #2a6f74; border: 1px solid #cfe7e9; border-radius: 9px; padding: 11px 14px; font-size: 14px; margin-bottom: 16px; }

/* Pillar batteries */
.batt { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.batt-svg { position: relative; }
.batt-tip { display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px;
  width: 190px; background: var(--navy); color: #fff; font-size: 12px; line-height: 1.4; padding: 8px 10px; border-radius: 8px;
  box-shadow: var(--shadow); z-index: 10; text-align: center; pointer-events: none; }
/* ⚠ CHILD combinator, not descendant: the tier battery nests a .batt-tip inside every
   .batt-hot, and the old `.batt-svg:hover .batt-tip` showed ALL of them at once. */
.batt-svg:hover > .batt-tip, .batt-svg:focus > .batt-tip { display: block; }
/* Per-cube hotspots (the Product Competency battery): each row reveals its own tip.
   Full battery width so the labels beside a cube count as its row; :focus is the
   tap and keyboard path, same as the battery-level tip above. */
.batt-hot { position: absolute; left: 0; width: 100%; z-index: 5; }
.batt-hot:hover .batt-tip, .batt-hot:focus .batt-tip { display: block; }

/* Reviews + order feedback */
.comment { padding: 12px 0; border-bottom: 1px solid var(--line); }
.comment:last-child { border-bottom: 0; }
.comment .meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13px; color: var(--muted); }
.stars { color: var(--orange); letter-spacing: 1px; }
.mention-tag { font-size: 10px; font-weight: 700; color: #2a6f74; background: #e7f1f2; border-radius: 999px; padding: 1px 7px; letter-spacing: .04em; }
.crit { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 999px; }
.crit.great { background: rgba(46,125,79,.12); color: var(--green); }
.crit.notgreat { background: rgba(225,108,53,.14); color: var(--orange); }

/* "How it works" explainer */
.hiw-item { padding: 14px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.hiw-item:last-child { border-bottom: 0; padding-bottom: 0; }
.hiw-item p { margin: 0 0 6px; }
.hiw-h { font-weight: 800; font-size: 15px; margin-bottom: 6px; display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.hiw-max { font-size: 12px; font-weight: 700; color: var(--orange); white-space: nowrap; }
.hiw-list { margin: 6px 0 10px 20px; }
.hiw-list li { margin: 2px 0; }

/* Toolbars / filters on drill-down pages */
.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
.select { font: inherit; padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: #f9fafb; color: var(--ink); }
.btn.sm { width: auto; margin: 0 6px 6px 0; padding: 6px 12px; font-size: 13px; }
td.right, th.right { text-align: right; }
.coming-soon { color: #adb0b3; }

.muted { color: var(--muted); }
.spinner { color: var(--muted); font-size: 14px; padding: 20px 0; display: flex; align-items: center; gap: 10px; }
.spinner::before { content: ''; width: 18px; height: 18px; flex: none; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--orange); animation: ndp-spin .8s linear infinite; }
@keyframes ndp-spin { to { transform: rotate(360deg); } }

/* ═══ Skeletons ═══════════════════════════════════════════════════════════════
   A spinner says "wait"; a skeleton says "this is what is coming". Because it is the same
   shape and height as the real thing, nothing JUMPS when the content lands — which is most
   of what reads as a page loading badly. Used where a whole surface is being filled for the
   first time (the conversation list, a thread's messages). A spinner is still right for a
   small in-place action, where there is no shape to promise.
   ⚠ The shimmer is decoration: it is switched off under prefers-reduced-motion, and the
   placeholder still reads correctly as "content is coming" without it. */
.skel { position: relative; overflow: hidden; background: var(--line); border-radius: 8px; }
.skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent);
  animation: ndp-shimmer 1.4s ease-in-out infinite; }
@keyframes ndp-shimmer { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }
/* One skeleton conversation row: the same 12px radius, padding and shadow as a real .trow,
   so the list does not resize under you when the real rows replace it. */
.skelrow { display: flex; align-items: center; gap: 10px; padding: 9px 12px; background: var(--white);
  border-radius: 12px; margin-bottom: 10px; box-shadow: 0 4px 14px rgba(13,21,41,.06);
  border: 1px solid transparent; }
.skelrow .skel-av { width: 28px; height: 28px; border-radius: 8px; flex: none; }
.skelrow .skel-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.skelrow .skel-l1 { height: 11px; width: 42%; }
.skelrow .skel-l2 { height: 10px; width: 72%; }
.skelrow .skel-time { width: 42px; height: 10px; flex: none; }
/* Skeleton message bubbles, alternating sides like a conversation does. */
.skelbub { height: 34px; border-radius: 14px; margin-bottom: 8px; max-width: 78%; }
.skelbub.out { align-self: flex-end; border-bottom-right-radius: 4px; }
.skelbub.in { align-self: flex-start; border-bottom-left-radius: 4px; }
footer { color: var(--muted); font-size: 12px; text-align: center; padding: 24px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 160px; }

/* ═══ Shared view chrome — the Notion-style view bar, its sheets, and the filter/sort
   drawer. Lives here, not in a page, because /admin/candidates and /admin/raw-media
   both wear it and two copies would drift within a week. Page-specific bits (kanban
   cards, the candidate table) stay in their own <style>. */

  /* The view tabs live inside .vbar's left half. Underline-style, one stroke icon each; the
     labels stay at every width and the strip scrolls sideways rather than dropping them. */
  .tabstrip { display: flex; align-items: flex-end; gap: 8px; flex: 1; min-width: 0; overflow-x: auto;
    scrollbar-width: none; -ms-overflow-style: none; }
  .tabstrip::-webkit-scrollbar { display: none; }
  .tabstrip button { display: inline-flex; align-items: center; gap: 7px; font: inherit; font-size: 14px;
    font-weight: 700; border: 0; background: transparent; white-space: nowrap;
    color: var(--muted); padding: 8px 12px; cursor: pointer; border-bottom: 2px solid transparent;
    margin-bottom: -1px; border-radius: 6px 6px 0 0; }
  .tabstrip button:hover { color: var(--ink); background: #eff1f3; }
  .tabstrip button.on { color: var(--ink); border-bottom-color: var(--orange); }
  .tabstrip button svg { display: block; flex: none; color: #a9a390; }
  /* Hairline between the built-in layouts and your saved views. */
  .tabstrip .tabsep { flex: none; width: 1px; align-self: center; height: 16px; background: var(--line); margin: 0 4px; }
  /* Drag a tab to reorder it (desktop only — HTML5 DnD has no touch equivalent, so the sheet
     carries move up/down as well). `grab` on hover is the only affordance there is space for
     in a strip this dense; the cursor IS the hint. */
  .tabstrip button[draggable="true"] { cursor: grab; }
  .tabstrip button.dragging { opacity: .4; cursor: grabbing; }
  /* The insertion mark. A vertical rule, because this strip is horizontal — the board's
     `.drop-line` is the same idea rotated. `flex: none` or it collapses to nothing between
     two flex siblings. */
  /* ⚠ The VERTICAL drop line (the client profile's order-column picker). It is a separate rule
     rather than a tweak to the one below because that one is scoped to `.tabstrip`, and the
     picker is not one — `wireTabReorder` now takes an axis, so the indicator needs both forms.
     `flex-basis: 100%` makes it take a row of its own in a column flex container. */
  .tab-drop.vert { flex: 0 0 100%; height: 2px; margin: 1px 0; border-radius: 2px;
    background: var(--orange); }
  .tabstrip .tab-drop { flex: none; width: 2px; align-self: stretch; margin: 6px 1px;
    border-radius: 2px; background: var(--orange); }
  .tabstrip button.on svg { color: var(--orange); }


  /* ── Notion-style view bar ──────────────────────────────────────────────────
     One row: where you are on the left, the controls that act on it on the right.
     Desktop keeps the named tabs; ≤640px they collapse into a single pill that opens the
     view list as a sheet, which is what Notion does and what makes the row fit a phone
     without shouting. The bar lives ABOVE #view and is never re-rendered by a filter
     change, so typing in the search box keeps focus. */
  .vbar { display: flex; align-items: center; gap: 8px; margin: 22px 0 10px;
    border-bottom: 1px solid var(--line); }
  .vb-left { display: flex; align-items: stretch; flex: 1; min-width: 0; }
  .vb-right { display: flex; align-items: center; gap: 4px; flex: none; padding-bottom: 6px; }
  .viewpill { display: none; align-items: center; gap: 7px; font: inherit; font-size: 14px;
    font-weight: 700; color: var(--ink); background: #ebeef1; border: 0; border-radius: 8px;
    padding: 6px 10px; margin-bottom: 6px; cursor: pointer; max-width: 100%; }
  .viewpill:hover { background: #e3e6e9; }
  .viewpill .vp-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .viewpill svg { display: block; flex: none; }
  /* A SOURCE pill is not the tabs' mobile stand-in — it names which dataset the tabs are
     filtering (the shared inbox's texts / atlanta@ / atlmediapro@), so it stays on screen at
     every width. Same shape as .viewpill, minus the display:none it inherits by default. */
  .srcpill { display: inline-flex; align-self: center; margin-right: 10px; max-width: 190px; }
  .srcpill svg:first-child { color: var(--muted); }
  /* Two pills side by side need to be SHRINKABLE, or the pair overflows .vb-left and slides
     under the search/filter buttons — measured at 390px, a 38px overlap. min-width:0 is what
     lets the ellipsis in .vp-name actually engage. */
  .vb-left > .viewpill { flex: 0 1 auto; min-width: 0; }
  /* A source pill can carry two labels; the wide one wins wherever there is room. */
  .srcpill .vp-tiny { display: none; }
  /* Quiet square icon buttons — the Notion treatment: no chrome until you touch them. */
  .vbtn { box-sizing: border-box; display: inline-grid; place-items: center; width: 30px; height: 30px;
    border: 1px solid transparent; border-radius: 7px; background: none; color: var(--muted);
    cursor: pointer; touch-action: manipulation; }
  .vbtn:hover { background: #e9ecef; color: var(--ink); }
  .vbtn.on { color: var(--orange); background: #fbf0e8; }
  .vbtn svg { display: block; }
  /* Search grows out of its icon rather than sitting there as a permanent box. */
  .vsearch { width: 0; opacity: 0; padding: 0; border: 0; margin: 0; transition: width .16s ease-out, opacity .12s;
    font: inherit; font-size: 13.5px; background: none; color: var(--ink); min-width: 0; }
  .vsearch.open { width: 190px; opacity: 1; padding: 0 6px; }
  .vsearch:focus { outline: none; }
  .vsearch::placeholder { color: var(--muted); }
  /* The split button: + makes a candidate, the chevron opens view settings. */
  .vsplit { display: flex; align-items: stretch; margin-left: 4px; border-radius: 8px; overflow: hidden; }
  .vsplit button { box-sizing: border-box; display: inline-grid; place-items: center; width: 34px; height: 30px;
    border: 0; background: var(--navy); color: var(--cream); cursor: pointer; touch-action: manipulation; }
  .vsplit button:hover { background: #1b2b4d; }
  .vsplit button + button { border-left: 1px solid rgba(238,241,244,.24); width: 28px; }
  .vsplit svg { display: block; }
  .fcountline { font-size: 12px; color: var(--muted); margin: 0 0 12px; min-height: 1px; }
  /* A button that reads as a link — Clear, Load older, Try again. It lived in two page files
     and was MISSING from a third, where "Clear" rendered as a raw browser button; a control
     that appears on three pages belongs here once. */
  .link-btn { border: 0; background: none; color: var(--orange); cursor: pointer; font: inherit;
    font-size: 13px; font-weight: 700; padding: 0; }
  .link-btn:hover { text-decoration: underline; }
  @media (max-width: 640px) {
    .vbar { margin-top: 14px; }
    .vb-left .tabstrip { display: none; }
    .viewpill { display: inline-flex; }
    /* The source pill carries an email address, the view pill carries "Open" — so on a phone
       the room goes to the source and the view keeps only what it needs. */
    .srcpill { max-width: 58%; margin-right: 6px; }
    .srcpill .vp-wide { display: none; }
    .srcpill .vp-tiny { display: inline; }
    .vb-left > .viewpill:not(.srcpill) { flex: 0 0 auto; }
    .vsearch.open { width: 110px; }
  }

  /* ── Sheets (settings / group / conditional colour / view picker) ───────────
     ONE component, two shapes: an anchored 320px popover on a desktop, a bottom sheet with a
     grabber + scrim ≤900px. Same rule as the filter drawer — a phone has no room to push. */
  .nscrim { position: fixed; inset: 0; background: rgba(13,21,41,.34); z-index: 68; display: none; }
  .nscrim.show { display: block; }
  .nsheet { position: fixed; z-index: 70; width: 330px; max-height: 78vh; overflow: auto;
    background: var(--white); border: 1px solid var(--line); border-radius: 12px;
    box-shadow: 0 18px 48px rgba(13,21,41,.22); padding: 6px; display: none; }
  .nsheet.show { display: block; }
  .ns-head { display: flex; align-items: center; gap: 8px; padding: 8px 8px 10px; }
  .ns-head .ns-t { flex: 1; text-align: center; font-size: 13px; font-weight: 800; }
  .ns-back, .ns-x { display: inline-grid; place-items: center; width: 26px; height: 26px; flex: none;
    border: 0; background: none; color: var(--muted); border-radius: 6px; cursor: pointer; }
  .ns-back:hover, .ns-x:hover { background: #e9ecef; color: var(--ink); }
  .ns-back[hidden], .ns-x[hidden] { display: none; }
  .ns-group { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin-bottom: 8px; }
  .ns-lbl { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
    color: var(--muted); padding: 4px 10px 6px; }
  /* A read-only facts grid inside a sheet screen — counts and timestamps that explain what a
     button above them will do. ⚠ Deliberately NOT .ns-row: these are not pressable, and giving
     them the row's hover and cursor would read as controls that do nothing. */
  .ns-facts { padding: 2px 2px 8px; }
  .ns-fact { display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    font-size: 12.5px; color: var(--muted); padding: 3px 10px; }
  .ns-fact b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
  .ns-note { font-size: 12px; line-height: 1.5; color: var(--muted); padding: 2px 10px 8px; }
  .ns-note code { font-size: 11.5px; background: #e9ecef; border-radius: 4px; padding: 1px 4px; }
  .ns-note.err { color: var(--orange); }
  /* ⚠ Which markets a sync will match. The chips are the one thing on that screen a human can
     check against "is mine in the list" — a count cannot be checked. */
  .ns-mkt { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; padding: 2px 10px 10px; }
  .ns-mkt > span { font-size: 12px; color: var(--muted); margin-right: 2px; }
  .ns-mkt .mk { font-style: normal; font-size: 11.5px; font-weight: 700; background: #eef6f7;
    color: #2a6f74; border: 1px solid #cfe7e9; border-radius: 999px; padding: 2px 9px;
    white-space: nowrap; }
  /* ⚠ A RETIRED name is still mirrored, so it must be visible — but it is an old name of a
     renamed market, and rendering it like a live one puts a market nobody recognises beside the
     real ones. Quiet, not hidden. */
  .ns-mkt .mk.gone { background: none; color: var(--muted); border-color: var(--line);
    border-style: dashed; font-weight: 500; }
  .ns-row { display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;
    font: inherit; font-size: 13.5px; text-align: left; color: var(--ink); background: var(--white);
    border: 0; border-bottom: 1px solid var(--line); padding: 10px 12px; cursor: pointer; }
  .ns-row:last-child { border-bottom: 0; }
  .ns-row:hover { background: #f6f8fa; }
  .ns-row[disabled] { color: var(--muted); cursor: default; }
  .ns-row[disabled]:hover { background: var(--white); }
  .ns-row .ns-i { flex: none; display: grid; place-items: center; color: var(--muted); }
  .ns-row .ns-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* A row carrying an explanation stops being one line, so the truncation has to come off the
     label and the row has to grow. */
  .ns-row.has-sub { align-items: flex-start; }
  .ns-row.has-sub .ns-n { white-space: normal; overflow: visible; text-overflow: clip; }
  .ns-row .ns-s { display: block; margin-top: 3px; font-size: 11.5px; font-weight: 500;
    line-height: 1.4; color: var(--muted); white-space: normal; }
  /* ⚠ The slot is always there, marked or not — a group whose rows shift sideways as you pick
     between them reads as the layout twitching. Hidden by opacity, not display. */
  .ns-row .ns-tick { flex: none; display: block; color: var(--teal); opacity: 0; }
  .ns-row .ns-tick.on { opacity: 1; }
  .ns-row .ns-v { flex: none; font-size: 12.5px; color: var(--muted); max-width: 46%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ns-row .ns-c { flex: none; color: #b9b3a0; display: grid; place-items: center; }
  .ns-row.on .ns-n { font-weight: 800; }
  .ns-row.on .ns-i { color: var(--orange); }
  /* An eye that toggles, and a drag dot column — Notion's group list shape. */
  .ns-eye { flex: none; border: 0; background: none; padding: 2px; color: var(--muted); cursor: pointer;
    display: grid; place-items: center; border-radius: 5px; }
  .ns-eye:hover { background: #e9ecef; color: var(--ink); }
  .ns-eye.off { color: #cdc7b4; }
  .ns-sw { flex: none; width: 38px; height: 22px; border-radius: 999px; background: #d4d8dc; position: relative;
    border: 0; cursor: pointer; transition: background .14s; }
  .ns-sw::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
    border-radius: 50%; background: #fff; transition: transform .14s; box-shadow: 0 1px 3px rgba(0,0,0,.2); }
  .ns-sw.on { background: var(--orange); }
  .ns-sw.on::after { transform: translateX(16px); }
  .ns-empty { padding: 14px 12px; font-size: 12.5px; color: var(--muted); }
  .ns-add { display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; font: inherit;
    font-size: 13px; font-weight: 700; color: var(--orange); background: none; border: 0; padding: 10px 12px;
    cursor: pointer; }
  .ns-add:hover { background: #f6f8fa; }
  .ns-field { padding: 8px 12px 12px; }
  .ns-field label { display: block; font-size: 11.5px; font-weight: 600; color: var(--muted); margin: 8px 0 3px; }
  .ns-field select, .ns-field input { width: 100%; box-sizing: border-box; font: inherit; font-size: 13px;
    padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; background: #f9fafb; }
  /* The colour picker: swatches, one row. */
  .ns-swatches { display: flex; flex-wrap: wrap; gap: 7px; padding: 4px 0 2px; }
  .ns-swatches button { width: 26px; height: 26px; border-radius: 7px; border: 2px solid transparent; cursor: pointer; }
  .ns-swatches button.on { border-color: var(--ink); }
  @media (max-width: 900px) {
    .nsheet { left: 0; right: 0; bottom: 0; top: auto !important; width: auto; max-height: 82vh;
      border-radius: 16px 16px 0 0; border-left: 0; border-right: 0; border-bottom: 0;
      box-shadow: 0 -14px 40px rgba(13,21,41,.28); padding: 8px 10px max(14px, env(safe-area-inset-bottom)); }
    .nsheet::before { content: ''; display: block; width: 38px; height: 4px; border-radius: 999px;
      background: var(--line); margin: 0 auto 2px; }
    .ns-row { padding: 13px 12px; font-size: 15px; }
  }


  /* Zoho-style filter drawer */
  /* The drawer PUSHES the page rather than covering it — the list stays readable while you
     filter, which is the whole point of filtering. Groups start closed: the wall of every field
     at once was the overwhelming part. */
  .fdrawer { position: fixed; top: 0; left: 0; bottom: 0; width: 290px; background: var(--white);
    border-right: 1px solid var(--line); box-shadow: 10px 0 26px rgba(13,21,41,.07); z-index: 55;
    display: flex; flex-direction: column; }
  .fdrawer[hidden] { display: none; }
  body.filters-open { padding-left: 290px; }
  /* It opens NEXT to the command rail, not over it — so both stay usable. The page's padding
     has to carry both widths (padding-left isn't additive across rules). */
  body.has-rail .fdrawer { left: 224px; }
  body.has-rail.rail-mini .fdrawer { left: 64px; }
  body.has-rail.filters-open { padding-left: calc(224px + 290px); }
  body.has-rail.rail-mini.filters-open { padding-left: calc(64px + 290px); }
  @media (max-width: 960px) {
    /* The rail is behind a hamburger down here, so the drawer takes the edge itself. */
    body.has-rail .fdrawer, body.has-rail.rail-mini .fdrawer { left: 0; }
    body.has-rail.filters-open, body.has-rail.rail-mini.filters-open { padding-left: 290px; }
  }
  .fd-head { display: flex; justify-content: space-between; align-items: center; gap: 10px;
    padding: 12px 14px; border-bottom: 1px solid var(--line); }
  /* Tabs, not a title: the sheet does two jobs and you switch between them in place. */
  .fd-tabs { display: flex; gap: 2px; flex: 1; min-width: 0; }
  .fd-tabs button { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 13.5px;
    font-weight: 800; border: 0; background: none; color: var(--muted); padding: 5px 10px;
    border-radius: 8px; cursor: pointer; touch-action: manipulation; }
  .fd-tabs button:hover { background: #eff1f3; color: var(--ink); }
  .fd-tabs button.on { background: var(--navy); color: var(--cream); }
  .fd-tn { font-size: 10.5px; font-weight: 800; background: var(--orange); color: #fff;
    border-radius: 999px; padding: 1px 6px; }
  .fd-tn:empty { display: none; }
  /* Sort tab */
  .srt-dir { display: flex; gap: 6px; padding: 12px 14px 4px; }
  .srt-dir button { flex: 1; box-sizing: border-box; font: inherit; font-size: 12px; font-weight: 700;
    border: 1px solid var(--line); background: var(--white); color: var(--muted); border-radius: 8px;
    padding: 7px 8px; cursor: pointer; touch-action: manipulation; }
  .srt-dir button.on { background: var(--navy); border-color: var(--navy); color: var(--cream); }
  .srt-lbl { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
    color: var(--muted); padding: 14px 14px 6px; }
  .srt-row { display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;
    font: inherit; font-size: 13.5px; text-align: left; color: var(--ink); background: none; border: 0;
    border-bottom: 1px solid var(--line); padding: 11px 14px; cursor: pointer; }
  .srt-row:hover { background: #f6f8fa; }
  .srt-row .srt-n { flex: 1; }
  .srt-row .srt-v { font-size: 12px; color: var(--muted); }
  .srt-row.on { color: var(--orange); font-weight: 800; }
  .srt-row.on .srt-v { color: var(--orange); }
  .srt-tick { width: 16px; flex: none; display: grid; place-items: center; color: var(--orange); }
  .fd-x { display: inline-grid; place-items: center; border: 0; background: none; cursor: pointer;
    padding: 2px; color: var(--muted); }
  .fd-x:hover { color: var(--ink); }
  .fd-search { padding: 10px 14px 8px; border-bottom: 1px solid var(--line); }
  .fd-search input { width: 100%; font: inherit; font-size: 12.5px; padding: 7px 10px;
    border: 1px solid var(--line); border-radius: 8px; background: #f9fafb; }
  .fd-body { overflow-y: auto; overscroll-behavior: contain; padding: 0 0 24px; flex: 1; }

  /* Group = a toggle. Heading reads as a heading; the count says "something's on in here". */
  .fd-group { border-bottom: 1px solid var(--line); }
  .fd-gh { width: 100%; display: flex; align-items: center; gap: 8px; border: 0; background: none;
    font: inherit; font-size: 11.5px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
    color: var(--ink); padding: 11px 14px; cursor: pointer; text-align: left; touch-action: manipulation; }
  .fd-gh:hover { background: #f6f8fa; }
  .fd-gh .chev { flex: none; color: var(--muted); transition: transform .15s ease-out; display: grid; }
  .fd-gh.open .chev { transform: rotate(90deg); }
  .fd-gh .gname { flex: 1; }
  .fd-gh .gn { flex: none; font-size: 10px; font-weight: 800; letter-spacing: 0; background: var(--orange);
    color: #fff; border-radius: 999px; padding: 1px 7px; }
  .fd-fields { display: none; padding: 2px 14px 12px; }
  .fd-fields.open { display: block; }
  .fd-row { margin-bottom: 8px; }
  .fd-row label { display: block; font-size: 11.5px; font-weight: 600; color: var(--muted); margin-bottom: 3px; }
  .fd-row.active label { color: var(--orange); }
  .fd-row select, .fd-row input { width: 100%; font: inherit; font-size: 12px; padding: 5px 8px;
    border: 1px solid var(--line); border-radius: 7px; background: #f9fafb; }
  .fd-none { padding: 14px; font-size: 12.5px; color: var(--muted); }
  .fd-row select + input, .fd-row select + .fd-picks, .fd-row select + .fd-two,
  .fd-row select + .fd-hint { margin-top: 5px; }
  .fd-hint { font-size: 10.5px; color: var(--muted); margin-top: 4px; }
  /* Multi-select values, Zoho-style: a checkbox list, so one condition can be
     "is New Applicant OR Screening". Capped and scrolled — a 30-value picklist would
     otherwise push every other field off the sheet. */
  .fd-picks { max-height: 148px; overflow: auto; border: 1px solid var(--line); border-radius: 7px;
    background: #f9fafb; }
  .fd-pick { display: flex; align-items: center; gap: 8px; padding: 5px 8px; font-size: 12px;
    cursor: pointer; border-bottom: 1px solid rgba(0,0,0,.04); }
  .fd-pick:last-child { border-bottom: 0; }
  .fd-pick:hover { background: #eff1f3; }
  .fd-pick.on { color: var(--ink); font-weight: 700; }
  .fd-pick input { width: 14px; height: 14px; flex: none; accent-color: var(--orange); margin: 0; }
  .fd-pick span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .fd-two { display: flex; align-items: center; gap: 6px; }
  .fd-two input { flex: 1; min-width: 0; }
  .fd-unit { font-size: 11.5px; color: var(--muted); flex: none; }
  .fd-quick { display: flex; gap: 4px; margin-top: 5px; }
  .fd-quick button { font: inherit; font-size: 11px; font-weight: 700; border: 1px solid var(--line);
    background: var(--white); color: var(--muted); border-radius: 6px; padding: 3px 8px; cursor: pointer; }
  .fd-quick button.on { background: var(--navy); border-color: var(--navy); color: var(--cream); }
  .fd-scrim { display: none; position: fixed; inset: 0; background: rgba(13,21,41,.4); z-index: 54; }
  @media (max-width: 900px) {
    /* No room to push. A full-height side drawer holding six collapsed rows was 74% of the
       screen and mostly empty — on a phone this is a BOTTOM SHEET instead: full width, sized
       to its content, capped so the list stays visible behind it. */
    body.filters-open, body.has-rail.filters-open, body.has-rail.rail-mini.filters-open { padding-left: 0; }
    /* ⚠ A FIXED height, not `auto` + a cap: the Sort tab is ~95px shorter than Filters, so
       content-sizing made the whole sheet jump every time you moved between them. It's one
       menu — it should hold still while you work it. The body scrolls inside. */
    .fdrawer, body.has-rail .fdrawer, body.has-rail.rail-mini .fdrawer {
      top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 76vh; max-height: 76vh;
      border-right: 0; border-top: 1px solid var(--line); border-radius: 16px 16px 0 0;
      box-shadow: 0 -14px 40px rgba(13,21,41,.28); padding-top: 8px; }
    /* The grabber is what makes it read as a sheet you can dismiss. */
    .fdrawer::before { content: ''; display: block; width: 38px; height: 4px; border-radius: 999px;
      background: var(--line); margin: 0 auto 4px; flex: none; }
    .fd-body { padding-bottom: max(16px, env(safe-area-inset-bottom)); }
    body.filters-open .fd-scrim { display: block; }
  }

/* A toast, not an inline banner: feedback about an action shouldn't shove the page's own
   content down the screen and then pull it back up a moment later. */
/* ⚠⚠ `border-radius: 18px`, NOT `999px` — and it renders IDENTICALLY for every toast that fits on
   one line. A single-line toast is ~36px tall (9px padding each side + an 18px line box), so 18px
   is exactly half of it and the pill is unchanged. `999px` was only ever correct at that height:
   the moment a toast WRAPPED, the radius clamped to half the height and drew a CIRCLE with the
   text spilling out of both sides. Measured on a 390px screen — a 260px-tall sync report rendered
   as a dark red disc, completely unreadable.
   ⚠ Portal-wide, so any long toast anywhere was affected; found on this one because it is the
   first to carry more than a sentence. */
.toast { position: fixed; left: 50%; bottom: 70px; transform: translateX(-50%); z-index: 80;
  max-width: calc(100vw - 32px); font-size: 13px; font-weight: 700; padding: 9px 16px;
  border-radius: 18px; pointer-events: none; text-align: center;
  background: var(--navy); color: var(--cream); box-shadow: 0 10px 30px rgba(13,21,41,.25); }
.toast.error { background: #9b2c2c; color: #fff; }
/* ⚠ A toast carrying an ACTION has to be clickable — the base rule sets `pointer-events: none`
   so a toast can never swallow a click on the content underneath, and leaving that in place
   would render an Undo button nobody can press. Turned back on only for this variant. */
.toast.act { pointer-events: auto; display: inline-flex; align-items: center; gap: 10px;
  padding: 7px 8px 7px 16px; }
.toast-act { font: inherit; font-size: 12.5px; font-weight: 800; cursor: pointer;
  border: 1px solid rgba(238,241,244,.3); background: rgba(238,241,244,.15); color: inherit;
  border-radius: 999px; padding: 5px 13px; box-sizing: border-box; }
.toast-act:hover { background: rgba(238,241,244,.28); }
/* 44px is the touch floor; the pill is 27px tall on a mouse, which is right for a transient. */
@media (hover: none) { .toast-act { min-height: 36px; padding: 7px 16px; } }

/* The split button's action half, mid-flight. */
.vsplit button.busy svg { animation: ndp-spin .9s linear infinite; }
@keyframes ndp-spin { to { transform: rotate(360deg); } }

/* ════════════════════════════════════════════════════════════════════════════════════════════
   THE TEXT WINDOW FAMILY — ONE chrome for every conversation surface (2026-08-15)

   ⚠⚠ FIVE PLACES IN THIS PORTAL OPEN A TEXT MESSAGE, and until this block existed each one
   carried its OWN copy of this CSS, drifting a little further every round:
     · the candidate profile  (`admin-candidate.html`  #sms-panel)   380x480, inset sheet on a phone
     · the inbox thread       (`admin-inbox.html`      #panel)       420x520, full screen on a phone
     · the inbox Slack window (`admin-inbox.html`      #spanel)      21px send plane, not 26
     · the client profile     (`admin-client.html`     #ctw)         no call, no expand, no + menu
     · the inbox New text     (`admin-inbox.html`      #ntmodal)     a modal, an orange pill Send
   Same window to whoever is using it, four different windows in the source. Eric asked for the
   CANDIDATE PROFILE's look everywhere (2026-08-15, with a screenshot), so that is what this is:
   its header treatment, its + menu, its 26px orange plane — plus the decisions the inbox got
   right and it never received (full screen on a phone, a back arrow, 44px icons on touch).

   ⚠ IT LIVES HERE, IN THE LINKED STYLESHEET, NOT IN A MODULE THAT INJECTS A <style>.
   `portal-templates.js` injects its own and can, because nothing overrides `.tplbtn`. This
   family IS overridden — the inbox flips the icons for its cream mail header, the Slack window
   drops the composer's left half — and an injected block lands AFTER every page's inline
   <style>, so it would win every tie and silently undo those. A <link> in the head loses ties
   to the page, which is the direction that has to be true.

   ⚠ MARKUP GOES WITH IT: `textWinHead()` / `textWinCompose()` in `/portal-textwin.js` emit the
   ids and classes these rules expect. Hand-writing a sixth window is how a sixth variant starts.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* The docked window itself — bottom-right, expandable upward, never full-screen on a desktop.
   ⚠ 380px is the candidate profile's width and it is also the value BOTH profile pages hand
   `--textwin-w`, which is how the live-call window knows where to dock beside it. Changing it
   here without changing that promise puts the two windows on top of each other. */
.sms-panel { position: fixed; right: 18px; bottom: 18px; width: 380px; max-width: calc(100vw - 24px);
  height: 520px; max-height: calc(100vh - 36px); background: var(--white); border-radius: 14px;
  box-shadow: 0 18px 50px rgba(13,21,41,.4); z-index: 50; display: none; flex-direction: column;
  overflow: hidden; }
.sms-panel.open { display: flex; }
.sms-panel.max { height: calc(100vh - 36px); height: calc(100dvh - 36px); }

.sms-head { background: #0D1529; color: var(--cream); padding: 12px 16px; }
.sms-head .row1 { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
/* ⚠ THE TITLE TAKES THE SLACK, always — not only on a phone. With the actions sized by how many
   of them are showing (the call button hides on a group thread, and on a number nobody can dial)
   a title that shares the space MOVES when a button appears. Left-aligned beside the back arrow,
   ellipsised, and it stays where it was. */
.sms-head .hdtitle { flex: 1; min-width: 0; }
.sms-head .t { font-weight: 800; font-size: 15px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* ⚠ The title is a LINK when the thread matched a candidate, and `.sms-head a` below is 12px for
   the other links in this header — so a matched name rendered two thirds the size of a bare
   phone number, which is the same slot. The title's size wins whatever is in it. */
.sms-head .t a { font-size: inherit; font-weight: inherit; }
.sms-head .p { font-size: 12px; color: rgba(238,241,244,.7); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.sms-head a { color: var(--teal); font-size: 12px; font-weight: 700; text-decoration: none; }
.sms-head a.namelink { color: var(--cream); text-decoration: none; }
.sms-head a.namelink:hover { color: #E16C35; }
/* The action cluster. 10px apart — the candidate profile's spacing, which is the one Eric picked. */
.sms-head .winacts { display: flex; align-items: center; gap: 10px; flex: none; }

/* Window header controls — ONE treatment everywhere (text window + Slack + mail reader +
   compose + the live-call window): 30px rounded square, 16px icon. Teal for the actions that
   START something (call, text), quiet cream for the window's own controls.
   ⚠⚠ `.mp-head` IS IN THE SELECTOR. The mail reader renders `class="winicon"` on every one of
   its buttons; while the rule was scoped to `.sms-head` alone NONE of it applied and those six
   came out as raw 20x23 buttons wearing the browser's default `2px outset` border. It looked
   deliberate enough at a glance on navy, which is why it survived.
   ⚠ `box-sizing` is not decoration: a flex item's base size includes its border, so a bordered
   button beside a borderless one comes out 2px wider — and these get injected into pages whose
   reset this file cannot assume.
   ⚠⚠ `.callpop` IS IN THE SELECTOR TOO (2026-08-15). The live-call window carried a FOURTH copy
   of this block, under a comment claiming it was "the SAME treatment as the text window" — and
   it had already drifted in two properties nobody would ever see in a screenshot: no
   `box-sizing`, so a bordered icon beside a borderless one comes out 2px wider, and no
   `font-size: 0`, so a stray text node inside the button pushes the glyph off centre. A comment
   asserting sameness is not sameness; one selector is. */
.sms-head button.winicon, .mp-head button.winicon,
.callpop button.winicon, .modal-head button.winicon { flex: none; width: 30px; height: 30px;
  display: inline-grid; place-items: center; padding: 0; font-size: 0; box-sizing: border-box;
  border: 1px solid rgba(91,182,188,.34); border-radius: 9px; background: rgba(91,182,188,.14);
  color: #5BB6BC; cursor: pointer; touch-action: manipulation; transition: background .12s, color .12s; }
.sms-head button.winicon:hover, .mp-head button.winicon:hover,
.callpop button.winicon:hover, .modal-head button.winicon:hover { background: #5BB6BC; color: #0D1529; }
.sms-head button.winicon svg, .mp-head button.winicon svg,
.callpop button.winicon svg, .modal-head button.winicon svg { display: block; }
.sms-head button.winicon.quiet, .mp-head button.winicon.quiet,
.callpop button.winicon.quiet {
  border-color: rgba(238,241,244,.22); background: transparent; color: rgba(238,241,244,.72); }
.sms-head button.winicon.quiet:hover, .mp-head button.winicon.quiet:hover,
.callpop button.winicon.quiet:hover {
  background: rgba(238,241,244,.14); color: #EEF1F4; }
/* ⚠ ON TOUCH THEY GROW TO 44px. 30px is a comfortable MOUSE target and a poor finger one — 44 is
   the floor every platform's guidance agrees on (Apple's HIG; Material asks 48dp), and under it
   people miss, hit the neighbour, or press twice. The GLYPH goes to 18px with it: scaling the box
   alone leaves a 16px icon adrift in a 44px square, which reads as a bigger button rather than a
   bigger icon.
   ⚠ Keyed on `(hover: none)`, NOT on width — this is about fingers, not screen size.
   ⚠ The candidate profile NEVER HAD THIS RULE. Its window is the one Eric uses on his phone and
   its three header icons were 30px there while the inbox's were 44px, on the same handset. */
@media (hover: none) {
  .sms-head button.winicon, .mp-head button.winicon,
  .callpop button.winicon, .modal-head button.winicon { width: 44px; height: 44px; border-radius: 11px; }
  .sms-head button.winicon svg, .mp-head button.winicon svg,
  .callpop button.winicon svg, .modal-head button.winicon svg { width: 18px; height: 18px; }
}
/* ⚠⚠ THIS RULE MUST SIT AFTER THE `.winicon` BLOCK AND CARRY `button`. `.sms-head button.winicon`
   is (0,2,1) and sets `display: inline-grid`; a `.sms-head .backbtn` at (0,2,0) earlier in the
   file therefore never applied, and the mobile back arrow showed on the desktop docked window
   too — pushing the contact's name 25px right. Specificity AND source order both have to be
   right; getting one of them wrong looks exactly like the rule not existing. */
.sms-head button.backbtn { display: none; }

/* The To row — extra recipients turn a send into ONE group thread. */
.sms-to { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 7px 12px;
  background: var(--white); border-bottom: 1px solid var(--line); }
.sms-to .tolbl { font-size: 12px; font-weight: 700; color: var(--muted); }
.sms-to input { flex: 1; min-width: 100px; border: 0; background: transparent; font: inherit;
  font-size: 13px; outline: none; padding: 3px; }
.tofield { display: flex; flex-wrap: wrap; gap: 6px; border: 1px solid var(--line); border-radius: 9px;
  padding: 7px 9px; background: #f9fafb; position: relative; cursor: text; }
.tofield input { flex: 1; min-width: 140px; border: 0; background: transparent; font: inherit;
  font-size: 13px; outline: none; padding: 3px; }
.tochip { display: inline-flex; align-items: center; gap: 6px; background: var(--navy); color: var(--cream);
  border-radius: 999px; padding: 3px 10px; font-size: 12px; font-weight: 700; }
.tochip.fixed { background: #2a6f74; }
.tochip button { border: 0; background: none; color: rgba(238,241,244,.7); cursor: pointer;
  font-size: 11px; padding: 0; }
.tochip button:hover { color: #fff; }
.tosugg { position: absolute; left: 0; right: 0; top: 100%; margin-top: 4px; background: var(--white);
  border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 14px 40px rgba(13,21,41,.18);
  z-index: 6; max-height: 240px; overflow-y: auto; }
.tosugg button { display: flex; justify-content: space-between; gap: 10px; width: 100%; text-align: left;
  border: 0; background: none; font: inherit; font-size: 13px; padding: 9px 12px; cursor: pointer; }
.tosugg button:hover { background: #f6f8fa; }
.tosugg .num { color: var(--muted); font-size: 12px; white-space: nowrap; }

/* The conversation. ⚠ WHITE, not a tinted scroller: the client profile's window used #f6f8fa and
   was the only one that did, so the same conversation had a different ground depending on which
   page you opened it from. */
.sms-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px;
  display: flex; flex-direction: column; gap: 8px; background: var(--white); }
.bubble { max-width: 78%; padding: 7px 11px; border-radius: 14px; font-size: 14px;
  white-space: pre-wrap; overflow-wrap: anywhere; }
.bubble.out { align-self: flex-end; background: var(--teal); color: #fff; border-bottom-right-radius: 4px; }
.bubble.in { align-self: flex-start; background: #ebedef; border-bottom-left-radius: 4px; }
.bubble .bt { display: block; font-size: 10px; opacity: .65; margin-top: 2px; }
/* Capped: an image taller than the window hides the conversation around it, and unbounded growth
   is what makes re-pinning after a late-loading image so fragile.
   ⚠ `height: auto` is load-bearing: loadMms sets the natural width/height ATTRIBUTES before the
   src, so the browser knows the aspect ratio and reserves the box on the first layout. Without
   auto the height attribute would fix the box and fight max-width. This is what stops an image
   growing from 0px and shoving the thread. */
.bubble img.mms { display: block; max-width: 100%; max-height: 42vh; height: auto; object-fit: contain;
  object-position: left; border-radius: 10px; margin-top: 4px; }
/* An attachment announces itself even before (or instead of) the image rendering — a picture that
   won't load must never look like an empty bubble. */
.bubble .mmsw { display: block; }
.bubble .mmsfb { display: inline-flex; align-items: center; gap: 5px; font-size: 11px;
  opacity: .8; margin-top: 4px; }
/* The optimistic copy of a message you just sent — solid enough to read, faded enough to say
   "not confirmed yet". */
.bubble.pending { opacity: .62; }

/* The composer. Templates on their own row above it, then + / box / plane. */
.sms-tplbar { display: flex; padding: 8px 12px 0; background: var(--white);
  border-top: 1px solid var(--line); }
.sms-compose { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--line);
  background: var(--white); }
/* One border between the two rows, not two — the tplbar already drew it. */
.sms-tplbar + .sms-compose { border-top: 0; }
.sms-compose textarea { flex: 1; min-width: 0; font: inherit; font-size: 14px;
  border: 1px solid var(--line); border-radius: 9px; padding: 8px 11px; resize: none; height: 44px; }
/* Bare tappable icons, RC-style — no button chrome around them. */
.sms-compose button.icbtn { flex: none; border: 0; border-radius: 0; background: none; padding: 3px;
  cursor: pointer; color: #64676a; display: grid; place-items: center; touch-action: manipulation;
  transition: color .12s; }
.sms-compose button.icbtn:hover { background: none; color: var(--ink); }
/* ⚠ The plane is ALONE in the send corner — the clock moved into the + menu — so it takes the
   whole corner rather than sharing it. 26px glyph in a 44px target: the minimum a thumb can hit
   reliably, on the one control here you must never miss. The Slack window drew it at 21px for no
   reason anybody recorded; it is 26 everywhere now. */
.sms-compose button.icbtn.send { color: var(--orange); min-width: 44px; min-height: 44px; }
.sms-compose button.icbtn.send:hover { color: #c9551f; }
.sms-compose button.icbtn svg { display: block; }
.sms-compose button.icbtn.plusic { align-self: center; }
/* ⚠⚠ `justify-content: CENTER`, and this is the fix Eric asked for by name ("the orange send
   button... looks a little off centred"). The column is `align-self: stretch`, so it is as tall
   as the grown textarea; it used to be `space-between`, which is right for TWO children and pins
   a lone child to the TOP. Measured on the profile window with a two-line message: the plane's
   centre sat 5.5px above the box's. The second child (the schedule clock) moved into the + menu
   and nobody revisited the rule it left behind. */
.sendcol { flex: none; display: flex; flex-direction: column; justify-content: center;
  align-self: stretch; }
/* The button has to fit INSIDE a one-line row (44px) or it drives the row taller than the text
   field and hangs below it. Padding goes horizontal-only: same tap target, no vertical growth. */
.sendcol button.icbtn { padding: 0 5px; }
/* The foot: the from-number on the left where a window has one, the attachment chip on the right. */
.sms-attbar { display: flex; justify-content: space-between; gap: 8px; background: var(--white);
  padding: 0 14px 8px; font-size: 11px; color: var(--muted); }
.attchip { display: inline-flex; align-items: center; gap: 6px; background: #e7f1f2; color: #2a6f74;
  border-radius: 999px; padding: 2px 9px; font-weight: 700; }
.attchip button { border: 0; background: none; cursor: pointer; color: inherit; font-size: 11px; padding: 0; }

/* The + menu. ⚠ It borrows the TEMPLATE PICKER's shell (`.tplpick`), whose rows are 800-weight
   link-blue — right for a list of template names, wrong for a settings menu, where it read as
   three links rather than two toggles and an action. These override it into the same quiet row
   the Notion-style sheets use: ink text at normal weight, a muted stroke icon, and the tick as
   the only coloured thing in the row. */
.plusmenu-item { display: flex; align-items: center; gap: 10px; line-height: 1; }
.plusmenu-item svg { flex: none; display: block; color: #2a6f74; }
#plusmenu button { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 11px 14px; }
#plusmenu .tp-n.plusmenu-item { font-size: 14px; font-weight: 600; color: var(--ink); letter-spacing: 0; }
#plusmenu .plusmenu-item svg { color: #6e6f70; }
/* The row keeps its width whether the tick is showing or not — a menu whose items reflow as you
   toggle them is a menu you have to re-aim at. Hidden by opacity, not display. */
.pm-tick { flex: none; display: block; color: var(--teal); opacity: 0; }
.pm-tick.on { opacity: 1; }

/* iOS centres the value inside date inputs — keep it left-aligned like every other field. */
input[type="datetime-local"] { text-align: left; display: block; width: 100%; max-width: 100%;
  min-width: 0; box-sizing: border-box; -webkit-appearance: none; appearance: none; }
input[type="datetime-local"]::-webkit-date-and-time-value { text-align: left; margin: 0; }

/* While a docked window or a modal is open on a phone, the page behind must not scroll with it. */
body.noscroll { position: fixed; left: 0; right: 0; overflow: hidden; }

@media (max-width: 640px) {
  /* ⚠ FULL SCREEN, not an inset sheet (Eric, 2026-08-05, for the inbox — and now everywhere).
     The candidate profile's window stayed an 80dvh card with 10px margins and a scrim, which
     reads as something floating over a list you cannot use, and spent ~20px of width and ~20% of
     the height saying so. A conversation on a phone IS the screen; the scrim, the radius and the
     border go with the margins, because there is nothing behind it left to separate from.
     ⚠ `dvh` stays: it tracks the iOS keyboard, and without it the composer sits under it. */
  .sms-panel { inset: 0; width: auto; max-width: none; height: 100vh; height: 100dvh;
    max-height: none; border-radius: 0; border: 0; box-shadow: none; }
  /* ⚠ `.max` is the expand-to-full-height toggle. Full screen already IS that, so it must not be
     able to set a DIFFERENT height and undo the rule above. */
  .sms-panel.max { height: 100dvh; }
  /* Full screen means the list is gone, so the exit belongs in the top-left corner like every app
     on the platform. On a desktop this is a small docked window with the page visible behind it
     and there is nothing to go "back" to — the × is right there and stays. */
  .sms-head button.backbtn { display: inline-grid; }
  /* Expand does nothing at full screen, and × is what the back arrow replaces — offering two
     exits in opposite corners is how you get people pressing the wrong one.
     ⚠ Keyed on `.winacts`, which `textWinHead()` always emits, NOT on `.sms-panel`. It used to
     be an id list in the inbox's own <style> (`#p-expand, #s-expand, #p-close, #s-close`), and
     the mail COMPOSER — a full-screen window on a phone with the same two buttons — was simply
     not on it. A rule that has to be extended by hand for each new window is a rule the next
     window silently is not covered by.
     ⚠⚠ `.sms-head … button.…` IS THE SPECIFICITY THIS NEEDS, not a bare `.winacts .wclose`.
     The base rule is `.sms-head button.winicon` at (0,2,1) and it sets `display: inline-grid`;
     a two-class selector is (0,2,0) and LOSES, so the buttons come straight back on a phone —
     which is how this shipped for a few minutes with a back arrow AND an × in opposite corners.
     Measured, not assumed: the harness reported 44x44 for both. */
  .sms-head .winacts button.wexpand, .sms-head .winacts button.wclose { display: none; }
  /* ⚠ ≥16px, or iOS AUTO-ZOOMS the page the moment the box is focused — and that zoom is what
     used to push the whole window off the right edge of the screen. The inbox and the candidate
     profile each carry a page-wide `!important` version of this; the client profile carried it
     on its own composer and lost it when that composer became the shared one. Scoped to the
     window so it is true for every text surface without touching any page's other typography. */
  .sms-panel input, .sms-panel select, .sms-panel textarea { font-size: 16px; }
}

/* A queued send, shown in the thread it will land in. Dashed and hollow so it can never be
   mistaken for a delivered message; the clock in its foot says when it goes. Shared because
   both thread windows — the inbox's and the candidate profile's — render one. */
/* ⚠ `.bubble.out.sched`, not `.bubble.sched`: `.bubble.out` above is a filled teal background.
   Equal specificity would lose the cascade and the queued bubble would render as a normal sent
   one. (It used to be each PAGE defining `.bubble.out` in a <style> that came after this file —
   same trap, one file closer.) */
.bubble.out.sched, .bubble.sched { background: none; color: var(--ink);
  border: 1px dashed var(--teal); opacity: .95; }
.bubble.out.sched .bt, .bubble.sched .bt { opacity: 1; color: var(--muted); }
.bubble.out.sched .sched-x, .bubble.sched .sched-x { border: 0; background: none; padding: 0;
  font: inherit; font-size: 10px; font-weight: 700; color: #9b2c2c; cursor: pointer; }
.bubble.sched .sched-x:disabled { opacity: .5; cursor: default; }

/* ═══ Setup cards — what a module still needs from THIS organisation ═══════════
   Phase H7, docs/phase-module-setup.md. Rendered by `public/portal-setupcard.js` from
   `/api/admin/module-readiness`; the RULE is `shared/types/moduleReadiness.ts` and nothing here
   decides anything.

   ⚠⚠ IT IS NOT AN ERROR STATE AND MUST NOT LOOK LIKE ONE. An organisation that has not connected
   a mailbox has done nothing wrong — the portal has simply never asked. Red here would read as a
   fault and train people to dismiss these, which is the opposite of the job. The ground is the
   same informational tint `.note` already uses; only `blocked` is allowed to go warm, and even
   then it is amber rather than red, because "we have not built this for you yet" is our
   limitation and not the reader's mistake.

   ⚠ `--sc-edge` is a 3px LEFT rule rather than a full coloured border: three stacked cards with
   three coloured outlines read as three warnings. The left edge separates them at a glance and
   still lets the block sit quietly above a board.

   ⚠ It sits ABOVE the empty thing it explains, never below and never in a toast — CLAUDE.md
   rule 5's stated exception. A toast is feedback about an action; this is a standing fact. */
.setupcard { --sc-edge: var(--teal); --sc-bg: #eef6f7; --sc-ink: #2a6f74; --sc-line: #cfe7e9;
  background: var(--sc-bg); color: var(--sc-ink); border: 1px solid var(--sc-line);
  border-left: 3px solid var(--sc-edge); border-radius: 9px; padding: 11px 14px 12px;
  margin-bottom: 12px; font-size: 14px; }
.setupcard.needs-setup { --sc-edge: var(--orange); }
/* Amber, never red — see the note above. */
.setupcard.blocked { --sc-edge: #b7791f; --sc-bg: #fdf6e7; --sc-ink: #6b5312; --sc-line: #ecdcb4; }

/* ⚠ The head wraps, and `.sc-title` is what it is allowed to wrap AROUND. Wrapping over the icon
   and the text directly put the icon alone on a line above the module name at 390px — measured,
   not guessed. `min-width: 0` lets a long heading wrap inside its own item instead of forcing the
   row wider than the card. */
.setupcard .sc-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-bottom: 7px; }
/* ⚠ `flex-start`, not `center`: against a heading that has wrapped to two lines at 390px a centred
   icon floats between them and stops reading as the thing that labels the title. The 2px nudge is
   the optical centring of a 15px icon on a 14px cap height, measured on the render. */
.setupcard .sc-title { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
.setupcard .sc-title svg { margin-top: 2px; }
.setupcard .sc-head b { font-size: 14px; font-weight: 800; }
.setupcard .sc-head svg { flex: none; }
/* ⚠ The state chip is pushed right on a wide screen and wraps under the title on a phone rather
   than squeezing the module name — measured at 390px, where a nowrap chip cost the heading 90px. */
.setupcard .sc-state { margin-left: auto; font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
  text-transform: uppercase; opacity: .75; }

/* ⚠ MEASURED, not guessed. Unconstrained, these lines ran to 896px at desktop — about 115
   characters, which is half again the width anything is comfortably read at, and these are
   paragraphs rather than table cells. Capped in `ch` so it tracks the font rather than a viewport
   the card does not own. The CARD still spans the content column: it is a page-level banner and a
   narrow box floating in a wide one reads as a component that failed to lay out. */
.setupcard .sc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px;
  max-width: 78ch; }
.setupcard .sc-list li { line-height: 1.5; padding-left: 15px; position: relative; }
/* ⚠ A CSS marker, not a bullet character and never a dingbat: `check:icons` scans rendered copy,
   and a glyph here would render at a different weight on every platform. */
.setupcard .sc-list li::before { content: ''; position: absolute; left: 2px; top: .62em;
  width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: .5; }
/* ⚠ The DONE lines are quiet but present. Deleting them would leave a card that can only say what
   is wrong, and "we checked and this part is fine" would be indistinguishable from "we never
   looked" — module-access.md's `zohoAnswered` rule. */
.setupcard .sc-done { opacity: .72; }

/* ⚠ The link is the only interactive thing on a card, and there is at most one per action —
   "then one link to the exact place to fix it". Two links on one line is a decision the reader
   has to make before they can act. */
.setupcard .sc-go { display: inline-flex; align-items: center; gap: 3px; margin-left: 6px;
  white-space: nowrap; font-weight: 800; color: inherit; text-decoration: none;
  border-bottom: 1px solid currentColor; }
.setupcard .sc-go svg { margin-bottom: -2px; }
.setupcard .sc-go:hover { opacity: .75; }
/* 44px touch floor. ⚠ Keyed on hover, never on width: a tablet in landscape is wide and still has
   no pointer — the same rule the access chips follow. */
@media (hover: none) {
  .setupcard .sc-go { min-height: 34px; padding: 4px 0; }
}

/* The settings screen's list of every card at once. ⚠ Its own wrapper so a page that shows ONE
   card is not styled by a rule written for a column of them. */
.setupall { display: block; }
.setupall .sc-empty { color: var(--muted); font-size: 14px; line-height: 1.55; }

/* ════════════════════════════════════════════════════════════════════════════════════════════
   THE TICKET VIEW SHELL — how a ticket OPENS, and how dense the queue rows sit (2026-08-18)

   Session 2 of ticket mode; behaviour in public/portal-ticketview.js, spec in
   docs/mock/ticket-mode.html ("How a ticket opens"). The shell wraps the queue session 1
   builds: <div class="tkv"> holds the queue slot, the scrim, the divider and the ticket pane,
   and data-mode / data-open / .fs on it are the whole state machine.

   ⚠ FOUR MODES, ONE MARKUP SET. Side peek floats the pane over the queue (fixed, scrim
   behind); Split ‖ and Split ═ bound the stage to the viewport and let each side scroll
   itself; Full page hides the queue and lets the PAGE scroll. The window control's full
   screen (.fs) is the transient fifth state: the pane takes the stage, whatever the mode.
   ⚠ CLOSED COSTS NOTHING: with no data-open the scrim, grip and pane are display:none and
   the queue renders exactly as it did before the shell existed.
   ⚠ ON A PHONE (≤960px, the inbox reader's own line) EVERY open mode presents as full page,
   in flow — the media block at the end outranks the desktop rules by ORDER at equal
   specificity, deliberately, so there is no !important war. The body must never scroll
   sideways there; the one thing that genuinely cannot reflow (fixed-width marketing mail)
   is scaled inside its frame by fitTicketFrame instead.
   ──────────────────────────────────────────────────────────────────────────────────────────── */
.tkv { display: block; }
.tkv-queueslot { min-width: 0; min-height: 0; }
.tkv-scrim, .tkv-grip, .tkv-pane { display: none; }

.tkv[data-open] .tkv-pane { display: flex; flex-direction: column; background: var(--white);
  border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden;
  min-width: 0; min-height: 0; }
.tkv[data-open] .tkv-pane:focus { outline: none; }
.tkv-scroll { flex: 1; min-height: 0; overflow: auto; }

/* ── The divider — a REAL control: hover and focus state, keyboard target, touch drag ── */
.tkv-grip { flex: none; width: 8px; border-radius: 999px; background: #DDE2E8;
  cursor: col-resize; align-self: stretch; position: relative; transition: background .15s;
  touch-action: none; }
.tkv-grip:hover, .tkv-grip:focus-visible { background: var(--teal); }
.tkv-grip:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.tkv-grip::after { content: ''; position: absolute; left: 2px; right: 2px; top: 50%;
  height: 26px; transform: translateY(-50%); border-radius: 999px; background: rgba(13,21,41,.16); }
body.tkv-dragging { user-select: none; cursor: row-resize; }
body.tkv-dragging-v { cursor: col-resize; }
/* ⚠ Every rendered email is an iframe, and an iframe swallows pointermove ENTIRELY — without
   this the drag dies the moment the pointer crosses a message (the reading pane's rp-dragging
   lesson, relearned here on purpose rather than by a bug report). */
body.tkv-dragging iframe { pointer-events: none; }

/* ── Side peek: floats over the queue, viewport-fixed, scrim behind, Escape closes ── */
.tkv[data-open][data-mode="peek"] .tkv-scrim { display: block; position: fixed; inset: 0;
  background: rgba(13,21,41,.10); z-index: 58; }
.tkv[data-open][data-mode="peek"] .tkv-pane { position: fixed; top: 10px; right: 10px;
  bottom: 10px; width: var(--tkv-peek, 44%); z-index: 60;
  box-shadow: 0 12px 40px rgba(13,21,41,.22); }
.tkv[data-open][data-mode="peek"] .tkv-grip { display: block; position: fixed; top: 10px;
  bottom: 10px; right: calc(var(--tkv-peek, 44%) + 16px); z-index: 61; }
.tkv.fs[data-open][data-mode="peek"] .tkv-pane { left: 10px; width: auto; }
.tkv.fs[data-open][data-mode="peek"] .tkv-grip { display: none; }

/* ── Split ‖ : queue left, ticket right. The stage is BOUNDED (viewport minus the measured
      chrome above it, --tkv-top) so each side scrolls itself and the divider means something. ── */
.tkv[data-open][data-mode="splitv"] { display: flex; gap: 10px;
  height: calc(100vh - var(--tkv-top, 0px) - 12px);
  height: calc(100dvh - var(--tkv-top, 0px) - 12px); }
.tkv[data-open][data-mode="splitv"] .tkv-queueslot { flex: 1; min-width: 0; overflow: auto; }
.tkv[data-open][data-mode="splitv"] .tkv-grip { display: block; }
.tkv[data-open][data-mode="splitv"] .tkv-pane { flex: none; width: var(--tkv-splitw, 58%); }
.tkv.fs[data-open][data-mode="splitv"] .tkv-queueslot,
.tkv.fs[data-open][data-mode="splitv"] .tkv-grip { display: none; }
.tkv.fs[data-open][data-mode="splitv"] .tkv-pane { flex: 1; width: auto; }

/* ── Split ═ : queue on top, ticket below — height is cheaper than width on a laptop ── */
.tkv[data-open][data-mode="splith"] { display: flex; flex-direction: column; gap: 10px;
  height: calc(100vh - var(--tkv-top, 0px) - 12px);
  height: calc(100dvh - var(--tkv-top, 0px) - 12px); }
.tkv[data-open][data-mode="splith"] .tkv-queueslot { flex: 1; min-height: 0; overflow: auto; }
.tkv[data-open][data-mode="splith"] .tkv-grip { display: block; width: auto; height: 8px;
  cursor: row-resize; align-self: stretch; }
.tkv[data-open][data-mode="splith"] .tkv-grip::after { left: 50%; right: auto; top: 2px;
  bottom: 2px; height: auto; width: 26px; transform: translateX(-50%); }
.tkv[data-open][data-mode="splith"] .tkv-pane { flex: none; height: var(--tkv-splith, 46%); }
.tkv.fs[data-open][data-mode="splith"] .tkv-queueslot,
.tkv.fs[data-open][data-mode="splith"] .tkv-grip { display: none; }
.tkv.fs[data-open][data-mode="splith"] .tkv-pane { flex: 1; height: auto; }

/* ── Full page: the queue steps aside, the PAGE scrolls, the URL carries ?ticket= ── */
.tkv[data-open][data-mode="full"] .tkv-queueslot { display: none; }
.tkv[data-open][data-mode="full"] .tkv-scroll { overflow: visible; }
.tkv[data-mode="full"] .tkv-winbar [data-tkv="fs"] { display: none; }

/* ── The pane's own window bar ── */
.tkv-winbar { display: flex; align-items: center; gap: 8px; padding: 7px 10px 7px 14px;
  border-bottom: 1px solid #E8EBEE; background: #FBFCFD; flex: none; }
.tkv-wref { flex: 1; font-size: 11.5px; color: #9BA0A6; font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tkv-wbtns { display: inline-flex; gap: 2px; }
.tkv-wb { background: none; border: 0; cursor: pointer; color: #9BA0A6; padding: 4px;
  border-radius: 5px; display: grid; place-items: center; }
.tkv-wb:hover { background: var(--cream); color: var(--ink); }
.tkv-wb:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }

/* ── The ticket, read-only: header, chips, meta, the inert action row ── */
.tkv-pane .dhead { padding: 14px 16px; border-bottom: 1px solid #E8EBEE; }
.tkv-pane .dhead .top { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin-bottom: 5px; }
.tkv-pane .dhead h3 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; margin: 0; line-height: 1.35; }
.tkv-pane .dhead .meta { font-size: 12px; color: #9BA0A6; margin: 6px 0 0; }
.tkv-pane .dhead .meta a { color: var(--ink); }
.tkv-pane .pill.open { background: #E2E8F5; color: #35507F; }
.tkv-pane .pill.pending { background: #FBEEDE; color: #A85A1F; }
.tkv-pane .pill.closed { background: #ECEEF1; color: var(--muted); }
.tkv-pane .chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.tkv-pane .chip { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 999px; white-space: nowrap; }
.tkv-pane .chip.task { background: #EDE9F7; color: #54468B; }
.tkv-pane .chip.itype { background: #E2E8F5; color: #35507F; }
.tkv-pane .chip.tag { background: #EDF0F3; color: var(--muted); }
.tkv-pane .chip.mention { background: #E7F1F2; color: #2A6F74; }
.tkv-pane .dact { display: flex; gap: 6px; flex-wrap: wrap; padding: 10px 16px;
  border-bottom: 1px solid #E8EBEE; background: #FBFCFD; align-items: center; }
/* ⚠ .tbtn, never .btn — portal.css's .btn is the full-width orange FORM button, and reusing it
   here would stretch every action across the pane. Same cascade-collision family as the mock's
   own .bar note. */
.tkv-pane .tbtn { font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; background: var(--white);
  color: var(--ink); display: inline-flex; align-items: center; gap: 6px; }
.tkv-pane .tbtn:hover { background: #F6F8FA; }
.tkv-pane .tbtn.primary { background: var(--orange); color: #fff; border-color: var(--orange); }
/* ⚠ A DEAD BUTTON MUST LOOK DEAD (session 9). The old `opacity: .55` on a white card still
   read as pressable — proven, because people pressed it and filed the bug. Grey ground, muted
   text, a dashed edge and the not-allowed cursor: nothing else on this pane wears any of
   those, so inert cannot be mistaken for idle. The title says WHY (rule 8). */
.tkv-pane .tbtn:disabled { background: #F1F3F5; color: #A9AFB6; border-style: dashed;
  border-color: #D8DCE0; cursor: not-allowed; }
.tkv-pane .tbtn:disabled:hover { background: #F1F3F5; }
.tkv-pane .tbtn.primary:disabled { background: #F1F3F5; color: #A9AFB6; border-color: #D8DCE0; }
.tkv-pane .tbtn.primary:disabled:hover { background: #F1F3F5; }
.tkv-pane .tbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* ⚠ THE SENDER NOBODY HAS FILED YET (2026-08-19). An unmatched sender used to render `—` on
   the row and nothing in the pane, so a new prospect's email was anonymous in ticket mode.
   These two say "this is a person, and they are not a record" — muted and unlinked, so a
   matched client still reads as different at a glance without a badge shouting about it. */
.tkv-pane .tkv-unk { color: var(--muted); }
.tkv-pane .tkv-unkd { color: var(--muted); opacity: .75; }
/* The queue cell's twin. ⚠ Dotted underline rather than a colour change alone — colour is
   already carrying severity on this row, and a second meaning on the same channel is how a
   table stops being readable. */
.tq td .unk { color: var(--muted); border-bottom: 1px dotted #C9CED4; cursor: help; }

/* The rail's "Not a client yet" card. ⚠ Sits in `.tkv-ord`'s shell so it matches Linked orders
   and Classification; only its own two lines need rules. */
.tkv-pane .ncw { display: grid; gap: 6px; justify-items: start; }
.tkv-pane .ncw-name { font-size: 13.5px; font-weight: 700; color: var(--ink); }
.tkv-pane .ncw-sub { font-size: 12.5px; color: var(--muted); word-break: break-all; }

.tkv-note { padding: 16px; color: var(--muted); font-size: 13px; }
/* Session 9 — the standing way out to the classic inbox, quiet in the meta line. */
.tkv-pane .dhead .meta a.tkv-classic { color: #2A6F74; font-weight: 600; }

/* ── THE PROPERTY ROW (session 12d) — the ticket's own fields, Notion-style: a small labelled
   icon over a value you click to set. ⚠ The VALUE is the control, so an unset field ("Unassigned",
   "Nobody") is exactly as pressable as a set one — that is the whole difference from the button
   it replaced, which could only ever say "Assign…". */
.tkv-pane .tkv-props { display: flex; flex-wrap: wrap; gap: 18px; margin: 10px 0 2px; }
.tkv-pane .tkv-field { min-width: 0; }
.tkv-pane .tkv-field .fl { display: flex; align-items: center; gap: 5px; font-size: 11px;
  font-weight: 600; color: var(--faint); }
.tkv-pane .tkv-field .fv { display: block; margin-top: 2px; font: inherit; font-size: 12.5px;
  font-weight: 600; color: var(--ink); background: none; border: 0; border-radius: 6px;
  padding: 2px 6px; margin-left: -6px; text-align: left; cursor: pointer; }
.tkv-pane .tkv-field .fv.empty { color: var(--faint); font-weight: 500; }
.tkv-pane .tkv-field button.fv:hover { background: #EEF1F4; color: var(--ink); }
.tkv-pane .tkv-field span.fv { cursor: default; padding-left: 0; margin-left: 0; }

/* ── SESSION 12: THE TEXT STRETCH (docs/phase-ticket-texts.md) ─────────────────────────
   A text ticket IS a topic — a named, closeable stretch of one endless conversation — so the
   pane draws the WHOLE stream and lights the stretch. ⚠ `.bubble.other` is OPACITY, never
   display:none: dimmed context is still readable, and the far end has no dividers at all.
   ⚠⚠ THE BUBBLES ARE `.bubble`, the CLASSIC window's own (session 12d), because that is the
   contract portal-reactions.js reads: the hover cluster, the picker, copy, the select checkbox
   and `paintReactions`' `bubble.nextElementSibling` chips row. They were `.tks-row > .tks-bub`
   for one round and every one of those affordances was missing — including the far end's
   tapback, which folds out of the stream and then had nowhere to land.
   ⚠ Only the CHROME around them is `.tks-*`: admin-inbox.html defines `.bubble` in its own
   inline <style>, which lands after this file, so the two agree by SHARING rather than by
   this file trying to win — the `.bubble.out.sched` trap a few hundred lines up. */
.tkv-pane .tkv-thread.tks { display: flex; flex-direction: column; padding: 10px 14px 14px; }
/* ⚠ Scoped to the pane so the classic window's own bubbles are untouched. */
.tkv-pane .tkv-thread.tks .bubble.other { opacity: .42; }
.tkv-pane .tks-day { align-self: center; font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); margin: 12px 0 6px; }
/* The stretch's opening rule — teal, because it is a heading for what follows. */
.tkv-pane .tks-div { display: flex; align-items: center; gap: 8px; margin: 16px 0 8px; }
.tkv-pane .tks-div::before, .tkv-pane .tks-div::after { content: ""; flex: 1; height: 1px;
  background: var(--teal); opacity: .35; }
.tkv-pane .tks-div .lab { font-size: 11.5px; font-weight: 600; color: #1D5F63; background: #EAF4F5;
  border: 1px solid rgba(91,182,188,.45); border-radius: 999px; padding: 3px 10px; }
.tkv-pane .tks-div.mine .lab { box-shadow: 0 0 0 2px rgba(91,182,188,.18); }
/* ⚠ THE CLOSING LINE — grey and quiet on purpose: a fact about our side, not a heading. */
.tkv-pane .tks-div.closed::before, .tkv-pane .tks-div.closed::after { background: var(--line); opacity: 1; }
.tkv-pane .tks-div.closed .lab { color: var(--muted); background: #F1F3F5; border-color: var(--line);
  font-weight: 500; }
.tkv-pane .tks-div.closed .lab b { color: #1E7A4D; font-weight: 600; }
/* Activity nobody has named. Amber, and it carries the button that names it. */
.tkv-pane .tks-loose { display: flex; align-items: center; gap: 9px; margin: 14px 0 6px;
  padding: 6px 10px; border: 1px dashed #E0A44A; border-radius: 8px; background: #FBEEDE;
  color: #8A5A1F; font-size: 12px; }
.tkv-pane .tks-loose span { flex: 1; font-weight: 600; }
.tkv-pane .tks-earlier { align-self: stretch; font: inherit; font-size: 11.5px; color: var(--muted);
  background: none; border: 0; border-bottom: 1px dashed var(--line); padding: 6px 0 8px;
  cursor: pointer; margin-bottom: 4px; }
.tkv-pane .tks-earlier:hover { color: var(--ink); }
/* The multi-select move (session 12b). ⚠ `[hidden]` needs its OWN rule here: `display:flex`
   beats the attribute, which is the trap `.topicbar[hidden]` and `.cw-row[hidden]` both hit. */
.tkv-pane .tks-move { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; border-top: 1px solid #E8EBEE; background: #FBEEDE; color: #8A5A1F;
  font-size: 12px; font-weight: 600; }
.tkv-pane .tks-move[hidden] { display: none; }
.tkv-pane .tks-move span { flex: 1; }
.tkv-pane .tks-move .tbtn.quiet { background: none; border-color: transparent; color: #8A5A1F; }
.tkv-pane .tks-move .tks-s { display: none; }
/* ⚠ Measured at 390px: the count and the three buttons wrapped into a ragged two-row block
   with one button stranded on each. The count takes its own row and the labels shorten, so the
   three verbs sit together on one — rule 4, and the same shape the action bar above uses. */
@media (max-width: 560px) {
  .tkv-pane .tks-move span[data-tks-count] { flex: 1 0 100%; }
  .tkv-pane .tks-move .tks-l { display: none; }
  .tkv-pane .tks-move .tks-s { display: inline; }
}

/* The pane's composer — the shared text-window markup, sitting under the stream rather than
   in a docked window (there is no thread column to dock into in ticket mode). */
.tkv-pane .tks-comp { border-top: 1px solid #E8EBEE; padding: 8px 10px; background: #fff; }
.tkv-pane .tks-comp .sms-compose { margin: 0; }
.tkv-pane .tbtn.on { background: var(--navy); border-color: var(--navy); color: #fff; }

/* ── The email thread: From/To, signatures, quoted replies — the mock's chrome ── */
.tkv-pane .emsg { border-bottom: 1px solid #E8EBEE; }
.tkv-pane .emsg:last-child { border-bottom: 0; }
.tkv-pane .ehead { display: flex; gap: 10px; padding: 12px 16px 8px; align-items: flex-start; }
.tkv-pane .av { width: 30px; height: 30px; border-radius: 9px; flex: none; background: #E4E8ED;
  color: var(--muted); display: grid; place-items: center; font-size: 11px; font-weight: 700; }
.tkv-pane .emsg.out .av { background: var(--navy); color: #fff; }
.tkv-pane .en { flex: 1; min-width: 0; }
.tkv-pane .nm { font-size: 13px; font-weight: 600; }
.tkv-pane .ad { font-size: 11.5px; color: #9BA0A6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tkv-pane .tm { font-size: 11.5px; color: #9BA0A6; white-space: nowrap; }
/* Session 9 — each message's own Reply: quiet beside the timestamp, the same glyph as the
   header's Reply so the two read as one family (rule 6 — a set is judged as a set). */
.tkv-pane .ereply { font: inherit; font-size: 11px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; background: var(--white);
  color: var(--muted); display: inline-flex; align-items: center; gap: 4px; flex: none; }
.tkv-pane .ereply:hover { color: var(--ink); border-color: #C6CCD2; background: #F6F8FA; }
.tkv-pane .ebody { padding: 0 16px 13px 56px; font-size: 13.2px; color: #26303F; }
.tkv-pane .ebody p { margin: 0 0 9px; }
.tkv-pane .esig { margin-top: 11px; padding-top: 9px; border-top: 1px solid #E8EBEE;
  font-size: 11.5px; color: #9BA0A6; line-height: 1.45; }
.tkv-pane .equote { margin-top: 9px; font-size: 11.5px; color: #9BA0A6;
  border-left: 2px solid var(--line); padding-left: 9px; }
/* The scaled-frame wrapper CLIPS — a transform does not change layout, so without this a wide
   message's frame keeps its natural box and pushes the pane sideways. */
.tkv-pane .eframe { overflow: hidden; }
.tkv-pane .eframe iframe { width: 100%; border: 0; display: block; }
.tkv-pane .eatt { padding: 0 16px 12px 56px; display: flex; gap: 6px; flex-wrap: wrap; }
.tkv-pane .eatt-chip { font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; background: var(--white);
  color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }
.tkv-pane .eatt-chip:hover { color: var(--ink); border-color: #C6CCD2; }
.tkv-pane .eatt-chip.busy { opacity: .55; cursor: default; }

/* An internal note on a TASK ticket — no customer thread to mix into, so the tint says so.
   ⚠ portal.css's own bare `.note` (the teal info box, line ~156) reaches in here, so EVERY
   property it sets is re-asserted, not just the ones that looked wrong — the `.prow .pn span`
   lesson. Same cascade-collision family as `.row` below and the mock's own `.bar` note. */
.tkv-pane .emsg.note { background: #FFFBEB; border: 0; border-top: 1px solid #F2E2B8;
  border-bottom: 1px solid #F2E2B8; border-radius: 0; padding: 0; margin: 0;
  font-size: inherit; color: inherit; }
.tkv-pane .emsg.note + .emsg.note { border-top: 0; }
.tkv-pane .note .nm { color: #8A6D1F; }
.tkv-pane .note .ebody { color: #6B5518; }
.tkv-pane .note .badge { font-size: 9.5px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; background: #F5E3B0; color: #7A5F14; padding: 1px 6px;
  border-radius: 999px; margin-left: 7px; }

/* ── SESSION 5 — the rail and its Classification card (docs/phase-ticket-categories.md).
      The pane is a CONTAINER (its width is the modes' business, and a media query cannot see
      it): wide panes put the rail beside the thread, the mock's own layout; narrow panes —
      a slim peek, every phone — stack the card ABOVE the thread, where grid-template-areas
      lets one DOM order serve both. A browser without container queries just keeps the
      stacked layout, which is the honest degrade. ── */
.tkv-pane { container: tkv-pane / inline-size; }
.tkv-body { min-width: 0; }
.tkv-rail { padding: 12px 16px 0; min-width: 0; }
@container tkv-pane (min-width: 700px) {
  /* ⚠ TWO ROWS since session 12d: the conversation, then its composer — and the rail spans BOTH,
     because it is named in both rows. Before this the rail was one short item and `align-items:
     start` left its `border-left` ending wherever the last field did: the dividing line that
     "stops suddenly", with blank space under it (Eric, live). Stretch is what runs it to the
     bottom; the second row is what stops the composer auto-placing into an implicit one. */
  .tkv-body { display: grid; grid-template-columns: minmax(0, 1fr) 264px;
    grid-template-areas: "thread rail" "foot rail"; grid-template-rows: 1fr auto;
    align-items: stretch; }
  .tkv-body .tkv-thread { grid-area: thread; min-width: 0; }
  .tkv-body .tks-foot { grid-area: foot; min-width: 0; }
  .tkv-body .tkv-rail { grid-area: rail; border-left: 1px solid #E8EBEE; padding: 12px 14px; }
}
.tkv-cls { background: #FBFCFD; border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 13px 13px; }
.tkv-cls h4 { display: flex; align-items: center; gap: 7px; margin: 0 0 4px; font-size: 11px;
  font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
/* ⚠ .fl is scoped — portal.css has no bare .fl, but the mock's pages do, and a bare rule here
   would be the .row/.note cascade collision one more time. */
.tkv-cls .fl { display: block; margin: 9px 0 3px; font-size: 11px; font-weight: 600;
  color: var(--muted); }
.tkv-cls select { width: 100%; font-size: 12.5px; padding: 6px 9px; }
.tkv-cls .rv { display: flex; align-items: center; gap: 7px; margin-top: 11px; font-size: 12.5px;
  font-weight: 600; color: var(--ink); cursor: pointer; }
.tkv-cls .csave { display: flex; align-items: center; gap: 9px; margin-top: 12px; }
.tkv-cls .chint { font-size: 11px; color: var(--muted); }
/* The Suggested block — visibly the ROBOT'S lane: tinted, named, and going nowhere until a
   person presses Accept or saves an edit. */
.tkv-cls .sugg { background: #F3F6FB; border: 1px solid #DCE4F0; border-radius: 8px;
  padding: 8px 10px 9px; margin: 2px 0 6px; }
.tkv-cls .sugg .sh { display: flex; align-items: center; gap: 6px; font-size: 10.5px;
  font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: #35507F; }
.tkv-cls .sugg .sv { font-size: 12px; color: #26303F; margin: 5px 0 8px; line-height: 1.45; }
.tkv-cls .sugg .sb { display: flex; gap: 6px; }
.tkv-cls .sugg .sb button { font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); border-radius: 7px; padding: 4px 11px; background: var(--white);
  color: var(--ink); }
.tkv-cls .sugg .sb button:hover { background: #F6F8FA; }
.tkv-cls .sugg .sb button.acc { background: var(--navy); border-color: var(--navy); color: #fff; }
.tkv-cls .sugg .sb button.acc:hover { background: #1d2a4a; }
.tkv-cls .sugg .sb button:disabled { opacity: .55; cursor: default; }
.tkv-cls .sugg .sb button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* ── SESSION 8 — the Linked orders card, the Branch panel and the relation chips
      (portal-ticketview.js; docs/phase-ticket-links.md; the mock's rail-card treatment). ── */
.tkv-ord { background: #FBFCFD; border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 13px 13px; margin-bottom: 10px; }
.tkv-ord h4 { display: flex; align-items: center; gap: 7px; margin: 0 0 8px; font-size: 11px;
  font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
/* ⚠ SESSION 11 — THE INPUT GETS ITS OWN ROW, and this was measured rather than judged. The rail
   is ~220px at desktop, so with Find and Link beside it the box was **80px** — wide enough for an
   order number, which is what it used to take, and not wide enough to see an address you are
   half-way through typing. Wrapping puts it on a full-width line with the two buttons under it.
   ⚠ `align-items: stretch` stays: the two buttons must still match each other's height. */
.tkv-ord .ofind { display: flex; flex-wrap: wrap; gap: 6px; align-items: stretch;
  margin-bottom: 9px; }
/* ⚠ The bare `input, select` rule near the top of this file sets width:100% — re-assert the
   flex sizing or the number input throws the two buttons out of the card (the .callopt radio
   lesson, one more time). */
/* ⚠ SESSION 11 — `tabular-nums` is GONE: the box takes an address and a client name now, and
   tabular figures inside prose set the digits apart from the letters around them. */
.tkv-ord .ofind input { width: auto; flex: 1 1 100%; min-width: 0; font-size: 12.5px;
  padding: 6px 9px; background: #F9FAFB; }
/* The two buttons share the second line — Find takes the slack so the pair fills the row rather
   than sitting as two lumps against the left edge. */
.tkv-ord .ofind .ofmagic { flex: 1 1 auto; justify-content: center; }
.tkv-ord .ofind .oflink { flex: 0 0 auto; }
.tkv-ord .ofmagic { font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--teal); background: #E7F1F2; color: #1D5F63; border-radius: 8px;
  padding: 6px 10px; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.tkv-ord .ofmagic:hover { background: #DDEEF0; }
.tkv-ord .oflink { font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); background: var(--white); color: var(--ink); border-radius: 8px;
  padding: 6px 11px; }
.tkv-ord .oflink:hover { background: #F6F8FA; }
.tkv-ord .ofmagic:disabled, .tkv-ord .oflink:disabled { opacity: .55; cursor: default; }
.tkv-ord .ofmagic:focus-visible, .tkv-ord .oflink:focus-visible,
.tkv-ord .opick button:focus-visible, .tkv-ord .ounlink:focus-visible {
  outline: 2px solid var(--teal); outline-offset: 2px; }
.tkv-ord .opick { border: 1px solid var(--line); border-radius: 9px; overflow: hidden;
  margin-bottom: 9px; background: var(--white); }
/* ⚠ SESSION 11 — the picker SCROLLS at ~5 rows rather than growing. It is a type-ahead now, so
   it repaints while the rail beside it stays still; a list that grows and shrinks under the
   Classification card shoves that card down the screen on every keystroke. */
.tkv-ord .opick { max-height: 268px; overflow-y: auto; }
.tkv-ord .opick .ph { font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: #9BA0A6; padding: 7px 10px; background: #F7F9FB;
  border-bottom: 1px solid #EEF1F4; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* An EMPTY answer's sentence — plain case, quiet: it explains, it does not warn. */
.tkv-ord .opick .pnone { font-size: 12px; color: var(--muted); padding: 8px 10px; }
.tkv-ord .opick button { font: inherit; display: block; width: 100%; text-align: left; border: 0;
  border-bottom: 1px solid #EEF1F4; background: var(--white); cursor: pointer; padding: 8px 10px;
  font-size: 12px; }
.tkv-ord .opick button:last-child { border-bottom: 0; }
.tkv-ord .opick button:hover { background: #F4F7F9; }
/* The ARROW-KEY cursor. ⚠ A background alone is not enough — it is the same grey as :hover, so a
   mouse resting anywhere over the list would read as the selection. The teal edge is what says
   "Enter takes THIS one". */
.tkv-ord .opick button.on { background: #E7F1F2; box-shadow: inset 3px 0 0 var(--teal); }
.tkv-ord .opick button.on .on-sub { color: #6C7278; }
.tkv-ord .on-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.tkv-ord .on-sub { display: block; color: #9BA0A6; font-size: 11px; margin-top: 1px; }
.tkv-ord .oitem { border-top: 1px solid #EEF1F4; padding-top: 6px; margin-top: 6px; }
.tkv-ord .oitem:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.tkv-ord .okv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  font-size: 12.5px; padding: 3px 0; }
.tkv-ord .okv .k { color: var(--muted); white-space: nowrap; }
.tkv-ord .okv .v { font-weight: 600; text-align: right; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; display: inline-flex; align-items: center; gap: 6px;
  justify-content: flex-end; }
.tkv-ord .okv .v b { font-variant-numeric: tabular-nums; }
.tkv-ord .olk { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px;
  font-weight: 600; color: #B4531F; text-decoration: none; font-variant-numeric: tabular-nums; }
.tkv-ord .olk:hover { text-decoration: underline; }
.tkv-ord .osoon { font-size: 9.5px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; background: #EDF0F3; color: #9BA0A6; padding: 1px 6px;
  border-radius: 999px; }
.tkv-ord .ounlink { border: 0; background: none; color: #9BA0A6; cursor: pointer; padding: 2px;
  display: inline-grid; place-items: center; border-radius: 5px; flex: none; }
.tkv-ord .ounlink:hover { color: #9B2C2C; background: #F7ECEC; }
.tkv-ord .onone { font-size: 12px; color: #9BA0A6; margin: 2px 0 0; }
/* The Branch action and its panel — the mock's teal "second lane" (like Suggested is the
   robot's): branching is additive, and the tint keeps it visually apart from Close. */
.tkv-pane .tbtn.branch { border-color: #CFE7E9; background: #E7F1F2; color: #1D5F63; }
.tkv-pane .tbtn.branch:hover { background: #DDEEF0; }
.tkv-branchbox { border-bottom: 1px solid #E8EBEE; background: #F7FBFB; padding: 13px 16px; }
.tkv-branchbox .bh { display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
  color: #1D5F63; }
.tkv-branchbox .bh b { font-size: 13px; font-weight: 600; flex: 1; }
.tkv-branchbox .bx { background: none; border: 0; cursor: pointer; color: #9BA0A6; padding: 2px;
  display: grid; place-items: center; border-radius: 5px; }
.tkv-branchbox .bx:hover { color: var(--ink); }
.tkv-branchbox .bp { margin: 0 0 11px; font-size: 12.5px; color: var(--muted); max-width: 70ch; }
.tkv-branchbox .fl { display: block; font-size: 11px; font-weight: 600; color: var(--muted);
  margin: 9px 0 3px; }
.tkv-branchbox select, .tkv-branchbox input { font: inherit; font-size: 12.5px; width: 100%;
  padding: 6px 9px; border: 1px solid #CFE7E9; border-radius: 8px; background: var(--white);
  color: var(--ink); }
.tkv-branchbox select:focus, .tkv-branchbox input:focus { outline: 2px solid var(--teal);
  outline-offset: -1px; }
.tkv-branchbox .brow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px; }
.tkv-branchbox .bbtns { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  margin-top: 12px; }
.tkv-branchbox .bnote { font-size: 11.5px; color: #9BA0A6; }
/* The relation chip is a BUTTON that opens the other ticket — same chip body as its
   neighbours, plus the affordances a control needs. */
.tkv-pane .chip.rel { border: 1px solid #CFE7E9; background: #E7F1F2; color: #1D5F63;
  cursor: pointer; font: inherit; font-size: 10px; font-weight: 600;
  display: inline-flex; align-items: center; gap: 4px; }
.tkv-pane .chip.rel:hover { background: #DDEEF0; }
.tkv-pane .chip.rel svg { width: 11px; height: 11px; }
.tkv-pane .chip.rel:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* ── SESSION 6 — the internal conversation and the History panel (portal-ticketview.js;
      docs/phase-ticket-comments.md). ⚠⚠ VISUALLY UNMISTAKABLE is the requirement, not a
      styling choice: the whole comments section sits on the amber internal tint the pane's
      task notes already taught (.emsg.note's family), with the lock, the Internal badge and
      the standing "never sent to the client" line — a comment that could be mistaken for a
      client-visible reply is the failure mode that kills the feature. ── */
.tkv-activity { padding: 0 18px 18px; }
.tkc { background: #FFFBEB; border: 1px solid #F2E2B8; border-radius: 10px;
  padding: 12px 14px 14px; margin-top: 14px; }
.tkc-top h4 { display: flex; align-items: center; gap: 7px; margin: 0; font-size: 12px;
  letter-spacing: .04em; text-transform: uppercase; color: #8A6D1F; }
.tkc-n { font-size: 10.5px; font-weight: 700; background: #F2E2B8; color: #6B5416;
  border-radius: 999px; padding: 1px 7px; }
.tkc-note { margin: 4px 0 0; font-size: 11px; color: #A08430; }
.tkc-list { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
.tkc-empty { margin: 2px 0 4px; font-size: 12px; color: #A08430; }
.tkc-item { display: flex; flex-direction: column; align-items: flex-start; }
.tkc-who { display: flex; align-items: center; gap: 7px; font-size: 11.5px; margin: 0 0 3px;
  flex-wrap: wrap; }
.tkc-who time { margin-left: auto; white-space: nowrap; }
.tkc-who .av { width: 22px; height: 22px; border-radius: 50%; background: #E9D9A8;
  color: #6B5416; font-size: 9.5px; font-weight: 700; display: grid; place-items: center; }
.tkc-who .nm { font-weight: 700; color: var(--ink); white-space: nowrap; }
.tkc-who .badge { font-size: 9.5px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: #8A6D1F; background: #F6ECCB; border-radius: 4px;
  padding: 1px 6px; }
.tkc-who time { color: #A08430; font-size: 10.5px; }
/* The bubble wears portal-reactions' .bubble so its hover tools and picker work unchanged.
   ⚠ Scoped .tkc .tkc-bubble, not bare — the inbox page's own inline `.bubble` rule (the SMS
   thread's grey bubbles) ties a one-class selector and, being an inline sheet, wins the tie:
   the comments rendered as grey SMS bubbles on the amber card (found by LOOKING at 390px). */
.tkc .tkc-bubble { background: var(--white, #fff); border: 1px solid #EBDCAE; border-radius: 10px;
  padding: 8px 11px; font-size: 12.5px; line-height: 1.5; color: var(--ink); max-width: 62ch; }
.tkc .tkc-bubble .tkc-m { background: #E7F1F2; color: #1d5f63; font-weight: 600;
  border-radius: 5px; padding: 0 3px; }
.tkc-item .rxrow { margin: 4px 0 0; }
.tkc-compose { margin-top: 12px; position: relative; }
.tkc-compose textarea { width: 100%; box-sizing: border-box; font: inherit; font-size: 12.5px;
  line-height: 1.5; border: 1px solid #E4D190; border-radius: 9px; padding: 8px 11px;
  background: var(--white, #fff); resize: vertical; min-height: 54px; }
.tkc-compose textarea:focus { outline: 2px solid var(--teal); outline-offset: 1px; }
.tkc-actions { display: flex; align-items: center; gap: 9px; margin-top: 7px; }
.tkc-actions .tkc-hint { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px;
  color: #A08430; margin-right: auto; order: 1; }
/* ≤480px the hint squeezed into a three-line sliver between the buttons; the section's own
   standing line above already says the same sentence, so the duplicate yields the room. */
@media (max-width: 480px) { .tkc-actions .tkc-hint { display: none; } }
.tkc-actions [data-tkc-at] { order: 0; }
.tkc-actions [data-tkc-send] { order: 2; }
/* The @mention picker — floats over the composer, one row per teammate. */
.tkc-menu { position: absolute; left: 8px; bottom: calc(100% - 46px); z-index: 60;
  background: var(--white, #fff); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(13,21,41,.22); padding: 4px; min-width: 200px;
  max-height: 220px; overflow: auto; }
.tkc-menu button { display: flex; align-items: center; gap: 8px; width: 100%; font: inherit;
  font-size: 12.5px; border: 0; background: none; cursor: pointer; padding: 6px 9px;
  border-radius: 7px; color: var(--ink); text-align: left; }
.tkc-menu button:hover, .tkc-menu button.on { background: #EDF4F5; }
.tkc-menu .av { width: 20px; height: 20px; border-radius: 50%; background: #DCE4F0;
  color: #35507F; font-size: 9px; font-weight: 700; display: grid; place-items: center; }

/* The History panel — a LOG on one continuous rail, newest first, collapsible. */
.tkh { margin-top: 12px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--white, #fff); }
.tkh-toggle { display: flex; align-items: center; gap: 7px; width: 100%; font: inherit;
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); background: none; border: 0; cursor: pointer; padding: 10px 14px; }
.tkh-toggle .tkc-n { background: #EEF1F4; color: var(--muted); }
.tkh-toggle .chev { margin-left: auto; display: inline-flex; transition: transform .15s; }
.tkh-toggle.open .chev { transform: rotate(180deg); }
.tkh-toggle:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.tkh-body { padding: 2px 14px 12px; }
.tkh-rail { list-style: none; margin: 0; padding: 0 0 0 16px; position: relative; }
.tkh-rail::before { content: ""; position: absolute; left: 4px; top: 6px; bottom: 6px;
  width: 2px; background: #E8EBEE; border-radius: 2px; }
.tkh-rail li { position: relative; padding: 5px 0; }
.tkh-rail .dot { position: absolute; left: -16px; top: 11px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--white, #fff); border: 2px solid #B9C2CC;
  box-sizing: border-box; margin-left: 1px; }
.tkh-rail li:first-child .dot { border-color: var(--teal); }
.tkh-rail .ln { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.tkh-rail .what { font-size: 12.5px; color: var(--ink); }
.tkh-rail .who2 { font-size: 11px; color: var(--muted); }
.tkh-rail .tkh-none { font-size: 12px; color: var(--muted); }

/* ── The two segmented pickers (open mode + row density) ──
   ⚠ ABOVE the peek's scrim (z 58), found by driving the mounted page: below it, a click on
   "Split ‖" while a peek was open landed on the scrim and CLOSED the ticket instead — a mode
   picker that dismisses what you are trying to re-arrange. The mock's picker stays live while
   the peek is up; so does this one. */
/* ⚠ The .tkv-controls / .tkv-seg segmented pickers were REMOVED 2026-08-20 with the toolbar
   they lived in — how a ticket opens, row density and Time left all moved onto the settings
   sheet (portal-ticketview.js says why). Nothing else uses these classes; if a future round
   wants a segmented control back, the classic inbox's own chrome is the place to copy from
   rather than reviving a block no markup references. */

/* ⚠⚠ THE MOCK'S QUEUE ROWS ARE `tr.row`, AND portal.css ALREADY OWNS BARE `.row` — the form
   layout row, display:flex + wrap — so a queue lifted from the mock renders every ticket's
   cells WRAPPED into two stacked bands, on a page with zero console errors. Found by LOOKING
   at the first screenshot, measured as tr { display:flex }. This guard restores the table
   semantics for rows inside the ticket queue only (.tkq is initTicketView's own scope class),
   so session 1 can lift the mock's markup without renaming every row. */
.tkq tr.row { display: table-row; }

/* ── Row density. ⚠ COMPACT HIDES THE SECOND LINE, it never shrinks it — a half-height row
      with clipped text reads as broken; an intentionally single-line row does not. Everything
      is scoped under .tkq, the class initTicketView puts on whatever element the page hands
      it, so nothing outside ticket mode is touched.
      ⚠ TWO ROW VOCABULARIES, both covered, because the two ticket sessions ran in parallel and
      only one of them could see the mock: session 1's real queue (admin-inbox.html, `.tq-subj
      .s2` is the second line; its chips ride INSIDE .s1 and cost no height, so they stay) and
      the mock's markup (.subline/.chips/.sub/.sla .cd — the driver's demo, and any surface
      lifted from the mock later). The padding rule is generic and already covers both. ── */
.tkq.density-compact tbody td { padding: 4px 12px; }
.tkq.density-compact .tq td { padding: 4px 10px; }
/* Compact keeps the PREVIEW now, inline after the subject in normal weight (Eric,
   2026-08-19 — the Gmail arrangement: bold subject, an em dash, then the message text,
   still one line). The TD is the clipper: an inline child cannot carry its own ellipsis,
   so both spans go display:inline and the cell's own max-width + nowrap does the cutting.
   ⚠ Verified in the harness, not assumed — text-overflow on a table cell only behaves with
   max-width + overflow hidden, which .tq .tq-subj already declares. */
.tkq.density-compact .tq-subj { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tkq.density-compact .tq-subj .s1, .tkq.density-compact .tq-subj .s2 { display: inline; }
.tkq.density-compact .tq-subj .s2::before { content: ' \2014  '; }
.tkq.density-compact .subline, .tkq.density-compact .chips,
.tkq.density-compact .sub, .tkq.density-compact .sla .cd { display: none; }
.tkq.density-compact .subj { max-width: 46ch; }
/* ⚠ COMPACT PROMISES ONE LINE PER TICKET — its own button says so — and the mailbox line
   broke it: measured at 51-54px against 32px for the rows without one, so the five email rows
   stood a row and a half tall in the density chosen to fit more on screen. Inline beside the
   channel here ("Email · atlanta"), stacked in Comfortable. The fact survives either way,
   which is the point: a density picker changes the shape, never what the row tells you. */
.tkq.density-compact .tq-mbox { display: inline; margin: 0 0 0 5px; }
.tkq.density-compact .tq-mbox::before { content: '\00b7  '; }
/* ⚠ …and the CELL has to stop wrapping, or inline buys nothing: "Email" and "atlanta" simply
   broke between the two spans instead of between two blocks, and the row measured 54px either
   way. The table is auto-layout, so a nowrap cell claims the width it needs.
   ⚠ HONEST REMAINDER, measured rather than assumed: four of the five email rows come back to
   33px, and ONE does not — the widened Channel column takes its width from Client, whose
   "Dana Whitfield" then wraps to two lines (it already wrapped that way in Comfortable, so
   this is column competition, not a second line of our own). Left alone: making the Client
   column nowrap too would push the table into its sideways scroller at every width, which is
   a worse trade than one wrapped name. */
.tkq.density-compact .tq-chancell { white-space: nowrap; }
.tkq.density-compact .sla .lab { font-size: 10px; padding: 1px 7px; }

/* ── ≤960px: EVERY open mode presents as full page, in flow, and the body never scrolls
      sideways. Selector arity deliberately MATCHES the desktop rules ([data-mode] with no
      value ties [data-mode="…"]) so source order settles it without !important. ── */
@media (max-width: 960px) {
  .tkv[data-open][data-mode] { display: block; height: auto; }
  .tkv[data-open][data-mode] .tkv-queueslot,
  .tkv[data-open][data-mode] .tkv-grip,
  .tkv[data-open][data-mode] .tkv-scrim { display: none; }
  .tkv[data-open][data-mode] .tkv-pane,
  .tkv.fs[data-open][data-mode] .tkv-pane { position: static; width: auto; height: auto; }
  .tkv[data-open][data-mode] .tkv-scroll { overflow: visible; }
  .tkv[data-open] .tkv-winbar [data-tkv="fs"] { display: none; }
  /* The 56px thread indent is a desktop luxury; at 390px it costs a fifth of the line. */
  .tkv-pane .ebody, .tkv-pane .eatt { padding-left: 16px; }
  /* ⚠ SIX ACTIONS ON A PHONE (2026-08-20). Adding Assign… pushed Branch onto a THIRD row at
     390px — measured in the harness, and exactly the defect rule 4 exists to catch (it is the
     same report Eric filed about the call window). The row pays for it out of its own padding
     and the buttons' own, rather than any button losing its icon or its label: at this width
     the pane IS the page, so the 16px gutter is decoration and the 12px inside each button is
     a comfortable-density touch target that 10px does not stop being. Two rows again, with
     room for a long name on the assign button — re-measure this block before adding a
     seventh. */
  .tkv-pane .dact { padding-left: 10px; padding-right: 10px; }
  .tkv-pane .dact .tbtn { padding-left: 10px; padding-right: 10px; }
}
