﻿/* ============================================================================
   TrialCatcher Network — shell + components. Every value is a --net-* token
   (tokens.css). No hex, no px shadow, no duration literal in here on purpose.
   ============================================================================ */

@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/SpaceGrotesk-Variable.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
html { overflow: hidden; }
body {
  font-family: var(--net-font-body);
  font-size: var(--net-fs-body);
  line-height: var(--net-lh-body);
  color: var(--net-ink);
  background: var(--net-canvas);
  -webkit-font-smoothing: antialiased;
  /* ⭐⭐ THE PAGE IS PINNED BY overflow, NOT BY position:fixed — and the difference is the whole
     Chrome-for-iOS restored-tab bug (Marcos, iPhone 16, reported 2026-09-03, measured 2026-09-04).

     THE MEASUREMENT, because four rounds of guessing came from not having one. The 09-04 screenshot was
     read pixel by pixel against the known 40px tab bar, which fixes the scale exactly (735 image px =
     402 CSS px, an iPhone 16):

       · web content area          684 CSS px tall   — which is EXACTLY what ?vhdebug=1 reported
       · shell height rendered     684 CSS px        — the height was never wrong, in any of the four rounds
       · shell TOP                 −112 CSS px       — the app is painted 112px ABOVE the content area
       · Chrome's own top chrome   113 CSS px

     The offset IS Chrome's top toolbar, to the pixel. It is not the app header (56px), it is not
     header+footer (96px), and it is not a scroll offset. On a restored tab Chrome for iOS anchors the
     FIXED viewport to the top of the screen instead of the top of the web content area, while still
     reporting the correct height to every API the page can call. That is why no amount of measuring
     could see it — and it is why the previous fix, which pinned <body> with position:fixed, handed the
     entire application to the one coordinate system Chrome was getting wrong.

     So the pin is done with overflow instead. `overflow: hidden` on <html> is what actually stops iOS
     scrolling the document (it scrolls the documentElement, so body alone was never enough) — that half
     was correct and is kept. position:fixed on <body> was belt-and-braces on top of it, and it is the
     half that has to go: an in-flow body is laid out in the DOCUMENT, whose origin is painted at the top
     of the content area on a restored tab like it is everywhere else.

     ⚠ The same reasoning is why every shell-anchored overlay below (drawers, scrim, dialogs, dev bar)
     is `position: absolute` against this body rather than `position: fixed`. Same box, same geometry,
     one coordinate system — do not "simplify" any of them back to fixed.

     ⛔ The page is not SUPPOSED to scroll — .net-col does. Any future rule that wants the window to
     scroll has to reckon with the shell being exactly viewport-height. */
  position: relative;                     /* the containing block for every shell-anchored overlay */
  overflow: hidden;                       /* the PAGE never scrolls — columns do */
}
a { color: var(--net-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font: inherit; color: inherit; }
img { max-width: 100%; display: block; }
/* ⭐ :focus is the FALLBACK, :focus-visible is the refinement — in that order, on purpose.
   The sheet used to style :focus-visible only, so any engine that does not implement it (older
   Safari/WebKit builds, some embedded webviews) showed NO focus indicator at all: a keyboard user had
   no idea where they were on the page (WCAG 2.4.7). The :not(:focus-visible) line immediately below
   takes the ring back off for mouse clicks in every modern browser, so nothing changes visually there.
   ⚠️ The fallback is OUTLINE-ONLY on purpose. It used to also set box-shadow and border-radius, which
   meant every mouse click ran :focus (painting a ring, then having box-shadow:none strip the element's
   OWN design shadow) and re-rounded corners the component had deliberately squared. Outline paints
   over nothing and rounds nothing, so removing it restores the element exactly. The shadow ring and the
   radius belong to :focus-visible, where they only ever apply to real keyboard focus. */
:focus { outline: var(--net-focus-width) solid var(--net-focus); outline-offset: var(--net-focus-offset); }
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: var(--net-focus-width) solid var(--net-focus); outline-offset: var(--net-focus-offset); box-shadow: var(--net-focus-ring); border-radius: var(--net-radius-xs); }
/* First tab stop on every page (MainLayout). Off-screen until focused, then pinned under the header. */
.skip-link { position: absolute; left: 8px; top: -60px; z-index: 100; padding: 10px 16px; border-radius: var(--net-radius-sm); background: var(--net-primary); color: var(--net-on-primary); font-weight: var(--net-fw-semibold); text-decoration: none; transition: top .12s ease; }
.skip-link:focus, .skip-link:focus-visible { top: 8px; text-decoration: none; }
#main:focus { outline: none; box-shadow: none; }   /* the skip target takes focus programmatically; don't ring the whole column */
h1, h2, h3, h4, p { margin: 0; }
h1:focus, h1:focus-visible { outline: none; box-shadow: none; }
h1 { font: var(--net-fw-bold) var(--net-fs-h1)/var(--net-lh-h1) var(--net-font-display); letter-spacing: var(--net-ls-h1); }
h2 { font: var(--net-fw-semibold) var(--net-fs-h2)/var(--net-lh-h2) var(--net-font-display); letter-spacing: var(--net-ls-h2); }
h3 { font: var(--net-fw-semibold) var(--net-fs-h3)/var(--net-lh-h3) var(--net-font-body); }
.display { font: var(--net-fw-bold) var(--net-fs-display)/var(--net-lh-display) var(--net-font-display); letter-spacing: var(--net-ls-display); }
.eyebrow { font: var(--net-fw-semibold) var(--net-fs-eyebrow)/var(--net-lh-eyebrow) var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); }
.small { font-size: var(--net-fs-small); line-height: var(--net-lh-small); }
.muted { color: var(--net-ink-2); }
.meta { color: var(--net-ink-3); font-size: var(--net-fs-small); }
.num { font-family: var(--net-font-display); font-variant-numeric: tabular-nums; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-5 { display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }

/* ─── SHELL ───────────────────────────────────────────────────────────────── */
.net-shell {
  display: grid;
  grid-template-rows: var(--net-header-h) minmax(0, 1fr) var(--net-footer-h);
  grid-template-columns: var(--net-menu-w) minmax(0, 1fr);
  grid-template-areas: "header header" "menu cols" "footer footer";
  /* Three deep, and the order is the whole point: 100vh for anything ancient, 100dvh for the modern
     default, then the MEASURED visualViewport height (network.js) which is the only value iOS Safari
     reports correctly while its toolbars are settling. Without the last one the app renders short and
     leaves a dead band of page background under the tab bar. */
  height: 100vh; height: 100dvh; height: var(--app-vh, 100dvh);
  transition: grid-template-columns var(--net-dur-slow) var(--net-ease);
}
.net-shell.menu-collapsed { --net-menu-w: var(--net-menu-w-collapsed); }

/* BARE SHELL (BareLayout) — header · body · footer, no menu and no columns. Used by /change-password,
   which must not offer a way out: see the note at the top of Components/Layout/BareLayout.razor.
   Same three rows as .net-shell so the header/footer heights and the .auth-body min-height calc that
   depends on them keep working unchanged. */
.net-bare {
  display: grid;
  grid-template-rows: var(--net-header-h) minmax(0, 1fr) var(--net-footer-h);
  height: 100vh; height: 100dvh; height: var(--app-vh, 100dvh);
}
/* .net-header/.net-footer are grid-area children of .net-shell; here they are plain rows. */
.net-bare > .net-header { grid-area: auto; display: flex; align-items: center; padding: 0 var(--net-space-4); }
.net-bare > .net-footer { grid-area: auto; }
.net-bare > .net-bare-work { min-height: 0; min-width: 0; overflow-y: auto; }
.net-bare .net-brand { cursor: default; }   /* not a link on purpose — nothing here navigates */

/* header */
.net-header {
  /* A GRID whose tracks mirror the shell (menu + context | work | chat) so .net-lead (brand + search) ends exactly at the
     context column's right border at every width / collapse state. Keep these track lists in lockstep with .net-shell / .net-cols. */
  grid-area: header; display: grid; align-items: center;
  grid-template-columns: var(--net-menu-w) minmax(230px, 3fr) minmax(0, 8fr) minmax(260px, 3fr);
  padding: 0;
  background: var(--net-header-bg); color: var(--net-header-ink); border-bottom: 1px solid var(--net-header-line);
  z-index: 30; transition: grid-template-columns var(--net-dur-slow) var(--net-ease);
}
.net-shell.right-none .net-header { grid-template-columns: var(--net-menu-w) minmax(230px, 3fr) minmax(0, 11fr) 0; }
.net-header .net-lead { grid-column: 1 / 3; display: flex; align-items: center; gap: var(--net-space-3); min-width: 0; padding-left: var(--net-space-3); }
.net-header .net-actions { grid-column: 3 / 5; display: flex; align-items: center; justify-content: flex-end; gap: var(--net-space-3); min-width: 0; padding-right: var(--net-space-4); }
.net-brand { display: flex; align-items: center; gap: var(--net-space-2); color: var(--net-header-ink-active); text-decoration: none; min-width: calc(var(--net-menu-w) - var(--net-space-3) * 2); transition: min-width var(--net-dur-slow) var(--net-ease); } /* − lead padding − lead gap, so .net-search starts exactly at the context column border */
.net-brand:hover { text-decoration: none; }
.net-brand .logo-wide { height: 40px; width: auto; display: block; background: #fff; padding: 3px 8px; border-radius: var(--net-radius-sm); }
.net-brand .logo-compact { height: 36px; width: 36px; flex: 0 0 36px; object-fit: contain; display: none; background: #fff; padding: 2px; border-radius: var(--net-radius-sm); }
.menu-collapsed .net-brand .logo-wide { display: none; }
.menu-collapsed .net-brand .logo-compact { display: block; }
.net-brand .word { font: var(--net-fw-bold) 17px/1 var(--net-font-display); letter-spacing: -.01em; white-space: nowrap; }
.net-brand .word b { color: var(--net-secondary); font-weight: inherit; }
.menu-collapsed .net-brand .word { display: none; }
.net-header .spacer { flex: 1; }
.net-search { display: flex; align-items: center; gap: var(--net-space-2); background: var(--net-header-raise); border: 1px solid var(--net-header-line); border-radius: var(--net-radius-sm); padding: 0 var(--net-space-3); height: 36px; flex: 1; min-width: 0; color: var(--net-header-ink-muted); } /* fills .net-lead to the context border */
.net-search input { flex: 1; background: none; border: 0; color: var(--net-header-ink-active); outline: none; font-size: var(--net-fs-small); min-width: 0; }
.net-search input::placeholder { color: var(--net-header-ink-muted); }
/* Conversation search inside the Smoke Signals panel — same pill, card colours. .col-body is a flex COLUMN, so the header's
   `flex: 1` (basis 0) squashed it to 19px; it must be a fixed-height row there. Specificity (0,3,0) also beats the header
   breakpoints (34vw width at 1100, display:none under 860) which this one must ignore. */
.col-body > .net-search.chat-search { display: flex; flex: 0 0 auto; width: 100%; height: 36px; background: var(--net-card); border-color: var(--net-hairline-strong); color: var(--net-ink-3); }
.col-body > .net-search.chat-search input { color: var(--net-ink); }
.net-header .hbtn { height: 36px; min-width: 36px; padding: 0 var(--net-space-2); display: inline-flex; align-items: center; justify-content: center; gap: var(--net-space-2); background: transparent; border: 0; border-radius: var(--net-radius-sm); color: var(--net-header-ink); cursor: pointer; position: relative; }
.net-header .hbtn:hover { background: var(--net-header-raise); color: var(--net-header-ink-active); }
.net-header .hbtn .dot { position: absolute; top: 6px; right: 6px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--net-radius-pill); background: var(--net-primary); color: var(--net-on-primary); font: var(--net-fw-bold) 10px/16px var(--net-font-display); text-align: center; }
.net-user { display: inline-flex; align-items: center; gap: var(--net-space-2); padding: 4px 8px 4px 4px; border-radius: var(--net-radius-sm); background: var(--net-header-raise); border: 1px solid var(--net-header-line); color: var(--net-header-ink-active); cursor: pointer; }
.net-user .name { font-size: var(--net-fs-small); font-weight: var(--net-fw-medium); white-space: nowrap; }
.net-user .role { font-size: var(--net-fs-eyebrow); color: var(--net-header-ink-muted); letter-spacing: .04em; text-transform: uppercase; }
.net-user-menu { position: absolute; right: var(--net-space-4); top: calc(var(--net-header-h) - 6px); width: 260px; background: var(--net-card); color: var(--net-ink); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-lg); box-shadow: var(--net-elev-3); padding: var(--net-space-2); z-index: 40; }
.net-user-menu .hd { padding: var(--net-space-2) var(--net-space-3); border-bottom: 1px solid var(--net-hairline); margin-bottom: var(--net-space-1); }
.net-user-menu a, .net-user-menu button { display: flex; align-items: center; gap: var(--net-space-2); width: 100%; padding: 8px var(--net-space-3); border: 0; background: none; border-radius: var(--net-radius-sm); color: var(--net-ink); text-align: left; cursor: pointer; font-size: var(--net-fs-small); }
.net-user-menu a:hover, .net-user-menu button:hover { background: var(--net-sunken); text-decoration: none; }
.net-user-menu .sec { padding: 8px var(--net-space-3) 2px; }
.hamb { display: none !important; }
/* Click-outside closes the user menu (Marcos 2026-08-31). A transparent full-viewport catcher UNDER
   the menu (39 vs the menu's 40) — not a document listener and not @onfocusout, which would fire on
   the act-as <select> the moment it takes focus and close the menu out from under the click. */
.net-user-scrim { position: absolute; inset: 0; z-index: 39; background: transparent; }


/* left menu */
.net-menu { grid-area: menu; background: var(--net-chrome); color: var(--net-chrome-ink); display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--net-chrome-line); z-index: 25; }
/* Scrolling matches the backoffice sidenav (simplebar): no visible scrollbar until you hover the menu, then a slim overlay thumb — the rows never lose width to a gutter. */
.net-menu nav { flex: 1; overflow-y: auto; overflow-x: hidden; padding: var(--net-space-3) var(--net-space-2); scrollbar-width: thin; scrollbar-color: transparent transparent; }
.net-menu nav:hover { scrollbar-color: var(--net-chrome-raise) transparent; }
.net-menu .grp { margin-bottom: var(--net-space-3); display: flex; flex-direction: column; gap: 2px; } /* 2px so hover + active rows never touch */
/* The group header is a BUTTON (Marcos 2026-09-02): click the category, the category collapses —
   the backoffice sidenav's behaviour, brought over. Label · hairline · chevron, same three parts. */
/* ⛔ NO `font`/`letter-spacing`/`text-transform` HERE. The header carries .eyebrow, whose `font:`
   shorthand IS the typography (11px caps, .08em); this selector out-specifies it, so declaring any of
   the three resets the label to the browser's button default — which is exactly what happened the
   first time it became a <button> (Marcos: "you also changed the text font family"). The UA button
   font loses to .eyebrow on its own, author beats user-agent — so say nothing and it inherits right.
   Padding-left is 10px, not 12px: it lines the label up with the rows below it. */
.net-menu .grp-label { display: flex; align-items: center; width: 100%; padding: var(--net-space-3) 10px var(--net-space-1); color: var(--net-chrome-ink-muted); background: none; border: 0; text-align: left; cursor: pointer; user-select: none; }
.net-menu .grp-label:hover, .net-menu .grp-label:focus-visible { color: var(--net-chrome-ink-active); }
/* currentColor so the rule tracks the label's own colour and brightens with it on hover. */
/* ⭐ THE LABEL WINS THE ROW, THE RULE TAKES WHAT IS LEFT (Marcos 2026-09-02: "TrialCatcher SuperAdmin
   doesn't have enough space for one line"). At 235px of menu the longest header measures 163px and the
   rule's old 12px floor + 20px of margin pushed it onto a second line. `min-width: 0` lets the hairline
   shrink instead of the words wrapping — it ends up ~9px on that header and full width on every other.
   The label ellipsizes rather than spilling, so a longer name later degrades quietly. */
.net-menu .grp-label > span:first-child { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.net-menu .grp-label .rule { flex: 1 1 auto; height: 1px; min-width: 0; margin: 0 6px; background: currentColor; opacity: .45; }
.net-menu .grp-label .chev { flex: none; display: grid; place-items: center; transition: transform var(--net-dur) var(--net-ease); }
.net-menu .grp-label[aria-expanded="false"] .chev { transform: rotate(-90deg); }
/* Collapsed = the rows are HIDDEN, not removed: keeping them in the DOM is what lets the rail and the
   narrow breakpoints below force every row back on screen, where the header that would re-open them
   is itself hidden. Removing them in C# would strand a clinic user with no way back to their tools. */
.net-menu .grp.collapsed a.item { display: none; }
/* "View as a clinic" — the SuperAdmin's clinic-context switch, in the user menu above Settings (Marcos 2026-08-26). */
.net-user-menu .actas { display: flex; flex-direction: column; gap: 4px; padding: 8px 12px 6px; border-top: 1px solid var(--net-hairline); }
.net-user-menu .actas .select { height: 32px; font-size: var(--net-fs-small); }
.menu-collapsed .net-menu .grp-label { display: none; }
/* ⛔ THE INVARIANT: wherever the header is hidden, the rows are shown. Rail mode hides every
   grp-label (the rule directly above), and the only control that re-opens a collapsed group is that
   header — so a collapsed group in the rail would be a group with no way back. Deliberately NOT
   wrapped in a breakpoint: .menu-collapsed survives a resize down to mobile, where it still hides the
   headers, and this must follow it there. Four classes, so it also outranks the 859px block below. */
.menu-collapsed .net-menu .grp.collapsed a.item { display: flex; }
.net-menu a.item { display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 10px; border-radius: var(--net-radius-sm); color: var(--net-chrome-ink); font-size: var(--net-fs-small); font-weight: var(--net-fw-medium); white-space: nowrap; text-decoration: none; position: relative; }
.net-menu a.item:hover { background: var(--net-chrome-raise); color: var(--net-chrome-ink-active); }
.net-menu a.item.active { background: var(--net-chrome-raise); color: var(--net-chrome-ink-active); }
.net-menu a.item.active::before { content: ""; position: absolute; left: -8px; top: 10px; bottom: 10px; width: 3px; border-radius: 3px; background: var(--net-secondary); }
.net-menu a.item .ico { width: 26px; height: 20px; display: grid; place-items: center; flex: none; }
.net-menu a.item .badge { margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--net-radius-pill); background: var(--net-primary); color: var(--net-on-primary); font: var(--net-fw-bold) 11px/20px var(--net-font-display); text-align: center; }
.menu-collapsed .net-menu a.item { justify-content: center; padding: 0; }
.menu-collapsed .net-menu a.item .label, .menu-collapsed .net-menu a.item .badge { display: none; }
.net-menu .mfoot { display: flex; align-items: center; justify-content: space-between; padding: var(--net-space-2); border-top: 1px solid var(--net-chrome-line); }
.net-menu .mfoot button { height: 32px; min-width: 32px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; background: none; border: 0; border-radius: var(--net-radius-sm); color: var(--net-chrome-ink-muted); cursor: pointer; font-size: var(--net-fs-eyebrow); letter-spacing: .04em; text-transform: uppercase; padding: 0 8px; }
.net-menu .mfoot button:hover { color: var(--net-chrome-ink-active); background: var(--net-chrome-raise); }
.menu-collapsed .net-menu .mfoot { justify-content: center; }
.menu-collapsed .net-menu .mfoot .lbl { display: none; }

/* the four columns */
.net-cols { grid-area: cols; display: grid; grid-template-columns: minmax(230px, 3fr) minmax(0, 8fr) minmax(260px, 3fr); min-height: 0; min-width: 0; }
/* Smoke Signals X (desktop): the right column slides to 0 and the work column takes the space; header/tabbar chat button brings it back. Mobile (≤859) ignores this — the chat is a drawer there. */
.net-cols { transition: grid-template-columns var(--net-dur-slow) var(--net-ease); }
@media (min-width: 860px) {
  .net-shell.right-none .net-cols { grid-template-columns: minmax(230px, 3fr) minmax(0, 11fr) 0; } /* context keeps its 3/14 share; the WORK column absorbs the chat's 3fr */
  .net-shell.right-none .net-col.chat { visibility: hidden; transition: visibility 0s linear var(--net-dur-slow); }
  .net-shell.right-none .net-col.work { border-right: 0; }
}
.net-col.work { container-type: inline-size; container-name: work; }
.net-col { min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; border-right: 1px solid var(--net-hairline); scrollbar-width: thin; scrollbar-color: var(--net-ink-faint) transparent; }
.net-col:last-child { border-right: 0; }
.net-col.context { background: var(--net-band-context); }
.net-col.work { background: var(--net-canvas); }
.net-col.chat { background: var(--net-band-chat); position: relative; overflow: hidden; }
/* Right column = three stacked layers (Smoke Signals · Notifications · Cetan); ONE visible at a time (.net-shell.right-*).
   The active layer sits at 0; an entering layer slides in from the right, the leaving one exits left — a swap, not a cover. */
.rp-layer { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; scrollbar-width: thin; background: var(--net-band-chat); transform: translateX(100%); visibility: hidden; transition: transform var(--net-dur-slow) var(--net-ease), visibility 0s linear var(--net-dur-slow); }
/* visibility flips at 0s when a layer ENTERS (so it is seen sliding in) and only after the slide when it LEAVES. */
.net-shell.right-chat .rp-chat, .net-shell.right-notifications .rp-notif, .net-shell.right-cetan .rp-cetan { transform: none; visibility: visible; transition-delay: 0s, 0s; }
.net-shell.right-notifications .rp-chat, .net-shell.right-cetan .rp-chat, .net-shell.right-cetan .rp-notif { transform: translateX(-100%); } /* layers "to the left" of the active one exit left */

/* ─── RIGHT PANELS: the backoffice sheet, in network colours ────────────────
   Marcos 2026-08-31 — Smoke Signals · Notifications · Cetan wear the backoffice panel combination
   (white sheet, grey hairlines, tonal grey for anything that used to be a raised card), with the
   backoffice's BLUE accent replaced by the network's green + orange. White background is the point:
   the beige band was costing readability on a surface that is mostly text.
   The band token is re-declared HERE, on the column, so every rule that already paints
   --net-band-chat inside it (.rp-layer, .thread .send, .cetan-send, .cetan-backbar) follows without
   being edited one by one. --net-card, never a literal #fff — dark theme keeps its own surface. */
.net-col.chat { --net-band-chat: var(--net-card); }
/* Solid head, like .tc-comm-panel-head — glass over a white sheet is just white with a blur cost. */
.net-col.chat .col-head { background: var(--net-card); backdrop-filter: none; -webkit-backdrop-filter: none; border-bottom-color: var(--net-hairline); }
/* Everything that read as a WHITE CARD on the old band would now vanish into the sheet: those take
   the tonal step down (--net-sunken), exactly as the backoffice does with --ct-light. */
.net-col.chat .bubble, .net-col.chat .bubble.cetan { background: var(--net-sunken); border-color: transparent; }
.net-col.chat .bubble.me { background: var(--net-primary); color: var(--net-on-primary); border-color: transparent; }
.net-col.chat .col-body > .net-search.chat-search { background: var(--net-sunken); }
.net-col.chat .cetan-starter { background: var(--net-sunken); }
.net-col.chat .cetan-starter:hover { background: var(--net-card); border-color: var(--net-primary); }
.net-col.chat .cetan-backbar--fwd { background: var(--net-sunken); }
/* Conversation list = the backoffice's .tc-comm-row: hairline-separated rows on the sheet, a flat
   tint on hover. The old "raised white card" hover cannot work when the sheet is already white. */
.net-col.chat .chat-list { gap: 0; }
.net-col.chat .chat-row { border-radius: 0; padding: 10px 8px; border-bottom: 1px solid var(--net-hairline); }
.net-col.chat .chat-row:hover, .net-col.chat .chat-row.active { background: rgba(0,0,0,.035); box-shadow: none; }
[data-theme="dark"] .net-col.chat .chat-row:hover, [data-theme="dark"] .net-col.chat .chat-row.active { background: rgba(255,255,255,.06); }
/* The one ORANGE accent: unread counts. Green is the panel's action colour (send, links, my bubble),
   so the thing demanding attention must not be the same colour as the thing you press.
   --net-secondary-ink, not --net-secondary: white on #C97A4E is 3.1:1 and this is 10px text. */
.net-col.chat .chat-row .unread { background: var(--net-secondary-ink); color: #fff; }
/* The panel's empty states get a bigger plate than a page's (Marcos 2026-08-31): a 360px column
   with one sentence in it needs the mark to carry the space. */
.net-col.chat .empty > .ico { width: 56px; height: 56px; }
.net-col.chat .empty > .ico svg { width: 26px; height: 26px; }


.net-header .hbtn.on { background: var(--net-header-raise); color: var(--net-header-ink-active); }
.net-shell.right-chat .hbtn-chat, .net-shell.right-notifications .hbtn-notif, .net-shell.right-cetan .hbtn-cetan { background: var(--net-header-raise); color: var(--net-header-ink-active); }
@media (max-width: 859px) { .net-shell:not(.chat-open) .hbtn-chat, .net-shell:not(.chat-open) .hbtn-notif, .net-shell:not(.chat-open) .hbtn-cetan { background: transparent; color: var(--net-header-ink); } }
.net-col.brand { background: var(--net-canvas); background-image: var(--net-aurora); }
.col-head { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: var(--net-space-2); min-height: 48px; padding: 0 var(--net-panel-pad); background: var(--net-glass-bg); backdrop-filter: blur(var(--net-glass-blur)); -webkit-backdrop-filter: blur(var(--net-glass-blur)); border-bottom: 1px solid var(--net-glass-border); }
.col-head .eyebrow { color: var(--net-ink-2); }
.col-head .grow { flex: 1; }
.col-body { padding: var(--net-panel-pad); display: flex; flex-direction: column; gap: var(--net-card-gap); }
.work .col-body { padding-bottom: var(--net-space-7); }
.work-wide { max-width: 1120px; }

/* footer */
.net-footer { grid-area: footer; display: flex; align-items: center; gap: var(--net-space-3); padding: 0 var(--net-space-4); background: var(--net-chrome); color: var(--net-chrome-ink-muted); border-top: 1px solid var(--net-chrome-line); font-size: var(--net-fs-small); z-index: 30; }
.net-footer .spacer { flex: 1; }
.net-footer .patent { color: var(--net-chrome-ink-muted); white-space: nowrap; letter-spacing: .01em; } /* centered between the two spacers (Marcos 2026-08-23) */
.net-footer a { color: var(--net-chrome-ink); }
.net-footer .links { margin-right: 10px; }
/* Language switcher (tc-translate.js drives value + reload; Blazor only renders the control). */
.net-footer .net-lang-wrap { position: relative; display: inline-flex; align-items: center; }
.net-footer .net-lang { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 8px; border: 1px solid var(--net-chrome-line); border-radius: var(--net-radius-sm); background: var(--net-chrome-raise); color: var(--net-chrome-ink); font: inherit; font-size: var(--net-fs-small); font-weight: 600; letter-spacing: .04em; cursor: pointer; }
.net-footer .net-lang:hover, .net-footer .net-lang:focus-visible, .net-footer .net-lang[aria-expanded="true"] { color: var(--net-chrome-ink-active); border-color: var(--net-chrome-ink-muted); outline: none; }
.net-footer .net-lang.is-busy svg { animation: net-lang-pulse 1s ease-in-out infinite; }
.net-footer .net-lang-menu { position: absolute; right: 0; bottom: calc(100% + 8px); min-width: 160px; padding: 4px; background: var(--net-card); color: var(--net-ink); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); box-shadow: var(--net-elev-3); display: flex; flex-direction: column; gap: 2px; z-index: 45; }
.net-footer .net-lang-menu[hidden] { display: none; }
.net-footer .net-lang-menu button { display: block; width: 100%; padding: 7px 10px; border: 0; border-radius: var(--net-radius-sm); background: none; color: var(--net-ink); font: inherit; font-size: var(--net-fs-small); text-align: left; cursor: pointer; }
.net-footer .net-lang-menu button:hover, .net-footer .net-lang-menu button:focus-visible { background: var(--net-sunken); outline: none; }
.net-footer .net-lang-menu button.is-active { font-weight: 600; cursor: default; }
/* Alt-text syntax ( content / "" ): the tick is drawn but announced as nothing — the button already
   carries aria-checked, so without it a screen reader reads "check mark English, checked". */
.net-footer .net-lang-menu button.is-active::before { content: "\2713\00a0" / ""; }
@keyframes net-lang-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
.net-footer .pill { display: inline-flex; align-items: center; gap: 6px; padding: 0 8px; height: 26px; border-radius: var(--net-radius-sm); background: var(--net-chrome-raise); color: var(--net-chrome-ink); }
.net-footer .pill b { font-family: var(--net-font-display); }
.net-footer .tabbar { display: none; }

/* ─── CARDS / PRIMITIVES ──────────────────────────────────────────────────── */
.card { background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); box-shadow: var(--net-elev-1); padding: var(--net-space-4); transition: box-shadow var(--net-dur) var(--net-ease), transform var(--net-dur) var(--net-ease); }
.card.hover:hover { box-shadow: var(--net-elev-2); }
.card.tight { padding: var(--net-space-3); }
.card.flush { padding: 0; overflow: hidden; }
.card-hd { display: flex; align-items: center; gap: var(--net-space-2); margin-bottom: var(--net-space-3); }
.card-hd .grow { flex: 1; }
.divider { height: 1px; background: var(--net-hairline); margin: var(--net-space-3) 0; }
.stack { display: flex; flex-direction: column; gap: var(--net-space-3); }
.row { display: flex; align-items: center; gap: var(--net-space-2); }
.row.wrap { flex-wrap: wrap; }
.row .grow { flex: 1; min-width: 0; }
.profile-row { align-items: flex-start; gap: var(--net-space-4); } /* Settings profile cards: avatar + field grid */
.profile-id { display: flex; flex-direction: column; align-items: center; gap: var(--net-space-2); flex: none; } /* avatar + (phones) the View-my-Tent / public-page CTA centered under it */
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--net-card-gap); }
/* Equal heights are the default and stay the default — they are what makes a row of comparable cards
   read as a row. `.top` is the deliberate exception for a row that pairs a tall form with a two-line
   card: stretching that one produces a card that is mostly empty space (same call Marcos made on the
   front-desk cards, 2026-08-26). Use it only where the height difference is large. */
.grid-2.top { align-items: start; }
.grid-2:has(> .tile), .grid-2:has(> div > .tile), .grid-2:has(> .card.flush), .grid-2.grid-posts { gap: var(--net-space-5); } /* study tiles + clinic cards breathe the SAME on both axes (Marcos 2026-08-24: side gap = bottom gap; supersedes the 08-23 rows-only rule) */
@container work (max-width: 760px) { .grid-posts { grid-template-columns: minmax(0,1fr); } } /* posts / content-heavy card pairs: 2-up only when the column is genuinely wide */
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: var(--net-card-gap); }
.grid-3:has(> .tile), .grid-3:has(> div > .tile) { gap: var(--net-space-5); }   /* study tiles breathe the same 3-up as they did 2-up; the wrapped form is Home's "why this matched" caption */
.grid-people { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: var(--net-card-gap); } /* people cells are small — 3 per row (Marcos 08-22), 2 on phones (08-23) */
.grid-people.cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } /* clan "Members you can see" sits in the context column — 2 per row (Marcos 2026-08-23) */
.grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--net-card-gap); }
@container work (max-width: 1100px) { .grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
/* grid-2 stays 2-up down to a 480px work column (Marcos 08-22: directories are always 2 by row); the 480 rule below collapses it */
@container work (max-width: 640px) { .grid-3, .ladder, .grid-people { grid-template-columns: repeat(2, minmax(0,1fr)); } .kpis { grid-template-columns: repeat(3, minmax(0,1fr)); } .page-hd .btn { height: 32px; padding: 0 10px; } }
@container work (max-width: 480px) { .profile-row { flex-direction: column; align-items: center; } /* avatar centered above the stacked fields (Marcos 2026-08-23) */ .profile-row > .grid-2 { width: 100%; } .saved-search { flex-wrap: wrap; } .grid-3, .grid-2 { grid-template-columns: minmax(0,1fr); } /* minmax(0,…) — a bare 1fr lets a wide table inside a card blow the column past the viewport */ .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* .grid-people stays 2-up on phones (Marcos 2026-08-23) */
/* Settings → "What your suggestions are built from": one row per source — text left, "Counts" select right */
.sugg-row { display: grid; grid-template-columns: minmax(0,1fr) 150px; gap: var(--net-space-3); align-items: start; padding: 10px 0; border-top: 1px solid var(--net-hairline); }
.sugg-row:first-child { border-top: 0; padding-top: 0; }
.sugg-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.sugg-txt > b { font-size: var(--net-fs-small); }
.sugg-state { color: var(--net-ink-2); }
.sugg-rows.stack { gap: 0; }
.chip.who { height: 20px; padding: 0 8px; font-size: 11px; font-weight: var(--net-fw-medium); vertical-align: 1px; }
@container work (max-width: 480px) { .sugg-row { grid-template-columns: minmax(0,1fr); } .sugg-sel { max-width: 220px; } }
.kpis { display: grid; grid-template-columns: repeat(5, minmax(0,1fr)); gap: var(--net-card-gap); }
.kpis.k4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
/* 2-up: for a KPI row in a card that is only ~38% of the work column (the ladder, beside the
   timeline). Four tiles at ~80px each stop being readable; 2x2 keeps the numbers legible. */
.kpis.k2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
/* 3-up KPI row — Cetán's day, in a card that is HALF the work column (Marcos 2026-09-02). Named like
   k4/k6 rather than reusing .grid-3: that is the CARD wall and steps down at 1100px, which is a card's
   threshold, not a tile's.
   ⚠️ auto-fit, NOT repeat(3), and that was measured rather than assumed (Playwright, 2026-09-02): a
   fixed 3-up put these tiles at 115px each on a 1024 viewport, which is narrower than the words
   "PATIENTS ANSWERED" needs. A breakpoint could not fix it honestly either — the tiles sit inside a
   half-width CARD, so what they have to fit is the card's width, and the viewport does not know it.
   auto-fit asks the only question that matters: does another 150px column still fit? */
.kpis.k3 { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
/* phones — declared AFTER the .kpis base so they win the cascade (the early container block at the grid section loses to it) */
@container work (max-width: 640px) { .kpis { grid-template-columns: repeat(3, minmax(0,1fr)); } }
@container work (max-width: 480px) { .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } .kpis > .kpi:last-child:nth-child(odd) { grid-column: 1 / -1; } } /* odd count: last tile spans the row instead of sitting alone */
@container work (max-width: 640px) { .kpis.k4 { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* 4-up → 2 per row on phones (Marcos 2026-08-23) */
@container work (max-width: 480px) { .kpis.k3 { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* 3-up → 2 on phones. No 640 rule: three narrow tiles are what .kpis itself falls to there. The base block's `.kpis > .kpi:last-child:nth-child(odd)` still spans the odd third tile. */

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 14px; border-radius: var(--net-radius-sm); border: 1px solid transparent; font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); cursor: pointer; white-space: nowrap; text-decoration: none; transition: background var(--net-dur-fast) var(--net-ease), box-shadow var(--net-dur-fast) var(--net-ease), transform var(--net-dur-fast) var(--net-ease); user-select: none; }
.btn:hover { text-decoration: none; }
.btn:disabled { opacity: .55; cursor: not-allowed; }
/* Follow CTA: a followed state reads "Following" at rest and "Unfollow" on hover/focus — the hover IS the call to action.
   Both labels share one grid cell; they cross-fade + nudge (dur-fast), the colour shifts with the button's own transition. */
.btn.following { display: inline-grid; place-items: center; transition: background var(--net-dur) var(--net-ease), color var(--net-dur) var(--net-ease), border-color var(--net-dur) var(--net-ease), box-shadow var(--net-dur-fast) var(--net-ease); }
.btn.following .lbl-rest, .btn.following .lbl-hover { grid-area: 1 / 1; transition: opacity var(--net-dur) var(--net-ease), transform var(--net-dur) var(--net-ease); }
.btn.following .lbl-hover { opacity: 0; transform: translateY(6px); }
.btn.following:hover .lbl-rest, .btn.following:focus-visible .lbl-rest { opacity: 0; transform: translateY(-6px); }
.btn.following:hover .lbl-hover, .btn.following:focus-visible .lbl-hover { opacity: 1; transform: none; }
.btn.following:hover, .btn.following:focus-visible { color: var(--net-danger-ink); border-color: var(--net-danger-ink); background: var(--net-card); }
/* Clinic card: logo sits on the cover edge inside a white plate (4px ring + shadow) */
.logo-plate { position: relative; z-index: 1; /* the cover is positioned (::after) and would paint over the plate's top ring */ display: inline-flex; line-height: 0; padding: 4px; background: var(--net-card); border-radius: var(--net-radius-sm); box-shadow: var(--net-elev-2); }
.btn.primary { background: var(--net-primary); color: var(--net-on-primary); }
.btn.primary:hover { background: var(--net-primary-strong); box-shadow: var(--net-glow); }
.btn.secondary { background: var(--net-primary-soft); color: var(--net-primary); }
.btn.secondary:hover { background: color-mix(in srgb, var(--net-primary) 18%, var(--net-card)); }
.btn.ghost { background: transparent; color: var(--net-ink-2); border-color: var(--net-hairline-strong); }
.btn.ghost:hover { background: var(--net-sunken); color: var(--net-ink); }
/* A ghost that still says "this is the way in" — green edge and green ink, no fill. For the main
   action in a row that already carries other ghosts, where a filled button would shout over the data. */
.btn.ghost.accent { color: var(--net-primary); border-color: var(--net-primary); }
.btn.ghost.accent:hover { background: var(--net-primary-soft); color: var(--net-primary); }
/* A button that reads as a LINK — for an in-place control that is navigation rather than a command
   (the worklist "View 12 more"). Keeps the button semantics; drops the button chrome. */
.btn.link { border: 0; background: none; color: var(--net-primary); font-weight: var(--net-fw-medium); }
.btn.link:hover { background: none; color: var(--net-primary-strong); text-decoration: underline; }
.btn.link:disabled { opacity: .55; }
/* ...and the same thing sitting INSIDE a line of text (a table row's meta line, beside "Edit"), where
   the button box would otherwise stand 30px tall next to 12px text and break the baseline. Extends
   .btn.link rather than forking a second link-button class. */
.btn.link.bare { height: auto; padding: 0; font-size: inherit; font-weight: inherit; vertical-align: baseline; }
/* STATE-OFF: the switch is live (still clickable) but what it controls is currently OFF. Amber wash +
   dashed edge so the row reads at a glance — the label alone was not enough (Marcos 2026-08-25). */
.btn.state-off { background: var(--net-warn-soft); color: var(--net-warn-ink); border-style: dashed; border-color: color-mix(in srgb, var(--net-warn-ink) 35%, transparent); }
.btn.state-off:hover { background: color-mix(in srgb, var(--net-warn-ink) 16%, var(--net-card)); color: var(--net-warn-ink); }
/* A suspended clinic's whole row steps back — the state chip says it, the row shows it. */
.table tr.row-off > td { opacity: .62; }
.table tr.row-off > td:first-child { opacity: 1; box-shadow: inset 3px 0 0 var(--net-warn-ink); }
.table tr.row-off > td:first-child > b { opacity: .62; }
.btn.danger { background: var(--net-danger-soft); color: var(--net-danger-ink); }
.btn.sm { height: 30px; padding: 0 10px; font-size: 12px; }
.btn.lg { height: 44px; padding: 0 20px; font-size: var(--net-fs-body); }
.btn.block { width: 100%; }
.btn.icon { width: 36px; padding: 0; }
.btn.icon.sm { width: 30px; }
.chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border-radius: var(--net-radius-sm); font-size: 12px; font-weight: var(--net-fw-medium); background: var(--net-sunken); color: var(--net-ink-2); white-space: nowrap; border: 1px solid transparent; }
.chip.primary { background: var(--net-primary-soft); color: var(--net-primary); }
.chip.success { background: var(--net-success-soft); color: var(--net-success-ink); }
.chip.warn { background: var(--net-warn-soft); color: var(--net-warn-ink); }
.chip.info { background: var(--net-info-soft); color: var(--net-info-ink); }
.chip.danger { background: var(--net-danger-soft); color: var(--net-danger-ink); }
.chip.outline { background: transparent; border-color: var(--net-hairline-strong); }
/* Model tiers (Advanced Settings → Anthropic). Three steps of capability and cost, so the chip reads
   as a scale — cheap/cool through expensive/warm — rather than three unrelated colours. Built on the
   existing semantic tokens so both themes come along for free. */
.chip.tier-haiku { background: var(--net-info-soft); color: var(--net-info-ink); }
.chip.tier-sonnet { background: var(--net-primary-soft); color: var(--net-primary); }
.chip.tier-opus { background: var(--net-warn-soft); color: var(--net-warn-ink); }
.chip.tier-custom { background: transparent; border-color: var(--net-hairline-strong); }
/* A row of chips that is ONE control: it shows a set and opens the editor for that set. Button chrome
   off, chips laid out as they would be anywhere else, and the focus ring lands on the whole group —
   which is the truth, because the group is what you are pressing. */
/* ── WEBSITE EDITOR: a vertical tab rail beside its panel (Shared/Website/WebsiteEditor.razor) ──────
   Twelve sections across the top wrapped to two rows, and a wrapped tab strip stops reading as an
   index — you lose which one is live. Down the side they are a list.
   ⚠️ minmax(0,1fr) on the panel, not 1fr: the panels hold tables and media rows, and a bare 1fr lets a
   wide child push the whole editor past the column. */
/* ⭐ THE VERTICAL RAIL, SHARED (Marcos 2026-09-01: "billing do like the website editor… put a left
   positioned vertical tabs menu"). `.vtabs*` is the SAME pattern the website editor uses, aliased onto
   its rules rather than copied: two sets of values for one control is how a rail comes to look slightly
   different on the second screen that adopts it. The `.wedit*` names stay for now because another
   session owns WebsiteEditor.razor today; when that lands they collapse to one name. */
.wedit, .vtabs { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: var(--net-card-gap); align-items: start; }
.wedit-rail, .vtabs-rail { display: flex; flex-direction: column; gap: 2px; position: sticky; top: var(--net-space-3); }
.wedit-rail button, .vtabs-rail button { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 34px; padding: 7px 10px; border: 0; border-radius: var(--net-radius-sm); background: none; color: var(--net-ink-2); font: var(--net-fw-medium) var(--net-fs-small)/1.3 var(--net-font-body); text-align: left; cursor: pointer; }
.wedit-rail button:hover, .vtabs-rail button:hover { background: var(--net-sunken); color: var(--net-ink); }
.wedit-rail button.active, .vtabs-rail button.active { background: var(--net-primary-soft); color: var(--net-primary); font-weight: var(--net-fw-semibold); }
.wedit-panel, .vtabs-panel { min-width: 0; }
.wedit-panel:focus, .vtabs-panel:focus { outline: none; }
/* The rail lies back down on a narrow container — a 190px sidebar on a phone is most of the screen.
   It becomes the same scrolling strip `.tabs` uses, so the pattern is one a person already knows. */
@container work (max-width: 720px) {
  .wedit, .vtabs { grid-template-columns: minmax(0, 1fr); }
  .wedit-rail, .vtabs-rail { position: static; flex-direction: row; gap: 4px; overflow-x: auto; scrollbar-width: thin; padding-bottom: 4px; border-bottom: 1px solid var(--net-hairline); }
  .wedit-rail button, .vtabs-rail button { width: auto; flex: none; white-space: nowrap; }
}
/* Same fallback for a browser with no container queries — the editor must never be a 190px rail on a
   360px screen just because @container did not apply. */
@media (max-width: 720px) {
  .wedit, .vtabs { grid-template-columns: minmax(0, 1fr); }
  .wedit-rail, .vtabs-rail { position: static; flex-direction: row; gap: 4px; overflow-x: auto; padding-bottom: 4px; border-bottom: 1px solid var(--net-hairline); }
  .wedit-rail button, .vtabs-rail button { width: auto; flex: none; white-space: nowrap; }
}
/* The Clinical Market's rail is three short labels (Scanner, Local results, Global results) — 190px left
   a dead band beside them (Marcos 2026-09-11: "a bit more tight, so we give more space to the content").
   WIDE ONLY: .vtabs.cm-vtabs out-specifies the narrow collapse above, so an unconditional rule would
   pin a 150px sidebar on a phone. */
@container work (min-width: 721px) {
  .vtabs.cm-vtabs { grid-template-columns: 150px minmax(0, 1fr); }
}
/* ── WEBSITE EDITOR ITEM LISTS (locations, team, openings) ─────────────────────────────────────────
   One shape for all three: a bordered card, real air between cards, and a head row whose last control
   is pinned to the right edge.
   ⛔ 14px between items, not 8: at 8 the borders read as one table and a person scanning for "which
   card am I deleting from" has to count edges. */
.wlist { display: flex; flex-direction: column; gap: 14px; }
.wlist-item { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); padding: 12px; min-width: 0; }
.wlist-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* DeleteControl swaps its button for a confirmation block in the same slot. Inside a flex head row that
   block would be squeezed between the other buttons, so it takes a full line of its own instead. */
.wlist-head > .alert, .wlist-head > .row { flex: 1 0 100%; }

/* ⭐ TEAM IS A GRID — four people across (Marcos 2026-09-01). minmax(0,1fr), never a bare 1fr: a long
   name or a media input inside a card would otherwise push the whole grid past its column. */
.wgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; align-items: start; }
/* The card being edited takes the whole row — the form needs the width the reading grid does not. */
.wgrid > .wlist-item.editing { grid-column: 1 / -1; }

/* Responsive, container-first with a viewport fallback for engines without @container — the editor must
   never stay four-up on a phone just because the container query did not apply. */
@container work (max-width: 1180px) { .wgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container work (max-width: 900px)  { .wgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container work (max-width: 600px)  { .wgrid { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 1180px) { .wgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .wgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px)  { .wgrid { grid-template-columns: minmax(0, 1fr); } }

.chipset { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; padding: 2px 0; border: 0; background: none; text-align: left; cursor: pointer; font: inherit; color: inherit; }
.chipset:hover .chip.outline { border-color: var(--net-primary); color: var(--net-primary); }
.chip.clickable { cursor: pointer; }
.chip.clickable:hover { border-color: var(--net-primary); color: var(--net-primary); }
.chip.selected { background: var(--net-primary); color: var(--net-on-primary); }
/* ⭐ A chosen chip must not be distinguishable by COLOUR ALONE (WCAG 1.4.1) — several of these were a
   blue fill and nothing else, which is invisible to a colour-blind member and on a monochrome or
   high-contrast display.
   ⚠️ Generated content is NOT decorative by default — a screen reader reads it as text, so a bare
   content: "\2713" makes the chip announce "check mark Yes" on top of the aria-checked/aria-pressed it
   already reports. The alt-text syntax ( content: <string> / "" ) draws the tick and gives assistive
   tech an empty accessible name, which is what makes it genuinely decorative.
   ⚠️ :not(:has(svg)) is load-bearing, not decoration. Half the selected chips in this app already
   render <Icon Name="check"> themselves (Settings interests, Lead Catcher tags, user access, schedule
   days) — an unconditional ::before would print a SECOND tick on every one of them. Where :has() is
   unsupported the rule simply does not apply, which is exactly today's behaviour: no regression. */
.chip.selected:not(:has(svg))::before { content: "\2713" / ""; font-weight: var(--net-fw-bold); margin-right: -2px; }
.chip.clickable.selected:hover { border-color: var(--net-primary-strong, var(--net-primary)); background: var(--net-primary-strong, var(--net-primary)); color: var(--net-on-primary); }   /* readable on hover — the generic hover painted primary-on-primary (Marcos 2026-08-24) */
.input, .select, textarea.input { width: 100%; height: 38px; padding: 0 12px; border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius-sm); background: var(--net-card); color: var(--net-ink); font-size: var(--net-fs-small); outline: none; }
textarea.input { height: auto; min-height: 84px; padding: 10px 12px; resize: vertical; line-height: var(--net-lh-body); font-family: inherit; } /* textareas default to monospace in every browser */
.input:focus, .select:focus, textarea.input:focus { border-color: var(--net-focus); box-shadow: var(--net-focus-ring); }

/* ══ THE DROPDOWN SYSTEM — one closed-control design for EVERY <select> in the app ══════════════
   (Marcos 2026-09-09: "the drop downs look too much native and less theme inspired… apply that style
   to all dropdowns to be consistent… make sure it is mobile compatible on a phone browser.")

   ⛔ IT STAYS A NATIVE <select>. Only the CLOSED control is ours — box, border, fill, chevron, states.
   The option list is the OS popup on purpose: it is the one widget that already works with VoiceOver,
   TalkBack, the iOS wheel picker and a hardware keyboard. A JS listbox re-implements all four badly.

   ⛔ THE NATIVE ARROW CANNOT BE MOVED — a browser paints it hard against the right edge and padding
   does not touch it. appearance:none is the only way to place it, and it also makes Chrome, Safari and
   Firefox agree, which their native arrows never do.

   WHAT MAKES IT OURS (visible at rest, not only on hover — a phone has no hover):
     · a 32px END CAP at the trailing edge, washed with 7% brand green,
     · a short hairline TICK dividing the cap from the label — the "instrument" detail no browser draws,
     · our own chevron, in --net-ink-2, centred in that cap.
   A text input keeps its plain white box, so a picker and a typing field are told apart at a glance
   without reading the glyph.

   ⚠️ GEOMETRY IS ONE VARIABLE. --sel-cap drives the padding, the chevron centre, the tick and the cap
   width together; change it in one place or a long option label runs under the arrow.

   ⛔ IF THE CHEVRON SITS ON THE TEXT IN A BROWSER, THE STYLESHEET IS STALE — DO NOT "FIX" IT WITH
   SPECIFICITY. Measured 2026-09-09 at 390x844 on /studies: `.filters .select` computed padding-right
   12px (the base rule below) AND height 44px. No rule in this sheet sets either of those on a phone —
   and the height is the proof: `.filters .select` (0-2-0) beats the phone `.select { height: 44px }`
   (0-1-0), so a browser that computes 44px is running a copy of network.css from BEFORE this block
   existed. App.razor loads it through `@Assets[...]` (MapStaticAssets), which fingerprints at BUILD
   time and ships `immutable` — edit the CSS without a rebuild and the browser keeps the old file.
   Rebuild + hard-reload before believing any measurement taken from this sheet.

   ⚠️ THE CHEVRON COLOUR LIVES IN --sel-chev BECAUSE A data: URL CANNOT READ A TOKEN. Four hardcoded
   values, each matching the token named beside it. ⛔ THE ORDER OF THE FIVE RULES BELOW IS LOAD-BEARING
   (light → OS-dark fallback → hover/focus → themed dark → themed dark hover/focus): several of them tie
   at 0-3-0, so source order decides, and moving the themed-dark rule below the hover rule silently
   kills the hover colour in dark mode. */
.select {
    /* #55604F = --net-ink-2 */
    --sel-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%2355604F' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    --sel-cap: 32px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    cursor: pointer;
    padding-right: calc(var(--sel-cap) + 8px);
    background-image: var(--sel-chev),
                      linear-gradient(var(--net-hairline), var(--net-hairline)),
                      linear-gradient(color-mix(in srgb, var(--net-primary) 7%, transparent), color-mix(in srgb, var(--net-primary) 7%, transparent));
    background-repeat: no-repeat;
    background-position: right calc(var(--sel-cap) / 2 - 6px) center, right var(--sel-cap) center, right center;
    background-size: 12px 8px, 1px 56%, var(--sel-cap) 100%;
}
/* OS-dark fallback for a page served without data-theme (tokens.css keeps that path alive). #ADB8AF = dark --net-ink-2. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .select { --sel-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%23ADB8AF' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
}
/* Hover/focus: the chevron picks up the brand. #35674B = --net-primary.
   ⚠️ The BORDER hover carries :not(:focus) deliberately — .input:focus/.select:focus above sets the teal
   focus border at the same 0-2-0 and sits EARLIER in the sheet, so an unguarded hover would repaint a
   focused control green and the keyboard user would lose the focus colour under their own mouse. */
.select:hover:not(:disabled), .select:focus {
    --sel-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%2335674B' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
[data-theme="dark"] .select { --sel-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%23ADB8AF' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
/* #74C596 = dark --net-primary */
[data-theme="dark"] .select:hover:not(:disabled), [data-theme="dark"] .select:focus {
    --sel-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%2374C596' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* GLOBAL, every form in the app: the edge answers the pointer. --net-primary is 8.14:1 on white, so the
   control border stays far above the 3:1 non-text floor in both states. */
.input:hover:not(:focus):not(:disabled),
.select:hover:not(:focus):not(:disabled),
textarea.input:hover:not(:focus):not(:disabled) { border-color: var(--net-primary); }
.input, .select, textarea.input { transition: border-color var(--net-dur-fast) var(--net-ease), background-color var(--net-dur-fast) var(--net-ease), box-shadow var(--net-dur-fast) var(--net-ease); }
/* Firefox draws a focus ring INSIDE the text for an appearance:none select; this is the standard mute. */
.select:-moz-focusring { color: transparent; text-shadow: 0 0 0 var(--net-ink); }
/* ⛔ A DISABLED CONTROL WAS STYLED NOWHERE — the Map's Distance select (disabled until a point is known)
   drew at full strength and invited a click that does nothing. Tokens, not opacity: opacity dims the
   BORDER too, and a control whose edge falls under 3:1 stops being findable at all. #626B5C on the
   sunken fill is 5.2:1, so the disabled label is still readable rather than merely present. */
.input:disabled, .select:disabled, textarea.input:disabled {
    background-color: var(--net-sunken);
    border-color: var(--net-hairline);
    color: var(--net-ink-3);
    cursor: not-allowed;
}
/* The popup belongs to the OS. Windows Chrome/Firefox DO honour these and otherwise paint system grey
   next to our card; macOS/iOS ignore them, which is correct — there the picker is a platform sheet. */
.select option { background-color: var(--net-card); color: var(--net-ink); }
.select option:disabled { color: var(--net-ink-3); }
/* ⚠️ ELEMENT-QUALIFIED ON PURPOSE. `.input.sm, .select.sm` (further down this sheet) sets the padding
   SHORTHAND, which resets padding-right to 8px and drops a long option label straight under the
   chevron. `select.select.sm` out-specifies it from HERE, so the whole dropdown system stays in one
   block instead of being split across two places that drift. */
select.select.sm { --sel-cap: 24px; padding-right: calc(var(--sel-cap) + 6px); }

/* ══ FILTER BARS — ONE SYSTEM FOR ALL FOUR (Studies · Map · Dashboard · NetworkDashboard) ═══════════
   Four surfaces were building the same row four ways: 38px controls beside 36px buttons beside 30px
   .btn.sm, white on white. One height variable now drives every control IN a bar — select, text input,
   date input and button — so the row reads as one instrument panel, and the controls sit on the sunken
   fill so the bar is legible as a control strip on a white card. `.hd-filters` (the Leads header bar)
   joins the same system rather than keeping its own sizes.

   ⛔ THE HEIGHT IS A VARIABLE, NOT A HEIGHT, AND THAT IS THE WHOLE POINT. The phone rule near the end of
   this sheet (`input.input, .select { height: 44px }`, 0-1-1) is out-specified by anything of the form
   `.filters .select` (0-2-0) — which is exactly how the first pass at this shrank every filter control
   on a phone to 34px and quietly broke the 44px HIG tap target. Re-pointing the variable changes the
   value without entering that specificity fight.
   ⚠️ Scoped to the two bars, never to base .input/.select: those are the shell for every form in the
   app, and shrinking them everywhere to tidy one row is how a control ends up too small to hit. */
.filters, .hd-filters { --net-ctl-h: 34px; }
:is(.filters, .hd-filters) :is(.input, .select, .btn) { height: var(--net-ctl-h); }
:is(.filters, .hd-filters) :is(.input, .select) { background-color: var(--net-sunken); }
/* The control lifts OFF the strip when you address it — the same "raise" the cards use, at input scale. */
:is(.filters, .hd-filters) :is(.input, .select):hover:not(:disabled),
:is(.filters, .hd-filters) :is(.input, .select):focus { background-color: var(--net-card); }
/* ⛔ TOUCH GETS 44px — HIG minimum, and --net-tap-min is that number. `pointer: coarse` is here as well
   as the phone width because a tablet held in two hands is 1024px wide and still a thumb.
   ⚠️ The 16px font that stops iOS Safari zooming the page on focus is already global at ≤767.98px
   further down; this rule deliberately changes HEIGHT ONLY, because raising the font on a wide
   coarse-pointer screen would re-flow every filter label for no accessibility gain. */
@media (max-width: 767.98px), (pointer: coarse) {
    .filters, .hd-filters { --net-ctl-h: var(--net-tap-min); }
}
/* The two dropdowns that live OUTSIDE a filter bar and still had sub-16px text on touch: a table-row
   control and the Act-as picker in the user menu. Both zoom the whole page on iOS at 12–13px.
   ⛔ Desktop is untouched — .net-user-menu .actas .select keeps its 32px there. */
@media (max-width: 767.98px) {
    /* ⚠️ Element-qualified for the same reason as the padding above: `.input.sm, .select.sm` sits LATER
       in this sheet at the same 0-2-0 and would win the tie on source order, leaving 12px text and the
       page zoom it causes. A media query adds no specificity — only the qualifier does. */
    select.select.sm, input.input.sm { height: 36px; font-size: 16px; }
    .net-user-menu .actas .select { height: 40px; font-size: 16px; }
}
/* Native file input, dressed as a .input — the browser's own chrome is the only control on the page we
   don't draw, so it gets the same 38px shell and its button is CENTRED in it, not baseline-dropped. */
input[type="file"].input { padding: 6px 8px; line-height: 24px; cursor: pointer; color: var(--net-ink-2); }
input[type="file"].input::file-selector-button { height: 24px; margin: 0 10px 0 0; padding: 0 10px; border: 1px solid transparent; border-radius: var(--net-radius-sm); background: var(--net-primary-soft); color: var(--net-primary); font-family: inherit; font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); cursor: pointer; }
input[type="file"].input::file-selector-button:hover { background: color-mix(in srgb, var(--net-primary) 18%, var(--net-card)); }
/* Author-typed prose: the blank line between two paragraphs is CONTENT, and HTML eats it by default. */
/* ⚠️ pre-line is kept for the SINGLE newlines a clinic types inside one paragraph, but it can no longer
   be what separates paragraphs: it collapses a blank line to one break, which is how a two-paragraph
   About rendered as a wall of text (Marcos 2026-09-01). ClinicProfile now emits a real <p> per paragraph
   via Fmt.Paragraphs; these rules give them their spacing, and the last one adds no trailing gap. */
.about-body { white-space: pre-line; }
.about-body p { margin: 0 0 var(--net-space-3); }
.about-body p:last-child { margin-bottom: 0; }
/* Study page: FAQ accordion + read-only screening preview (Marcos 2026-08-25) */
.faq-list { display: flex; flex-direction: column; }
.faq { border-top: 1px solid var(--net-hairline); padding: 8px 0; }
.faq:first-child { border-top: 0; padding-top: 0; }
.faq > summary { cursor: pointer; font-weight: var(--net-fw-semibold); font-size: var(--net-fs-small); list-style: none; display: flex; align-items: center; gap: 8px; }
.faq > summary::before { content: "+"; width: 18px; height: 18px; border-radius: var(--net-radius-pill); background: var(--net-primary-soft); color: var(--net-primary); display: inline-grid; place-items: center; font-size: 13px; flex: none; }
.faq[open] > summary::before { content: "–"; }
.faq > summary::-webkit-details-marker { display: none; }
.faq > p { margin: 6px 0 0 26px; color: var(--net-ink-2); }
.screen-preview { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 8px; }
.screen-preview li::marker { color: var(--net-ink-3); font-size: var(--net-fs-small); }
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: var(--net-fs-small); font-weight: var(--net-fw-medium); color: var(--net-ink-2); }
.check { display: flex; gap: 10px; align-items: flex-start; font-size: var(--net-fs-small); line-height: 1.45; cursor: pointer; }
.check input { margin-top: 3px; width: 16px; height: 16px; flex: none; accent-color: var(--net-primary); }
/* ── SWITCH (Components/Shared/NetSwitch.razor) ────────────────────────────────────────────────
   A boolean the operator flips in place, with no Save. ⛔ Deliberately NOT a styled checkbox:
   `accent-color` cannot draw a track-and-knob, so every caller that wanted one would hand-roll its
   own — the exact wall we keep collapsing. One class, one component, every switch on the network.
   The 36x20 track sits inside a 24px-tall hit area (WCAG 2.5.8) via .tswitch-wrap's padding rather
   than by growing the visible control. */
.tswitch-wrap { display: inline-flex; align-items: center; gap: 8px; padding: 2px 0; }
.tswitch { position: relative; flex: none; width: 36px; height: 20px; padding: 0; border-radius: var(--net-radius-pill); border: 1px solid var(--net-hairline-strong); background: var(--net-sunken); cursor: pointer; transition: background-color .14s ease, border-color .14s ease; }
.tswitch .knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--net-ink-3); transition: transform .14s ease, background-color .14s ease; }
.tswitch.on { background: var(--net-primary); border-color: var(--net-primary); }
.tswitch.on .knob { transform: translateX(16px); background: var(--net-on-primary); }
.tswitch:disabled { opacity: .5; cursor: not-allowed; }
/* The global :focus-visible squares the ring to --net-radius-xs; on a pill that reads as a bug. */
.tswitch:focus-visible { border-radius: var(--net-radius-pill); }
.tswitch-text { font-size: var(--net-fs-small); color: var(--net-ink-2); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .tswitch, .tswitch .knob { transition: none; } }
/* Consent boxes are a GROUP with a name, and each box says out loud whether it is required —
   "Required to apply" / "Optional" are real text, referenced by aria-describedby, not a red asterisk. */
.consent-set { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); padding: 12px 14px; margin: 0; display: flex; flex-direction: column; gap: 12px; }
.consent-set > legend { padding: 0 6px; font: var(--net-fw-semibold) var(--net-fs-eyebrow)/var(--net-lh-eyebrow) var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); }
.consent-flag { display: inline-block; margin-left: 4px; padding: 0 6px; border-radius: var(--net-radius-xs); background: var(--net-primary-soft); color: var(--net-primary); font-size: 11px; font-weight: var(--net-fw-semibold); white-space: nowrap; }
.consent-flag.optional { background: var(--net-sunken); color: var(--net-ink-3); }
.alert { border-radius: var(--net-radius-sm); padding: 10px 12px; font-size: var(--net-fs-small); border: 1px solid transparent; display: flex; gap: 8px; align-items: flex-start; }
.alert.info { background: var(--net-info-soft); color: var(--net-info-ink); }
.alert.success { background: var(--net-success-soft); color: var(--net-success-ink); }
.alert.warn { background: var(--net-warn-soft); color: var(--net-warn-ink); }
.alert.danger { background: var(--net-danger-soft); color: var(--net-danger-ink); }
.empty { text-align: center; padding: var(--net-space-6) var(--net-space-4); color: var(--net-ink-2); border: 1px dashed var(--net-hairline-strong); border-radius: var(--net-radius-md); }
/* ⚠️ CHILD combinator is LOAD-BEARING (Marcos 2026-08-31). Icon.razor renders <svg class="ico">, so a
   descendant `.empty .ico` matched the PLATE and the GLYPH INSIDE IT — the svg inherited this rule's
   `margin: 0 auto 12px` and sat 6px from the top of a 44px plate with 18px below it. Every empty state
   in the app had its icon riding high. Keep the `>`; every `.empty` in the app puts .ico directly
   inside it (10 pages + NoData, checked). */
.empty > .ico { display: grid; place-items: center; width: 44px; height: 44px; margin: 0 auto var(--net-space-3); border-radius: var(--net-radius-pill); background: var(--net-primary-soft); color: var(--net-primary); }
.empty > .ico svg { display: block; width: 20px; height: 20px; }   /* the plate stays 44px; the glyph sits smaller inside it (Marcos 2026-08-24). display:block — an inline svg would carry a baseline descender the plate has to absorb. */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--net-hairline); overflow-x: auto; scrollbar-width: none; }
/* Phones: a truncated tab strip needs a "there's more" hint — soft right-edge fade (audit 2026-08-25 #43). */
@media (max-width: 859px) { .tabs { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); } }
.tabs button, .tabs a { flex: none; height: 40px; padding: 0 14px; background: none; border: 0; border-bottom: 2px solid transparent; color: var(--net-ink-2); font-size: var(--net-fs-small); font-weight: var(--net-fw-medium); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; text-decoration: none; margin-bottom: -1px; }
.tabs button.active, .tabs a.active { color: var(--net-primary); border-bottom-color: var(--net-primary); }
.tabs button:hover, .tabs a:hover { color: var(--net-ink); text-decoration: none; }
/* NetTabs.razor (2026-08-30). Sticky pins the strip directly under .col-head, which is itself sticky at
   top:0 with a 48px min-height — the same offset the backoffice's TcTabStrip uses under its topbar.
   The background is opaque BECAUSE it is sticky: the strip spans exactly the card width inside
   .col-body's gutters, so a card scrolling underneath disappears at the strip's edge with no sliver.
   z-index sits below .col-head's 5 so the head always wins the overlap. */
.tabs.sticky { position: sticky; top: var(--net-tabs-top, 48px); z-index: 4; background: var(--net-canvas); }
/* ── Vertical tab strip (Marcos 2026-09-01) ────────────────────────────────────────────────────────
   The same <NetTabs> component with Vertical="true": a column of tabs beside its panels instead of a
   bar above them. Used where one tab hid too many unrelated cards to scan — Cetán's settings, and the
   dashboard's recommended-actions/insights split.
   The active marker moves from the bottom edge to the LEFT edge, which is what makes it read as a
   sidebar rather than a row that happened to wrap. */
.tabs-split { display: grid; grid-template-columns: minmax(170px, 210px) minmax(0, 1fr); gap: var(--net-space-4); align-items: start; }
.tabs.vertical {
  display: flex; flex-direction: column; gap: 2px;
  border-bottom: 0; border-right: 1px solid var(--net-hairline);
  overflow-x: visible; padding-right: 6px;
  position: sticky; top: var(--net-tabs-top, 48px);   /* follows the reader down a long panel */
}
.tabs.vertical button, .tabs.vertical a {
  height: auto; min-height: 36px; width: 100%;
  padding: 8px 12px; text-align: left; justify-content: flex-start;
  border-bottom: 0; border-left: 2px solid transparent; margin-bottom: 0; margin-right: -1px;
  border-radius: var(--net-radius-sm) 0 0 var(--net-radius-sm);
}
.tabs.vertical button.active, .tabs.vertical a.active {
  border-bottom-color: transparent; border-left-color: var(--net-primary);
  background: var(--net-primary-soft);
}
/* Below the split's comfortable width the column becomes a normal strip again — a 170px sidebar and a
   content column do not both fit on a phone, and a squeezed sidebar is worse than no sidebar. */
@container work (max-width: 700px) {
  .tabs-split { grid-template-columns: minmax(0, 1fr); }
  .tabs.vertical { flex-direction: row; border-right: 0; border-bottom: 1px solid var(--net-hairline); overflow-x: auto; position: static; padding-right: 0; }
  .tabs.vertical button, .tabs.vertical a { width: auto; flex: none; border-left: 0; border-bottom: 2px solid transparent; border-radius: 0; margin-right: 0; margin-bottom: -1px; }
  .tabs.vertical button.active, .tabs.vertical a.active { border-left-color: transparent; border-bottom-color: var(--net-primary); background: none; }
}
/* A tab panel STANDS IN for .col-body as its cards' parent, so it has to carry .col-body's own column
   gap — otherwise every card inside a panel stacks flush (.card has no margin of its own). Same reason
   .dash-widgets carries it. [hidden] is restated because the flex display would otherwise beat the UA
   rule and every panel would render at once. */
.net-tabpanel { display: flex; flex-direction: column; gap: var(--net-card-gap); }
.net-tabpanel[hidden] { display: none; }
.kpi { display: flex; flex-direction: column; gap: 4px; padding: var(--net-space-3) var(--net-space-4); background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); box-shadow: var(--net-elev-1); }
.kpi .val { font: var(--net-fw-bold) clamp(1.15rem, 2.2cqi + .6rem, 1.625rem)/1.1 var(--net-font-display); letter-spacing: -.02em; }
.kpi .delta { font-size: 12px; color: var(--net-success-ink); }
.kpi .delta.down { color: var(--net-danger-ink); }
.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--net-fs-small); }
.table th { text-align: left; font: var(--net-fw-semibold) var(--net-fs-eyebrow)/1.2 var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); padding: 10px 12px; border-bottom: 1px solid var(--net-hairline); white-space: nowrap; }
.table td { padding: 10px 12px; border-bottom: 1px solid var(--net-hairline); vertical-align: middle; white-space: nowrap; } /* nowrap everywhere (was <=640 only) — narrow columns must never fold names/phones into slivers; the td-trunc column absorbs the slack. Opt out per cell with .wrap. */
.table td.wrap { white-space: normal; }
/* ⭐ STRIPED ROWS, EVERY TABLE (Marcos 2026-08-31). Not a component — the tables are plain markup —
   but they all carry .table, so one rule reaches every one of them.
   ⚠️ Two things this has to get right, both learned the hard way:
     1. EVEN rows, never odd. Striping odd rows double-shades against the default and the first row
        reads as selected.
     2. The stripe paints the TD, so it would out-paint the row hover (and .tc-row-click's hover, which
        sets the TR). The hover rule below is therefore restated at the SAME specificity and AFTER the
        stripe, so hovering still visibly changes the row on a striped table.
   The tint is derived from --net-sunken, so it follows light and dark without a second colour to keep
   in step; the flat fallback on the line above serves anything without color-mix. */
.table tbody tr:nth-child(even) > td { background: var(--net-sunken); }
.table tbody tr:nth-child(even) > td { background: color-mix(in srgb, var(--net-sunken) 45%, transparent); }
.table tbody tr:nth-child(n):hover > td { background: var(--net-sunken); }
.table tr:hover td { background: var(--net-sunken); }
.table tr.clickable { cursor: pointer; }
/* ⭐ THE ROW BEING EDITED, in a list-beside-editor screen (Lead Catcher FAQs and screening questions).
   ⚠️ Specificity is the whole point of the selector shape: the stripe above is `.table tbody
   tr:nth-child(even) > td` (0,2,3), so a shorter selector here loses on every even row and the person
   cannot tell which of two rows their editor is pointing at. Same three parts, declared AFTER the
   stripe and the hover, so a selected row stays marked while the pointer is somewhere else. */
.table tbody tr.is-selected > td { background: var(--net-primary-soft); box-shadow: inset 3px 0 0 var(--net-primary); }
/* ⭐ LIST BESIDE ITS EDITOR (2026-09-02). The backoffice's own Lead Catcher shape: the set on the left,
   the one you are editing on the right, so a catcher with twelve FAQs is a list you can read rather
   than twelve stacked textareas. Collapses to one column on a narrow workspace — the editor lands
   under the list, which is the order you use it in. */
.editor-split,
/* ⭐ Cetán's timeline beside the ladder — FIFTY / FIFTY (Marcos 2026-09-04). It shipped as 1.6fr /
   1fr on the reading that the timeline is the primary widget and the ladder is reference; both are
   things a coordinator acts on, so neither gets to be the narrow one.
   Shares the .editor-split declaration below only for the parts that ARE the same idea (grid, gap,
   align-items: start) — the track list is its own, because .editor-split's asymmetry is correct for a
   list beside an editor and wrong for two peer widgets.
   ⚠ The old `minmax(330px, 1fr)` floor on the ladder half is gone with it and is not missed: the grid
   already collapses to one column at 900px, and half of 900 is 450 — the floor could never bind. */
.tc-op-split { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: var(--net-card-gap); align-items: start; }
.tc-op-split { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* 900, not the 860 above: a table in the narrow half stops fitting sooner than an editor pane. The
   @media twin is not belt-and-braces — without it a browser lacking container queries renders a
   330px table rail on a phone. */
@container work (max-width: 900px) { .tc-op-split { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 900px)          { .tc-op-split { grid-template-columns: minmax(0, 1fr); } }
@container work (max-width: 860px) { .editor-split { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 860px) { .editor-split { grid-template-columns: minmax(0, 1fr); } }
/* One-line truncating text cell. NEVER put .clamp-1 on a <td> — display:-webkit-box un-tables the cell
   (clipped 2nd line + column gap). Numeric cells shrink to content so the text column takes the slack. */
.table td.td-trunc { max-width: 0; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.table td.td-trunc.tight { width: 26%; }   /* still truncates, but stops absorbing ALL the slack — for tables where a later column deserves the room (Marcos 2026-08-25) */
/* ⭐ PUBLISHED / DRAFT DOT (Marcos 2026-09-11: "green for published, orange for draft"). Colour is never the only
   cue: every dot carries role=img + a label and a tooltip, and the card header names both colours. */
/* Dot only, no word (Marcos 2026-09-11): filled pale green / pale orange, one thin grey edge. The word lives in the
   dot's aria-label + tooltip, and the card header's legend names both colours. */
.pub-dot { display: inline-block; flex: none; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%; margin-right: 8px; vertical-align: middle; border: 1px solid var(--net-ink-faint); background: color-mix(in srgb, var(--net-success) 45%, transparent); }
.pub-dot.draft { background: color-mix(in srgb, var(--net-warn) 45%, transparent); }
/* The row's second line (visits · Edit) starts under the title, not under the dot. */
.pub-cell { display: flex; align-items: center; }   /* the dot sits in the vertical middle of the row (Marcos 2026-09-11) */
.pub-body { min-width: 0; }
.pub-legend { display: inline-flex; align-items: center; gap: 4px; margin-left: var(--net-space-3); }
.pub-legend .pub-dot { margin-right: 2px; }
.pub-legend .pub-dot.draft { margin-left: var(--net-space-2); }
/* ⭐ SOURCE IS READABLE, NOT GREEDY (Marcos 2026-09-01, capped 2026-09-02). A floor of 150px so real
   acquisition labels stop ellipsing to nothing — and a CEILING, because the moment the Latest note
   column was removed Source became the table's only width:100% cell and swallowed every pixel the
   other columns didn't claim. It still truncates on a genuinely narrow table; it just can't take the
   whole row. The slack belongs to Lead Catcher, whose titles are the longest text here. */
.table td.td-source { width: 18%; min-width: 150px; }
/* Last touch is a SENTENCE, not a label. Same treatment Source needed: a ceiling so it cannot take the
   row, a floor so it stays readable. Without it the column ran to 44% of the table and put a horizontal
   scrollbar under the leads list (Marcos 2026-09-09). */
.table td.td-touch { width: 18%; min-width: 110px; }
/* ⚠️ The floor is 110, not Source's 150, deliberately. Source carries a LABEL that is meaningless
   half-shown; this carries a sentence that is a hover away in full, so it is the column that should give
   ground first when the window is narrow. */
/* ⭐ VERTICAL LEAD TIMELINE (Marcos 2026-09-01, "VERY IMPORTANT"). One spine, newest at the top.
   The history used to be a stack of rows with a chip on the left, which reads as a table and loses
   the two things a history is FOR: the order, and the gaps between entries. A note left by a person
   is marked apart from something the system did - they carry very different weight when you are
   deciding what to do next. */
/* ══ THE LEAD TIMELINE ══════════════════════════════════════════════════════════════════════════
   Redesigned 2026-09-01 (Marcos: "the timeline still looks too tight man, we need a more wow cool
   factor there"), with the graphic designer, the UI dev and the business analyst.

   THE IDEA: day bands on one spine. The history was a flat stack of identical rows at a 14px pitch,
   so it read as one grey block and you had to READ every row to find the call among the notes. The
   air now lands BETWEEN days rather than inside rows — it breathes without showing fewer entries,
   which matters on a screen a coordinator scans dozens of times a day.

   ⛔ COLOUR IS NEVER THE ONLY SIGNAL. Every kind carries its own icon and its own sentence, and the
   three lifecycle kinds also carry a different dot SHAPE (squared, not round). Greyscale-safe, and
   readable to a screen reader, which is why the shape split exists at all.
   ⛔ EVERY COLOUR IS AN EXISTING --net-* TOKEN, so dark mode moves with the rest of the shell. */

/* --- Rail --------------------------------------------------------------------------------- */
.tl { list-style: none; margin: 6px 0 0; padding: 0 0 0 34px; position: relative; }
/* Drawn on the LIST, not per row, so it cannot break between rows of different heights. The
   gradient is the one piece of pure decoration here and it earns its keep: recent is bright, older
   recedes down the page. */
.tl::before {
  content: ""; position: absolute; left: 12px; top: 4px; bottom: 4px; width: 2px; border-radius: 1px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--net-primary) 55%, transparent) 0%,
    var(--net-hairline) 42%, var(--net-hairline) 100%);
}

/* --- Day band ----------------------------------------------------------------------------- */
.tl-day { position: sticky; top: 0; z-index: 2; margin: 0 0 8px -34px; padding: 6px 0 6px 34px;
  background: linear-gradient(180deg, var(--net-card) 62%, transparent); }
.tl-item + .tl-day { margin-top: 20px; }          /* the ONLY generous gap in the list */
.tl-day-chip { display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--net-font-display); font-size: var(--net-fs-eyebrow); font-weight: var(--net-fw-semibold);
  letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-2);
  background: var(--net-sunken); border: 1px solid var(--net-hairline);
  border-radius: var(--net-radius-pill); padding: 3px 10px; }
.tl-day.is-today .tl-day-chip { color: var(--net-primary); background: var(--net-primary-soft);
  border-color: color-mix(in srgb, var(--net-primary) 34%, transparent); }
.tl-day-count { color: var(--net-ink-3); font-weight: var(--net-fw-medium); letter-spacing: 0; }

/* --- Row ---------------------------------------------------------------------------------- */
.tl-item { position: relative; padding: 0 0 8px; --tl-key: var(--net-ink-2); --tl-tint: var(--net-sunken); }
.tl-item:last-child { padding-bottom: 0; }
.tl-body { position: relative; padding: 9px 12px; border-radius: var(--net-radius-sm);
  border: 1px solid transparent; border-left: 3px solid var(--tl-key); background: transparent;
  transition: background var(--net-dur-fast) var(--net-ease), border-color var(--net-dur-fast) var(--net-ease); }
.tl-item:hover > .tl-body,
.tl-item:focus-within > .tl-body { background: var(--tl-tint); border-color: var(--net-hairline); border-left-color: var(--tl-key); }

/* --- Dot: ringed in the card colour so the spine passes cleanly behind it ------------------ */
.tl-dot { position: absolute; left: -34px; top: 6px; width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tl-tint); color: var(--tl-key);
  border: 1px solid color-mix(in srgb, var(--tl-key) 40%, transparent);
  box-shadow: 0 0 0 4px var(--net-card); }
.tl-dot svg { display: block; }
/* A milestone is SQUARED, and that is the signal that survives greyscale. */
.tl-item.is-milestone > .tl-dot { border-radius: var(--net-radius-sm); border-width: 2px; }
.tl-item.is-milestone > .tl-body { background: var(--tl-tint); border-color: color-mix(in srgb, var(--tl-key) 26%, transparent); }
.tl-item.is-milestone .tl-act { font-size: var(--net-fs-h3); font-weight: var(--net-fw-semibold); }

/* --- Header line -------------------------------------------------------------------------- */
.tl-hd { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; }
.tl-act { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); color: var(--tl-key); }
/* WHO did it, as its own pill — it used to run together with the timestamp in one grey clause, and
   "who" is the half a coordinator is actually scanning for. */
.tl-who { font-size: var(--net-fs-eyebrow); font-weight: var(--net-fw-medium); color: var(--net-ink-2);
  background: var(--net-sunken); border: 1px solid var(--net-hairline);
  border-radius: var(--net-radius-pill); padding: 1px 8px; }
/* Tabular numerals so a column of times lines up instead of shimmering. */
.tl-time { font-family: var(--net-font-mono); font-size: var(--net-fs-eyebrow); color: var(--net-ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* pre-wrap: a handoff note is written with line breaks and they are part of what was said. */
.tl-note { font-size: var(--net-fs-small); line-height: 1.5; white-space: pre-wrap;
  margin-top: 4px; color: var(--net-ink-2); }

/* --- The newest entry, marked with a WORD as well as a ring -------------------------------- */
.tl-latest { font-size: var(--net-fs-eyebrow); font-weight: var(--net-fw-semibold);
  letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-primary); }
.tl-item.is-latest > .tl-dot::after { content: ""; position: absolute; inset: -5px; border-radius: inherit;
  border: 2px solid color-mix(in srgb, var(--tl-key) 55%, transparent); animation: tl-ping 2.4s var(--net-ease) infinite; }

/* --- Entrance, first six rows only: a 200-row history must never cascade ------------------- */
.tl-item { animation: tl-in var(--net-dur) var(--net-ease) both; }
.tl-item:nth-child(2) { animation-delay: 30ms; }  .tl-item:nth-child(3) { animation-delay: 60ms; }
.tl-item:nth-child(4) { animation-delay: 90ms; }  .tl-item:nth-child(5) { animation-delay: 120ms; }
.tl-item:nth-child(6) { animation-delay: 150ms; } .tl-item:nth-child(n+7) { animation-delay: 0ms; }
@keyframes tl-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes tl-ping { 0% { opacity: .9; transform: scale(.92); } 70%, 100% { opacity: 0; transform: scale(1.25); } }
@media (prefers-reduced-motion: reduce) {
  .tl-item { animation: none; }
  .tl-item.is-latest > .tl-dot::after { animation: none; opacity: .9; transform: none; }
}

/* --- Kind = ONE custom-property pair per row. Every value is an existing --net-* token. -----
   ⛔ --net-hairline-strong is a BORDER token and fails contrast as text (3.06:1), so "system"
   keys off --net-ink-2 instead; the faint look comes from the tint, not from unreadable text. */
.tl-item.tl-note     { --tl-key: var(--net-primary);       --tl-tint: var(--net-primary-soft); }
.tl-item.tl-new      { --tl-key: var(--net-primary);       --tl-tint: var(--net-primary-soft); }
.tl-item.tl-call     { --tl-key: var(--net-info-ink);      --tl-tint: var(--net-info-soft); }
/* A call we could NOT place is a different fact from one we did — the clinic keeps two log types
   for exactly this, and collapsing them hid "we tried and were refused". */
.tl-item.tl-callfail { --tl-key: var(--net-danger-ink);    --tl-tint: var(--net-danger-soft); }
.tl-item.tl-sms,
.tl-item.tl-chat     { --tl-key: var(--net-tertiary-ink);  --tl-tint: color-mix(in srgb, var(--net-tertiary) 14%, var(--net-card)); }
.tl-item.tl-email    { --tl-key: var(--net-secondary-ink); --tl-tint: var(--net-warn-soft); }
/* Someone coming BACK to a link we sent is the cheapest re-engagement signal there is, and it used
   to render as an anonymous grey "system" dot. */
.tl-item.tl-open     { --tl-key: var(--net-tertiary-ink);  --tl-tint: color-mix(in srgb, var(--net-tertiary) 14%, var(--net-card)); }
.tl-item.tl-claim    { --tl-key: var(--net-warn-ink);      --tl-tint: var(--net-warn-soft); }
.tl-item.tl-stage,
.tl-item.tl-screen   { --tl-key: var(--net-success-ink);   --tl-tint: var(--net-success-soft); }
.tl-item.tl-system   { --tl-key: var(--net-ink-2);         --tl-tint: var(--net-sunken); }

/* Phones: the rail narrows and the dot shrinks; nothing else changes, because the row is already
   one wrapping flex line. */
@container work (max-width: 640px) {
  .tl { padding-left: 28px; } .tl::before { left: 10px; }
  .tl-dot { left: -28px; width: 22px; height: 22px; box-shadow: 0 0 0 3px var(--net-card); }
  .tl-day { margin-left: -28px; padding-left: 28px; }
  .tl-body { padding: 8px 10px; }
}
.table td.num, .table th.num { width: 1%; white-space: nowrap; }
.table td.fit, .table th.fit { width: 1%; white-space: nowrap; }   /* snug to its content, without .num's figure styling (Applicable sites "Where", 2026-09-11) */
/* phones: a wide table SCROLLS inside .table-wrap instead of squeezing every cell into a 3-line column */
@container work (max-width: 640px) { .table-wrap .table td { white-space: nowrap; } .table-wrap .table td.td-trunc { max-width: 220px; width: auto; } .table-wrap .table td.wrap { white-space: normal; } }
.hide-phone { } @container work (max-width: 640px) { .hide-phone { display: none !important; } }
.phone-only { display: none !important; } @container work (max-width: 640px) { .phone-only { display: inline-flex !important; } } /* counterpart of .hide-phone */
.select-auto { width: auto; }
/* ⭐ HEADER FILTER BAR: ONE SIZE, PINNED RIGHT (Marcos 2026-09-01). The leads header grew a search
   box capped at 190px, a sort select at its content width and a date filter at the full .select
   width - three controls doing the same job, three sizes, none of them aligned. margin-left:auto
   rather than a .grow spacer, so the group stays pinned right however many controls it has. */
.hd-filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: flex-end; margin-left: auto; }
/* ⭐ ONE BAR (Marcos 2026-09-01: "move the search bar and the filters to the same line"). The status
   chips were a row of their own above the table while search/sort/date sat in the card header — two
   halves of one filter bar, in two places. .hd-filters keeps its margin-left:auto inside this, so the
   chips take the slack on the left and the controls stay pinned right; both wrap together when the
   column is narrow rather than one shoving the other off the line. */
.filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 10px; }
.filter-bar .filter-chips { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.hd-filters > .select, .hd-filters > .input { width: 168px; max-width: 168px; flex: none; }
/* ...except the search box, which holds a name, an email or a phone number rather than one word. */
.hd-filters > .input.search { width: 260px; max-width: 260px; }
/* The custom From/To pickers appear only on "Date Range" and hold a date, not a sentence - sizing
   them like the others would push the row to wrap for no reason. */
.hd-filters > .input.date { width: 140px; max-width: 140px; }

/* ⭐ THE STAGE IS THE STRIPE (Marcos 2026-09-01). A queue of 163 rows reads as one grey wall; the
   stage is the thing you scan for, and it was a word in the ninth column. Same five tones the stage
   chip uses (DashChips.StageChip), so the stripe and the pill can never disagree.
   ⛔ THE LIFECYCLE STAGE OWNS THE COLOUR, and only it. Pre-screened is a SECOND fact about the same
   lead - it says they answered the questionnaire, not where they are - so it stays its own chip in
   its own column. Two facts competing for one stripe is how a colour stops meaning anything. */
.table tbody tr[class*="stage-"] > td:first-child { border-left: 3px solid transparent; }
/* Pale, not dark (Marcos 2026-09-01). The -ink tones are sized for TEXT on a soft background; as a
   4px rule down 50 rows they read as a hard rail and fight the content. Mixed back toward the card so
   the stripe tints rather than divides - and mixed against --net-card specifically, so it stays pale
   in dark mode instead of inverting into a bright bar. */
.table tbody tr[class*="stage-"] > td:first-child { border-left-width: 4px; }
.table tbody tr.stage-warn    > td:first-child { border-left-color: color-mix(in srgb, var(--net-warn) 42%, var(--net-card)); }
.table tbody tr.stage-danger  > td:first-child { border-left-color: color-mix(in srgb, var(--net-danger) 42%, var(--net-card)); }
.table tbody tr.stage-info    > td:first-child { border-left-color: color-mix(in srgb, var(--net-info) 42%, var(--net-card)); }
.table tbody tr.stage-success > td:first-child { border-left-color: color-mix(in srgb, var(--net-success) 42%, var(--net-card)); }
.table tbody tr.stage-outline > td:first-child { border-left-color: var(--net-hairline); }

/* ⭐ THE RECORD GRID (Marcos 2026-09-01: "just use the UI of lead details in the backoffice here").
   The backoffice packs a lead into a wall of small cards you take in at once. .grid-2 is a fixed
   2-up and .kpis is tile-shaped for numbers, so neither fits free-height cards - this is the one new
   rule, on the same @container breakpoints the rest of the sheet already uses. */
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--net-card-gap); align-items: start; }
@container work (min-width: 1100px) { .card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container work (max-width: 640px)  { .card-grid { grid-template-columns: minmax(0, 1fr); } }
/* A cell that holds two cards stacks them; no second class needed. */
.card-grid > .stack { gap: var(--net-card-gap); }

/* ⭐ ANSWERS READ AS ANSWERS (Marcos 2026-09-01: "the ui of what they told you needs tidy up").
   .qa-row was scoped to .lead-qa - a wrapper the record page never had - so the Q&A rendered with no
   styling at all, and where it DID apply it put question and answer side by side and crushed anything
   longer than three words. Stacked: the question is a label, the answer is the line. */
.qa { display: flex; flex-direction: column; gap: 12px; }
.qa-item { display: flex; flex-direction: column; gap: 3px; padding-bottom: 12px; border-bottom: 1px solid var(--net-hairline); }
.qa-item:last-child { border-bottom: 0; padding-bottom: 0; }
.qa-q { font-size: var(--net-fs-small); color: var(--net-ink-2); }
.qa-a { font-weight: var(--net-fw-semibold); overflow-wrap: anywhere; }
/* ⚠️ This block must restate the phone rule: .hd-filters > .select (0,2,1) out-specifies the
   .card-hd .select full-width rule (0,1,1), so without it the controls stay 168px on a phone and the
   header spills sideways. */
@container work (max-width: 480px) {
  .hd-filters { width: 100%; }
  .hd-filters > .select, .hd-filters > .input, .hd-filters > .input.date { width: 100%; max-width: none; flex: 1 1 100%; }
}
/* A form control sitting INSIDE a table row, where a 38px box next to 12px text breaks the row rhythm.
   Same height as .btn.sm so a control and a button on the same line agree. */
.input.sm, .select.sm { height: 30px; padding: 0 8px; font-size: 12px; }
.select.sm { min-width: 150px; }
/* A boolean COLUMN in a table: three of them side by side have to read as one control repeated, so they
   share a width and the switch sits centred in it. Left-aligned and auto-width, the knobs landed at
   three different x-positions and stopped scanning as a row of states. */
.table th.col-switch, .table td.col-switch { width: 84px; text-align: center; }
/* ⛔ The header WRAPS here, against .table th's global nowrap. "HAS WEBSITE" as one uppercase,
   letter-spaced line is 103px wide and drags the whole column with it — three of those pushed the row's
   buttons off the card. Two short lines over a 76px switch cost nothing and let the clinic NAME have
   the room instead. */
.table th.col-switch { white-space: normal; }
/* ⛔ AND THE TABLE HAS TO BE FIXED FOR ANY OF IT TO HOLD. Under the default `table-layout: auto` a
   `width` is only a HINT the algorithm weighs against every other column's content — three columns
   asking for 76px rendered 60/81/75, and a row of knobs at three different x-positions stops reading
   as one control repeated. `fixed` makes the declared widths the answer; the one column left without
   a width takes the remainder. ⚠️ Opt-in per table: every column in a fixed table needs a width, so
   this must never become a global default. */
.table.fixed-cols { table-layout: fixed; }

/* ── Lead detail drawer (Leads expanded row) ─────────────────────────────────
   Lives inside a <td colspan> — the td MUST carry .wrap or the global td nowrap
   makes long questions run under the Record column. */
.lead-detail-cell { background: var(--net-sunken); padding: 14px 16px 16px; }
.lead-detail { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: var(--net-space-5); align-items: start; }
.lead-detail .eyebrow { margin-bottom: 8px; }
/* Both panels are WHITE cards on the sunken drawer, light hairline wrap (Marcos 2026-08-25) */
.lead-qa, .lead-record { background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); padding: 12px 14px; }
.lead-qa .qa-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: 1px solid var(--net-hairline); }
.lead-qa .qa-row:last-child { border-bottom: 0; padding-bottom: 0; }
.lead-qa .q { font-size: var(--net-fs-small); color: var(--net-ink-2); line-height: 1.45; overflow-wrap: anywhere; }
.lead-qa .a { flex-shrink: 0; max-width: 45%; display: inline-flex; align-items: center; min-height: 24px; padding: 3px 10px; border-radius: var(--net-radius-sm); background: var(--net-sunken); font-size: 12px; font-weight: var(--net-fw-semibold); color: var(--net-ink); overflow-wrap: anywhere; } /* free-text answers can be long — chip grows instead of overflowing */
/* key/value row — unscoped from .lead-record (Marcos 2026-08-27): the campaign detail wants the same
   pair, and a second copy of these three rules is how two parallel implementations start. Same
   declarations, broader selector; nothing else in the sheet styles .rec-row. */
.rec-row { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--net-hairline); font-size: var(--net-fs-small); }
.rec-row .k { flex: 0 0 var(--rec-k, 110px); color: var(--net-ink-3); }   /* --rec-k: a caller with longer keys widens the column instead of forking the rule */
.rec-row .v { color: var(--net-ink); overflow-wrap: anywhere; }
.lead-record .rec-note { margin-top: 10px; line-height: 1.45; }
@container work (max-width: 640px) { .lead-detail { grid-template-columns: minmax(0,1fr); } }
@container work (max-width: 480px) { .composer .tools .meta { display: none; } .card-hd { flex-wrap: wrap; } .card-hd .select, .card-hd .select-auto { flex-basis: 100%; width: 100%; } }
.ribbon { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: var(--net-radius-md); background: linear-gradient(90deg, var(--net-primary-soft), transparent 70%); border: 1px solid var(--net-hairline); font-size: var(--net-fs-small); }
.ribbon .grow { flex: 1; }
.pager { display: flex; justify-content: center; gap: 6px; }

/* avatar */
.avatar { position: relative; display: inline-grid; place-items: center; width: var(--net-avatar-md); height: var(--net-avatar-md); border-radius: 50%; color: #fff; font: var(--net-fw-semibold) .9rem/1 var(--net-font-display); flex: none; overflow: visible; }
.avatar.sm { width: var(--net-avatar-sm); height: var(--net-avatar-sm); font-size: .75rem; }
.avatar.lg { width: var(--net-avatar-lg); height: var(--net-avatar-lg); font-size: 2rem; }
.avatar.xl { width: 120px; height: 120px; font-size: 2.4rem; }
.net-col.context .avatar.lg { width: 64px; height: 64px; font-size: 1.5rem; } /* narrow column: keep the identity anchor but let the name sit beside it */
.avatar.square { border-radius: var(--net-radius-md); }
.avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
.avatar .presence { position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--net-card); background: var(--net-ink-faint); }
.avatar .presence.on { background: var(--net-tertiary); }
.avatar.lg .presence, .avatar.xl .presence { width: 16px; height: 16px; right: 6px; bottom: 6px; }
.tint-a { background: linear-gradient(135deg, #0A5C2B, #17889A); }
.tint-b { background: linear-gradient(135deg, #11707F, #17889A); }
.tint-c { background: linear-gradient(135deg, #A84A08, #E8700A); }
.tint-d { background: linear-gradient(135deg, #0C2E1E, #0B8040); }
.tint-e { background: linear-gradient(135deg, #0B8040, #5FCF8A); }
.tint-f { background: linear-gradient(135deg, #E8700A, #F5B36E); }

/* AMS ring */
.ams { position: relative; width: 56px; height: 56px; border-radius: 50%; background: conic-gradient(var(--net-primary) calc(var(--p) * 1%), var(--net-ams-track) 0); display: grid; place-items: center; flex: none; }
.ams::before { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--net-card); }
.ams > * { position: relative; }
.ams.lg { width: 112px; height: 112px; }
.ams.lg::before { inset: 6px; }
.ladder { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.ladder .step { padding: 10px 8px; border-radius: var(--net-radius-sm); background: var(--net-sunken); border: 1px solid var(--net-hairline); text-align: center; }
.ladder .step.done { background: var(--net-primary-soft); border-color: transparent; }
.ladder .step.cur { border-color: var(--net-primary); box-shadow: var(--net-focus-ring); }
.ladder .amt { font: var(--net-fw-bold) 1.1rem/1.1 var(--net-font-display); }
.ladder .nm { font-size: 11px; color: var(--net-ink-2); margin-top: 2px; }
@container work (max-width: 640px) { .ladder { grid-template-columns: repeat(2, minmax(0,1fr)); } } /* must sit AFTER the .ladder base (same specificity) — the early container rule at the grid section lost the cascade */

/* study tile (marketplace card) */
.tile { display: flex; flex-direction: column; background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); box-shadow: var(--net-elev-1); overflow: hidden; transition: box-shadow var(--net-dur) var(--net-ease), transform var(--net-dur) var(--net-ease); position: relative; color: inherit; text-decoration: none; }
.tile:hover { box-shadow: var(--net-elev-2); transform: translateY(-1px); text-decoration: none; }
.tile .media { aspect-ratio: 16/9; position: relative; display: flex; align-items: flex-end; padding: 12px; color: #fff; }
.tile .media .plate { position: absolute; inset: 0; }
.tile .media .topic { position: relative; font: var(--net-fw-semibold) var(--net-fs-eyebrow)/1.1 var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; opacity: .95; text-shadow: 0 1px 2px rgba(0,0,0,.35); }
.tile .media .plate.tint-a { background: linear-gradient(135deg, #074620 0%, #0A5C2B 55%, #17889A 140%); }
.tile .media .plate.tint-b { background: linear-gradient(135deg, #11707F, #17889A 70%, #5FCF8A 150%); }
.tile .media .plate.tint-c { background: linear-gradient(135deg, #A84A08, #E8700A 60%, #F5B36E 150%); }
.tile .media .plate.tint-d { background: linear-gradient(135deg, #0C2E1E, #0B8040 110%); }
.tile .media .plate.tint-e { background: linear-gradient(135deg, #0B8040, #5FCF8A 70%, #17889A 150%); }
.tile .media .plate.tint-f { background: linear-gradient(135deg, #C45A08, #F59A4A 120%); }
.tile .media .plate::after { content: ""; position: absolute; inset: 0; background: radial-gradient(120% 90% at 100% 0%, rgba(255,255,255,.22), transparent 55%); }
.tile .fav { position: absolute; top: 10px; right: 10px; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; background: var(--net-glass-bg); backdrop-filter: blur(var(--net-glass-blur)); border: 1px solid var(--net-glass-border); color: var(--net-ink); cursor: pointer; transition: transform var(--net-dur) var(--net-spring); z-index: 2; }
.tile .fav:hover { transform: scale(1.06); }
.tile .fav.on { color: var(--net-danger); }
.tile .body { padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.tile .title { font: var(--net-fw-semibold) var(--net-fs-h3)/1.3 var(--net-font-body); color: var(--net-ink); }
.tile .clinic { color: var(--net-ink-3); font-size: var(--net-fs-small); display: flex; gap: 6px; align-items: center; }
.tile .foot { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 4px; }
.tile .foot .stats { color: var(--net-ink-3); font-size: 12px; display: flex; gap: 10px; }
.tile .foot .btn { margin-left: auto; }
.tile.list { flex-direction: row; }
.tile.list .media { width: 200px; aspect-ratio: auto; flex: none; }

/* member card / people rows */
.person { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--net-radius-sm); }
.person:hover { background: var(--net-sunken); }
/* ⭐ A .person ROW THAT IS A BUTTON (the lead record's conversation list, Marcos 2026-09-02). It opens
   a popup rather than navigating, so it must BE a button — and a bare <button> arrives with a border,
   a centred label, its own font and a shrink-to-fit width, none of which the row wants. */
button.person { width: 100%; text-align: left; background: none; border: 0; padding: 8px;
  font: inherit; color: inherit; cursor: pointer; }
.person.tile { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); background: var(--net-card); padding: 10px; } /* directory cell — every "people" grid uses grid-2 + this */
.person.tile:hover { background: var(--net-card); box-shadow: var(--net-elev-1); }
.person .nm { font-weight: var(--net-fw-semibold); font-size: var(--net-fs-small); }
.person .hd { color: var(--net-ink-3); font-size: 12px; }
.person .act { margin-left: auto; display: flex; gap: 6px; }

/* Notification row — the member bell's row, on /notifications and in the right panel.
   Marcos 2026-09-01: the row was a bare <a>, so it could only navigate. It is now a row with a main
   control plus its own actions, mirroring the backoffice bell (.tc-noti-row). The unread wash moved
   off the anchor's inline style and onto the row, so it covers the actions too. */
.noti-row { display: flex; align-items: center; gap: 4px; border-radius: var(--net-radius-sm); }
.noti-row > .person { flex: 1; min-width: 0; }
.noti-row.is-unread { background: var(--net-primary-soft); }
.noti-row .act { display: flex; gap: 2px; padding-right: 4px; flex: none; }
/* Present but quiet until the row is under the pointer — loud enough to be found, never louder than
   the notification itself. Kept fully opaque on touch, where there is no hover to reveal them. */
.noti-act { opacity: .45; transition: opacity .12s ease, color .12s ease; }
.noti-row:hover .noti-act, .noti-act:focus-visible { opacity: 1; }
.noti-act-del:hover { color: var(--net-danger-ink); }
@media (hover: none) { .noti-act { opacity: .8; } }
.stat3 { display: grid; grid-template-columns: repeat(3, 1fr); text-align: center; gap: 6px; }
.stat3 .v { font: var(--net-fw-bold) 1.2rem/1.1 var(--net-font-display); }
.stat3 .l { font-size: 11px; color: var(--net-ink-3); }

/* posts */
.post { display: flex; flex-direction: column; gap: 10px; }
.post .who { display: flex; align-items: center; gap: 10px; }
.post .who .nm { font-weight: var(--net-fw-semibold); font-size: var(--net-fs-small); }
.post .who .when { color: var(--net-ink-3); font-size: 12px; }
.post .body { white-space: pre-wrap; }
.post .acts { display: flex; gap: 4px; }
.post .acts button { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border: 0; background: none; border-radius: var(--net-radius-sm); color: var(--net-ink-2); font-size: var(--net-fs-small); cursor: pointer; }
.post .acts button:hover:not(:disabled) { background: var(--net-sunken); color: var(--net-ink); }
.post .acts button:disabled, [aria-disabled="true"] { opacity: .5; cursor: default; }   /* onlookers SEE the controls (Marcos 2026-08-25) — they just never fire */
.post .acts button[aria-disabled="true"]:hover { background: none; color: var(--net-ink-2); }
.post .acts button.on { color: var(--net-danger-ink); }
.comment { display: flex; gap: 8px; align-items: flex-start; }
.comment .bub { background: var(--net-sunken); border-radius: var(--net-radius-md); padding: 8px 12px; font-size: var(--net-fs-small); }
.comment .bub .nm { font-weight: var(--net-fw-semibold); margin-right: 6px; }
.composer { display: flex; flex-direction: column; gap: 10px; }
.composer textarea { min-height: 64px; }
.composer .tools { display: flex; gap: 8px; align-items: center; padding-left: calc(var(--net-avatar-md) + var(--net-space-2)); } /* Photo CTA lines up with the textarea's left edge (past the avatar), Marcos 2026-08-23 */
.composer .tools .grow { flex: 1; }
/* ── #study tags in posts and comments (2026-09-10; TagPicker.razor / TaggedText.razor / tc-tag.js) ──
   .tag-host wraps the field so the picker can hang off its bottom edge; the field itself keeps .input. */
.tag-host { position: relative; }
.post .body a.tag, .comment .bub a.tag { color: var(--net-primary); font-weight: var(--net-fw-semibold); text-decoration: none; }
.post .body a.tag:hover, .comment .bub a.tag:hover { text-decoration: underline; }
.tag-pick { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30; display: flex; flex-direction: column; gap: 2px; padding: 4px; max-height: 240px; overflow-y: auto; background: var(--net-card); border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius-sm); box-shadow: var(--net-elev-1); }
.tag-pick button { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; padding: 6px 10px; border: 0; background: none; border-radius: var(--net-radius-sm); text-align: left; cursor: pointer; color: var(--net-ink); }
.tag-pick button .t { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); color: var(--net-primary); }
.tag-pick button .d { font-size: 12px; color: var(--net-ink-3); max-width: 100%; }
.tag-pick button.on, .tag-pick button:hover { background: var(--net-sunken); }

/* chat column */
.chat-list { display: flex; flex-direction: column; gap: 2px; }
.chat-row { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--net-radius-sm); cursor: pointer; background: none; border: 0; text-align: left; width: 100%; color: inherit; }
.chat-row:hover, .chat-row.active { background: var(--net-card); box-shadow: var(--net-elev-1); }
.chat-row .nm { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); display: flex; align-items: center; gap: 6px; }
.chat-row .last { font-size: var(--net-fs-small); color: var(--net-ink-3); }
.chat-row .when { margin-left: auto; font-size: 11px; color: var(--net-ink-3); white-space: nowrap; align-self: flex-start; }
.chat-row .unread { margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--net-radius-pill); background: var(--net-primary); color: var(--net-on-primary); font: var(--net-fw-bold) 10px/18px var(--net-font-display); text-align: center; }
/* The panel is a full-height column: header, then the thread FILLS the rest so the composer sits at the
   bottom and the messages scroll — the old calc(100% - 48px) resolved against an auto-height parent and
   left the composer hugging the top with a void below it (Marcos 2026-08-26). */
.chat-col-inner { display: flex; flex-direction: column; height: 100%; }
.chat-col-inner > .col-body { flex: 1; }
.thread { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.thread .msgs { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: var(--net-panel-pad); }
.bubble { max-width: 82%; padding: 8px 12px; border-radius: var(--net-radius-md); font-size: var(--net-fs-small); line-height: 1.45; background: var(--net-card); border: 1px solid var(--net-hairline); align-self: flex-start; border-bottom-left-radius: 4px; }
.bubble.me { background: var(--net-primary); color: var(--net-on-primary); border-color: transparent; align-self: flex-end; border-bottom-left-radius: var(--net-radius-md); border-bottom-right-radius: 4px; }
.bubble .t { display: block; font-size: 10px; opacity: .7; margin-top: 2px; }
.thread .send { display: flex; gap: 8px; padding: var(--net-space-2) var(--net-panel-pad) var(--net-panel-pad); border-top: 1px solid var(--net-hairline); background: var(--net-band-chat); }
.thread .notice { margin: 0 var(--net-panel-pad); }

/* brand column */
.brand-mark { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: var(--net-space-5) var(--net-space-3) var(--net-space-3); }
.brand-mark .logo { width: 200px; max-width: 100%; }
.brand-mark .logo img { width: 100%; height: auto; }
.brand-mark .word { font: var(--net-fw-bold) 20px/1.1 var(--net-font-display); letter-spacing: -.015em; }
.brand-mark .tag { color: var(--net-ink-2); font-size: var(--net-fs-small); }
.match { display: flex; gap: 10px; align-items: center; padding: 10px; border-radius: var(--net-radius-sm); background: var(--net-card); border: 1px solid var(--net-hairline); color: inherit; text-decoration: none; }
.match:hover { box-shadow: var(--net-elev-2); text-decoration: none; }
.match .sw { width: 36px; height: 36px; border-radius: var(--net-radius-sm); flex: none; }
.match .t { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); }
.match .s { font-size: 12px; color: var(--net-ink-3); }

/* page header in work column */
.page-hd { display: flex; align-items: flex-end; gap: var(--net-space-3); flex-wrap: wrap; }
.page-hd .grow { flex: 1; min-width: 200px; }
.page-hd .sub { color: var(--net-ink-2); margin-top: 4px; }
@container work (max-width: 480px) { .page-hd .grow { flex-basis: 100%; min-width: 0; } .page-hd > :not(.grow):last-child { margin-left: auto; } } /* phones: title block owns the row; buttons/controls drop beneath it (never squeeze the subtitle) */
.filters { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.filters .select { width: auto; min-width: 150px; }
.filters .input { width: 260px; }
/* Dashboard filter strip: controls sit at the RIGHT of the bar (Marcos 2026-08-29).
   ⚠️ A MODIFIER, not a change to `.filters` — that class is shared with Studies, the Map and the
   SuperAdmin dashboard, and right-aligning those is not what was asked for. On a narrow screen the
   row still wraps, and wrapped rows stay right-aligned so the controls keep one edge. */
.filters.filters-end { justify-content: flex-end; }

/* ⛔⭐ A LINK THAT CANNOT GO ANYWHERE MUST NOT LOOK LIKE ONE (Marcos 2026-09-09: "if clicked
   campaign tool in the campaigns it opens nothing").

   `<a class="btn primary disabled">` was rendered with `href="#"` and `.disabled` was styled NOWHERE in
   this stylesheet — so it drew as a full-strength primary button, invited the click, and did nothing.
   A <button disabled> gets this from the browser; an anchor gets nothing unless we say so.

   ⚠️ `pointer-events:none` is the half that actually stops the click; the dimming only explains it. */
.btn.disabled, a.btn[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
    pointer-events: none;
}

/* ⭐⭐ THE LOCATION BAR — StudyLocationBar.razor (Marcos 2026-09-09: "always present in the header
   the zip input and miles around drop down"). It sits ABOVE the other filters on every surface that
   lists studies, because "near me" is the first question a patient asks and the last one a filter row
   should bury.

   ⛔ ITS OWN CLASS, NOT A `.filters` VARIANT. `.filters` is shared by Studies, the Map and the clinic
   pages, and widening it to carry a pinned bar would move three surfaces to fix one. */
/* ⭐ A study's generated card picture, laid OVER its tinted plate (Marcos 2026-09-09). The plate stays
   behind it deliberately: it is the placeholder while the image loads and the fallback if the file has
   gone, and an <img> cannot render an error. */
.tile .media .plate-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.loc-bar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* ⭐ Inline: the wrapper stops being a box so its children join the host's filter row. One line that
   wraps as a whole, instead of a bar stacked on top of a bar (Marcos 2026-09-09). */
.loc-bar.loc-inline { display: contents; }
.loc-bar .select { width: auto; min-width: 150px; }
.loc-zip { display: flex; align-items: center; gap: 6px; }
/* Same type as every sibling in the row (var(--net-fs-small) from .input). The 16px iOS anti-zoom size
   is NOT set here: the global touch-width rule near the end of this file already applies it to every
   .input at <=767.98px, and a desktop 16px made the ZIP box the one control in the bar whose text did
   not match (Marcos 2026-09-11). */
.loc-zip .input { width: 130px; }
.loc-bar .loc-custom { width: 92px; }
@media (max-width: 640px) {
    /* The ZIP box earns the width on a phone — it is the control the whole bar exists for. */
    .loc-zip { flex: 1 1 100%; }
    .loc-zip .input { width: 100%; }
}
@media (max-width: 640px) { .filters.filters-end { justify-content: stretch; } }
.filters .input.date { width: auto; min-width: 150px; } /* a date picker sized like its sibling selects, not like a search box */
.search-inline { width: 260px; }
@container work (max-width: 640px) { .search-inline, .card-hd .search-inline { width: 100% !important; flex-basis: 100%; } } /* phones: inline search boxes go full width (inline width is the desktop value) */
.filters-checks { display: contents; } /* desktop: the two checks flow like siblings; phones: one wrapped row (see container rule) */
/* ⭐ Studies + clinic page: the filter card is OPTIONAL at every width (Marcos 2026-09-15: "this same UX
   also for web"). Collapsed by default; the search-icon button in the tools row opens it. The tools row
   sits between the title and the card, every icon right-aligned (Marcos: "all to the right is better"). */
.filters-card:not(.open) { display: none; }
.tools-row { justify-content: flex-end; gap: 6px; margin: 4px 0 12px; }
/* phones: filters become a tidy 2-column grid — search + save span full width */
@container work (max-width: 640px) {
  .filters { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 8px; }
  .filters .input, .filters .btn { grid-column: 1 / -1; width: 100%; }
  .filters .input.date { grid-column: auto; width: 100%; min-width: 0; } /* the two date pickers pair up on one row instead of eating two */
  .filters .btn { justify-content: center; }
  .filters .select { width: 100%; min-width: 0; }
  .filters .filters-checks { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px 16px; }
  .filters .grow { display: none; }
}

/* clinic page */
.cover { height: 168px; border-radius: var(--net-radius-lg) var(--net-radius-lg) 0 0; position: relative; overflow: hidden; }
.cover::after { content: ""; position: absolute; inset: 0; background: radial-gradient(120% 90% at 100% 0%, rgba(255,255,255,.25), transparent 55%); }
/* ⭐ A PAGE'S H1 SITS ON THIS BANNER — so the banner has to guarantee the contrast, not the tint.
   The study and clan headers print white text straight onto tint-a…f, and several of those tints are
   light enough that #fff falls under 4.5:1 (WCAG 1.4.3) — worst on the lighter accents, and the
   radial highlight above makes the top-right corner lighter still. This scrim darkens only the band
   the text occupies, so the tint stays recognisable and the heading is legible on every one of them. */
/* ⛔ z-index IS LOAD-BEARING (2026-09-09). A study can now carry a generated picture in this
   banner, and the picture is a later sibling than this ::before — so at equal stacking the PHOTO
   would paint over the scrim and the white h1 would sit on raw imagery at unknown contrast. The
   scrim is pinned above the image (0) and below the title block (1), which is the only order that
   keeps this class doing the job it exists for. */
.cover.cover-titled::before { content: ""; position: absolute; inset: 0; z-index: 1; background: linear-gradient(to top, rgba(0,0,0,.55), rgba(0,0,0,.15) 55%, transparent 85%); }
.clinic-hd { display: flex; gap: var(--net-space-4); align-items: flex-end; padding: 0 var(--net-space-4) var(--net-space-4); margin-top: -48px; position: relative; flex-wrap: wrap; }
.clinic-hd .logo { width: 96px; height: 96px; border-radius: var(--net-radius-lg); background: var(--net-card); box-shadow: var(--net-elev-2); display: grid; place-items: center; padding: 6px; }
.clinic-hd .logo .avatar { width: 100%; height: 100%; font-size: 1.6rem; }
.clinic-hd .info { flex: 1; min-width: 220px; padding-top: 56px; }
.clinic-hd .acts { display: flex; gap: 8px; padding-top: 56px; }
/* member Tent variant: round 88px avatar, shallower cover */
.clinic-hd.member { margin-top: -40px; }
.clinic-hd.member .logo { border-radius: 50%; width: 88px; height: 88px; padding: 4px; }
.clinic-hd.member .info, .clinic-hd.member .acts { padding-top: 44px; }
/* phones: hero stacks — avatar, then name/bio full width, then full-width actions */
@container work (max-width: 640px) {
  .clinic-hd { flex-direction: column; align-items: flex-start; gap: 6px; }
  .clinic-hd .info, .clinic-hd.member .info { padding-top: 2px; min-width: 0; width: 100%; }
  .clinic-hd .acts, .clinic-hd.member .acts { padding-top: 4px; width: 100%; flex-wrap: wrap; }
  .tabs button, .tabs a { padding: 0 10px; }
}
@container work (max-width: 480px) { .clinic-hd .acts .btn { flex: 1 1 auto; justify-content: center; } } /* phones only: full-width action buttons */
.verified { color: var(--net-tertiary-ink); display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: var(--net-fw-semibold); }

/* dev switcher */
.dev-bar { position: absolute; left: 50%; bottom: calc(var(--net-footer-h) + 10px); transform: translateX(-50%); display: flex; gap: 6px; align-items: center; padding: 6px 8px; background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); box-shadow: var(--net-elev-3); z-index: 50; font-size: 12px; width: max-content; max-width: 96vw; overflow-x: auto; }
.dev-bar .lbl { color: var(--net-ink-3); padding: 0 6px; font: var(--net-fw-semibold) var(--net-fs-eyebrow)/1 var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; white-space: nowrap; flex-shrink: 0; }
.dev-bar a, .dev-bar button { height: 28px; padding: 0 10px; border-radius: var(--net-radius-sm); border: 1px solid transparent; background: var(--net-sunken); color: var(--net-ink-2); font-size: 12px; font-weight: var(--net-fw-medium); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; flex-shrink: 0; }
.dev-bar a.on, .dev-bar button.on { background: var(--net-primary); color: var(--net-on-primary); }
.dev-bar a:hover { text-decoration: none; border-color: var(--net-primary); }
/* ─── CETAN layer (AI operator) — lives in the right column like Smoke Signals; mirrors the backoffice AiChatPanel ─── */
.cetan-inner { display: flex; flex-direction: column; min-height: 100%; height: 100%; }
.cetan-title .nm { font: var(--net-fw-bold) 17px/1.15 var(--net-font-display); color: var(--net-ink); }
.cetan-title .sub { font-size: 10px; color: var(--net-ink-3); line-height: 1.2; margin-top: 3px; white-space: nowrap; } /* backoffice .fs-10 */
.cetan-avatar { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(135deg, var(--net-primary), var(--net-secondary)); color: var(--net-on-primary); }
.rp-cetan .col-head { min-height: 60px; }
.cetan-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--net-panel-pad); display: flex; flex-direction: column; gap: var(--net-card-gap); scrollbar-width: thin; }
/* Conversations CTA / back bar — mirrors backoffice .tc-ai-backbar(--fwd) */
.cetan-backbar { position: sticky; top: 0; z-index: 3; display: flex; align-items: center; justify-content: flex-start; gap: 6px; border: 0; background: var(--net-band-chat); padding: 6px 0; margin: 0; font-size: 12px; font-weight: var(--net-fw-semibold); color: var(--net-primary); cursor: pointer; transition: opacity .15s; }
.cetan-backbar:hover { opacity: .7; }
.cetan-backbar--fwd { justify-content: center; padding: 10px 12px; border: 1px solid var(--net-primary); border-radius: var(--net-radius-md); background: var(--net-card); }
.cetan-backbar--fwd:hover { opacity: 1; background: color-mix(in srgb, var(--net-primary) 7%, transparent); }
.cetan-hero { text-align: center; padding-top: 6px; }
.cetan-hero-img { display: block; width: 200px; height: 200px; max-width: 84%; margin: 2px auto 10px; border-radius: 50%; object-fit: contain; border: 2px solid color-mix(in srgb, var(--net-primary) 28%, transparent); box-shadow: 0 6px 18px color-mix(in srgb, var(--net-primary) 22%, transparent); }
.cetan-hero-name { font: var(--net-fw-bold) 14px/1.2 var(--net-font-display); color: var(--net-ink); }
.cetan-hero-tag { font-size: var(--net-fs-small); font-style: italic; color: var(--net-ink-3); margin: 2px 0 8px; }
.cetan-hero-desc { font-size: var(--net-fs-small); color: var(--net-ink-2); }
.cetan-starters { display: flex; flex-direction: column; gap: 6px; }
.cetan-starter { text-align: left; padding: 9px 12px; border-radius: var(--net-radius-sm); border: 1px solid var(--net-hairline); background: var(--net-card); color: var(--net-ink-2); font-size: var(--net-fs-small); cursor: pointer; }
.cetan-starter:hover { border-color: var(--net-primary); color: var(--net-ink); }
.cetan-body .msgs { display: flex; flex-direction: column; gap: 6px; }
.bubble.cetan { background: var(--net-card); }
.bubble.typing { display: inline-flex; gap: 4px; padding: 10px 12px; }
.bubble.typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--net-ink-3); animation: cetan-blink 1.2s infinite ease-in-out; }
.bubble.typing span:nth-child(2) { animation-delay: .2s; } .bubble.typing span:nth-child(3) { animation-delay: .4s; }
@keyframes cetan-blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
.cetan-send { display: flex; gap: 8px; align-items: flex-end; padding: var(--net-space-2) var(--net-panel-pad) var(--net-space-2); border-top: 1px solid var(--net-hairline); background: var(--net-band-chat); }
.cetan-send textarea { flex: 1; resize: none; min-height: 56px; }
.cetan-disclaimer { font-size: 10px; color: var(--net-ink-3); text-align: center; padding: 0 var(--net-panel-pad) 10px; }
@media (prefers-reduced-motion: reduce) { .bubble.typing span { animation: none; opacity: .6; } }

/* TEMPORARY dev toggle for the View-as bar (footer, next to trialcatcher.com) — dies with the dev bar. */
.net-footer .dev-eye { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; margin-left: -4px; border: 0; border-radius: var(--net-radius-sm); background: transparent; color: var(--net-chrome-ink-muted); cursor: pointer; opacity: .55; }
.net-footer .dev-eye:hover, .net-footer .dev-eye.on { opacity: 1; color: var(--net-chrome-ink); }

/* overlays for slide-over columns (mobile). Drawers SLIDE (transform) and the scrim FADES —
   never toggle display on a drawer, display can't animate. Reduced-motion collapses --net-dur-slow to 0. */
.scrim { display: none; }

/* ─── RESPONSIVE ──────────────────────────────────────────────────────────── */
/* ⚠️ min-width bound is LOAD-BEARING: without it the .right-none rules here (2-class specificity)
   out-rank the ≤859 single-column .net-cols rule and phones get three tracks — clinics default to
   right-none, so every clinic page collapsed to a 230px sliver on mobile (audit 2026-08-25 #1). */
@media (max-width: 1399px) and (min-width: 1100px) {
  .net-cols { grid-template-columns: minmax(230px,3fr) minmax(0,7fr) minmax(230px,3fr); }
  .net-shell.right-none .net-cols { grid-template-columns: minmax(230px,3fr) minmax(0,10fr) 0; }
  .net-header { grid-template-columns: var(--net-menu-w) minmax(230px,3fr) minmax(0,7fr) minmax(230px,3fr); }
  .net-shell.right-none .net-header { grid-template-columns: var(--net-menu-w) minmax(230px,3fr) minmax(0,10fr) 0; }
}
@media (max-width: 1099px) and (min-width: 860px) {
  .net-shell.right-none .net-cols { grid-template-columns: minmax(0,1fr) 0; }
}
@media (max-width: 1099px) {
  /* context column is a drawer here — nothing to align to; header goes back to a plain flex row */
  .net-header, .net-shell.right-none .net-header { display: flex; gap: var(--net-space-3); padding: 0 var(--net-space-4) 0 var(--net-space-3); }
  .net-header .net-lead { padding-left: 0; flex: 0 1 auto; }
  .net-header .net-actions { padding-right: 0; flex: 1; }
  .net-search { flex: 0 1 auto; width: min(420px, 34vw); }
  .scrim { display: block; position: absolute; inset: var(--net-header-h) 0 var(--net-footer-h) 0; background: rgba(16,24,40,.35); z-index: 34; opacity: 0; pointer-events: none; transition: opacity var(--net-dur-slow) var(--net-ease); }
  .net-cols { grid-template-columns: minmax(0,1fr) 320px; }
  .net-col.context { display: none; }
  .net-shell { --net-menu-w: var(--net-menu-w-collapsed); }
  .net-menu .grp-label, .net-menu a.item .label, .net-menu a.item .badge, .net-menu .mfoot .lbl { display: none; }
  /* Rail: no group headers, so nothing can re-open a collapsed group — every row stays reachable. */
  .net-menu .grp.collapsed a.item { display: flex; }
  .net-menu a.item { justify-content: center; padding: 0; }
  .net-brand .word { display: none; }
  .net-col.context { display: block; position: absolute; left: var(--net-menu-w); top: var(--net-header-h); bottom: var(--net-footer-h); width: 320px; z-index: 35; box-shadow: var(--net-elev-3); transform: translateX(calc(-100% - var(--net-menu-w))); visibility: hidden; transition: transform var(--net-dur-slow) var(--net-ease), visibility 0s linear var(--net-dur-slow); }
  .net-shell.ctx-open .net-col.context { transform: none; visibility: visible; transition-delay: 0s; }
  .net-shell.ctx-open .scrim { opacity: 1; pointer-events: auto; }
}
@media (max-width: 859px) {
  .net-shell { grid-template-columns: 0 minmax(0,1fr); grid-template-areas: "header header" "cols cols" "footer footer"; }
  .net-menu { display: flex; position: absolute; left: 0; top: var(--net-header-h); bottom: var(--net-footer-h); width: 240px; z-index: 36; box-shadow: var(--net-elev-3); --net-menu-w: 240px; transform: translateX(-100%); visibility: hidden; transition: transform var(--net-dur-slow) var(--net-ease), visibility 0s linear var(--net-dur-slow); }
  .net-shell.menu-open .net-menu { transform: none; visibility: visible; transition-delay: 0s; }
  .net-menu a.item .label, .net-menu a.item .badge, .net-menu .mfoot .lbl { display: initial; }
  /* ⚠️ The group header is a FLEX button (label · rule · chevron) — `display: initial` here would make
     it inline and drop the hairline and the chevron onto their own line. */
  .net-menu .grp-label { display: flex; }
  /* The drawer is the full menu, headers and all, so collapse means collapse again. */
  .net-menu .grp.collapsed a.item { display: none; }
  .net-menu a.item { justify-content: flex-start; padding: 0 10px; }
  .net-shell.menu-open .scrim, .net-shell.chat-open .scrim { opacity: 1; pointer-events: auto; }
  .net-cols { grid-template-columns: minmax(0,1fr); }
  .net-col.chat { display: block; position: absolute; right: 0; top: var(--net-header-h); bottom: var(--net-footer-h); width: min(360px, 100vw); z-index: 35; box-shadow: var(--net-elev-3); transform: translateX(100%); visibility: hidden; transition: transform var(--net-dur-slow) var(--net-ease), visibility 0s linear var(--net-dur-slow); }
  .net-shell.chat-open .net-col.chat { transform: none; visibility: visible; transition-delay: 0s; }
  .net-shell.ctx-open .net-col.context { left: 0; width: min(320px, 100vw); }
  .hamb { display: inline-flex !important; }
  .net-brand { min-width: 0; flex: 0 0 auto; }
  .net-brand .logo-wide { display: none; }
  .net-brand .logo-compact { display: block; }
  .net-search { display: none; }
  .net-header { gap: var(--net-space-1); padding-right: var(--net-space-3); }
  /* Mobile user tile = same 36x36 footprint as the compact logo, avatar centered, no chevron (Marcos 2026-08-24). */
  .net-user .name, .net-user .role { display: none; }
  .net-user .net-user-caret { display: none; }
  .net-user { width: 36px; height: 36px; padding: 0; gap: 0; justify-content: center; }
  .net-footer .copy, .net-footer .dev-eye, .net-footer .links, .net-footer .pill, .net-footer .net-lang-wrap, .net-footer .patent { display: none; }
  .net-footer { padding: 0; }
  .net-footer .tabbar { display: flex; width: 100%; height: 100%; }
  .net-footer .tabbar a, .net-footer .tabbar button { flex: 1; display: inline-flex; align-items: center; justify-content: center; background: none; border: 0; color: var(--net-chrome-ink-muted); text-decoration: none; height: 100%; }
  .net-footer .tabbar svg { width: 24px; height: 24px; }
  .net-footer .tabbar a.active, .net-footer .tabbar a:hover, .net-footer .tabbar button.active { color: var(--net-chrome-ink-active); }
  .grid-3, .grid-2 { grid-template-columns: minmax(0,1fr); }
  .grid-people { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .tile.list { flex-direction: column; }
  .tile.list .media { width: auto; aspect-ratio: 16/9; }
  .dev-bar { bottom: calc(var(--net-footer-h) + 6px); max-width: 96vw; overflow-x: auto; }
  .filters .input { width: 100%; }
  .work .col-body { padding: var(--net-space-3) 17px; } /* phones: 12px top/bottom, 17px sides (Marcos 2026-08-23: +5px) */
}

/* Referrals — generic de-identified progress rows (never .person / <tr>: the e2e gate forbids $ there) */
.ref-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); background: var(--net-sunken); }
.ref-row .prog { display: block; height: 4px; border-radius: var(--net-radius-pill); background: var(--net-ams-track); overflow: hidden; margin-bottom: 4px; }
.ref-row .prog i { display: block; height: 100%; width: calc(var(--p) * 1%); background: var(--net-primary); border-radius: inherit; }
@container work (max-width: 640px) { .ref-row { flex-wrap: wrap; } .ref-row .grow { flex-basis: 100%; order: 3; } }

/* Clinics Management: the action cluster stays on ONE line (buttons stacking read as a broken column). */
.table td.acts-cell { width: 1%; white-space: nowrap; }
@container work (max-width: 900px) { .table td.acts-cell .btn .lbl, .table td.acts-cell .btn span:not(.ico) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); } }

/* One suggested study, as a block you can read in one pass. */
.fit-row { padding: 10px 12px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); background: var(--net-sunken); }

/* Communications: what we are DOING to this person on the left, what has been SAID on the right
   (Marcos 2026-09-01, "50% of the screen the other 50% the conversations card"). Two equal columns,
   and `align-items: start` so the shorter column stops where its content stops instead of stretching
   a card to match its neighbour. Collapses at the same width the tab split does — two half-columns on
   a phone is two unreadable columns. */
.comms-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--net-card-gap); align-items: start; }
@container work (max-width: 1000px) { .comms-split { grid-template-columns: minmax(0, 1fr); } }
/* ⭐ EQUAL HEIGHT, where the two columns are ONE pair rather than a stack beside a card (Marcos
   2026-09-01: "Notes on this lead in height is the height of the add note card"). Writing a note and
   reading the notes is one act, and a tall empty box beside a short list read as two unrelated widgets.
   ⛔ NOT the default: Communications is three cards beside one, and stretching that one to match three
   would leave a conversation list floating in a card four times its own height. */
.comms-split.equal { align-items: stretch; }
.comms-split.equal > .card, .comms-split.equal > .stack > .card { height: 100%; }

/* ── The follow-up ladder, as steps (Marcos 2026-09-01) ────────────────────────────────────────────
   "what if the ladder is running and we want to see the ladder schedule". The engine only ever
   materialises the NEXT attempt, so a table of future calls would be invented — what IS knowable is the
   rung ORDER and which rung is live, and that is what this draws. The booked table below it carries the
   times, because those are real.
   Numbered, not bulleted: the whole point is that step 2 happens after step 1. */
.ladder-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.ladder-step { display: flex; align-items: center; gap: 10px; padding: 8px 10px; font-size: var(--net-fs-small);
  border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); background: var(--net-sunken); }
.ladder-step .n { display: grid; place-items: center; flex: none; width: 22px; height: 22px; border-radius: var(--net-radius-pill);
  background: var(--net-card); border: 1px solid var(--net-hairline); color: var(--net-ink-3);
  font: var(--net-fw-semibold) var(--net-fs-eyebrow)/1 var(--net-font-display); }
.ladder-step .ch { font-weight: var(--net-fw-medium); }
/* The live rung is the one thing a coordinator is looking for, so it is the only one that is tinted.
   ⛔ The "running now" chip carries the same fact in words — colour is never the only signal. */
.ladder-step.is-here { border-color: var(--net-primary); background: var(--net-primary-soft); }
.ladder-step.is-here .n { border-color: var(--net-primary); color: var(--net-primary); }
.ladder-step.is-done { opacity: .62; }

/* ── EQUAL CARDS ON A TAB PANEL (Marcos 2026-09-02) ──────────────────────────────────
   "the Lead & study and Consent on file cards, same size", and the same for the patient tab's three
   and the source tab's two.

   TWO SEPARATE PROBLEMS, so two separate modifiers — a tab that needs one does not always need both:

   · .pair — TWO cards must be TWO columns. .card-grid opens to THREE at a wide container, so a panel
     holding exactly two cards drew them at a third of the width each with a hole beside them, and the
     hole read as a card that failed to load.
   · .equal — the cards in a row end at the same line. `align-items: start` (the grid's default here)
     lets a four-row card stop halfway up its six-row neighbour, which is what "not the same size"
     actually looked like.

   ⛔ Neither is the default. A tab whose cards are genuinely different weights — a short summary beside
   a long table — is better ragged than padded to match. */
.card-grid.pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@container work (max-width: 640px) { .card-grid.pair { grid-template-columns: minmax(0, 1fr); } }
.card-grid.equal { align-items: stretch; }
.card-grid.equal > .card { height: 100%; }

/* ⭐ THE THREE CARDS IN THE COMMUNICATIONS COLUMN START AT ONE HEIGHT (Marcos 2026-09-02: "make
   Follow-up ladder, Booked and What's been tried same min height"). They are STACKED, not side by
   side, so `stretch` is meaningless here — the thing that made the column read as three unrelated
   scraps was an empty "Booked" collapsing to two lines under a 400px ladder. A floor, not a fixed
   height: a card with more to say still grows. */
.card.min-tall { min-height: 240px; }

/* Two buttons in a table's action cell had NOTHING between them (Marcos 2026-09-02: "the booked card
   needs some margin between change time and cancel"). ⛔ The cell stays a table cell — making it a
   flex row would take it out of the row's own vertical alignment for the sake of one gap. */
.table td.acts-cell > * + * { margin-left: 6px; }

/* ── What's been tried: the attempts, as rows ──────────────────────────────────────
   They were four bare <div class="small"> lines running together (Marcos 2026-09-02: "What's been
   tried add some margin and ui tidy up the events"). Each attempt is now its own row on the same
   hairline rhythm as .rec-row, with the channel, how it went and when in three fixed places — so a
   column of them scans down rather than having to be read sentence by sentence. */
.try-list { display: flex; flex-direction: column; }
.try-row { display: flex; align-items: baseline; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--net-hairline); font-size: var(--net-fs-small); }
.try-row:last-child { border-bottom: 0; }
.try-row .ch { flex: 0 0 92px; font-weight: var(--net-fw-medium); color: var(--net-ink); display: flex; align-items: center; gap: 6px; }
.try-row .out { flex: 1 1 auto; min-width: 0; color: var(--net-ink-2); overflow-wrap: anywhere; }
.try-row .wn { flex: none; color: var(--net-ink-3); font-variant-numeric: tabular-nums; }
@container work (max-width: 560px) { .try-row { flex-wrap: wrap; } .try-row .ch { flex-basis: 100%; } }

/* ── Next recommended action (Marcos 2026-09-02) ─────────────────────────────────
   Beside the timeline, half and half: what HAS happened, and what to do about it.
   The left stripe is the severity, and every row also carries its own icon and its own sentence. */
.nba { display: flex; gap: 10px; align-items: flex-start; padding: 10px; border: 1px solid var(--net-hairline);
  border-left: 3px solid var(--net-hairline-strong); border-radius: var(--net-radius-sm); background: var(--net-card); }
.nba + .nba { margin-top: 8px; }
.nba.sev-danger  { border-left-color: var(--net-danger-ink); }
.nba.sev-warn    { border-left-color: var(--net-warn-ink); }
.nba.sev-primary { border-left-color: var(--net-primary); }
.nba.sev-success { border-left-color: var(--net-success-ink); }
.nba-ico { flex: none; display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--net-radius-sm);
  background: var(--net-sunken); color: var(--net-ink-2); }
.nba.sev-danger  .nba-ico { background: var(--net-danger-soft);  color: var(--net-danger-ink); }
.nba.sev-warn    .nba-ico { background: var(--net-warn-soft);    color: var(--net-warn-ink); }
.nba.sev-primary .nba-ico { background: var(--net-primary-soft); color: var(--net-primary); }
.nba.sev-success .nba-ico { background: var(--net-success-soft); color: var(--net-success-ink); }
.nba-bd { min-width: 0; flex: 1 1 auto; }
.nba-ti { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); color: var(--net-ink); }
.nba-dt { font-size: var(--net-fs-small); color: var(--net-ink-2); margin-top: 2px; }
/* ⛔ THE FACT IT FIRED ON, always. A recommendation whose reason a coordinator cannot check is advice
   they have no reason to trust — the same rule the dashboard's recommendation cards follow. */
.nba-wy { font-size: var(--net-fs-eyebrow); color: var(--net-ink-3); margin-top: 5px; display: flex; align-items: center; gap: 5px; }
.nba-first { box-shadow: var(--net-elev-1); }

/* Billing ledger */
.bill-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-bottom: 1px solid var(--net-hairline); font-size: var(--net-fs-small); }
.bill-row:last-child { border-bottom: 0; }

/* ─── FLOAT-RIGHT (clinic workspace, Marcos 2026-08-24) ────────────────────────
   Clinic pages need the width: the right panels (Smoke Signals / Notifications / Cetan)
   OVERLAY the work column instead of pushing it. Member pages keep the pushing column.
   The shell gets .float-right when the signed-in persona is a clinic (MainLayout). */
@media (min-width: 860px) {
  .net-shell.float-right .net-cols { grid-template-columns: minmax(230px, 3fr) minmax(0, 11fr) 0; }
  .net-shell.float-right .net-header { grid-template-columns: var(--net-menu-w) minmax(230px, 3fr) minmax(0, 11fr) 0; }
  .net-shell.float-right .net-col.work { border-right: 0; }
  .net-shell.float-right .net-col.chat { display: block; position: absolute; right: 0; top: var(--net-header-h); bottom: var(--net-footer-h); width: min(380px, 92vw); z-index: 35; border-left: 1px solid var(--net-hairline); box-shadow: var(--net-elev-3); transform: translateX(100%); visibility: hidden; transition: transform var(--net-dur-slow) var(--net-ease), visibility 0s linear var(--net-dur-slow); }
  .net-shell.float-right:not(.right-none) .net-col.chat { transform: none; visibility: visible; transition-delay: 0s; }
}
@media (max-width: 1399px) and (min-width: 1100px) {
  .net-shell.float-right .net-cols { grid-template-columns: minmax(230px, 3fr) minmax(0, 10fr) 0; }
  .net-shell.float-right .net-header { grid-template-columns: var(--net-menu-w) minmax(230px, 3fr) minmax(0, 10fr) 0; }
}
@media (max-width: 1099px) and (min-width: 860px) {
  .net-shell.float-right .net-cols { grid-template-columns: minmax(0, 1fr) 0; }
}

/* ─── NO-CTX (clinic workspace, Marcos 2026-08-24) ─────────────────────────────
   The "Your clinic" context column shows ONLY on Network-category routes
   (/studies /clinics /bonfire /clans). On workspace pages the shell gets .no-ctx
   and the WORK column takes the context track. Header tracks stay untouched so
   the search pill keeps its size. Below 1100px the context is a drawer anyway. */
@media (min-width: 1100px) {
  .net-shell.no-ctx .net-cols,
  .net-shell.no-ctx.right-none .net-cols,
  .net-shell.no-ctx.float-right .net-cols { grid-template-columns: 0 minmax(0, 1fr) 0; }
  .net-shell.no-ctx .net-col.context { visibility: hidden; border-right: 0; }
}

/* auth ─────────────────────────────────────────────────────────────────────
   Login / Register / Apply-to-join-the-directory (Marcos 2026-08-24 kickoff):
   centered narrow cards on the work column; the guest menu's "Join TrialCatcher"
   row stands out from the rest of the Discover/Account nav. */
.auth-card { max-width: 640px; margin: 0 auto; width: 100%; }   /* was 420px — THIS was the real cap; .auth-center matches (Marcos 2026-08-24) */

/* ⛔ HONEYPOT (2026-09-01). Off-screen, NOT display:none and NOT hidden — a bot that skips hidden inputs
   would skip the trap too, and the whole point is that it fills this in. Nobody who can see or tab
   reaches it: the wrapper is aria-hidden and the input is tabindex="-1". Do not "tidy" this to
   display:none; that is the version that stops catching anything. */
.tc-hp { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
.ico-plate { width: 32px; height: 32px; border-radius: var(--net-radius-sm); background: var(--net-primary-soft); color: var(--net-primary); display: grid; place-items: center; flex: none; }
/* Join TrialCatcher = a PLAIN menu item (Marcos 2026-08-24): no special color, standard active state, one link selected alone. */

/* auth pages: form centered in the work column both axes (Marcos 2026-08-24) */
.auth-body { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - var(--net-header-h) - var(--net-footer-h) - 49px); }
.auth-center { width: min(640px, 100%); display: flex; flex-direction: column; gap: var(--net-space-3); }
.auth-center .page-hd { justify-content: center; text-align: center; }
.auth-center .page-hd .grow { flex: 0 1 auto; }

/* "Run a research clinic?" CTA on the auth pages — one calm clickable band, not a second form card */
.clinic-cta { display: flex; align-items: center; gap: var(--net-space-3); margin-top: var(--net-space-5); padding: 14px 16px; border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius-md); background: var(--net-card); color: inherit; text-decoration: none; transition: border-color var(--net-dur) var(--net-ease), box-shadow var(--net-dur) var(--net-ease), transform var(--net-dur) var(--net-ease); }
.clinic-cta:hover { border-color: var(--net-secondary); box-shadow: var(--net-elev-1); text-decoration: none; transform: translateY(-1px); }
.clinic-cta .txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.clinic-cta .arr { display: grid; place-items: center; color: var(--net-ink-3); transition: transform var(--net-dur) var(--net-ease), color var(--net-dur) var(--net-ease); }
.clinic-cta:hover .arr { transform: translateX(3px); color: var(--net-secondary); }

/* Work-column gutters (Marcos 2026-08-24): the main panel gets wider left/right breathing room than the side panels; head stays aligned with body. */
@media (min-width: 860px) {
  .net-col.work > .col-body, .net-col.work > .col-head { padding-left: var(--net-space-6); padding-right: var(--net-space-6); }
}
@media (min-width: 1400px) {
  .net-col.work > .col-body, .net-col.work > .col-head { padding-left: var(--net-space-7); padding-right: var(--net-space-7); }
}

/* Mobile form inputs (Marcos 2026-08-24). 16px on touch widths = the iOS auto-zoom guard (same LAW as Admin app.css —
   never the viewport maximum-scale hack, it kills pinch-zoom). Taller controls for touch targets. */
@media (max-width: 767.98px) {
  .input, .select, textarea.input, .net-search input, .composer textarea { font-size: 16px; }
  input.input, .select { height: 44px; }
}
.net-user-caret { display: inline-flex; align-items: center; }

/* Tent header actions (Marcos 2026-08-25): ONE horizontal row — moderation (Ban) sits to the LEFT of the
   member CTA, and the tribe-only Smoke Signals note stays on a single line. Was a column until 08-25;
   the column put Ban above the CTA and squeezed the note onto two lines. */
.acts.acts-stack { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
.acts.acts-stack > .meta { text-align: right; white-space: nowrap; }
/* phones: the row already wraps (see .clinic-hd .acts above) — let the note wrap with it rather than
   forcing a horizontal overflow on a 360px screen. */
@container work (max-width: 640px) {
  .acts.acts-stack { justify-content: flex-start; }
  .acts.acts-stack > .meta { white-space: normal; text-align: left; }
}

/* ── Map (spec 2026-08-24): one engine, role-adaptive. Split view = list rail + canvas. ───────── */
.tc-map-page .tc-map-split { display: grid; grid-template-columns: 340px 1fr; gap: var(--net-space-4, 16px); min-height: 480px; height: calc(100vh - 320px); }
.tc-map-rail { overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding-right: 2px; }
.tc-map-canvas { position: relative; border-radius: var(--net-radius, 12px); overflow: hidden; border: 1px solid var(--net-hairline); background: var(--net-sunken); }
.tc-map { position: absolute; inset: 0; z-index: 0; }
.tc-map-mobile-toggle { display: none; }

.tc-map-row { display: flex; gap: 10px; align-items: flex-start; text-align: left; width: 100%; border: 1px solid var(--net-hairline); background: var(--net-card); border-radius: var(--net-radius, 12px); padding: 10px 12px; cursor: pointer; font: inherit; color: var(--net-ink); }
.tc-map-row:hover { border-color: var(--net-hairline-strong); }
.tc-map-row.selected { border-color: var(--net-primary); box-shadow: 0 0 0 1px var(--net-primary); }
.tc-map-row .t { display: block; font-weight: 600; font-size: 13.5px; }
.tc-map-row .s { display: block; color: var(--net-ink-3); font-size: 12px; margin-top: 1px; }
.tc-map-row .chips-row { gap: 5px; margin-top: 5px; align-items: center; }
.tc-map-row .sm-chip { font-size: 10.5px; padding: 1px 7px; }
.tc-map-row .pinmark { flex: none; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #fff; margin-top: 2px; }
.tc-map-row .pinmark.clinic { border-radius: 8px; }
.pinmark.accent-a { background: linear-gradient(135deg, #0A5C2B, #17889A); }
.pinmark.accent-b { background: linear-gradient(135deg, #11707F, #17889A); }
.pinmark.accent-c { background: linear-gradient(135deg, #A84A08, #E8700A); }
.pinmark.accent-d { background: linear-gradient(135deg, #0C2E1E, #0B8040); }
.pinmark.accent-e { background: linear-gradient(135deg, #0B8040, #5FCF8A); }
.pinmark.accent-f { background: linear-gradient(135deg, #E8700A, #F5B36E); }

/* Pins & clusters (Leaflet divIcons) */
.tc-map-pin-wrap, .tc-map-cluster-wrap { background: none; border: none; }
.tc-map-pin { width: 30px; height: 30px; border-radius: 50% 50% 50% 4px; transform: rotate(0deg); display: flex; align-items: center; justify-content: center; border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,.35); background: var(--net-primary); }
.tc-map-pin.kind-clinic { border-radius: 8px; }
.tc-map-pin .dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; }
.tc-map-pin.accent-a { background: linear-gradient(135deg, #0A5C2B, #17889A); }
.tc-map-pin.accent-b { background: linear-gradient(135deg, #11707F, #17889A); }
.tc-map-pin.accent-c { background: linear-gradient(135deg, #A84A08, #E8700A); }
.tc-map-pin.accent-d { background: linear-gradient(135deg, #0C2E1E, #0B8040); }
.tc-map-pin.accent-e { background: linear-gradient(135deg, #0B8040, #5FCF8A); }
.tc-map-pin.accent-f { background: linear-gradient(135deg, #E8700A, #F5B36E); }
.tc-map-cluster { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--net-chrome); color: #fff; font-weight: 700; font-size: 13px; border: 2px solid #fff; box-shadow: 0 2px 8px rgba(0,0,0,.35); font-family: var(--net-font-display); }
.tc-map-cluster.md { background: var(--net-primary); }
.tc-map-cluster.lg { background: var(--net-secondary-ink); font-size: 14px; }
/* A listing cluster wears the listing pin's squared shape — a badge over a clinic map is either a
   count of people or a count of listings, and shape says which before the hover title does. */
.tc-map-cluster.listings { border-radius: 10px; }

/* Density cells + viewer dot */
.tc-map-density { fill: var(--net-tertiary); fill-opacity: .28; }
.tc-map-viewer { fill: var(--net-info); fill-opacity: .95; stroke: #fff; stroke-width: 2; }
.tc-map-density-tip { font-family: var(--net-font-body); }
.tc-map-legend { position: absolute; left: 10px; bottom: 10px; z-index: 500; background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: 8px; padding: 5px 10px; font-size: 11.5px; color: var(--net-ink-2); display: flex; align-items: center; gap: 7px; box-shadow: 0 2px 8px rgba(0,0,0,.12); }
.tc-map-legend .swatch.density { width: 12px; height: 12px; border-radius: 50%; background: var(--net-tertiary); opacity: .45; flex: none; }

/* Popup card — match the app's card language */
.tc-map-popup .leaflet-popup-content-wrapper { border-radius: var(--net-radius, 12px); background: var(--net-card); color: var(--net-ink); box-shadow: 0 6px 24px rgba(0,0,0,.22); font-family: var(--net-font-body); }
.tc-map-popup .leaflet-popup-content { margin: 12px 14px; line-height: 1.35; }
.tc-map-popup .leaflet-popup-tip { background: var(--net-card); }
.tc-map-card { min-width: 220px; }
.tc-map-card .title { font-weight: 700; font-size: 13.5px; color: var(--net-ink); text-decoration: none; display: block; }
.tc-map-card .title:hover { color: var(--net-primary); text-decoration: underline; }
.tc-map-card .sub { color: var(--net-ink-3); font-size: 12px; margin-top: 2px; }
.tc-map-card .chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.tc-map-card .chip { font-size: 10.5px; padding: 1px 8px; border-radius: 999px; background: var(--net-sunken); color: var(--net-ink-2); border: 1px solid var(--net-hairline); }
.tc-map-card .chip.ok { background: var(--net-success-soft); color: var(--net-success-ink); border-color: transparent; }
.tc-map-card .chip.st { background: var(--net-primary-soft); color: var(--net-primary); border-color: transparent; }
.tc-map-card .actions { display: flex; gap: 7px; margin-top: 10px; }

/* Mobile: one pane at a time, toggle in the page header */
@media (max-width: 859.98px) {
  .tc-map-page .tc-map-split { grid-template-columns: 1fr; height: auto; }
  .tc-map-mobile-toggle { display: flex; }
  .tc-map-split.pane-map .tc-map-rail { display: none; }
  .tc-map-split.pane-list .tc-map-canvas { display: none; }
  .tc-map-canvas { height: calc(100vh - 300px); min-height: 380px; }
  .tc-map-rail { max-height: none; }
}

/* Dark theme: keep pins/cards legible on the dark card surface; dim the basemap slightly. */
[data-theme="dark"] .tc-map-canvas .leaflet-tile { filter: brightness(.82) contrast(1.05); }
[data-theme="dark"] .tc-map-pin, [data-theme="dark"] .tc-map-cluster { border-color: var(--net-card); }

/* Map roles B/C/D (spec 2026-08-24): lead pins + whitespace demand cells. */
.tc-map-pin.kind-lead { border-radius: 50%; }
.tc-map-row.kind-lead .pinmark { border-radius: 50%; }

/* ⭐ A CLINIC MUST NOT LOOK LIKE ITS OWN STUDIES (Marcos 2026-09-03: "icon of the clinic in the map is
   too similar to the study one — we need a different colour and maybe a map tag icon, slight but
   noticeable").

   The shapes already differed (teardrop / rounded square / circle) but the COLOUR did not: every pin,
   whatever its kind, was painted with the clinic's own accent gradient, so a clinic sat on the map in
   exactly the livery of the studies around it. At 30px, one shared colour beats three subtle shapes.

   These rules come AFTER the .accent-* block on purpose — same specificity (0,2,0), so source order is
   what lets the clinic override the accent it would otherwise inherit. Moving this block above them
   silently restores the bug.

   ⛔ NEVER COLOUR ALONE (WCAG 1.4.1): the clinic keeps its own silhouette (a squarer map tag with a
   pointed corner, sized up 2px) and a SQUARE glyph where a study has a round one, so the distinction
   survives greyscale and colour-blindness. */
.tc-map-pin.kind-clinic {
  width: 32px; height: 32px;
  background: var(--net-ink);            /* one flat ink for every clinic, never the brand accent */
  border-radius: 9px 9px 2px 9px;        /* a tag: the pointed corner reads as "a place", not "a listing" */
  border-color: #fff;
}
.tc-map-pin.kind-clinic .dot { width: 9px; height: 9px; border-radius: 2px; }   /* square glyph = a building */
[data-theme="dark"] .tc-map-pin.kind-clinic { background: var(--net-ink-2); }
/* The list rail beside the map carries the same mark, so the row and the pin agree. */
.tc-map-row.kind-clinic .pinmark { background: var(--net-ink); border-radius: 5px 5px 1px 5px; }
[data-theme="dark"] .tc-map-row.kind-clinic .pinmark { background: var(--net-ink-2); }
.tc-map-density.whitespace { fill: var(--net-secondary); fill-opacity: .30; }
.tc-map-legend .swatch.density.ws { background: var(--net-secondary); opacity: .5; }

/* 6-up KPI strip — the count-named sibling of .kpis.k4, one wall for every six-tile row (the clinic
   dashboard's Overview / Patients / Financial tabs and the SuperAdmin all-clinics view). Mirrored
   breakpoints so mobile compaction still applies. Was .ndash-kpis, named for one page it outgrew. */
.kpis.k6 { grid-template-columns: repeat(6, minmax(0,1fr)); }
@container work (max-width: 640px) { .kpis.k6 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
@container work (max-width: 480px) { .kpis.k6 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
/* Heat map (TcHeat) — 7 days × 12 two-hour bands, magnitude by opacity over the primary colour.
   Hand-rolled grid, no library; the 0.06 opacity floor keeps an empty cell reading as grid, not hole. */
.tc-heat-wrap { display: flex; flex-direction: column; gap: 8px; }
.tc-heat { display: grid; grid-template-columns: 30px repeat(12, minmax(0, 1fr)); gap: 2px; }
.tc-heat-cell { aspect-ratio: 1; border-radius: 2px; background: var(--net-primary); min-width: 0; }
.tc-heat-lab { font-size: 10px; color: var(--net-ink-3); align-self: center; justify-self: start; line-height: 1; }
.tc-heat-scale { display: flex; gap: 4px; align-items: center; justify-content: flex-end; }
.tc-heat-scale .tc-heat-cell { aspect-ratio: auto; border-radius: 2px; }

/* Sparkline — a bare row of bars, no library. Each bar's HEIGHT is a percentage of the series max,
   set inline by the caller; the track gives them a floor to stand on. Used by Portal Visits. */
.tc-spark { display: flex; align-items: flex-end; gap: 2px; height: 56px; }
.tc-spark-bar { flex: 1 1 0; min-width: 2px; border-radius: 2px 2px 0 0; background: var(--net-primary); opacity: .75; }
.tc-spark-bar:last-child { opacity: 1; }   /* today reads as today */
/* Message preview — a campaign's copy shown as something close to what lands, not as a form field.
   The body keeps the author's line breaks; nothing here is user HTML, Blazor escapes it. */
.tc-msg-preview { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); overflow: hidden; }
.tc-msg-subject { padding: 10px 12px; border-bottom: 1px solid var(--net-hairline); background: var(--net-sunken); font-weight: 600; }
.tc-msg-body { padding: 12px; white-space: pre-wrap; line-height: 1.55; }
.tc-msg-foot { padding: 8px 12px; border-top: 1px solid var(--net-hairline); background: var(--net-sunken); }
/* A switched-ON override reads as a decision, not a default. */
.tc-warn-box { border-color: var(--net-warn) !important; }
.tc-warn { color: var(--net-warn-ink); }   /* the ink variant — --net-warn is a border/fill colour, not a text one */
/* Ad preview — the CANVAS holds the clinic's REAL rendered creative when one exists (streamed through
   /media-studio/{id}/file, 2026-08-30); when it doesn't, it falls back to a described placeholder and
   never a fabricated photo. The copy panel under it is the real thing, laid out the way it lands.
   The hatched background stays: it is what a canvas with no artifact looks like. Behind an artifact whose
   own ratio differs from the declared aspect the letterbox is the media element's own black. */
.tc-ad-preview { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); overflow: hidden; }
.tc-ad-canvas { display: flex; align-items: center; justify-content: center; padding: 16px; text-align: center;
  background: repeating-linear-gradient(45deg, var(--net-sunken) 0 10px, var(--net-card) 10px 20px); }
/* contain, never cover: an ad is TYPE on a picture, and cropping it to fill the box is how a headline
   loses its first word. padding:0 on the canvas would fight the placeholder's inset, so the artifact
   simply fills the padded box instead. */
.tc-ad-canvas:has(.tc-ad-media) { padding: 0; }
/* ⛔⭐ THE CAP BELONGS ON THE BOX, NOT ONLY ON WHAT IS INSIDE IT (Marcos 2026-09-02: "that video is too
   big in screen and its panel wrapper. why so much top and bottom margin?").
   The canvas carries an inline `aspect-ratio` from the creative's own format. At 9:16 in a ~715px
   column that is 1271px TALL — taller than any laptop screen. `max-height` on the media capped the
   VIDEO but not the box holding it, so the box kept its full height and the extra showed up as two
   enormous striped bands above and below: the "margin" was the canvas itself.
   Capping the canvas makes aspect-ratio yield to it, and object-fit:contain keeps the whole frame
   visible inside — the sides letterbox instead, which is the right trade for an ad (never crop type). */
/* ⭐ And CENTRED (Marcos 2026-09-02). Once the height cap bites, aspect-ratio shrinks the box to match
   — a 9:16 becomes ~389px in a ~715px column — so a block-level canvas sat hard against the left edge
   with the slack all on one side. `margin-inline:auto` is the whole fix: the box keeps its ratio and
   the leftover width splits evenly. Portrait and landscape both centre; a creative wider than the
   column is unaffected because there is no slack to split. */
.tc-ad-canvas { max-height: 70dvh; margin-inline: auto; }
.tc-ad-media { display: block; width: 100%; height: 100%; max-height: 70dvh; object-fit: contain; background: #000; }
/* A library tile's thumbnail. Fixed height so a grid of mixed aspect ratios still reads as a grid. */
.tc-lib-thumb { display: block; width: 100%; height: 128px; object-fit: cover; border-radius: var(--net-radius-sm, 6px);
  margin-bottom: 8px; background: var(--net-sunken); }
/* A library tile with a row CTA. The anchor IS the tile (a.kpi, unchanged); the wrapper only exists so
   the trash can be a SIBLING of the link rather than a button inside it, pinned to the corner the
   timestamp already sits in. The anchor's own bottom row keeps its right-side breathing room, so the
   date is never under the icon. */
.tc-lib-tile { position: relative; display: flex; }
.tc-lib-tile > a.kpi { flex: 1 1 auto; min-width: 0; }
.tc-lib-tile > a.kpi > .row:last-child { padding-right: 36px; }
.tc-lib-tile-act { position: absolute; right: 10px; bottom: 10px; }
.tc-ad-note { color: var(--net-ink-3); font-size: var(--net-fs-small); max-width: 34ch; line-height: 1.5; }
.tc-ad-burn { font-weight: 800; font-size: clamp(18px, 4vw, 30px); line-height: 1.15; color: var(--net-ink); max-width: 16ch; }
.tc-ad-copy { padding: 12px; border-top: 1px solid var(--net-hairline); }
.tc-ad-headline { font-weight: 700; font-size: var(--net-fs-body); margin-bottom: 4px; }
.tc-ad-body { color: var(--net-ink-2); font-size: var(--net-fs-small); line-height: 1.5; }
.tc-ad-comp { color: var(--net-ink-3); font-size: var(--net-fs-small); margin-top: 6px; }
/* A report reads on screen and PRINTS to PDF through the browser — no PDF engine on this side. */
/* The open row of a list whose detail is rendered below it. Subtle on purpose: the detail card IS the
   feedback; this only answers "which one am I reading". */
.row-selected > td { background: var(--net-surface-2, rgba(127,127,127,.06)); }
.tc-report-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--net-hairline); margin-bottom: 10px; }
/* A PIPED clinic's report is their own PDF, embedded. Tall by design — a report read in a 300px
   letterbox sends people to the download button, which is the trip we are removing. dvh, not vh, so
   the mobile URL bar does not crop the last band of the page. */
.tc-report-doc { width: 100%; height: min(1100px, 78dvh); min-height: 420px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: var(--net-surface-2, #fff); display: block; }
@media (max-width: 720px) { .tc-report-doc { height: 70dvh; min-height: 340px; } }
@media print {
  .net-menu, .net-col:not(.work), .tc-noprint, .dev-bar, footer { display: none !important; }
  .net-col.work, .col-body { overflow: visible !important; height: auto !important; padding: 0 !important; }
  .tc-report { border: 0 !important; box-shadow: none !important; }
  .ndash-bar-fill { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
}
/* Lead Map inside a dashboard card. POSITION: RELATIVE is load-bearing, not decoration: .tc-map is
   `position: absolute; inset: 0`, so without a positioned ancestor here it anchors to the page and the
   map covers the whole tab. The full-page map gets this from .tc-map-canvas; a card has to say it too. */
.tc-dash-map { position: relative; height: 260px; border-radius: var(--net-radius-md); overflow: hidden;
  border: 1px solid var(--net-hairline); background: var(--net-sunken); }
/* Dashboard widgets stand a common height (Marcos 2026-08-27) so a tab does not read as a ragged wall
   of different-sized boxes. Applies to the WIDGET cards only — the filter strip sits outside the
   wrapper, and KPI tiles are .kpi, not .card. A card with more than 400px of content still grows. */
/* The wrapper is a layout element, not just a hook: it stands in for .col-body as these widgets'
   parent, so it has to carry .col-body's own column gap or every widget stacks flush. */
.dash-widgets { display: flex; flex-direction: column; gap: var(--net-card-gap); }
.dash-widgets .card { min-height: 400px; }
@container work (max-width: 640px) { .dash-widgets .card { min-height: 0; } } /* phones: height is scroll, not symmetry */

.ndash-bar-track { height: 8px; border-radius: var(--net-radius-pill); background: var(--net-sunken); overflow: hidden; }
.ndash-bar-fill { height: 100%; border-radius: inherit; background: var(--net-primary); }

/* Leads by Source & Channel: pre-screened against the rest, the clinic's own two-segment bar
   (parity pass 2026-08-31). Both segments scale to the BIGGEST row, not to their own row, so the
   widths compare across rows — a per-row percentage would make every bar full width and say nothing. */
.ndash-bar-track.nd-split { display: flex; }
.ndash-bar-track.nd-split i { display: block; height: 100%; }
.ndash-bar-track.nd-split .seg-a { background: var(--net-success); }
.ndash-bar-track.nd-split .seg-b { background: var(--net-primary); opacity: .45; }
/* The rate's own verdict, on the clinic's 40 / 15 thresholds. */
.meta.ok   { color: var(--net-success-ink); }
.meta.warn { color: var(--net-warn-ink); }
.meta.bad  { color: var(--net-danger-ink); }

/* Front-desk score: one number, three zones, a pin — the network's read of the backoffice's
   .tc-frontdesk-scorebar (parity audit 2026-08-30, row #22). Same 0-49 / 50-79 / 80-100 thresholds. */
.nd-score { margin-bottom: 10px; }
.nd-score-pin { position: relative; height: 16px; }
.nd-score-pin b { position: absolute; transform: translateX(-50%); font-size: 12px; font-weight: var(--net-fw-bold); color: var(--net-ink); white-space: nowrap; }
.nd-score-track { display: flex; height: 8px; border-radius: var(--net-radius-pill); overflow: hidden; }
.nd-score-track i { display: block; height: 100%; }
.nd-score-track .z-red { width: 50%; background: #d9534f; }
.nd-score-track .z-amber { width: 30%; background: #e0a232; }
.nd-score-track .z-green { width: 20%; background: #35674b; }
.nd-score-labels { display: flex; justify-content: space-between; margin-top: 3px; font-size: 10px; color: var(--net-ink-2); }

/* ── Dashboard widget furniture (Marcos 2026-08-27, round 2) ─────────────────────────────────────
   Everything below exists because a clinic looks at this page and its own backoffice side by side.
   Where the backoffice has an affordance, the network has the SAME affordance — not an approximation. */

/* Segmented lane toggle ("slide filters"), the network's read of the backoffice btn-group that sits
   top-right of Messages Needing Attention and the Automated Calls Queue. Never wraps: it is pinned in
   a header row whose title truncates instead. */
.dash-seg { display: inline-flex; flex: none; gap: 0; border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius-sm); overflow: hidden; background: var(--net-card); }
.dash-seg button { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 8px; border: 0; border-right: 1px solid var(--net-hairline); background: none; color: var(--net-ink-2); font-size: 11px; font-weight: var(--net-fw-medium); cursor: pointer; white-space: nowrap; }
.dash-seg button:last-child { border-right: 0; }
.dash-seg button:hover { color: var(--net-ink); background: var(--net-sunken); }
.dash-seg button.on { background: var(--net-primary-soft); color: var(--net-primary); }
.dash-seg .n { display: inline-grid; place-items: center; min-width: 16px; height: 15px; padding: 0 4px; border-radius: var(--net-radius-pill); background: var(--net-sunken); color: var(--net-ink-2); font-size: 10px; font-variant-numeric: tabular-nums; }
.dash-seg button.on .n { background: var(--net-primary); color: var(--net-on-primary); }
.dash-seg .n.alarm { background: var(--net-danger-soft); color: var(--net-danger-ink); }

/* A worklist row that opens something. The whole row is the target — same as the backoffice's
   tc-worklist-row--clickable — so the hit area is the card width, not the name. */
.dash-row-link { display: flex; align-items: center; gap: 10px; width: 100%; margin: 5px 0; padding: 6px; text-align: left; background: none; border: 1px solid transparent; border-radius: var(--net-radius-sm); color: inherit; text-decoration: none; cursor: pointer; }
.dash-row-link:hover { background: var(--net-sunken); border-color: var(--net-hairline); text-decoration: none; color: inherit; }
/* A worklist row that ALSO carries a trailing action ("Mark handled"). The row itself is then a plain
   div — a <button> inside a <button> is invalid HTML and swallows the outer click — and this is the
   part of it that opens the record. Stretches to fill the row so the hit area is still the card width.
   Marcos 2026-08-30: the action used to sit in its own row underneath and dangled below every entry. */
.dash-row-open { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; padding: 0; margin: 0; background: none; border: 0; text-align: left; color: inherit; font: inherit; cursor: pointer; }
/* ⛔ THE SPLIT ROW IS NOT A CONTROL. .dash-row-link is worn by an <a> and by a <button> elsewhere, so
   the pointer cursor and hover wash above are right there — but the split row is a plain <div> with no
   handler, and it inherited both. A hand cursor over something that does nothing when clicked is the
   affordance-without-behaviour the row split was created to remove. The cue moves to the part that IS
   the control: hovering .dash-row-open still washes the whole row, so nothing is lost visually. */
.dash-row-link.dash-row-split { cursor: default; }
.dash-row-link.dash-row-split:hover { background: none; border-color: transparent; }
.dash-row-link.dash-row-split:has(.dash-row-open:hover) { background: var(--net-sunken); border-color: var(--net-hairline); }
.dash-row-link:focus-within { border-color: var(--net-hairline); }

/* Patient Pool tiles. The backoffice paints four soft-tinted tiles inside the card; plain label/value
   rows read as a table there and as an afterthought here. */
.tc-pool { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--net-space-3); }
.tc-pool-tile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: var(--net-space-4) var(--net-space-3); border-radius: var(--net-radius-md); text-align: center; }
.tc-pool-tile .v { font: var(--net-fw-bold) 1.5rem/1.1 var(--net-font-display); letter-spacing: -.02em; }
.tc-pool-tile .k { font-size: 11px; color: var(--net-ink-2); }
.tc-pool-tile.t-primary { background: var(--net-primary-soft); } .tc-pool-tile.t-primary .v { color: var(--net-primary); }
.tc-pool-tile.t-warn    { background: var(--net-warn-soft); }    .tc-pool-tile.t-warn .v    { color: var(--net-warn-ink); }
.tc-pool-tile.t-info    { background: var(--net-info-soft); }    .tc-pool-tile.t-info .v    { color: var(--net-info-ink); }
.tc-pool-tile.t-success { background: var(--net-success-soft); } .tc-pool-tile.t-success .v { color: var(--net-success-ink); }
.tc-pool-tile.t-danger  { background: var(--net-danger-soft); }  .tc-pool-tile.t-danger .v  { color: var(--net-danger-ink); }

/* Pie / donut. No chart library on this side, so the slices are ONE conic-gradient built server-side
   and the legend carries the numbers — a pie nobody can read a value off is decoration. */
.tc-pie-wrap { display: flex; align-items: center; gap: var(--net-space-5); flex-wrap: wrap; }
.tc-pie { flex: none; width: 148px; height: 148px; border-radius: 50%; box-shadow: var(--net-elev-1); }
.tc-pie.donut { -webkit-mask: radial-gradient(circle, transparent 44%, #000 45%); mask: radial-gradient(circle, transparent 44%, #000 45%); }
.tc-legend { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1 1 140px; }
.tc-legend .row { align-items: center; gap: 8px; }
.tc-swatch { flex: none; width: 10px; height: 10px; border-radius: 3px; }

/* ── TcDonut, the interactive one (Marcos 2026-08-31) ────────────────────────────────
   The old .tc-pie above is a single div painted with a conic-gradient — nothing to hover.
   These rules dress the SVG version, where each slice is a real <path> button. .tc-pie is
   kept because other things still use the flat swatch look; do not delete it here. */
.tc-pie-svg { position: relative; flex: none; width: 148px; height: 148px; }
.tc-pie-svg svg { width: 100%; height: 100%; display: block; overflow: visible; }
.tc-slice { cursor: pointer; transform-origin: 50% 50%; transition: transform var(--net-dur, .18s) var(--net-ease, ease), opacity var(--net-dur, .18s) var(--net-ease, ease); }
/* Everything that is NOT the picked slice steps back, so the pick reads at a glance rather than
   relying on a 3-unit nudge the eye has to hunt for. */
.tc-slice.dim { opacity: .38; }
.tc-slice.on { filter: brightness(1.06); }
/* SVG outlines are unreliable across browsers; a stroke is the focus ring here. */
.tc-slice:focus-visible { outline: none; stroke: var(--net-ink); stroke-width: 2; paint-order: stroke; }
.tc-pie-hole { fill: var(--net-card); pointer-events: none; }
/* The middle of the ring names what is highlighted — the reason the ring has a hole at all. */
/* ⛔ THE LABEL MUST FIT INSIDE THE HOLE, not merely inside the square. The ring's hole is r=24 of a
   100 viewBox on a 148px chart — about 71px across — so a box sized to the chart's width spills the
   text out over the coloured ring, where it is unreadable ("Alopecia Areat" against dark green).
   64px keeps it inside the hole with a margin, and long names ellipsis rather than overflow. */
.tc-pie-mid { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; pointer-events: none; padding: 0 42px; }
.tc-pie-mid .v { display: block; font-size: 19px; font-weight: 700; line-height: 1.1; color: var(--net-ink); }
.tc-pie-mid .k { display: block; font-size: 10px; line-height: 1.25; color: var(--net-ink-2); max-width: 64px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The legend row is a button now; strip the UA chrome and keep the .row layout it already had. */
.tc-legend-row { display: flex; width: 100%; background: none; border: 0; padding: 3px 6px; margin: 0; border-radius: var(--net-radius-sm); cursor: pointer; text-align: left; color: inherit; font: inherit; transition: background var(--net-dur, .18s) var(--net-ease, ease); }
.tc-legend-row:hover, .tc-legend-row.on { background: var(--net-sunken); }
.tc-legend-row:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }

/* A pointer-driven lift is decoration; someone who asked for less motion still gets the highlight
   through opacity and the legend wash. */
@media (prefers-reduced-motion: reduce) {
    .tc-slice { transition: none; transform: none !important; }
    .tc-legend-row { transition: none; }
}
/* Six slice colours, in the order the network already uses for accents. A seventh category folds into
   "Other" upstream rather than repeating a colour — two slices in one colour is a lie about the data. */
.tc-sl-1 { background: #35674b; } .tc-sl-2 { background: #6b9e7f; } .tc-sl-3 { background: #c8a45c; }
.tc-sl-4 { background: #8a6fa8; } .tc-sl-5 { background: #4d84a8; } .tc-sl-6 { background: #b1735e; }

/* Expand-to-fullscreen dialog. The backoffice expands the Lead Map into a modal ON the dashboard; it
   never navigates away, because leaving the page loses the filters the map was answering for. */
.tc-dlg { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(16,24,40,.45); }
.tc-dlg-box { display: flex; flex-direction: column; width: min(1240px, 96vw); height: min(860px, 92vh); background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-lg); box-shadow: var(--net-elev-3); overflow: hidden; }
.tc-dlg-hd { display: flex; align-items: center; gap: var(--net-space-2); padding: var(--net-space-3) var(--net-space-4); border-bottom: 1px solid var(--net-hairline); }
/* ⭐ THE CLOSE BUTTON GOES TO THE RIGHT EDGE (Marcos 2026-09-01, on the invoice popup).
   Every one of these dialogs already writes class="grow" on its title block expecting exactly that, and
   for four of them the class did NOTHING — there was no rule for it, so the title kept only the width of
   its text and the close button sat glued to the last word. It was patched once, scoped to the video
   dialog, and deliberately not fixed globally at the time; the same bug then showed up on the next
   dialog built. Fixing the SHELL is the fix: one rule, and no dialog can be born with it again. */
.tc-dlg-hd .grow { flex: 1 1 auto; min-width: 0; }
/* And the button never shrinks to make room for a long title — a squashed close target is worse than a
   truncated heading. */
.tc-dlg-hd > .btn { flex: none; }
.tc-dlg-body { display: flex; min-height: 0; flex: 1; }
.tc-dlg-map { position: relative; flex: 1 1 auto; min-width: 0; }
.tc-dlg-rail { flex: none; width: 320px; border-left: 1px solid var(--net-hairline); overflow-y: auto; padding: var(--net-space-3); display: flex; flex-direction: column; gap: 4px; }
@media (max-width: 860px) { .tc-dlg-rail { display: none; } }

/* The card-size Lead Map fills whatever the card gives it (the backoffice's flex-grow-1), instead of
   standing at a fixed 260px inside a 400px card and leaving a hole under itself. */
.dash-map-card { display: flex; flex-direction: column; }
.dash-map-card .tc-dash-map { flex: 1 1 auto; height: auto; min-height: 240px; }

/* Recommended Actions. Two shapes, deliberately different at a glance: an ACTION card is a button and
   carries a CTA, an INSIGHT card is read-only. Colour is the severity, not decoration. */
/* ⭐ THE TWO INSIGHT WIDGETS, SIDE BY SIDE (Marcos 2026-09-01): what Cetán found | what we recommend.
   ⛔ A half with nothing in it is never rendered, so :only-child hands the survivor the full width —
   an empty column beside a full one reads as a widget that failed to load, not as an empty widget. */
.dash-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--net-space-5); align-items: start; }
.dash-pair > section { min-width: 0; }
.dash-pair > section:only-child { grid-column: 1 / -1; }
@container work (max-width: 1000px) { .dash-pair { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 1000px) { .dash-pair { grid-template-columns: minmax(0, 1fr); } }
.tc-rec-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--net-card-gap); }
@container work (max-width: 860px) { .tc-rec-grid { grid-template-columns: 1fr; } }
.tc-rec { display: flex; flex-direction: column; gap: var(--net-space-3); width: 100%; text-align: left; color: inherit; border-left: 3px solid var(--net-hairline-strong); }
.tc-rec.act { cursor: pointer; }
.tc-rec.act:hover { box-shadow: var(--net-elev-2); }
.tc-rec.sev-success { border-left-color: var(--net-success-ink); }
.tc-rec.sev-warn    { border-left-color: var(--net-warn-ink); }
.tc-rec.sev-danger  { border-left-color: var(--net-danger-ink); }
.tc-rec.sev-primary { border-left-color: var(--net-primary); }
.tc-rec-ico { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--net-radius-sm); background: var(--net-sunken); color: var(--net-ink-2); }
.tc-rec.sev-success .tc-rec-ico { background: var(--net-success-soft); color: var(--net-success-ink); }
.tc-rec.sev-warn    .tc-rec-ico { background: var(--net-warn-soft);    color: var(--net-warn-ink); }
.tc-rec.sev-danger  .tc-rec-ico { background: var(--net-danger-soft);  color: var(--net-danger-ink); }
.tc-rec.sev-primary .tc-rec-ico { background: var(--net-primary-soft); color: var(--net-primary); }
.tc-rec h3 { font-size: var(--net-fs-body); font-weight: var(--net-fw-bold); margin: 0; }
.tc-rec-detail { color: var(--net-ink-2); font-size: var(--net-fs-small); line-height: 1.5; }
.tc-rec-metric { display: flex; align-items: center; gap: 6px; color: var(--net-ink-3); font-size: 12px; }
.tc-rec-legend-key { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
.tc-rec-legend-key.act { background: var(--net-primary); }
.tc-rec-legend-key.ins { background: var(--net-hairline-strong); }

/* Loading placeholder. "Reading your website analytics…" as one line of grey text read as a page that
   had given up; a skeleton reads as work in progress, which is what it is. */
.tc-skel { border-radius: var(--net-radius-sm); background: linear-gradient(90deg, var(--net-sunken) 25%, var(--net-hairline) 37%, var(--net-sunken) 63%); background-size: 400% 100%; animation: tc-skel-slide 1.4s ease-in-out infinite; }
@keyframes tc-skel-slide { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .tc-skel { animation: none; } }
.tc-skel-line { height: 10px; }
.tc-skel-bar  { height: 8px; border-radius: var(--net-radius-pill); }

/* Vertical column chart. Months read left-to-right as TIME, which a horizontal bar list does not —
   the clinic’s own New Patients per Month is a column chart for exactly that reason (Marcos
   2026-08-27). Columns grow from a shared baseline; the value sits above each one and the period
   under it, so nothing has to be read off an axis that is not there. */
.tc-vbars { display: flex; align-items: stretch; gap: var(--net-space-2); min-height: 180px; padding-top: 6px; }
/* Three rows: the value, the PLOT (the only row that flexes), then the period. The plot being its own
   definite-height row is what lets a column height be a percentage — the shares then hold at any card
   height instead of being baked in as pixels. */
.tc-vbar { flex: 1 1 0; min-width: 0; display: grid; grid-template-rows: auto minmax(0,1fr) auto; justify-items: center; gap: 4px; }
.tc-vbar .plot { align-self: stretch; width: 100%; display: flex; align-items: flex-end; justify-content: center; }
.tc-vbar .v { font-size: 12px; font-weight: var(--net-fw-semibold); color: var(--net-ink-2); font-variant-numeric: tabular-nums; }
.tc-vbar .col { width: 100%; max-width: 46px; border-radius: var(--net-radius-sm) var(--net-radius-sm) 0 0; background: var(--net-primary); }
.tc-vbar .col.zero { background: var(--net-hairline); }   /* a month with nobody still keeps its place on the axis */
.tc-vbar .k { font-size: 11px; color: var(--net-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
@container work (max-width: 480px) { .tc-vbar .k { font-size: 10px; } }

/* A card header that carries a control as well as a title. The control is pinned and never shrinks,
   so the TITLE would be the thing that truncates — and "Messages Needing…" tells a reader nothing.
   Wrapping drops the control to its own line on a narrow card instead (Marcos 2026-08-27). */
.card-hd.wrap-hd { flex-wrap: wrap; row-gap: 6px; }
.card-hd.wrap-hd > h2 { min-width: 0; }

/* ── Widget footer: ONE shape for every card that carries an overflow control (Marcos 2026-08-29) ──
   The two controls were inverted. The in-card expand was the loudest thing on the card (a bordered
   button) and the way OUT to the full page was a whisper of a link. The expand is navigation WITHIN
   the card, so it now reads as a link and sits in the MIDDLE; anything that leaves the card
   ("Open Conversations") is the real action, so it is a ghost button pinned RIGHT.
   Grid, not flex: the middle cell stays centred on the CARD whether or not a side action exists —
   which a flex row with a spacer cannot promise. */
.dash-foot { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: var(--net-space-2); row-gap: 6px; margin-top: 8px; }
.dash-foot > .fl, .dash-foot > .fc, .dash-foot > .fr { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.dash-foot > .fl { grid-column: 1; justify-self: start; }
.dash-foot > .fc { grid-column: 2; justify-self: center; }
.dash-foot > .fr { grid-column: 3; justify-self: end; justify-content: flex-end; }
/* ⭐ The foot sits on the widget's BOTTOM EDGE, not under its last row (Marcos 2026-09-12). Widgets
   stand a common 400px, so a short list left "Open Leads" floating mid-card. :has scopes the flex
   column to cards that HAVE a foot; their children are all blocks, and the one margin pair they carry
   (card-hd 12 + p.meta -6) nets 6 in block and flex alike, so nothing else moves. padding-top keeps
   the old 8px gap when content fills the card and `auto` has nothing to give (and on phones, where
   the 400px floor is off). */
.dash-widgets .card:has(> .dash-foot) { display: flex; flex-direction: column; }
.dash-widgets .card > .dash-foot { margin-top: auto; padding-top: 8px; }
/* Narrow card: three cells side by side stop fitting long before the card does — stack and centre. */
@container work (max-width: 480px) {
  .dash-foot { grid-template-columns: 1fr; }
  .dash-foot > .fl, .dash-foot > .fc, .dash-foot > .fr { grid-column: 1; justify-self: center; justify-content: center; }
}

/* Widget titles sit one notch under the global h2 (18px) on the dashboard — a wall of widgets reads
   calmer at 17. Scoped to .dash-widgets on purpose: no other page's card titles move. */
.dash-widgets .card-hd h2 { font-size: 17px; }

/* Recommendation cards are a GRID of small cards, not page-width widgets — the dashboard’s common
   400px widget height turns each of them into a mostly-empty box. The grid already equalises the two
   cards in a row, which is the symmetry that rule exists for. */
.dash-widgets .card.tc-rec { min-height: 0; }

/* Same opt-out, same reason, for the Cetán tab (Marcos 2026-09-04: "there is a margin issue").
   Every card on that tab is content-sized — a ladder table, a three-KPI row, a one-line-per-touch
   feed — so the 400px floor was leaving measured dead space of 107px under "The ladder" and 230px
   under each of the two cards below it. The pair that shares a row sits in a `grid-2`, which
   stretches them to each other anyway, so the symmetry the floor exists for is not lost. */
.dash-widgets .card.tc-op { min-height: 0; }

/* ⛔ `.tc-op-feed` WAS HERE AND IS DELETED. It capped the old "What Cetán has been doing" card, which
   the work timeline replaced on 2026-09-04 — the class has no element left in any markup. Dead CSS on
   a 2,600-line sheet is the thing the next person greps for and cannot find. */

/* ⭐ WHATEVER FOLLOWS THE KPI ROW GETS AIR (Marcos 2026-09-04: "when no data the empty panel is still
   sticked to the top one"). Measured on the ladder card: `.kpis` ends at 153px and the `.empty` box
   begins at 153px — both carry margin 0, so the grey empty panel butts straight into the tiles above
   it and reads as one broken block. The table does the same thing when there IS data.
   ⛔ Scoped to `.tc-op`, not to `.card .kpis`, on purpose: the KPI row appears on six other dashboard
   tabs whose spacing nobody has complained about, and a global rule here would move all of them. */
.tc-op .kpis + * { margin-top: var(--net-space-3); }

/* ══ CETÁN'S WORK TIMELINE ═══════════════════════════════════════════════════════════════════════
   Everything Cetán did in the window, newest first, grouped by day. Rail + day divider + node glyph
   is the pattern every reader already knows from GitHub, Linear and Stripe — this applies it in the
   house tokens rather than inventing a fifth activity-list style. */

/* ⭐ THE TIMELINE GROWS, IT DOES NOT SCROLL (Marcos 2026-09-04: "dont scroll the widget let it grow").
   It was capped at 520px with its own scrollbar, which put a second scroll region inside a page that
   already scrolls — you lose your place, and the rows below the fold are invisible to anyone skimming.
   ⛔ THIS IS ONLY SAFE BECAUSE `.tc-op-split` CARRIES `align-items: start`. Without it the grid would
   stretch the ladder to match a 2,900px timeline and hand back the 2,600px of dead card that started
   this whole thread. If you ever remove that line, this cap has to come back — they are one decision.
   The 50-row bound the clinic enforces is what keeps "grows" from meaning "unbounded". */
.tc-op-tl-scroll { padding-right: 4px; }

/* The day divider — what makes this read as a timeline rather than a list.
   ⛔ IT IS NO LONGER STICKY, and that follows directly from the card growing instead of scrolling.
   Sticky needs a scroll container to pin inside; with the card's own scroller gone, `top: 0` would
   pin these to the VIEWPORT and float every day header up over the app header and the tab strip as
   the page scrolls. A divider that leaves its own group is worse than one that simply sits there.
   ⚠️ If the scroller ever comes back, sticky comes back with it — same decision, both directions.
   The opaque --net-card background stays: it is what punches the rail out from behind the row. */
.tc-op-tl-day { display: flex; align-items: center;
  gap: var(--net-space-2); padding: 10px 0 6px; background: var(--net-card);
  border-bottom: 1px solid var(--net-hairline); }
.tc-op-tl-day .n { margin-left: auto; font-size: 11px; color: var(--net-ink-3); font-variant-numeric: tabular-nums; }

/* ⛔ THE RAIL IS ON THE ROWS, NOT ON THE GROUP (fixed 2026-09-04 — Marcos: "that line is crossing the
   header line"). It was on the group, whose first child is the day header, so `top: 22px` measured
   from the TOP OF THE HEADER and the line ran straight through the header's own bottom border. The
   rows are their own box now, so the same two insets measure inside the entries where they belong.
   22px = 10px of entry padding + half a 24px marker, so the line starts and ends at a node's centre.
   ⛔ Still not per-entry: that needs first/last/only-child cases, and a one-day group would render a
   stub of line hanging off a single node. Here the two insets simply cross and the line has no height. */
.tc-op-tl-group { position: relative; }
.tc-op-tl-rows { position: relative; }
.tc-op-tl-rows::before { content: ""; position: absolute; left: 13px; top: 22px; bottom: 22px;
  width: 1px; background: var(--net-hairline); }

/* ⭐ THE DAY HEADER IS A DISCLOSURE (Marcos 2026-09-04: "make every day collapsed in the time period
   shown and collapse by default all of them except today"). Fifty rows over two weeks is a wall; the
   question people arrive with is "what happened TODAY", and the other days are there when they want
   them. A real <button> with aria-expanded, not a div with a click handler — this is the one control
   on the card a keyboard user needs, and role/state have to be in the markup for it. */
.tc-op-tl-day { width: 100%; text-align: left; border: 0; border-radius: 0; cursor: pointer;
  font: inherit; color: inherit; }
.tc-op-tl-day:hover { background: var(--net-sunken); }
.tc-op-tl-day .chev { display: inline-flex; transition: transform var(--net-dur) var(--net-ease); }
.tc-op-tl-day[aria-expanded="true"] .chev { transform: rotate(90deg); }
/* ⚠️ A person who asked for less motion still needs to SEE the state — the chevron keeps its rotation,
   it just stops animating into it. */
@media (prefers-reduced-motion: reduce) { .tc-op-tl-day .chev { transition: none; } }

/* One entry: marker · body · side (time above actions). The side column is a fixed rail whether or
   not the row has CTAs — that is what stops a row with neither looking like a row whose buttons
   failed to render.
   ⛔ THERE IS NO ROW HOVER AND NO ROW CLICK, on purpose. Most rows carry no action at all — a desk
   check belongs to no lead, a text has no transcript — and a hover wash on a row that does nothing is
   a promise the row cannot keep. The buttons carry their own affordance. */
.tc-op-tl-entry { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto;
  column-gap: var(--net-space-3); align-items: start; padding: 10px 0; }
.tc-op-tl-mark { position: relative; z-index: 1; display: grid; place-items: center;
  width: 24px; height: 24px; border-radius: var(--net-radius-pill);
  background: var(--net-sunken); color: var(--net-ink-2); border: 1px solid var(--net-hairline);
  /* the card-coloured halo punches the rail out from behind the node — it tracks --net-card so it
     stays invisible when the theme flips, and is never a literal white */
  box-shadow: 0 0 0 3px var(--net-card); }
.tc-op-tl-body { min-width: 0; }
.tc-op-tl-who { font-size: var(--net-fs-small); font-weight: var(--net-fw-semibold); color: var(--net-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tc-op-tl-act { font-size: 12px; color: var(--net-ink-3); }
.tc-op-tl-out { margin-top: 2px; font-size: var(--net-fs-small); line-height: var(--net-lh-small);
  color: var(--net-ink-2); overflow-wrap: anywhere; }
.tc-op-tl-side { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.tc-op-tl-when { font-size: 11px; color: var(--net-ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tc-op-tl-acts { display: flex; gap: 4px; }

/* Severity, and it is READ FROM A FIELD — never derived from the outcome sentence. The clinic writes
   ok|miss|bad|none in the same branch that composes the words, so the two can never disagree.
   ⛔ Do not "improve" this by substring-matching the sentence: that is precisely what dropped
   "Link opened" into the wrong lane and merged "AI call placed" with "not placed" on the lead
   timeline. An unmapped provider status arrives as `none` and renders neutral, which is honest.
   The tone is on the MARKER, not the row — a wash across every no-answer row makes the common case
   shout. sev-bad additionally gets an alert glyph in the markup, so the one genuinely alarming class
   is never signalled by colour alone. */
.tc-op-tl-entry.sev-ok   > .tc-op-tl-mark { background: var(--net-success-soft); color: var(--net-success-ink); border-color: transparent; }
.tc-op-tl-entry.sev-miss > .tc-op-tl-mark { background: var(--net-warn-soft);    color: var(--net-warn-ink);    border-color: transparent; }
.tc-op-tl-entry.sev-bad  > .tc-op-tl-mark { background: var(--net-danger-soft);  color: var(--net-danger-ink);  border-color: transparent; }
.tc-op-tl-entry.sev-bad  .tc-op-tl-out { color: var(--net-danger-ink); font-weight: var(--net-fw-medium); }

/* "We could not read it" and "nothing happened" are different facts and must not look alike, or a
   broken pipe reads as a quiet day and nobody investigates. Both use .empty, so this tone change plus
   a bold headline and a Try again button in the markup is the ONLY thing separating them.
   ⛔ Do not add a plate to the empty state to "balance" them — the asymmetry is the message. */
.empty.tc-op-tl-fail > .ico { background: var(--net-warn-soft); color: var(--net-warn-ink); }

/* Genuinely narrow work column (a phone). At the split's own collapse the timeline is already full
   width, so nothing else needs to shrink. Keyed on `work` like the rest of the sheet — the card is
   not a container and making it one would silently re-point every @container work rule inside it. */
@container work (max-width: 520px) {
  .tc-op-tl-entry { column-gap: var(--net-space-2); }
  .tc-op-tl-when { font-size: 10.5px; }
}

/* Matched fill pair for a two-segment bar (passed / did not pass). Both are FILL tokens at the same
   visual weight, so the bar reads as two shares of one whole rather than one green bar with an alarm
   attached. A hairline between them keeps the boundary readable when one share is tiny. */
.ndash-bar-fill.tc-fill-ok { background: var(--net-primary); }
.ndash-bar-fill.tc-fill-no { background: var(--net-against); box-shadow: inset 1px 0 0 var(--net-card); }

/* The designed empty state. A tab that can legitimately be empty on a clinic’s first day still has
   to say what it is for and how to fill it — one grey sentence in a box reads as broken software.
   The tiles are LABELLED placeholders and deliberately carry no numbers: a preview that looks like
   data is worse than no preview. */
.tc-blank { display: flex; flex-direction: column; }
.tc-blank-hd { display: flex; align-items: flex-start; gap: var(--net-space-4); }
.tc-blank-ico { flex: none; display: grid; place-items: center; width: 52px; height: 52px; border-radius: var(--net-radius-md); background: var(--net-primary-soft); color: var(--net-primary); }
.tc-blank-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: var(--net-space-3); }
@container work (max-width: 860px) { .tc-blank-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@container work (max-width: 520px) { .tc-blank-grid { grid-template-columns: 1fr; } }
.tc-blank-tile { display: flex; align-items: flex-start; gap: 10px; padding: 10px; border: 1px dashed var(--net-hairline-strong); border-radius: var(--net-radius-md); background: var(--net-sunken); }
.tc-blank-tile-ico { flex: none; display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--net-radius-sm); background: var(--net-card); color: var(--net-ink-3); }

/* A card whose CONTENT is a chart. Dashboard widgets stand a common height, so a chart that only
   knows its own size sits at the top with a hole under it. These two make the chart the part that
   grows — the same job the backoffice’s .tc-chart-center does inside its card body. */
.card.tc-chart-card { display: flex; flex-direction: column; }
.tc-chart-center { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; }
.tc-chart-center.tc-vbars { align-items: stretch; }   /* columns stretch, they do not centre */
/* A GRID that centres: the flex shorthand above would make it a flex container and drop the columns.
   Keeping display:grid and centring the rows instead is what preserves the 2x2 tile layout. */
.tc-chart-center.tc-pool { display: grid; align-content: center; }
/* A centred chart still owns the card’s WIDTH — without this the flex child shrinks to its content
   and a pie + legend that fits side by side folds into a column for no reason. */
.tc-chart-center > .tc-pie-wrap { width: 100%; justify-content: center; }
/* ⭐ The legend must NOT grow (Marcos 2026-08-31: "the chart should be horizontally centered").
   .tc-legend is flex:1 1 140px so it swallowed every spare pixel, leaving justify-content:center with
   no free space to distribute — the pie sat hard against the left edge of all three donut cards
   (Lead Status, TrialCatcher Page Visits, Sex Distribution). Shrinkable and wrappable still, just not
   greedy, so the pie and its legend centre as one group. */
.tc-chart-center > .tc-pie-wrap > .tc-legend { flex: 0 1 auto; }
/* ⭐ justify-content was missing, so the pie + legend filled the width and sat hard LEFT while the
   card centred them vertically only (Marcos 2026-08-29). Centred on both axes now. The wrap keeps
   flex-wrap, so on a narrow card the legend drops under the pie and stays centred rather than
   hanging off the left edge. */


/* ── Reconnect (see App.razor) ────────────────────────────────────────────────────────────────
   Blazor toggles the state classes on #components-reconnect-modal; everything here is presentation.
   Hidden by default and shown only by those classes — a deploy is the common case, and it must read
   as "restarting", never as "broken". */
.tc-reconnect { display: none; position: absolute; inset: 0; z-index: 3000; background: color-mix(in srgb, var(--net-ink) 45%, transparent); backdrop-filter: blur(2px); }
.tc-reconnect.components-reconnect-show,
.tc-reconnect.components-reconnect-failed,
.tc-reconnect.components-reconnect-rejected { display: grid; place-items: center; }
.tc-reconnect-card { background: var(--net-card); color: var(--net-ink); border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius); padding: 20px 22px; max-width: 380px; margin: 16px; box-shadow: 0 18px 50px rgb(0 0 0 / 22%); text-align: center; }
.tc-reconnect-card b { display: block; font-size: var(--net-fs-body); margin-bottom: 6px; }
.tc-reconnect-card span { display: block; font-size: var(--net-fs-small); color: var(--net-ink-2); line-height: 1.5; }
.tc-reconnect-card button { margin-top: 14px; height: 36px; padding: 0 16px; border: 0; border-radius: var(--net-radius-sm); background: var(--net-primary); color: #fff; font: inherit; font-size: var(--net-fs-small); font-weight: var(--net-fw-medium); cursor: pointer; }
.tc-reconnect-card button:hover { filter: brightness(1.06); }
/* One state visible at a time — Blazor sets exactly one class on the container. */
.tc-reconnect-show, .tc-reconnect-failed, .tc-reconnect-rejected { display: none; }
.components-reconnect-show    .tc-reconnect-show     { display: block; }
.components-reconnect-failed  .tc-reconnect-failed   { display: block; }
.components-reconnect-rejected .tc-reconnect-rejected { display: block; }
@media (prefers-reduced-motion: reduce) { .tc-reconnect { backdrop-filter: none; } }

/* ── Dashboard semantics (2026-08-29) ────────────────────────────────────────
   Marcos: "the dashboard in the network has no colours at all — they are
   meaningful in the backoffice." The palette already existed in tokens.css; the
   dashboard simply never used it. Three mechanisms, never mixed:
     3px left edge = a human must act on this      (warn | danger only)
     chip          = the record's state, one word, always with the colour
     bar fill      = category or threshold, never urgency
   LAW: the KPI number itself is NEVER coloured. Six coloured numbers read as
   six alarms, which reads as none. State lives on the edge and the delta. */
.kpi.s-warn   { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-warn); }
.kpi.s-danger { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-danger); }
.kpi.s-warn   .delta { color: var(--net-warn-ink); }
.kpi.s-danger .delta { color: var(--net-danger-ink); }
.kpi.s-ok     .delta { color: var(--net-success-ink); }

/* A worklist row that is overdue. The timestamp already on the row is the
   redundant cue, so the colour is never carrying the meaning alone. */
.dash-row-link.f-due   { box-shadow: inset 3px 0 0 var(--net-warn); }
.dash-row-link.f-stale { box-shadow: inset 3px 0 0 var(--net-danger); }

/* Lane counts take their lane's meaning — but ONLY when the count is non-zero.
   An empty lane is not a state, and tinting it is half the noise on the card. */
.dash-seg .n.l-wait  { background: var(--net-warn-soft);    color: var(--net-warn-ink); }
.dash-seg .n.l-sched { background: var(--net-info-soft);    color: var(--net-info-ink); }
.dash-seg .n.l-done  { background: var(--net-success-soft); color: var(--net-success-ink); }
.dash-seg button.on .n { background: var(--net-primary); color: var(--net-on-primary); }

/* An UNKNOWN is not an all-clear. Reserved for the cards that cannot see their
   data — never for a genuinely empty list. */
.empty.unknown { border-color: var(--net-info); background: var(--net-info-soft); color: var(--net-info-ink); }

/* ── NoData — the empty state that FILLS its widget (Marcos 2026-08-31) ──────────────
   The backoffice's .tc-empty-state centres on both axes and takes the room the chart
   would have taken, so an empty card reads as an empty card instead of a stray
   sentence pinned to the top of 400px of white. This is that, in this app's tokens.
   `flex:1 1 auto` is what does the vertical centring — it only works because the
   chart cards are already flex columns (.card.tc-chart-card above, .dash-widgets .card
   min-height floor). A widget that is NOT a flex column still gets the horizontal
   centring and simply does not stretch, which is the correct degradation. */
.no-data { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-height: 160px; padding: var(--net-space-5) var(--net-space-4); }
.no-data > .ico { margin: 0 0 var(--net-space-2); }   /* `>` for the same reason as .empty above — the inner svg also carries .ico */
.no-data .nd-title { font-weight: 600; font-size: var(--net-fs-14, 14px); color: var(--net-ink); }
.no-data.unknown .nd-title { color: var(--net-info-ink); }
.no-data .nd-detail { font-size: var(--net-fs-12, 12px); color: var(--net-ink-2); max-width: 34ch; text-align: center; line-height: 1.45; }
.no-data.unknown .nd-detail { color: var(--net-info-ink); opacity: .85; }

/* ── KPI value transition (Marcos 2026-08-29) ─────────────────────────────────
   The number lifts and fades in when it CHANGES — not on every render. Each value
   element is @key'd on the value it renders, so Blazor rebuilds the node only when
   the figure is genuinely different and the animation replays exactly then.
   Deliberately short and small: this is a change CUE, not decoration. A dashboard
   whose numbers bounce every time you touch a filter is harder to read, not easier.
   ⛔ Motion is suppressed entirely under prefers-reduced-motion — the number still
   updates, it simply does not move. */
@keyframes kpi-value-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.kpi .val { animation: kpi-value-in 260ms cubic-bezier(.22,.61,.36,1) both; }
/* The supporting line follows a beat later, so the pair reads as one movement. */
.kpi .delta { animation: kpi-value-in 260ms cubic-bezier(.22,.61,.36,1) 60ms both; }
@media (prefers-reduced-motion: reduce) {
  .kpi .val, .kpi .delta { animation: none; }
}

/* ── Conversation popup ───────────────────────────────────────────────────────
   ⚠️ The .tc-dlg shell is shared with the Lead Map, whose box is a fixed
   1240x860 and whose body is a flex ROW (map + rail). Reusing it verbatim for a
   conversation put the thread and the reply side by side inside a half-empty
   1240px box that ran off the viewport. These modifiers give the conversation
   the shape it actually needs: a narrow column, thread scrolling above a
   composer pinned to the bottom. */
.tc-dlg-box--convo { width: min(560px, 96vw); height: auto; max-height: min(760px, 88vh); }
.tc-dlg-body--convo { flex-direction: column; gap: var(--net-space-3); padding: var(--net-space-4); overflow: hidden; }

/* ── Confirm / ask dialogs (delete a lead, and anything else that asks one question) ───
   ⚠️ THE SAME TRAP THE CONVERSATION POPUP HIT, and it caught the delete dialog too (Marcos
   2026-09-04, screenshot). `.tc-dlg-box` is a FIXED `height: min(860px, 92vh)` built for the Lead
   Map, and `.tc-dlg-body` is a flex ROW. A confirm dialog inherits both: its warning and its reason
   picker land SIDE BY SIDE, each stretched down an 860px box, leaving ~600px of empty pink under a
   two-sentence question. It reads as a broken screen at the exact moment a person is being asked to
   destroy something — which is the worst possible moment to look untrustworthy.
   ⭐ Height AUTO and a column body. The box grows to its content and stops. */
.tc-dlg-box--confirm { width: min(520px, 96vw); height: auto; max-height: min(680px, 88vh); }
.tc-dlg-body--confirm { flex-direction: column; gap: var(--net-space-2); padding: var(--net-space-4); overflow-y: auto; }
/* ⛔ Children must not stretch to the row's cross-axis, or the alert grows a tail of empty colour. */
.tc-dlg-body--confirm > * { flex: 0 0 auto; }

/* The thread takes the room that is left and scrolls on its own. */
.tc-convo-thread { flex: 1 1 auto; min-height: 120px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding-right: 6px; }

/* A message reads as a message: a bubble, a side, and a time under it. */
.tc-convo-msg { display: flex; flex-direction: column; gap: 3px; max-width: 78%; }
/* A run of messages from the same speaker sits tight under the first. */
.tc-convo-msg--cont { margin-top: -6px; }
/* ⛔ word-break: break-word IS WHY SHORT MESSAGES CAME OUT ONE LETTER WIDE (Marcos 2026-09-01, with a
   screenshot: "Hello!" rendered as "Hell / o!", "Peter" as "Pet / er"). That value is a legacy alias
   that engines treat like `overflow-wrap: anywhere`, and `anywhere` — unlike `break-word` — CHANGES THE
   MIN-CONTENT WIDTH to a single character. The bubble is a flex item, so it was then free to shrink to
   almost nothing and hyphen-less-wrap the middle of a word.
   The replacement keeps the useful half (a genuinely unbreakable string still wraps instead of
   overflowing) and restores a sane floor: size to the content, never narrower than the longest word. */
.tc-convo-msg .bubble {
  padding: 8px 11px; border-radius: 12px; font-size: var(--net-fs-13, 13px); line-height: 1.45;
  white-space: pre-wrap;
  width: fit-content; max-width: 100%;
  min-width: min-content;      /* the floor: never narrower than one whole word */
  word-break: normal;          /* was break-word — see above */
  overflow-wrap: break-word;   /* only breaks a word that cannot fit on its own line */
}
.tc-convo-msg .when { font-size: 11px; font-weight: 400; color: var(--net-ink-3); }
/* Them: left, neutral. Us: right, tinted — the side carries the speaker, so colour is never alone. */
.tc-convo-msg--them { align-self: flex-start; }
.tc-convo-msg--them .bubble { background: var(--net-sunken); color: var(--net-ink); border-bottom-left-radius: 4px; }
.tc-convo-msg--us { align-self: flex-end; align-items: flex-end; }
.tc-convo-msg--us .bubble { background: var(--net-primary-soft); color: var(--net-ink); border-bottom-right-radius: 4px; align-self: flex-end; }
.tc-convo-msg--ai .bubble { background: var(--net-info-soft); color: var(--net-ink); }
.tc-convo-msg .who { font-size: 11px; font-weight: 600; color: var(--net-ink-3); }
.tc-convo-msg .who svg { vertical-align: -1px; }

/* A call with a transfer has two legs; the caption separates them. Full-width inside the thread's
   flex column (the bubbles are max-width:78% and self-aligned, so this must not inherit that). */
.tc-leg-hd { align-self: stretch; display: flex; align-items: center; gap: 5px; margin: var(--net-space-2) 0 2px; padding-top: var(--net-space-2); border-top: 1px solid var(--net-hairline); font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--net-ink-3); }
.tc-leg-hd:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* Composer sits at the bottom and never grows into the thread. */
.tc-convo-compose { flex: none; border-top: 1px solid var(--net-hairline); padding-top: var(--net-space-3); }
.tc-convo-compose textarea { width: 100%; min-height: 64px; max-height: 140px; resize: vertical; }
.tc-convo-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }

/* A table row that opens something says so on hover, and takes a focus ring like the button it is. */
.tc-row-click { cursor: pointer; }
.tc-row-click:hover { background: var(--net-sunken); }
.tc-row-click:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }

/* Website media thumbnail — a fixed, predictable tile so a row of upload fields does not jump as
   photos of different aspect ratios load. `object-fit: cover` crops rather than distorting: a squashed
   face reads as a broken product. The placeholder sits UNDER the image and is revealed by the img's own
   onerror, so an unreachable origin degrades to an icon instead of a browser's broken-image glyph. */
.tc-media-thumb { position: relative; flex: none; width: 76px; height: 52px; border: 1px solid var(--net-hairline-strong); border-radius: var(--net-radius-sm); overflow: hidden; background: var(--net-sunken); display: inline-flex; align-items: center; justify-content: center; }
.tc-media-thumb img, .tc-media-thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
.tc-media-thumb__ph { position: absolute; inset: 0; align-items: center; justify-content: center; color: var(--net-ink-3); }

/* ── Status filter chips (§15.1 P1/P2) ────────────────────────────────────────────
   The count IS the answer to "how much is waiting on me", so it is set apart from the label rather
   than run into it as words. Tabular figures stop the strip twitching as numbers change on a re-read. */
.filter-chips { gap: 6px; margin-bottom: 10px; }
.filter-chips .chip b { font-variant-numeric: tabular-nums; font-weight: var(--net-fw-semibold); opacity: .7; }
.filter-chips .chip.selected b { opacity: 1; }
/* ⭐ EVEN CHIPS (Marcos 2026-09-01) — every chip the same size whatever its word is, which is what the
   backoffice’s full-width btn-group gives its three. Natural-width chips in the narrow chat column
   wrapped into a ragged block where "All" was a quarter the width of "Smoke Signals".
   auto-fit + minmax means the columns stay EQUAL at any panel width and simply re-flow, so this needs
   no breakpoint and no count-specific rule. min 120px keeps the longest label on one line — .chip is
   nowrap, so a narrower cell would overflow it rather than wrap it. */
.filter-chips.chips-even { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 6px; }
.filter-chips.chips-even .chip { width: 100%; justify-content: center; }

/* A KPI tile that is a LINK stays a tile: same box, no underline, no colour change — only the hover
   lift and the focus ring say it can be pressed. An anchor that repaints itself blue would break the
   scoreboard reading of the row. */
a.kpi { color: inherit; text-decoration: none; }
/* ⛔ NO box-shadow on hover, deliberately. `.kpi.s-warn` / `.kpi.s-danger` carry their status stripe IN
   the box-shadow (inset 3px), and a hover shadow at higher specificity would wipe the stripe off exactly
   the tiles that have one — the alerting tiles losing their alert the moment you point at them. Border
   and eyebrow do the hover instead. */
a.kpi:hover { text-decoration: none; color: inherit; border-color: var(--net-hairline-strong); }
a.kpi:hover .eyebrow { color: var(--net-primary); }
a.kpi:focus-visible { border-radius: var(--net-radius-md); }   /* match the tile, not the global default */
/* One escalation chain, boxed (§15.6 P6). The read-only page drew this inline; the editable one has a
   dozen controls per chain, and without a boundary the rungs of two chains read as one list. */
.tc-chain { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); padding: 10px; }

/* ─── clinic public page: team / careers ──────────────────────────────────────
   /clinics/{slug} Team + Careers, and the member page at /clinics/{slug}/team/{key}.
   MOBILE FIRST: one column is the base rule and the two-column rule is the addition.
   ⚠️ The breakpoint is a CONTAINER query, not a viewport one, and deliberately: these cards live in
   .net-col.work, which is the ONLY sized container in this file (line ~166) and is narrower than the
   viewport by the whole nav rail plus, when it is open, the context column. Every other grid here
   (.grid-2, .grid-3, .hide-phone) already answers to `work`; a viewport @media rule beside them would
   put two cards in a 500px column at a 1000px window. */
/* ⭐ The team is a CONTACT SHEET, not a row of hero tiles. auto-fill against a small track is what
   makes it compact and what keeps it compact at every width: a clinic with 13 published staff filled
   thirteen full-width tiles on a phone, which is a page nobody scrolls to the end of. Two up on a
   phone, five or six on a desktop column, one rule. */
.team-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 14px; }
.role-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; align-items: start; }
@container work (min-width: 720px) {
  .team-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
  /* ⭐ TWO-UP ONLY WHEN THERE ARE TWO (Marcos 2026-09-01: a single role rendered as a thin half-width
     strip with an empty right half, which reads as a rendering fault, not as a job board). A hard
     repeat(2) reserves the second track whether or not anything fills it; :has(> :nth-child(2)) asks
     the question instead — one posting takes the full column, two or more sit side by side exactly as
     before. This is also what every hosted board does (Greenhouse/Lever render a small board as
     full-width rows and only grid at volume); a lone posting is a headline, not a list item.
     ⚠️ Not auto-fit/minmax: that would go THREE-up on a wide column and squeeze a five-line posting. */
  .role-grid:has(> .role-card:nth-child(2)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ── a person, compact. Photo, name, role, one line of their own words. ── */
.team-card {
  position: relative;   /* the stretched name link and the play button both anchor to this */
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: 8px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md);
  background: var(--net-card); color: inherit; text-decoration: none;
  transition: border-color var(--net-dur) var(--net-ease), box-shadow var(--net-dur) var(--net-ease);
}
/* ⭐ The card is no longer ONE anchor (2026-08-31): a button cannot live inside a link, and the play
   mark had to become a real control. The NAME carries the link and stretches over the whole card, so
   the card still clicks through to the member's page exactly as before. */
.team-link { color: inherit; text-decoration: none; }
.team-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.team-link:hover { text-decoration: none; color: inherit; }
.team-card:has(.team-link:focus-visible) { outline: 2px solid var(--net-primary); outline-offset: 2px; }
/* …and the play button sits ABOVE that stretched link, so it is the thing you hit when you aim at it. */
.team-play-btn { z-index: 2; border: 0; padding: 0; cursor: pointer; }
.team-play-btn:focus-visible { outline: 2px solid var(--net-on-primary); outline-offset: -3px; }
.team-card:hover { text-decoration: none; color: inherit; border-color: var(--net-hairline-strong); box-shadow: var(--net-elev-1); }
.team-card:focus-visible { border-radius: var(--net-radius-md); }
/* A fixed square well, so a portrait and a landscape snapshot produce the SAME grid — the clinic's
   photos are whatever their phone took, and a row of mismatched heights is what "looks awful" means. */
.team-photo {
  position: relative; display: grid; place-items: center; width: 100%; aspect-ratio: 1;
  margin-bottom: 6px; border-radius: var(--net-radius-sm); background: var(--net-sunken); overflow: hidden;
}
.team-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The "there is a video" mark, same affordance the clinic's own site uses. Decorative: the page behind
   the card names the video in words, so this is not the only way to know. */
.team-play, .loc-play {
  position: absolute; right: 6px; bottom: 6px; display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: var(--net-radius-pill);
  background: var(--net-primary); color: var(--net-on-primary); box-shadow: var(--net-elev-1);
}
/* ⭐ THE SITE WALKTHROUGH IS A REAL BUTTON AND HAS TO LOOK LIKE ONE (Marcos 2026-09-01: "the video play
   icon needs to be more visible"). A 22px dot in the corner of a 132px photograph reads as a badge
   saying "there is a video somewhere", not as "press this". Centred, 44px, with a white ring against
   whatever the clinic's photo happens to be behind it — the shape everyone already knows.
   ⚠️ .loc-play only. The team roster's 22px corner mark stays exactly as it is: those thumbnails sit in
   a dense grid where a 44px target would cover the person's face. */
.loc-play {
  right: auto; bottom: auto; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 44px; height: 44px;
  border: 2px solid var(--net-on-primary);
  box-shadow: var(--net-elev-2), 0 0 0 1px rgba(16, 24, 40, .18);
  transition: transform var(--net-dur) var(--net-ease), background-color var(--net-dur) var(--net-ease);
}
.loc-play-btn { z-index: 2; border-width: 2px; padding: 0; cursor: pointer; }
.loc-row:hover .loc-play { transform: translate(-50%, -50%) scale(1.06); }
.loc-play-btn:focus-visible { outline: 2px solid var(--net-primary); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .loc-play { transition: none; } .loc-row:hover .loc-play { transform: translate(-50%, -50%); } }
.team-nm { font-weight: var(--net-fw-semibold); font-size: var(--net-fs-small); }
.team-role { color: var(--net-ink-3); font-size: 12px; }
.team-bio { color: var(--net-ink-2); font-size: 12px; line-height: 1.4; margin-top: 2px; }

/* ── one open role, boxed ── */
/* ⭐ A POSTING, NOT A ROW (Marcos 2026-09-01: "it reads as a list item that lost its list"). Three
   devices, in order of how much work each does:
   1. the green SPINE — the same hairline-plus-coloured-edge device .bill-hero uses further down, and
      for the same reason: an edge colour states standing. A hiring notice from the clinic is a claim,
      not a table row, and the spine is what says so before a single word is read.
   2. the TITLE at h2 scale — the job title is the only thing most readers will ever read. At 13px it
      carried the same weight as the "Open" chip beside it, which is a flat hierarchy by definition.
   3. a real HEADER/BODY split — the rule under the header is what LinkedIn, Indeed and every hosted
      board draw, because it separates "what the job is" from "what the job involves".
   ⚠️ --net-primary, never a literal: the spine has to survive the dark theme, where the brand green
   is re-picked (#35674B → #74C596) against a near-black card. */
.role-card {
  display: flex; flex-direction: column; min-width: 0;
  padding: var(--net-space-4);
  border: 1px solid var(--net-hairline);
  border-left: 3px solid var(--net-primary);
  border-radius: var(--net-radius-md); background: var(--net-card);
}
/* A posting with NOTHING but a title and a chip (the shape Marcos saw) has no body to give it height.
   It gets air instead of a min-height: a floor would open a hole under the header on exactly the card
   it was meant to help — the same trap the ⛔ note on .role-benefits below records. Scoped with :has so
   a posting that DOES have a description keeps its normal padding. */
/* ⛔ A POSTING WITH NOTHING IN IT MUST GET SMALLER, NOT ROOMIER (Marcos 2026-09-01: "the careers cards
   are so big and layout so ugly and text missing"). The first pass PADDED this case out, on the theory
   that a short card needs presence — the opposite is true: a full-width card holding a title and one
   chip reads as a card that failed to load its content. Real postings run ~5,800 characters with
   benefits and a read-more; a title-only one is a ROW, and should look like one.
   The missing text itself is DATA, not layout: that clinic published no description. */
.role-card:not(:has(.role-desc, .role-benefits)) {
  padding-block: var(--net-space-3);
}
.role-card:not(:has(.role-desc, .role-benefits)) .role-hd { align-items: center; }
.role-card:not(:has(.role-desc, .role-benefits)) .ico-plate {
  width: var(--net-avatar-sm, 32px); height: var(--net-avatar-sm, 32px);
}
.role-card:not(:has(.role-desc, .role-benefits)) .ico-plate > svg { width: 16px; height: 16px; }
/* Title and chips share the line instead of stacking — the whole posting is one row of information. */
.role-card:not(:has(.role-desc, .role-benefits)) .role-hd > div {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px;
}
.role-card:not(:has(.role-desc, .role-benefits)) .role-hd .row.wrap { margin-top: 0; }
.role-card:not(:has(.role-desc, .role-benefits)) .role-title { font-size: var(--net-fs-body); }
.role-hd { display: flex; align-items: flex-start; gap: 12px; }
/* Only when there is a body to separate — an unconditional rule leaves a hairline dangling under the
   last line of a title-only posting, which looks like a component that failed to finish rendering. */
.role-card:has(.role-desc, .role-benefits) .role-hd {
  padding-bottom: var(--net-space-3); border-bottom: 1px solid var(--net-hairline);
}
/* Top-aligned to the title's first line, avatar-beside-headline style: centring floats the plate into
   the middle of nowhere the moment a long title wraps to two lines. */
.role-hd .ico-plate { width: var(--net-avatar-md); height: var(--net-avatar-md); }
.role-hd .ico-plate > svg { width: 20px; height: 20px; }
/* The chip line under the title. ⚠️ INERT until the inline style="margin-top:6px" is taken off that div
   in ClinicProfile.razor — an inline declaration outranks any selector short of !important, and this
   sheet does not use !important for spacing. Landed here first so removing it is a pure win rather than
   a change that silently drops the gap to zero. */
.role-hd .row.wrap { margin-top: 8px; }
/* Reuses the h2 style verbatim (display face, 18/600, its own tracking) rather than inventing an
   eighth type style — the scale in tokens.css is capped at seven on purpose.
   overflow-wrap: a job title can be one unbroken 40-character phrase; it must wrap, never widen. */
.role-title {
  margin: 0;
  font: var(--net-fw-semibold) var(--net-fs-h2)/var(--net-lh-h2) var(--net-font-display);
  letter-spacing: var(--net-ls-h2);
  overflow-wrap: anywhere;
}
/* The description is the clinic's OWN posting, and they write it with line breaks and blank lines
   between sections ("Responsibilities:", "Requirements:"). Rendered as one <p> those all collapsed and
   the card became the brick of text Marcos saw. pre-line keeps their shape - the same treatment the
   benefits list below already gets - and the measure and leading make it readable rather than dense.
   -wrap, not -line, so a long line still wraps inside the card instead of forcing it wide. */
.role-desc {
  margin: var(--net-space-3) 0 0; white-space: pre-line; line-height: 1.55;
  max-width: 62ch;                 /* a comfortable measure; a full-width posting is a wall */
  overflow-wrap: anywhere;         /* an unbroken URL or code cannot widen the card */
}
/* The clamp needs the same pre-line, or a clamped description reflows differently to an expanded one. */
.role-desc.clamp-5 { white-space: pre-line; }
.role-more { align-self: flex-start; margin-top: 6px; }
/* ⛔ NOT margin-top:auto. Pushing benefits to the bottom aligned the two cards but opened a 180px
   hole inside the shorter posting, which reads as a rendering fault. The grid is align-items:start,
   so a short role is simply a short card. */
/* Its own hairline: benefits are a different KIND of statement to the description (what you get, not
   what you do), and unruled they read as the last paragraph of the posting. Paints only when the
   clinic actually published benefits, so nothing dangles when they did not. */
.role-benefits { margin-top: var(--net-space-3); padding-top: var(--net-space-3); border-top: 1px solid var(--net-hairline); }
/* The clinic writes benefits one per line. pre-line keeps that list a list. */
.role-benefits .meta { white-space: pre-line; margin-top: 2px; }

/* ── one site, boxed (Location tab) ── */
/* Patient testimonials - Google reviews rendered by tc-reviews.js. The block ships HIDDEN and the
   loader unhides it only once at least one review has actually arrived: an empty "What patients say"
   heading on a clinic's public page reads as a clinic nobody has anything good to say about. */
.rev-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
@container work (min-width: 700px) { .rev-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container work (min-width: 1040px) { .rev-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.rev-card { margin: 0; padding: 14px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: var(--net-card); display: flex; flex-direction: column; gap: 8px; }
.rev-stars { color: var(--net-warn-ink); letter-spacing: 2px; font-size: 13px; }
.rev-text { margin: 0; font-size: var(--net-fs-small); line-height: 1.55; color: var(--net-ink); overflow-wrap: anywhere; }
.rev-by { display: flex; align-items: center; gap: 8px; margin-top: auto; }
.rev-ph { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex: none; }
.rev-nm { display: block; font-size: 12px; font-weight: var(--net-fw-semibold); }
.rev-when { display: block; font-size: 11px; color: var(--net-ink-3); }

/* SPONSOR TICKER (Marcos 2026-08-31) - the network's read of the portal's own bh-sponsor-ticker at
   the bottom of its About page. The track holds the logos TWICE and slides exactly -50%, so the second
   copy is under the eye the instant the first leaves; that is what makes it seamless rather than
   snapping back.
   ADA: pausing on hover or focus lets anyone stop it to read a name, and prefers-reduced-motion stops
   it dead and lets the row scroll by hand. Motion is never the only way to see every sponsor. */
.spon-wrap { overflow: hidden; position: relative; }
.spon-wrap::before, .spon-wrap::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 48px; z-index: 1; pointer-events: none;
}
.spon-wrap::before { left: 0; background: linear-gradient(to right, var(--net-card), transparent); }
.spon-wrap::after { right: 0; background: linear-gradient(to left, var(--net-card), transparent); }
.spon-track { display: flex; align-items: center; gap: 42px; width: max-content; animation: spon-slide 38s linear infinite; }
.spon-wrap:hover .spon-track, .spon-wrap:focus-within .spon-track { animation-play-state: paused; }
.spon-item { flex: none; display: grid; place-items: center; height: 54px; }
.spon-item img { max-height: 54px; max-width: 160px; object-fit: contain; filter: grayscale(1); opacity: .72; transition: filter var(--net-dur) var(--net-ease), opacity var(--net-dur) var(--net-ease); }
.spon-wrap:hover .spon-item img { filter: grayscale(0); opacity: 1; }
@keyframes spon-slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .spon-track { animation: none; width: auto; flex-wrap: wrap; justify-content: center; }
  .spon-wrap { overflow-x: auto; }
  .spon-wrap::before, .spon-wrap::after { content: none; }
}

/* ⭐ LIST LEFT, MAP RIGHT (Marcos 2026-08-31). The map is sticky so it stays beside the list while a
   long roster of sites scrolls past it, and it collapses ABOVE the list on a narrow card — a map you
   have to scroll past to reach the addresses is worse than no map. */
.loc-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
@container work (min-width: 860px) { .loc-split { grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr); } }
.loc-map { position: relative; min-height: 320px; border-radius: var(--net-radius-md); overflow: hidden; }
@container work (min-width: 860px) { .loc-map { position: sticky; top: 12px; height: 420px; } }
.loc-map > * { height: 100%; }

/* ONE column inside the split: the row is already two-up horizontally (photo + body), and a second
   column would halve the photo again on the very screens Marcos asked to see bigger images on. */
/* gap was 10px — too tight to read as separate places (Marcos 2026-09-01: "add some margin between
   the locations cards"). Each card is a photo card with its own hairline; 18px is what stops a run of
   them collapsing back into a list. */
.loc-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
/* The whole card is the link to that site's page — same treatment as .team-card, which is the point:
   two "open the thing behind this card" affordances on one page should not feel like two patterns. */
/* ⭐ THE PHOTO NO LONGER SETS THE HEIGHT (Marcos 2026-09-01: "location cards don't look good"). It used
   to: a 132px SQUARE well against three short lines of text made every card 156px tall with the text
   pinned to the top and a third of the card empty underneath. min-height + align-items:stretch inverts
   the relationship — the ROW declares its height, the photo fills whatever that turns out to be, and
   the text is centred against it. That is the shape every locator card has (Google Business, Booking,
   Airbnb): landscape media flush to the row, never a square that dictates it.
   ⚠️ min-height, not height: a two-line address on a narrow column must still be able to grow the card
   rather than overflow it. 116px − 20px padding − 2px border leaves a 94px well ≈ 16:9 at 168px wide,
   which is also the shape a building is photographed in. */
.loc-row {
  position: relative;   /* the stretched name link and the play button both anchor to this */
  display: flex; align-items: stretch; gap: 14px; min-width: 0; min-height: 116px;
  padding: 10px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md);
  background: var(--net-card); color: inherit; text-decoration: none;
  transition: border-color var(--net-dur) var(--net-ease), box-shadow var(--net-dur) var(--net-ease);
}
.loc-row:hover { text-decoration: none; color: inherit; border-color: var(--net-hairline-strong); box-shadow: var(--net-elev-1); }
.loc-row:focus-visible { border-radius: var(--net-radius-md); }
/* The card is no longer an <a> — the NAME is, stretched over the whole card. Identical to .team-link,
   and for the identical reason: a <button> cannot live inside an <a>, and the play mark had to become
   a real button so that aiming at it plays the video instead of navigating away. */
.loc-link { color: inherit; text-decoration: none; }
.loc-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.loc-link:hover { text-decoration: none; color: inherit; }
.loc-row:has(.loc-link:focus-visible) { outline: 2px solid var(--net-primary); outline-offset: 2px; }
/* ⭐ THE WELL IS THE FALLBACK, and a photo simply covers it. An empty grey square with a 20px pin in
   the middle of it looked like a failed image, not like a clinic that has not published one — and the
   <img> carries onerror="display:none", so a 404 on the clinic's origin leaves the element in the DOM
   and no icon behind it. Painting the plate on the WELL answers both cases with one rule: no photo,
   broken photo, or slow photo, you get a deliberate tinted plate; a photo that loads hides it entirely.
   The gradient is two existing surface tokens, so it re-derives itself in dark (primary-soft #1F3B2B
   over sunken #19211B) instead of glowing pale-green on a near-black card. */
.loc-thumb {
  position: relative; flex: none; align-self: stretch; display: grid; place-items: center;
  /* ⭐ BIGGER (Marcos 2026-08-31), and LANDSCAPE since 09-01: these are the clinic's own pictures of
     the place a patient is being asked to drive to, and buildings are photographed wide.
     ⚠️ clamp, not a fixed 168px, because this list is NOT the width of the container it queries: from
     860px up .loc-split puts the map beside it and the list column is roughly half of `work`. A fixed
     thumb there left the address two words per line, and a container query on `work` cannot see it —
     34% of the row is the only measure that tracks the column the card is actually standing in. */
  width: clamp(120px, 34%, 168px); border-radius: var(--net-radius-sm);
  background: linear-gradient(135deg, var(--net-primary-soft), var(--net-sunken));
  color: var(--net-primary); overflow: hidden;
}
/* Child combinator on purpose: .loc-play's own <svg> also lives inside this well and must stay 16px. */
.loc-thumb > svg { width: 28px; height: 28px; }
.loc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--net-dur-slow) var(--net-ease); }
/* The photo leans in when the card is hovered — the standard "this tile opens something" cue on every
   media card in the market, and the only motion on the card. overflow:hidden above is what crops it. */
.loc-row:hover .loc-thumb img { transform: scale(1.04); }
/* ⭐ THREE WEIGHTS, THREE JOBS. Name / address / phone were all 13px and differed only by one font
   weight, so the card had no entry point — the eye had nowhere to land first. The name takes the h3
   role (15/600, the existing style, not a new one), the address drops to ink-2, and the phone drops
   again to ink-3 and switches to the display face with tabular figures so a number reads as a number
   and not as a third line of prose. All three clear AA on card, canvas and band. */
.loc-body { display: flex; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; }
.loc-nm { font: var(--net-fw-semibold) var(--net-fs-h3)/var(--net-lh-h3) var(--net-font-body); letter-spacing: var(--net-ls-h3); color: var(--net-ink); }
/* The name answers the card's hover, so the whole tile reads as one target rather than a photo that
   happens to sit beside a link. Colour is never the ONLY cue — the border and elevation move too. */
.loc-row:hover .loc-nm { color: var(--net-primary); }
.loc-body .meta { color: var(--net-ink-2); }
/* The SECOND meta is the phone (address is written first, and is optional). Adjacent-sibling, so when
   a site has only one of the two nothing here matches and the single line keeps the address style. */
.loc-body .meta + .meta { margin-top: 4px; color: var(--net-ink-3); font-family: var(--net-font-display); font-variant-numeric: tabular-nums; }
/* The file kills the play button's hover nudge under reduced motion rather than relying on the 0ms
   duration token; the photo's lean gets the same treatment, for consistency and because a scale is a
   state, not only a transition. */
@media (prefers-reduced-motion: reduce) {
  .loc-thumb img { transition: none; }
  .loc-row:hover .loc-thumb img { transform: none; }
}
/* PHONE: photo on top, full width, 16:9. The clamp above already handles the half-width desktop column;
   this breakpoint is the case the clamp cannot save — under ~560px there is no split-and-share that
   leaves the address readable, so the two stop competing for the same line. Container query, not
   viewport: this card lives in .net-col.work, which is narrower than the window by the nav rail plus
   the context column (see the note at .team-grid). */
@container work (max-width: 560px) {
  .loc-row { flex-direction: column; min-height: 0; gap: 10px; }
  /* width:auto so the column's stretch owns it; aspect-ratio can only drive the height once the height
     is no longer being stretched by the row. */
  .loc-thumb { width: auto; aspect-ratio: 16 / 9; }
  .loc-body { justify-content: flex-start; }
}

/* ── a site's own page ── */
/* 16:9, because a building is photographed wide and a square hero would crop the entrance out of it.
   The plate underneath is what an unpublished (or unresolvable) photo looks like: an icon and a line of
   text, never a broken image and never an empty src. */
.loc-hero {
  position: relative; display: grid; place-items: center;
  width: 100%; aspect-ratio: 16 / 9; max-height: 420px;
  border-radius: var(--net-radius-md); background: var(--net-sunken); overflow: hidden;
}
.loc-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.loc-hero-empty { display: grid; justify-items: center; gap: 6px; color: var(--net-ink-3); }
/* ⚠️ MUST UNDO THE CENTRING, not just the size (regression caught in review, 2026-09-01). `.loc-play`
   became a centred 44px button on the LIST cards, and this modifier only reset right/bottom/width/height
   — so top/left/transform still applied and the hero's mark rendered dead-centre at 30px instead of in
   its corner. A modifier has to unset every property of the base rule it means to override. */
.loc-play--hero {
  top: auto; left: auto; transform: none;
  right: 10px; bottom: 10px; width: 30px; height: 30px;
  border-width: 0;
}
/* auto-fill against a small track, exactly like the team contact sheet: two up on a phone, four or five
   on a desktop column, one rule — and the count is whatever the clinic published, never a fixed N. */
.loc-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
@container work (min-width: 720px) { .loc-gallery { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } }
.loc-shot {
  display: block; aspect-ratio: 1; border-radius: var(--net-radius-sm);
  border: 1px solid var(--net-hairline); background: var(--net-sunken); overflow: hidden;
}
.loc-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── the member's own page ── */
.tm-hero { display: flex; flex-direction: column; gap: var(--net-space-4); }
@container work (min-width: 720px) { .tm-hero { flex-direction: row; align-items: flex-start; } }
.tm-hero-media {
  display: grid; place-items: center; flex: none;
  width: 100%; max-width: 240px; aspect-ratio: 1;
  border-radius: var(--net-radius-md); background: var(--net-sunken); overflow: hidden;
}
.tm-hero-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tm-hero-body { min-width: 0; }
.tm-hero-body h1 { margin: 0; }
.tm-role { color: var(--net-primary); font-weight: var(--net-fw-semibold); margin: 2px 0 6px; }
/* The video dialog reuses the .tc-dlg shell; only the box is a different shape — the Lead Map's is a
   fixed 860px tall, which would letterbox a portrait phone clip inside a black frame. */
.tc-dlg-box--video { width: min(900px, 96vw); height: auto; max-height: 92vh; }
/* (The video-only `.tc-dlg-hd .grow` override that stood here is GONE — 2026-09-01. It was the scoped
   half of a shell bug that has now been fixed at the shell, beside .tc-dlg-hd itself. Keeping both would
   leave a more-specific rule shadowing the general one, which is how a later change to the shell appears
   to do nothing on exactly one dialog.) */
.tc-dlg-body--video { flex-direction: column; padding: 0; background: #000; }
.tm-video { width: 100%; max-height: calc(92vh - 64px); display: block; background: #000; }

/* ⚠ THIS BLOCK MUST STAY BELOW .tc-dlg-box — not a style choice. `.tc-dlg-box--thread` and
   `.tc-dlg-box` are both ONE class, so they tie on specificity and source order decides. Written up
   beside .ladder-step (where the lead record's other rules live) it lost silently: the popup opened at
   the Lead Map's 1240×860 and the message bubbles ran the full width of the screen. */
/* ── The conversation popup (Marcos 2026-09-02) ──────────────────────────────────
   "clicking a conversation must open a popup with the conversation flow not go to the conversations
   page". Reading a thread was a NAVIGATION — it dropped the lead record, its open tab and its loaded
   ladder to answer one question, and coming back re-read all of it.
   Reuses the .tc-dlg shell; only the box shape and the message flow are its own. */
.tc-dlg-box--thread { width: min(660px, 96vw); height: auto; max-height: 86vh; }
.tc-dlg-body--thread { flex-direction: column; padding: var(--net-space-4); gap: 10px; overflow-y: auto; background: var(--net-sunken); }
.thr-msg { display: flex; gap: 8px; align-items: flex-end; }
/* Ours on the right, theirs on the left — the arrangement every messaging surface already teaches. */
.thr-msg.mine { flex-direction: row-reverse; }
.thr-bub { max-width: 78%; min-width: 0; padding: 8px 12px; border: 1px solid var(--net-hairline); border-radius: 12px;
  background: var(--net-card); font-size: var(--net-fs-small); line-height: var(--net-lh-body); overflow-wrap: anywhere; }
/* ⛔ pre-wrap GOES ON THE MESSAGE, NEVER ON THE BUBBLE. On the bubble it also preserves the Razor
   markup's OWN indentation — the newline and thirty-odd spaces sitting between the speaker label and
   the body — and every message rendered with a ragged indent that read as right-aligned text. The
   patient's newlines still survive, because they are inside this element. */
.thr-tx { display: block; white-space: pre-wrap; }
.thr-msg.mine .thr-bub { background: var(--net-primary-soft); border-color: transparent; }
/* ⛔ WHO SPOKE IS NEVER LEFT TO THE SIDE ALONE. A greyscale print, a narrow phone that stacks the
   bubbles, a screen reader — all three get the speaker in words above the bubble. */
.thr-who { display: block; font: var(--net-fw-semibold) var(--net-fs-eyebrow)/1.2 var(--net-font-display);
  letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); margin-bottom: 3px; }
.thr-when { display: block; margin-top: 3px; font-size: var(--net-fs-eyebrow); color: var(--net-ink-3); }
.thr-msg.mine .thr-who, .thr-msg.mine .thr-when { text-align: right; }
.tc-dlg-ft { display: flex; align-items: center; gap: var(--net-space-2); flex-wrap: wrap;
  padding: var(--net-space-3) var(--net-space-4); border-top: 1px solid var(--net-hairline); background: var(--net-card); }

/* a portrait that fails to load leaves the tinted well, not a broken-image glyph (batch-A review) */
.team-photo, .tm-hero-media { background: var(--net-sunken); }
/* settings tabs: a lone (odd last) CARD in a two-up row takes the full row - a half-width card
   beside a hole (batch-B review D2/D3).
   ⛔ :not(.field) — THIS RULE COUNTS DOM POSITION, WHICH IS NOT THE GRID ROW (Marcos 2026-09-04:
   "Desk is staffed until to be on teh right of Desk is staffed from"). Inside a settings card an
   earlier field can already span both columns ("Days you're open"): it is ONE child but a whole
   row, so every field after it has the wrong parity and the last one spanned while its partner
   sat alone above it. A field knows its own width — SettingsGroupCard sets it — so the parity
   guess is scoped out of .field and left to the cards it was written for. */
.net-tabpanel .grid-2 > :last-child:nth-child(odd):not(.field) { grid-column: 1 / -1; }
/* legal pages (About / Terms / Privacy): prose, not a dashboard. A measure of ~72ch is the readable
   width; the list and quote rules exist because these pages are read top to bottom, not scanned. */
.legal { max-width: 74ch; }
.legal .lead { font-size: var(--net-fs-body-lg, 1.05rem); color: var(--net-ink-1); }
.legal h2 { margin: 22px 0 8px; font-size: 1.05rem; font-weight: var(--net-fw-semibold); }
.legal p { margin: 0 0 10px; line-height: 1.62; }
.legal .legal-list { margin: 0 0 12px; padding-left: 20px; }
.legal .legal-list li { margin: 0 0 6px; line-height: 1.55; }
.legal .legal-quote { margin: 0 0 12px; padding: 10px 12px; border-left: 3px solid var(--net-primary);
  background: var(--net-sunken); border-radius: var(--net-radius-sm, 6px); font-style: normal; }
.legal .legal-foot { margin-top: 20px; padding-top: 12px; border-top: 1px solid var(--net-hairline);
  color: var(--net-ink-3); font-size: var(--net-fs-small); }
.legal .legal-cta { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px; }

/* ══ THE INVOICE POPUP (Marcos 2026-09-01) ════════════════════════════════════════════════════════
   Opened from the clinic's own /billing page and from the SuperAdmin's Clinic Setup billing tab —
   one component, so one set of rules. It reuses the .tc-dlg shell; only the box shape and the
   statement's own internals live here.

   ⚠️ The Lead Map's .tc-dlg-box is a fixed 1240x860 with a flex-ROW body. A statement is a narrow
   document that scrolls, so it needs both modifiers — inheriting the map's shape put an invoice in a
   half-empty landscape box, which is the same mistake .tc-dlg-box--convo was written to undo. */
.tc-dlg-box--invoice { width: min(680px, 96vw); height: auto; max-height: min(880px, 92vh); }
.tc-dlg-body--invoice { flex-direction: column; gap: var(--net-space-3); padding: var(--net-space-4);
  overflow-y: auto; }

/* The three facts a statement is read for, before any arithmetic: which tier, how many leads, what it
   comes to. Grid rather than flex so the total column holds its right edge whatever the tier is called. */
.inv-tier { display: grid; grid-template-columns: 1fr 1fr auto; gap: var(--net-space-3);
  align-items: start; padding: var(--net-space-3); background: var(--net-sunken);
  border-radius: var(--net-radius-md, 8px); }
.inv-tier-name { font-size: 20px; font-weight: 650; line-height: 1.2; }
.inv-tier-total { text-align: right; }
@media (max-width: 560px) {
  .inv-tier { grid-template-columns: 1fr 1fr; }
  .inv-tier-total { grid-column: 1 / -1; text-align: left; }
}

/* The line-item table. Rates and amounts are numeric columns and stay narrow; the description takes
   what is left, because it is the only cell whose length varies. */
.inv-table { width: 100%; }
.inv-table th.num, .inv-table td.num { width: 1%; white-space: nowrap; }
.inv-table tfoot td { border-top: 1px solid var(--net-hairline); }
.inv-table tfoot .inv-sub td { color: var(--net-ink-2); font-size: var(--net-fs-small); border-top: 0; }
.inv-table tfoot .inv-total td { font-weight: 650; }
/* A discount is the one figure on a statement that is good news — it is the only place colour is
   earned here, and it never carries the meaning alone: the row is labelled and the number is signed. */
.inv-off { color: var(--net-success-ink); font-weight: 600; }

/* The ledger behind the total, boxed so it reads as evidence for the table above rather than as more
   of it. Scrolls on its own: a cycle with 90 leads must not push the total off the screen. */
.inv-lines { max-height: 260px; overflow-y: auto; border: 1px solid var(--net-hairline);
  border-radius: var(--net-radius-sm, 6px); }

/* A row that opens its own statement has to look like it does. Pointer + a hover ground, and the
   keyboard gets the same affordance through the focus ring the shell already defines. */
.inv-open { cursor: pointer; }
.inv-open:hover { background: var(--net-sunken); }

/* ⭐ THE FREE TRIAL MARKS THE FEES — IT NO LONGER DIMS THEM.
   Marcos asked for opacity ("make opacity go for the other fields of fees"), and opacity was the wrong
   instrument for what he wanted said. At .45 the labels measured 2.03:1 and the help text 1.92:1 against
   4.5:1 — a contrast failure over ~40% of the tab — and controls that LOOK disabled while being fully
   live are the worst of both readings. Hover restoring it does not help a magnifier or a touch user.
   The state is now said in words (a chip in the card header) and marked with a rail, which is legible at
   every zoom level and carries the same "this is paused, not broken" meaning. */
.fees-paused { position: relative; padding-left: var(--net-space-3); }
.fees-paused::before {
  content: ""; position: absolute; left: 0; top: 2px; bottom: 2px; width: 2px;
  border-radius: var(--net-radius-pill, 999px); background: var(--net-success-ink);
}

/* ── The statement as a document ─────────────────────────────────────────────────────────────── */
/* Parties left, reference right. A statement gets forwarded to whoever signs, and they need something
   to quote back at us. Tabular figures so the reference and the dates line up. */
.inv-doc-hd { display: grid; grid-template-columns: 1fr auto; gap: var(--net-space-3);
  padding-bottom: var(--net-space-3); border-bottom: 1px solid var(--net-hairline); }
.inv-doc-hd .inv-ref { text-align: right; font-variant-numeric: tabular-nums; }
@media (max-width: 560px) { .inv-doc-hd { grid-template-columns: 1fr; } .inv-doc-hd .inv-ref { text-align: left; } }

/* "Was / now" stacks instead of running inline. Inline, the pair forced the whole numeric column to the
   width of TWO prices, so nothing in it aligned — and the struck price sat on the lowest-contrast ink. */
.inv-was { display: inline-flex; flex-direction: column; align-items: flex-end; line-height: 1.15; }
.inv-was s { font-size: var(--net-fs-eyebrow); color: var(--net-ink-3); }

/* The total is the fact; the tier and the lead count are context. It was a collapsed `auto` track at
   108px beside two 245px siblings — the least prominent cell on the block. */
.inv-tier { grid-template-columns: 1fr 1fr minmax(170px, auto); }
.inv-tier-total { padding-left: var(--net-space-3); border-left: 1px solid var(--net-hairline); }
.inv-tier-total .inv-tier-name { font-size: 28px; font-weight: 700; letter-spacing: -.01em; }
@media (max-width: 560px) {
  /* ⚠️ The total moves to the TOP on a phone, not the bottom. Spanning it full-width at the end put the
     one number people open this for in the least-read corner of the block. */
  .inv-tier-total { order: -1; border-left: 0; padding-left: 0;
    padding-bottom: var(--net-space-2); border-bottom: 1px solid var(--net-hairline); }
}

/* The row you are looking at, distinguishable from the even-row stripe it used to be painted with —
   both were var(--net-sunken), so "selected" and "every other row" looked identical. */
.inv-current > td:first-child { box-shadow: inset 2px 0 0 var(--net-primary); }
.inv-current > td { background: var(--net-sunken); }

/* ── PRINTING A STATEMENT ────────────────────────────────────────────────────────────────────────
   The one job this document has, and it could not do it: .tc-dlg is position:fixed, its body scrolls
   and .inv-lines is capped at 260px, so printing produced one clipped page of the app behind it. */
@media print {
  body:has(.tc-dlg) .net-header,
  body:has(.tc-dlg) .net-col,
  body:has(.tc-dlg) .net-rail,
  body:has(.tc-dlg) footer { display: none !important; }
  .tc-dlg { position: static; padding: 0; background: none; display: block; }
  .tc-dlg-box--invoice { width: 100%; max-height: none; height: auto; border: 0; box-shadow: none; }
  .tc-dlg-body--invoice, .inv-lines { overflow: visible; max-height: none; }
  .inv-lines { border: 0; }
  .inv-noprint { display: none !important; }
}

/* ══ THE BILLING "THIS CYCLE" HERO (Marcos 2026-09-01) ═════════════════════════════════════════
   "i dont see meaningful data on this cycle… bigger totals". The old strip gave three equal-weight
   20px numbers and left the operator to work out which one they came for. The money is the headline
   now, the tier stands beside it as a badge, and the four stats below explain how it got there. */
/* ⭐ OUTLINED, NOT FILLED (Marcos 2026-09-01: "remove the background and use a border alone with a
   left border in a meaningful colour, maybe green"). The sunken fill made the hero read as an inset
   panel — something set into the page rather than the headline of it — and it collided with the same
   fill used by table stripes and the sum row further down. A hairline with a green spine reads as a
   statement of standing: this is the money, and it is healthy.
   ⚠️ --net-success is the token that flips per theme (#2F7D53 light, #74C596 dark); a literal green
   here would go muddy the moment someone switched to dark. */
.bill-hero { display: flex; align-items: flex-start; gap: var(--net-space-4); flex-wrap: wrap;
  padding: var(--net-space-4); margin-bottom: var(--net-card-gap);
  background: none;
  border: 1px solid var(--net-hairline);
  border-left: 3px solid var(--net-success);
  border-radius: var(--net-radius-md, 8px); }
.bill-hero-money { flex: 1 1 260px; min-width: 0; }
/* Big enough to be read across a desk, tabular so it does not jitter as the preview follows typing. */
.bill-hero-total { font-size: 40px; line-height: 1.05; font-weight: 700; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; }
.bill-hero-money s { color: var(--net-ink-3); }
.bill-hero-tier { flex: 0 0 auto; text-align: right; display: flex; flex-direction: column;
  align-items: flex-end; gap: 4px; }
.chip.lg { font-size: var(--net-fs-small); padding: 5px 11px; }
@media (max-width: 560px) { .bill-hero-tier { text-align: left; align-items: flex-start; } }

/* The two priced lines, and the sum under them. A row, not a table: the description is the long part
   and the money is the short part, which is the one shape flex does better than a grid. */
.bill-lines { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm, 6px); }
.bill-line { display: flex; align-items: flex-start; gap: var(--net-space-3);
  padding: 11px var(--net-space-3); border-bottom: 1px solid var(--net-hairline); }
.bill-line:last-child { border-bottom: 0; }
.bill-line .ico { flex: none; width: 16px; color: var(--net-ink-3); padding-top: 2px; }
.bill-line .num { flex: none; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The unit is not part of the number — bolding "/lead" at digit weight made the price harder to scan. */
.bill-line .num .meta { font-weight: var(--net-fw-regular, 400); margin-left: 2px; }
.bill-line--sum { background: var(--net-sunken); }

/* ⭐ WHERE THEY ARE, IN THE TIER TABLE. It used to be `background: var(--net-sunken)` — which is the
   even-row stripe, so the current tier was indistinguishable from every other row. Three signals now:
   a left accent, a brand tint, and a check mark in the cell (never colour alone). */
.tier-now > td { background: var(--net-primary-soft); }
.tier-now > td:first-child { box-shadow: inset 3px 0 0 var(--net-primary); font-weight: var(--net-fw-semibold); }
.tier-now > td:first-child svg { vertical-align: -2px; margin-right: 4px; color: var(--net-primary); }

/* ══ THE DISCOUNT EDITOR, IN THREE GROUPS (Marcos 2026-09-01) ══════════════════════════════════
   "meaningful group per leads and per tier — all is together at this moment and can't read it
   properly." Five controls in one grid put "dollars off a lead" next to "dollars off the base" with
   nothing but a label between them. Each group is now a bounded block with its own header and its own
   RESULT, so the effect sits beside the control that causes it. */
/* ⚠️ 24px between them, not the 12px card rhythm (Marcos 2026-09-01: "add some margin between those
   discount cards"). These sit INSIDE a card, so they need more air than cards stacked on a page do —
   at 12px the borders read as one ruled list rather than three separate decisions, which is the whole
   point of splitting them. Padding goes up to match, or the gap outside beats the gap inside. */
.disc-group { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm, 6px);
  padding: var(--net-space-4); margin-bottom: var(--net-space-5); }
.disc-group:last-of-type { margin-bottom: 0; }
.disc-hd { display: flex; align-items: baseline; gap: var(--net-space-2);
  padding-bottom: var(--net-space-2); margin-bottom: var(--net-space-3);
  border-bottom: 1px solid var(--net-hairline); }
/* The live number for this group. Tabular so it does not jitter while the preview follows typing. */
.disc-effect { font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; }

/* The cog panel: a settings card drawn INSIDE another card, so it drops its own border, background and
   padding and reads as the same card turned over rather than a card sitting in a card. */
.tc-cog-panel > .card { border: 0; background: none; padding: 0; box-shadow: none; }
.tc-cog-panel > .card > .card-hd { padding-top: 0; }

/* ── The creative picker inside the ad composer (Marcos 2026-09-02) ───────────────────────────
   A creative is a PICTURE, and a list of text chips made people open the library in another tab to
   find out which one "Untitled creation" was. Tiles, with the file streamed through the clinic on
   every view — the label lives UNDER the thumb, never over it, so a file that fails to load still
   leaves a readable row. */
.media-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.media-pick { display: flex; flex-direction: column; gap: 6px; padding: 8px; text-align: left; cursor: pointer;
    background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); }
.media-pick:hover { border-color: var(--net-hairline-strong); }
.media-pick.is-selected { border-color: var(--net-primary); box-shadow: inset 0 0 0 1px var(--net-primary); }
.media-pick__thumb { position: relative; display: flex; align-items: center; justify-content: center;
    aspect-ratio: 4 / 3; overflow: hidden; border-radius: var(--net-radius-sm); background: var(--net-sunken); color: var(--net-ink-3); }
.media-pick__thumb img, .media-pick__thumb video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Sits UNDER the media, so it is what shows when the file 404s or the poster frame never decodes. */
.media-pick__fallback { display: flex; }
.media-pick__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.media-pick__body b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Even halves — for a split where BOTH sides are worked in (the ad campaign's map and its ads),
   rather than a wide subject with a narrow rail beside it. (Marcos 2026-09-02) */
.editor-split.even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
@container work (max-width: 860px) { .editor-split.even { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 860px) { .editor-split.even { grid-template-columns: minmax(0, 1fr); } }

/* A button that reads as a link — for an inline "edit them" inside a sentence, where a real <a> would
   be a lie (it navigates nowhere) and a .btn would break the line. (Marcos 2026-09-02) */
.linklike { background: none; border: 0; padding: 0; font: inherit; color: var(--net-primary); cursor: pointer; text-decoration: underline; }
.linklike:hover { text-decoration: none; }
/* Both halves the same height (Marcos 2026-09-02): the map card and the ads card sit side by side and
   a short one next to a tall one reads as an unfinished layout. `start` stays the default for the
   uneven split, where the narrow rail genuinely should hug its content. */
.editor-split.even { align-items: stretch; }

/* ══ THE CAMPAIGN SETUP CONSOLE ═══════════════════════════════════════════════════════════════
   Settings are READ before they are changed (Marcos 2026-09-02: "all the fields are too tight,
   reorganize more logical, use vertical space, more margin"). So: groups separated by a rule with
   equal air either side, an uppercase heading saying what each group decides, and one rhythm for
   every field — label, control, hint, always the same distance apart.
   ⚠️ The markup for this shipped on 09-02 with no CSS at all; the console rendered as bare stacked
   divs and the group titles read as sentences. */
.setup-form { display: flex; flex-direction: column; padding-top: var(--net-space-4); }
.setup-group { display: flex; flex-direction: column; gap: var(--net-space-3); }
.setup-group + .setup-group { margin-top: var(--net-space-5); padding-top: var(--net-space-5); border-top: 1px solid var(--net-hairline); }
.setup-group__title { font: var(--net-fw-semibold) var(--net-fs-eyebrow)/var(--net-lh-eyebrow) var(--net-font-display); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); }
.setup-form label.small { display: flex; flex-direction: column; gap: 6px; }
.setup-form label.small > b { color: var(--net-ink); font-weight: var(--net-fw-semibold); }
/* A hint belongs to the control above it, not to the gap below. */
.setup-form label.small > .meta { margin: 0; }

/* ══ ONE AD, AS AN OBJECT ═════════════════════════════════════════════════════════════════════
   Picture · words · money, with the status on the edge and the actions on hover — the skeleton
   every ads product uses (Meta, Google, LinkedIn, TikTok), because it is the one that reads at a
   glance. Before this the row was three near-identical weights in a plain box, which is exactly
   why it read flat. */
.ad-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) auto; column-gap: var(--net-space-3); row-gap: var(--net-space-2);
    align-items: start; padding: var(--net-space-3) var(--net-space-3) var(--net-space-3) var(--net-space-4);
    background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md);
    box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-ink-faint);
    transition: box-shadow var(--net-dur) var(--net-ease), border-color var(--net-dur) var(--net-ease); }
.ad-row:hover, .ad-row:focus-within { border-color: var(--net-hairline-strong); box-shadow: var(--net-elev-2), inset 3px 0 0 var(--net-ink-faint); }
/* ⛔ The stripe is COLOUR ONLY, so the status chip with its word stays in the row. Never trade one
   for the other — the stripe is the glance, the chip is the answer. */
.ad-row.s-live    { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-success); }
.ad-row.s-draft   { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-primary); }
.ad-row.s-paused  { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-warn); }
.ad-row.s-stopped { box-shadow: var(--net-elev-1), inset 3px 0 0 var(--net-danger); }

.ad-row__thumb { position: relative; width: 64px; aspect-ratio: 1; overflow: hidden; border-radius: var(--net-radius-sm);
    background: var(--net-primary-soft); color: var(--net-primary); display: grid; place-items: center; }
.ad-row__thumb img, .ad-row__thumb video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

.ad-row__main { min-width: 0; display: flex; flex-direction: column; }
/* ⭐ THE NAME IS THE FILING LABEL, THE HEADLINE IS WHAT THE PUBLIC SEES. The form itself says the
   name is "yours — people never see it", so it reads as an eyebrow and the headline takes the weight. */
.ad-row__name { font: var(--net-fw-semibold) var(--net-fs-eyebrow)/var(--net-lh-eyebrow) var(--net-font-display);
    letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ad-row__headline { font: var(--net-fw-semibold) var(--net-fs-h3)/1.3 var(--net-font-body); color: var(--net-ink); margin-top: 3px; }
.ad-row__text { font-size: var(--net-fs-small); line-height: var(--net-lh-small); color: var(--net-ink-2); margin-top: 4px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Money right-aligned in tabular numerals — a column of figures that does not line up is the
   loudest "unfinished" tell there is. */
.ad-row__num { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; text-align: right; font-variant-numeric: tabular-nums; }
.ad-row__num .n { font: var(--net-fw-bold) var(--net-fs-h2)/1.1 var(--net-font-display); letter-spacing: -.01em; color: var(--net-ink); }
.ad-row__num .n.zero { color: var(--net-ink-3); }
.ad-row__num .u { font-size: var(--net-fs-eyebrow); letter-spacing: var(--net-ls-eyebrow); text-transform: uppercase; color: var(--net-ink-3); }

/* ⛔ ALWAYS VISIBLE (Marcos 2026-09-02: "make them always visible, not only hover"). The ads
   products hide these on hover because their lists run to hundreds of rows; a clinic has three, and
   an action nobody can see is an action nobody uses. The row still lifts on hover — that is what
   says it is an object — but its controls never go away. */
.ad-row__acts { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }

/* A panel INSIDE a card — the composer, the creative flip, one pin. Four hand-rolled copies with
   two different border colours became this. */
.inset { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); padding: var(--net-space-3); background: var(--net-sunken); }
.inset.raised { border-color: var(--net-hairline-strong); background: var(--net-card); }

/* The one exception to "the column gap owns the rhythm": a destructive card has to read as OUTSIDE
   the working stack rather than as another tool in it. */
.card.terminal { margin-top: var(--net-space-3); border-color: color-mix(in srgb, var(--net-danger) 22%, var(--net-hairline)); }

/* The clinic's own email, rendered in a sandboxed frame — its markup, never ours. Height is fixed
   rather than auto: a sandboxed frame cannot report its content height, and guessing produces a
   frame that jumps on every re-render. */
.tc-email-frame { width: 100%; height: 520px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: #fff; }

/* ── Prospects CRM (2026-09-15) ── */
/* A campaign draft's own sandboxed preview — same reasoning as .tc-email-frame (fixed height, the
   frame's own document), just taller: a campaign carries a heading, a body and a CTA button. */
.crm-preview { width: 100%; height: 640px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: #f3f4f6; }
.crm-timeline { display: flex; flex-direction: column; gap: 10px; list-style: none; margin: 0; padding: 0; }
.crm-timeline > li { display: flex; align-items: flex-start; gap: 10px; }
.crm-timeline > li > .ico { flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: var(--net-radius-pill); background: var(--net-primary-soft); color: var(--net-primary); }
.crm-timeline > li > .grow { min-width: 0; }
/* What Cetán opens a call with. A bubble, because it is speech. */
.tc-call-bubble { margin-top: 6px; padding: 12px 14px; border-radius: 14px 14px 14px 4px; background: var(--net-primary-soft); color: var(--net-ink); white-space: pre-wrap; max-width: 60ch; }
/* The campaign's link as a QR — for a printed flyer, where the code has to survive the walk to a phone. */
.tc-qr { flex: none; display: grid; place-items: center; min-width: 148px; min-height: 148px; padding: 8px;
    background: #fff; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); }
.tc-qr canvas { display: block; }

/* ⭐ NO RAIL BESIDE THE CAMPAIGN TABS (Marcos 2026-09-02, screenshot). The strip's border-right stops
   under the LAST TAB — a short line hanging in space that reads as unfinished, and with a sticky strip
   it stops somewhere different as you scroll.
   ⛔ And it cannot simply be made taller: .tabs.vertical is position:sticky, so a pseudo-element on it
   resolves against ITSELF, not the grid cell — measured, it came back 264px tall, exactly the tab list.
   The 16px gutter and the panel's own card border already separate the two, so the rail is deleted
   rather than faked. */
.tabs-split > .tabs.vertical { border-right: 0; }

/* ⭐ A TAB PANE IS A STACK, NOT A BLOCK (Marcos 2026-09-02). A pane holding two cards had them
   touching at exactly 0px — measured — so "Where it sends people" and "Tracking" read as one card
   with a line through it. The gap lives on the pane rather than on the cards, so a pane with three
   cards is right for free and no card needs a margin of its own.
   ⛔ AND [hidden] HAS TO BE RESTATED. The UA stylesheet's `[hidden]{display:none}` is weaker than a
   class rule, so `display:flex` here would have shown EVERY pane at once — all seven sections of the
   campaign stacked down one page. The same trap is already documented on .net-tabpanel a few hundred
   lines up; this is the second component to hit it. */
.tab-pane-stack { display: flex; flex-direction: column; gap: var(--net-card-gap); }
.tab-pane-stack[hidden] { display: none; }

/* Map view of the studies directory (Studies.razor). Same reason .tc-dash-map is positioned: .tc-map is
   `position: absolute; inset: 0`, so the wrapper must be a positioned ancestor WITH a height or the map
   escapes and covers the page. Taller than the dashboard's 260px card because this is the view itself,
   not a widget beside others; capped against the viewport so the filter strip above stays on screen. */
.tc-directory-map { position: relative; height: calc(100vh - 380px); min-height: 420px;
  border-radius: var(--net-radius, 12px); overflow: hidden; border: 1px solid var(--net-hairline);
  background: var(--net-sunken); }
@container work (max-width: 640px) { .tc-directory-map { height: calc(100vh - 300px); min-height: 360px; } }

/* ── CLINICAL MARKET SCANNER — SuperAdmin (/admin/clinical-market, Marcos 2026-09-06) ──────────────
   The network mirror of the backoffice tool. It reuses .vtabs, .card, .field, .table and .chip and
   adds only what those do not cover: a facility list, one expandable study, and a registry record
   rendered as facts. Nothing here is a second copy of an existing wall. */

/* A stack of .check labels has no rhythm of its own: .check is a flex ROW and carries no margin, so
   four of them in a card render as one solid block. The only existing stack helper is .filters-checks,
   which is display:contents for a grid — the wrong shape for a column inside a card. */
.cm-checks { display: flex; flex-direction: column; gap: 8px; }

/* A ranked list, not a bulleted one: position is what a competitive-intelligence reader looks for. */
.rank { list-style: none; margin: 0; padding: 0; counter-reset: rank; }
.rank li {
    display: flex; align-items: center; gap: var(--net-space-2);
    padding: 7px 0; border-bottom: 1px solid var(--net-hairline);
    font-size: var(--net-fs-small);
}
.rank li:last-child { border-bottom: 0; }
.rank li .grow { flex: 1; min-width: 0; }
.rank li::before {
    counter-increment: rank; content: counter(rank, decimal-leading-zero);
    flex: 0 0 1.6rem; color: var(--net-ink-3); font-weight: var(--net-fw-semibold);
    font-variant-numeric: tabular-nums;
}
/* Only the top three carry the accent — everyone highlighted is nobody highlighted. */
.rank li:nth-child(-n+3)::before { color: var(--net-primary); }

/* ── Facility list ───────────────────────────────────────────────────────────────────────────── */
.sitelist { display: flex; flex-direction: column; }

/* A row is a real button, not a div with a click handler: Tab reaches it and both Enter and Space
   activate it without a line of JS. */
.siterow {
    display: flex; align-items: center; gap: var(--net-space-3);
    width: 100%; padding: 9px 8px;
    border: 0; border-bottom: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm);
    background: none; text-align: left; cursor: pointer;
    transition: background-color var(--net-dur) var(--net-ease);
}
.siterow:last-child { border-bottom: 0; }
.siterow:hover { background: var(--net-sunken); }
.siterow:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }
.siterow .grow { flex: 1; min-width: 0; }
.siterow .nm { display: block; font-weight: var(--net-fw-semibold); color: var(--net-ink); }
.siterow .meta { display: block; }
/* Distance is the one number that decides whether a site matters, so it is the only part of the
   location line that gets the accent. */
.siterow .dist { color: var(--net-primary); font-weight: var(--net-fw-semibold); font-variant-numeric: tabular-nums; }

/* ── One facility, its studies, one open at a time ───────────────────────────────────────────── */
.studylist { display: flex; flex-direction: column; gap: 6px; }
.studyitem { border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); overflow: hidden; }
.studyitem > .hd {
    display: flex; align-items: center; gap: var(--net-space-2);
    width: 100%; padding: 8px 10px;
    border: 0; background: none; text-align: left; cursor: pointer;
    font-size: var(--net-fs-small); color: var(--net-ink);
}
.studyitem > .hd:hover { background: var(--net-sunken); }
.studyitem > .hd:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }
.studyitem > .hd .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An NCT is an identifier people copy, so it is monospaced and never truncated. */
.studyitem .nct { flex: 0 0 auto; font-family: var(--net-font-mono, ui-monospace, monospace); font-size: 11px; color: var(--net-ink-3); }

/* ── The registry record, as a fact list ─────────────────────────────────────────────────────── */
/* A <dl> because that is what this is: a screen reader announces "Sponsor, Novo Nordisk" rather
   than two unrelated strings. */
.facts { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 6px var(--net-space-5); margin: 0 0 var(--net-space-3); }
.facts > div { display: grid; grid-template-columns: 9rem minmax(0,1fr); gap: var(--net-space-2); align-items: baseline; }
.facts dt { font-size: 11px; font-weight: var(--net-fw-semibold); letter-spacing: .04em; text-transform: uppercase; color: var(--net-ink-3); }
.facts dd { margin: 0; font-size: var(--net-fs-small); color: var(--net-ink); min-width: 0; overflow-wrap: anywhere; }

/* Registry criteria arrive as plain text with meaningful line breaks. pre-wrap keeps them and still
   wraps long lines; the house font, not monospace, because this is prose and not code. */
.criteria {
    /* ⭐ THE CARD SURFACE, NOT A SUNKEN WELL (Marcos 2026-09-07: "background color i dont like it").
       The tint was doing the job the border already does - saying "this is a quoted block" - and two
       signals for one idea reads as a grey wash across half the study. The hairline keeps the edge. */
    /* ⚠️ The cap is generous rather than absent: side by side with the drafted questions, an
       unbounded 200-line criteria section would leave the right-hand column stranded under a screen
       of whitespace. Ordinary records never reach it. */
    max-height: 34rem; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
    margin: 0; padding: var(--net-space-4);
    border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md);
    background: var(--net-card);
    font-family: inherit; font-size: 12.5px; line-height: 1.6; color: var(--net-ink-2);
    white-space: pre-wrap; overflow-wrap: anywhere;
}
/* A scroll region is a keyboard destination, so it is focusable — and focusable gets a ring. */
.criteria:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }

/* A tab that is not ready yet stays reachable (aria-disabled, never the disabled attribute) so a
   keyboard or screen-reader user can land on it and hear why — its title carries the reason. */
/* The dimming itself is already handled by the unscoped [aria-disabled="true"] rule further up this
   file — restating it here would put a second "disabled" opacity constant in one stylesheet. Only the
   hover suppression is new: the existing one is scoped to .post .acts. */
.tabs button[aria-disabled="true"]:hover { background: none; color: var(--net-ink-2); }

@media (max-width: 720px) {
    .facts { grid-template-columns: minmax(0,1fr); }
    .facts > div { grid-template-columns: minmax(0,1fr); gap: 0; }
}
/* The site filter sits in the card head beside the count. Capped so a long placeholder cannot push
   the count onto its own line; .card-hd already wraps on narrow columns. */
.cm-sitesearch { width: 15rem; max-width: 100%; flex: 0 1 auto; }
@container work (max-width: 480px) { .cm-sitesearch { width: 100%; } }

/* The study view is the card's own body, not a panel nested inside a tinted list row — that is the
   contrast fix: the old accordion body sat on .studyitem.open's primary tint and put muted body text
   and a sunken <pre> on top of it (Marcos 2026-09-06). On the card surface every token keeps the
   contrast it was chosen for. */
.studyitem > .hd .nct { margin-left: auto; }

/* ── Clinical Market: the drafted prescreen, and the add-to-a-clinic picker ──────────────────────
   ⭐ SOURCE BESIDE SUGGESTION. Each question shows the criterion it was quoted from, in the
   registry's own words, so the reviewer is checking the draft rather than trusting it. The quote is
   deliberately quieter than the question but never hidden behind a toggle - a criterion nobody reads
   is a criterion nobody checked. */
.screener { margin: 8px 0 0; padding-left: 24px; display: grid; gap: var(--net-space-3); }
.screener > li { padding: var(--net-space-4); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: var(--net-card); }
.screener .q { font-weight: var(--net-fw-semibold); line-height: 1.35; }
.screener .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.screener .src { margin-top: 12px; padding-left: 10px; border-left: 2px solid var(--net-hairline);
                 color: var(--net-ink-muted); font-size: 12px; line-height: 1.45; white-space: pre-wrap; }

/* The picker is a short form, not a workspace: it sizes to its content instead of taking the
   full-height shell every other dialog on this app wants. ONE definition, used by the Clinical
   Market's "assign lead catcher" and the pool's "assign this lead" - two dialogs that ask the same
   shape of question must not drift into two looks.
   ⭐ EVERY ELEMENT GETS AIR (Marcos 2026-09-07). The first pass leaned on the shell's own padding and
   the fields ended up shoulder to shoulder with the header, the copy and the footer. Spacing is set
   here on the CHILDREN rather than as inline styles per element, so a control added later inherits it
   instead of being the one that looks wrong. */
.pick-dlg { height: auto; max-height: 88vh; width: min(560px, 96vw); }
.pick-dlg .tc-dlg-hd { padding: var(--net-space-4) var(--net-space-5); gap: var(--net-space-3); }
.pick-dlg .tc-dlg-hd .meta { margin-top: 4px; line-height: 1.45; }
.pick-dlg .bd { padding: var(--net-space-5); overflow: auto; display: grid; gap: var(--net-space-4); }
/* The grid owns the rhythm, so nothing inside carries its own stray margin. */
.pick-dlg .bd > * { margin: 0; }
.pick-dlg .field { display: grid; gap: 6px; }
.pick-dlg .check { display: flex; align-items: flex-start; gap: var(--net-space-2); line-height: 1.45; }
.pick-dlg p { line-height: 1.55; }
.pick-dlg .dash-foot { padding: var(--net-space-4) var(--net-space-5); border-top: 1px solid var(--net-hairline); }
.pick-dlg .dash-foot .fr { display: flex; gap: var(--net-space-2); }
/* Scanner bulk add (2026-09-11): wider, one row per Lead Catcher to create; an umbrella lists its protocols. */
.pick-dlg.pick-dlg--wide { width: min(720px, 96vw); }
.cm-bulk { display: grid; gap: 6px; max-height: 340px; overflow: auto; }
.cm-bulk-row { display: flex; gap: var(--net-space-2); align-items: flex-start; padding: 8px 10px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); }
.cm-bulk-row.umbrella { background: var(--net-sunken); }
.cm-bulk-row > input[type="checkbox"] { margin-top: 3px; flex: none; }
.cm-bulk-row .grow { min-width: 0; }
.cm-bulk-proto { display: flex; gap: 6px; align-items: center; margin-top: 4px; }
.cm-bulk-proto .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Clinical Market: the source and the draft, read together ───────────────────────────────────
   ⭐ 50/50 (Marcos 2026-09-07). The questions exist to be checked against the criteria, and a
   comparison you have to scroll between is not one. `align-items:start` so the shorter column keeps
   its own height instead of stretching to match its neighbour. */
/* The gutter is wider than the usual card gap on purpose: these two columns are read ACROSS, and a
   narrow gap makes a criterion and the question beside it look like one block. */
.cm-split { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 32px; align-items: start; }
.cm-split > section { min-width: 0; }
.cm-split__hd { padding-left: 0; padding-right: 0; }
@container work (max-width: 900px) { .cm-split { grid-template-columns: minmax(0,1fr); } }

/* The receipt after an add. Air on BOTH sides - it is a result, not a caption on the button. */
.cm-receipt { margin: var(--net-space-4) 0; }

/* ── Segmented view switch (Table / Map) ────────────────────────────────────────────────────────
   Two buttons that pick how a panel is DRAWN, not what it holds - so they read as one control with
   a current state rather than as two actions. aria-pressed carries that state to a screen reader;
   the tint is only its visible half. */
.seg { display: inline-flex; flex: none; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); overflow: hidden; background: var(--net-card); }
.seg > button { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 0; background: none;
                font: inherit; font-size: var(--net-fs-small); color: var(--net-ink-2); cursor: pointer; }
.seg > button + button { border-left: 1px solid var(--net-hairline); }
.seg > button:hover { background: var(--net-sunken); }
.seg > button.on { background: var(--net-primary); color: var(--net-on-primary); }
.seg > button.on:hover { background: var(--net-primary); }
.seg > button:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }

/* The scanner's map needs a height of its own: .tc-map is sized by whatever page hosts it, and inside
   a card with no set height it would collapse to nothing.
   ⭐ SIZED TO THE VIEWPORT, not to a fixed box (Marcos 2026-09-07: "bigger by default"). A results
   map that occupies a third of the screen makes the reader pan for the answer; the floor keeps it
   usable on a laptop and the ceiling stops it running off a tall monitor. */
.cm-map { height: clamp(420px, 62vh, 820px); }

/* ⭐ THE RESULTS PANEL FILLS THE SCREEN IT IS GIVEN. Sized to content, a two-site answer left the card
   floating in a page of white with the eye travelling to nothing. A floor - never a fixed height - so
   a long list still grows past it instead of scrolling inside a box.
   ⛔ THE TWO RESULTS PANELS ONLY, NAMED ONE BY ONE. `.net-tabpanel > .card` would have put a 72vh
   floor under every tabbed card on the product; `[id^="market-panel-"]` then caught this page's own
   SCANNER card and stretched a short form to most of the screen (Marcos 2026-09-07: "What to scan
   card doesnt need to be so big"). A floor belongs to a panel that holds an ANSWER, not to one that
   holds a question. */
#market-panel-local > .card,
#market-panel-global > .card { min-height: min(72vh, 900px); }

/* ── Roomier cards on the two SuperAdmin work surfaces (Marcos 2026-09-07) ───────────────────────
   ⛔ SCOPED, not a change to `.card` itself. That class is on every surface of the product - feed,
   profile, billing, clinic setup - and widening it globally would reflow pages nobody asked me to
   touch and nobody would connect back to this request. If the whole app should breathe more, that is
   one deliberate change to the token, made on purpose. */
.cm-page .card,
.pool-page .card { padding: var(--net-space-5); }
.cm-page .card-hd,
.pool-page .card-hd { margin-bottom: var(--net-space-4); }

/* ── Clinical Market: section headings inside a card, and the competitor lists ───────────────────
   ⭐ A heading that follows content needs air ABOVE it, not just below (Marcos 2026-09-07). Without
   it "Eligibility criteria" and "Recommended screening questions" sat flush against the block before
   them and read as part of it. */
.cm-sec { margin-top: var(--net-space-6); }
/* A section's one-line description sits WITH its title and clear of the controls under it (Marcos
   2026-09-11: "the text is too far from the title and too close to the select bar"). */
.cm-split__hd:has(+ .cm-lede) { margin-bottom: var(--net-space-1); }
.cm-lede { margin-bottom: var(--net-space-3); }
/* Both split headings follow the facts grid, so both keep the top margin. */

.rivals { display: grid; gap: var(--net-space-2); margin-top: var(--net-space-2); }
.rival { display: flex; align-items: center; gap: var(--net-space-3); padding: 10px var(--net-space-4);
         border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); background: var(--net-card); }
.rival .grow { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rival .meta { white-space: nowrap; }
.rival b { overflow-wrap: anywhere; }

/* ── Clinical Market: the panel under an in-card tab strip ───────────────────────────────────────
   ⭐ Air between the strip and its content, and around the competitor rows (Marcos 2026-09-07,
   "the margins for the competitors view might need some more padding"). The strip is a lid; content
   welded to it reads as part of the tab rather than as the answer underneath. */
.cm-subpanel { padding-top: var(--net-space-5); }
/* ── Scanner waits and the competitor ↔ study link (Marcos 2026-09-11) ───────────────────────
   .net-spin: the one spinner the network shell has. Opening a site runs a pivot over every rival in
   the scan; reading a record is a registry round-trip; a draft is a ~20 s model pass. Each now shows
   this rather than nothing. */
.net-spin { display: inline-block; width: 14px; height: 14px; flex: none; vertical-align: -2px;
            border: 2px solid var(--net-hairline-strong); border-top-color: var(--net-primary); border-radius: 50%;
            animation: net-spin .7s linear infinite; }
@keyframes net-spin { to { transform: rotate(360deg); } }
/* On a filled button the brand-coloured arc would vanish into the fill: draw it in the button's own ink. */
.btn .net-spin { border-color: color-mix(in srgb, currentColor 30%, transparent); border-top-color: currentColor; margin-right: 6px; }
@media (prefers-reduced-motion: reduce) { .net-spin { animation-duration: 1.8s; } }
.siterow.opening { background: var(--net-sunken); }
.siterow[disabled] { cursor: progress; }
.siterow[disabled]:not(.opening) { opacity: .6; }
/* The competitor row is a button now: press it and the studies it shares light up on the left. */
button.rival { width: 100%; text-align: left; cursor: pointer; font: inherit; color: inherit;
               transition: background-color var(--net-dur) var(--net-ease), border-color var(--net-dur) var(--net-ease); }
button.rival:hover, .rival:has(> .rival-toggle):hover { background: var(--net-sunken); }
button.rival:focus-visible { outline: 2px solid var(--net-primary); outline-offset: -2px; }
.rival.on { border-color: var(--net-danger); background: var(--net-danger-soft); }
.studyitem.contested { background: var(--net-danger-soft); border-color: color-mix(in srgb, var(--net-danger) 45%, var(--net-hairline)); }
.studyitem.contested > .hd:hover { background: color-mix(in srgb, var(--net-danger-soft) 60%, var(--net-sunken)); }
.cm-split--tight { gap: var(--net-space-5); }
/* ⭐ Already assigned to a clinic or the pool (Marcos 2026-09-11): pale yellow, and an ⓘ that says where. */
.studyitem.assigned { background: var(--net-warn-soft); border-color: color-mix(in srgb, var(--net-warn-ink) 22%, var(--net-hairline)); }
.studyitem.assigned > .hd:hover { background: color-mix(in srgb, var(--net-warn-soft) 65%, var(--net-sunken)); }
.studyitem > .cm-asg-info { flex: none; align-self: center; margin-right: 4px; }
tr.cm-assigned > td { background: var(--net-warn-soft); }
td > .cm-asg-info { margin-left: 4px; vertical-align: middle; }
.cm-asg { list-style: none; margin: 0 0 var(--net-space-3); padding: 0; display: grid; gap: 6px; }
.cm-asg li { padding: 8px 10px; border: 1px solid var(--net-hairline); border-radius: var(--net-radius-sm); }
/* ── An opened study: ONE view, three cards (Marcos 2026-09-11) ────────────────────────────────
   Left column: Study details, then the screening questions. Right column: that study's competitors.
   .cm-study is the shell that REPLACES the outer card while a study is open, so the three cards sit
   on the page like every other card row instead of cards inside a card. */
.cm-study { min-width: 0; }
.cm-study__hd { margin-bottom: var(--net-card-gap); }
.cm-study-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: var(--net-card-gap); align-items: start; }
.cm-study-col { display: flex; flex-direction: column; gap: var(--net-card-gap); min-width: 0; }
/* The card is the section now: its first heading sits at the top instead of a section gap above it. */
.cm-study-grid .cm-split .cm-sec { margin-top: 0; }
/* "View N more" centres here too — it used to inherit that from .cm-subpanel. */
.cm-study-grid .fc { display: flex; justify-content: center; margin-top: var(--net-space-4); }
@container work (max-width: 1100px) { .cm-study-grid { grid-template-columns: minmax(0, 1fr); } }
.cm-subpanel > .rivals { margin-top: var(--net-space-3); }
.cm-subpanel > .empty { margin-top: var(--net-space-3); }
.rivals { gap: var(--net-space-3); }
.rival { padding: var(--net-space-4); gap: var(--net-space-4); }
.rival .grow { gap: 4px; }
/* The "view N more" control sits away from the last row rather than against it. */
.cm-subpanel .fc { display: flex; justify-content: center; margin-top: var(--net-space-4); }

/* Scanner footnotes: one idea per line, each with its own air. A paragraph of caveats in a form is a
   paragraph nobody reads (Marcos 2026-09-07). */
.cm-notes { margin: var(--net-space-3) 0 0; padding-left: 18px; display: grid; gap: var(--net-space-2);
            color: var(--net-ink-2); font-size: var(--net-fs-small); line-height: 1.45; }

/* ── An operator's group header in the site list ────────────────────────────────────────────────
   A lid on its branches, not a replacement for them: the sites stay their own rows underneath,
   because distance, the map pin and every pool assignment read a SITE, not a company. */
.opgroup { margin-top: var(--net-space-3); }
.opgroup:first-child { margin-top: 0; }
.ophd { display: flex; align-items: center; gap: var(--net-space-3); padding: 10px var(--net-space-4);
        border: 1px solid var(--net-hairline); border-bottom: 0;
        border-radius: var(--net-radius-md) var(--net-radius-md) 0 0; background: var(--net-sunken); }
.ophd .grow { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ophd .nm { font-weight: var(--net-fw-semibold); }
/* The header row is a toggle (2026-09-11): the whole left side folds/unfolds the operator's branches. */
.ophd-toggle { all: unset; display: flex; align-items: center; gap: var(--net-space-2); flex: 1; min-width: 0; cursor: pointer; color: inherit; }
.ophd-toggle:focus-visible { outline: 2px solid var(--net-focus, currentColor); outline-offset: 2px; border-radius: var(--net-radius-sm); }
.ophd-toggle .grow { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* A competitor row: the toggle highlights its shared studies, the arrow opens it as the subject (2026-09-11). */
.rival-toggle { all: unset; display: flex; align-items: center; gap: var(--net-space-3); flex: 1; min-width: 0; cursor: pointer; color: inherit; }
.rival-toggle:focus-visible { outline: 2px solid var(--net-focus, currentColor); outline-offset: 2px; border-radius: var(--net-radius-sm); }
.rival-toggle .grow { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rival > .btn.icon { flex: none; }

/* ══ Lead Catcher editor — Search listing card (Marcos 2026-09-11 "wow factor" redesign) ══════════
   Fields left, a sticky "how patients find this study" preview right: a Google result, the clinic's
   own website heading, and the map pins that distance is actually measured from. Focusing a field
   highlights the exact part of the preview it controls via :has() — no JS, no extra @code state.
   Every piece here — .card/.chip/.meta/.alert/.tc-skel — is reused from the rest of this sheet on
   purpose; a redesign that forks the design system is not a redesign, it is a second one to maintain. */
.lc-listing { display: grid; grid-template-columns: 1.2fr 1fr; gap: var(--net-space-5); align-items: start; }
.lc-listing-fields { display: flex; flex-direction: column; gap: var(--net-space-5); min-width: 0; }
.lc-listing-group { display: flex; flex-direction: column; gap: var(--net-space-3); }
.lc-listing-group + .lc-listing-group { padding-top: var(--net-space-4); border-top: 1px solid var(--net-hairline); }
.lc-listing-add { display: flex; flex-direction: column; gap: 4px; }

.lc-listing-preview { position: sticky; top: var(--net-space-4); display: flex; flex-direction: column; gap: var(--net-space-3); background: var(--net-sunken); border-radius: var(--net-radius-md); padding: var(--net-space-4); min-width: 0; }
.lc-listing-sheet { background: var(--net-card); border: 1px solid var(--net-hairline); border-radius: var(--net-radius-md); box-shadow: var(--net-elev-1); padding: var(--net-space-3) var(--net-space-4); transition: border-color var(--net-dur) var(--net-ease), box-shadow var(--net-dur) var(--net-ease); }

/* Google result sheet — favicon + site row, url breadcrumb, blue title, snippet: the SERP layout
   every search engine already renders, so nothing here has to be explained to be recognized. */
.lc-listing-crumb-top { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: var(--net-fw-medium); color: var(--net-ink); }
.lc-listing-favicon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: var(--net-primary); color: var(--net-on-primary); font-size: 9px; font-weight: var(--net-fw-bold); flex: none; }
.lc-listing-crumb { font-size: 12px; color: var(--net-ink-3); margin: 2px 0 6px; }
.lc-listing-serp-title { color: var(--net-info-ink); font-size: 15px; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-listing-snippet { margin-top: 4px; font-size: 13px; line-height: 1.45; color: var(--net-ink-2); }

/* The clinic's own website heading — display font, same weight/leading tokens as every h2 on this sheet. */
.lc-listing-heading { font: var(--net-fw-medium) var(--net-fs-h2)/var(--net-lh-h2) var(--net-font-display); letter-spacing: var(--net-ls-h2); color: var(--net-ink); }

/* An empty slot reads as a placeholder, not a blank: italic, muted, dashed-underlined — never the
   live text, so nobody mistakes it for what will actually publish. */
.lc-listing-slot { border-bottom: 1px dashed var(--net-ink-faint); border-radius: 2px; padding: 0 2px; transition: background var(--net-dur) var(--net-ease), color var(--net-dur) var(--net-ease); }
.lc-listing-slot.is-empty { font-style: italic; color: var(--net-ink-3); }

/* Meta-description length meter — a bar under the counter, so "too long" reads at a glance instead
   of requiring the person to do the subtraction themselves. */
.lc-listing-meter { height: 4px; border-radius: var(--net-radius-pill); background: var(--net-hairline); overflow: hidden; }
.lc-listing-meter > span { display: block; height: 100%; width: var(--fill, 0%); background: var(--net-primary); border-radius: inherit; transition: width var(--net-dur) var(--net-ease), background var(--net-dur) var(--net-ease); }
.lc-listing-meter.over > span { background: var(--net-danger-ink); }

/* Chip remove buttons in this card get a bigger invisible tap target (HIG 44px) without growing the
   chip itself — the same trick a native app uses for a small "x" inside a pill. */
.lc-listing-x { position: relative; }
.lc-listing-x::after { content: ""; position: absolute; inset: -7px; }

/* The registry protocol number inside its chip (2026-09-14) — was an <a> to clinicaltrials.gov, now a
   button that opens RegistryStudyDialog in-app instead; reset to plain text so the chip's own look
   carries it, same idea as the naked "x" button beside it. */
.lc-listing-nct-btn { border: 0; background: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; }
.lc-listing-nct-btn:hover, .lc-listing-nct-btn:focus-visible { text-decoration: underline; }

/* Focus-linked preview highlight — the FIELD is the state; no JS, no extra @code, nothing to get out
   of sync. Sheet-level rules draw the ring around the whole result; slot-level rules pick out the
   exact word or line that field controls. */
.lc-listing:has(#lc-topic:focus) .lc-listing-sheet[data-live~="topic"],
.lc-listing:has(#lc-locations:focus) .lc-listing-sheet[data-live~="locations"],
.lc-listing:has(#lc-slug:focus) .lc-listing-sheet[data-live~="slug"],
.lc-listing:has(#lc-meta:focus) .lc-listing-sheet[data-live~="meta"],
.lc-listing:has(#lc-zip:focus) .lc-listing-sheet[data-live~="zip"] {
  border-color: var(--net-focus); box-shadow: var(--net-focus-ring);
}
.lc-listing:has(#lc-topic:focus) .lc-listing-slot[data-live~="topic"],
.lc-listing:has(#lc-locations:focus) .lc-listing-slot[data-live~="locations"],
.lc-listing:has(#lc-slug:focus) .lc-listing-crumb .lc-listing-slot[data-live~="slug"],
.lc-listing:has(#lc-meta:focus) .lc-listing-snippet[data-live~="meta"] {
  background: var(--net-primary-soft); color: var(--net-primary);
}

/* ≤860px: one column, preview drops below the fields and stops being sticky — there is no second
   viewport-width column left to stick alongside. @container first (this sheet's own convention,
   `.net-col.work` on <main>), @media as the fallback for engines without container queries. */
@container work (max-width: 860px) { .lc-listing { grid-template-columns: minmax(0, 1fr); } .lc-listing-preview { position: static; top: auto; } }
@media (max-width: 860px) { .lc-listing { grid-template-columns: minmax(0, 1fr); } .lc-listing-preview { position: static; top: auto; } }

/* ≤400px: tighter rhythm, and the ZIP/NCT "add" row goes full width instead of a cramped inline pair. */
@container work (max-width: 400px) {
  .lc-listing { gap: var(--net-space-3); }
  .lc-listing-fields { gap: var(--net-space-4); }
  .lc-listing-add .row { flex-wrap: wrap; }
  .lc-listing-add .row > .input { flex: 1 1 100% !important; max-width: none !important; }
  .lc-listing-add .row > .btn { flex: 1 1 100%; }
}
@media (max-width: 400px) {
  .lc-listing { gap: var(--net-space-3); }
  .lc-listing-fields { gap: var(--net-space-4); }
  .lc-listing-add .row { flex-wrap: wrap; }
  .lc-listing-add .row > .input { flex: 1 1 100% !important; max-width: none !important; }
  .lc-listing-add .row > .btn { flex: 1 1 100%; }
}
/* Coarse pointers (touch) get the full HIG tap target on the add buttons — a mouse cursor does not need it. */
@media (pointer: coarse) { .lc-listing-add .btn { min-height: var(--net-tap-min); } }

@media (prefers-reduced-motion: reduce) {
  .lc-listing-slot, .lc-listing-sheet, .lc-listing-meter > span { transition: none; }
}
