/* ═══════════════════════════════════════════════════════════════════════════════════════
   MediHarbor Ops — the house style.

   A dense internal tool for a Medicare DMEPOS supplier. Staff read this for hours: claim
   numbers, MH references, phone numbers, ages in hours. Everything below serves legibility of
   a NUMBER IN A COLUMN, not first impressions.

   THE RULES, so nobody has to reverse-engineer them:
     · One ink, one paper. Every other value is DERIVED from those two in OKLCH. No stray greys.
     · ONE accent (harbor teal), reserved for things you can interact with. If it is not
       clickable, focusable, or currently selected, it is not accent-coloured.
     · Semantic states are DESATURATED and read as a family — a tinted ground plus text from
       the same hue. Never a saturated fill. On a page listing held leads, failed faxes and
       pending approvals at once, three fire-engine chips fight each other and none of them wins.
     · Radius is a language: 3px controls, 2px badges, 0 table rows, 6px floating panels. A
       badge and a modal are not the same kind of object and should not share a corner.
     · Shadow means ELEVATION, nothing else. If it does not float above the page, it is
       separated by a 1px border or a background shift.
     · Max three backgrounds on screen at once (paper, panel, row-hover). The sidebar is a
       separate surface and does not count against that.
     · Nothing above weight 600.
   ═══════════════════════════════════════════════════════════════════════════════════════ */

/* ═══ COLOUR ════════════════════════════════════════════════════════════════════════
   EXPLICIT PER-THEME VALUES. An earlier version of this file derived the whole ramp with
   relative colour syntax off a light-dark() base — `oklch(from var(--ink) l c h / .94)` — and
   it broke the entire app.

   THE TRAP, because it is not obvious and I fell in it: that expression PARSES fine, so the
   literal fallback declared above it is discarded as a losing declaration. It then fails at
   COMPUTED-VALUE time, because light-dark() does not resolve until used-value time while
   relative colour syntax needs a resolved colour before that. A custom property invalid at
   computed-value time becomes the guaranteed-invalid value, so every var() referencing it
   resolves to unset and inherits. Symptoms seen live: dark mode unreadable, the sidebar's
   active accent bar gone, and every status badge collapsing to one colour.

   Declaring a fallback FIRST protects against a parse error. It does nothing about this. So the
   values below are literal, written twice, and that duplication is the price of them working.

   The semantic family keeps LIGHTNESS AND CHROMA LOCKED and rotates only hue, so no state
   shouts louder than another — that is what makes it read as one system rather than a traffic
   light. */

:root{
  color-scheme:light;

  --ink:oklch(22% .045 250);
  --paper:oklch(98.4% .004 85);      /* warm. Pure white beside warm greys looks like a bug. */
  --panel:oklch(100% 0 0);
  /* Compatibility alias, the same one ops-universal.css already carries. A page on THIS sheet
     that says background:var(--card) was getting nothing at all — an invalid value, so the
     element rendered transparent. Found 2026-08-16 on the leads board's New lead dialog, which
     was a floating set of form fields over the table with only a 20%-ink scrim behind it: you
     could barely see it was there. Defining the alias can only turn an invalid value into a
     real surface, never change one that already resolved. */
  --card:var(--panel);
  /* The rest of the same alias layer ops-universal.css carries (its lines 239-256). Pages on
     THIS sheet were using the legacy colour names and getting nothing: on the leads board that
     is every red/amber/green chip drawn with an inline style, including the "possible
     duplicate" flag — which is why a duplicate could be on the board and look like any other
     row. Each alias points at a semantic token ops.css already defines, so the palette, the
     dark-mode swap and the contrast floors all come along unchanged. */
  /* Eight BFlow pages set `body{background:var(--bg);color:var(--fg)}` and NOTHING defines
     --fg — not this sheet, not ops-universal. It has survived because an invalid var() makes
     the declaration unset, and `color` then inherits from <html>, which ops.css does style. So
     the page reads correctly by accident, and would go black-on-dark the moment anything gave
     <html> a different colour. Name it. */
  --fg:var(--ink);
  --grey:var(--ink-2);    --light:var(--ink-3);      --greybg:var(--idle-bg);
  --green:var(--ok);      --greenbg:var(--ok-bg);    --success:var(--ok);
  --amber:var(--warn);    --amberbg:var(--warn-bg);
  --red:var(--bad);       --redbg:var(--bad-bg);     --danger:var(--bad);
  --blue:var(--info);     --bluebg:var(--info-bg);

  --ink-1:oklch(22% .045 250 / .94);
  --ink-2:oklch(22% .045 250 / .72);
  --ink-3:oklch(22% .045 250 / .76);
  --ink-4:oklch(22% .045 250 / .38);
  --line:oklch(22% .045 250 / .11);
  --line-strong:oklch(22% .045 250 / .34);
  --tint:oklch(22% .045 250 / .04);
  --tint-2:oklch(22% .045 250 / .07);

  /* Harbor teal: deep, low-chroma, survives warm paper, nowhere near the framework blue. */
  --accent:oklch(45% .072 184);
  --accent-hi:oklch(53% .072 184);
  --accent-08:oklch(45% .072 184 / .08);
  --accent-12:oklch(45% .072 184 / .12);
  --accent-20:oklch(45% .072 184 / .20);
  --accent-40:oklch(45% .072 184 / .40);
  --on-accent:#fff;

  /* One L, one C, six hues: amber / green / blue / clay / red / violet. */
  --warn:oklch(48% .090 85);   --warn-bg:oklch(48% .090 85 / .10);   --warn-line:oklch(48% .090 85 / .24);
  --ok:oklch(48% .090 155);    --ok-bg:oklch(48% .090 155 / .10);    --ok-line:oklch(48% .090 155 / .24);
  --info:oklch(48% .090 240);  --info-bg:oklch(48% .090 240 / .10);  --info-line:oklch(48% .090 240 / .24);
  --hold:oklch(48% .090 45);   --hold-bg:oklch(48% .090 45 / .10);   --hold-line:oklch(48% .090 45 / .24);
  --bad:oklch(48% .090 25);    --bad-bg:oklch(48% .090 25 / .10);    --bad-line:oklch(48% .090 25 / .24);
  /* Physician. Violet because it is the one hue not already carrying a status meaning — a
     doctor field is not good, bad, pending or held, and colouring it with a status colour
     would make every prescriber look like a warning. */
  --md:oklch(48% .090 305);    --md-bg:oklch(48% .090 305 / .10);    --md-line:oklch(48% .090 305 / .24);
  --idle:oklch(22% .045 250 / .54);
  --idle-bg:oklch(22% .045 250 / .06);
  --idle-line:oklch(22% .045 250 / .15);

  /* ── the dark rail, a fixed surface in BOTH themes ─────────────────────────────────
     A ~45-point lightness spread so something in the sidebar actually reads as primary. The
     previous set sat inside about 15 points and the whole rail read as one grey block. */
  --nav-bg:oklch(18% .025 250);
  /* 11px UPPERCASE, so it needs the 4.5 floor, not the 3:1 one — small caps with tracking
     lose word shape and the design notes call this out explicitly. At 52% lightness on the
     fixed dark rail this measured 3.44:1 and had been failing since the rail was built; it
     only got looked at when a tenant opened in light mode and the whole rail came under
     scrutiny. 62% measures ~5.6:1 and stays clearly quieter than --nav-item at 74%, which is
     the hierarchy this token exists to express. */
  --nav-section:oklch(62% .015 250);   /* PIPELINE, INTAKE — dimmest thing in the rail */
  --nav-item:oklch(74% .015 250);      /* resting links */
  --nav-item-hover:oklch(90% .015 250);
  --nav-active:oklch(97% .010 250);    /* brightest thing in the rail */
  --nav-active-bg:oklch(28% .050 184); /* accent, darkened for a dark ground */

  /* ── SPACING SCALE — see the page-header block near the end of this file ──────────
     The third token set. Without it every gap was eyeballed, which is how a form ends up
     with negative visual space while a table looks fine. */
  --space-1:4px;
  --space-2:8px;
  --space-3:12px;
  --space-4:16px;
  --space-5:24px;
  --space-6:32px;
  --space-7:48px;


  /* ── LEAD SOURCE: a categorical scale, validated not eyeballed ────────────────────
     Fixed order, never cycled. These are DATA colours, not the accent — the accent stays
     reserved for interactive affordances.

     Validated with the dataviz validator rather than picked by eye, because the first attempt
     looked fine and was not: phone and web were literally the same token, and teal-vs-green sat
     29 degrees apart. That set scored ΔE 9.7 normal-vision (below the 15 floor) and 1.2 under
     deuteranopia — invisible to a colourblind reader.

     LIGHT passes all six checks. DARK passes chroma, CVD (18.4), normal-vision (28.2) and
     contrast, and misses only the lightness band: separating six hues for CVD REQUIRES varying
     lightness, and a narrow band re-collapses the CVD score to 6.4. Rather than pretend that
     away, colour is not carrying identity alone — every row also shows a three-letter source
     code, which is the secondary encoding the method requires and which also survives
     forced-colors mode. */
  --src-meta:#4869b8;
  --src-google:#bd9121;
  --src-phone:#006630;
  --src-web:#009faf;
  --src-partner:#7f448d;
  --src-yelp:#bc5853;
  --src-other:var(--ink-3);

  --r-control:3px;
  --r-badge:2px;
  --r-float:6px;
  --lift:0 1px 2px oklch(22% .045 250 / .06), 0 8px 24px -6px oklch(22% .045 250 / .18);

  --sidebar-w:240px;
  --gutter:20px;

  --sans:"Instrument Sans","Public Sans","Helvetica Neue",Arial,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  --exit:120ms;
  --enter:0ms;
  --panel-in:180ms;
  --panel-out:120ms;
  --ease-out:cubic-bezier(.2,.7,.3,1);
  --ease-in:cubic-bezier(.5,0,.8,.3);
}

/* Dark: lightness inverted, hue and chroma held so the family stays siblings. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    color-scheme:dark;
    --ink:oklch(93% .015 250);
    --paper:oklch(17% .022 250);
    --panel:oklch(21% .025 250);
    --ink-1:oklch(93% .015 250 / .94);
    --ink-2:oklch(93% .015 250 / .70);
    --ink-3:oklch(93% .015 250 / .76);
    --ink-4:oklch(93% .015 250 / .34);
    --line:oklch(93% .015 250 / .13);
    --line-strong:oklch(93% .015 250 / .38);
    --tint:oklch(93% .015 250 / .05);
    --tint-2:oklch(93% .015 250 / .08);
    --accent:oklch(72% .085 184);
    --accent-hi:oklch(80% .085 184);
    --accent-08:oklch(72% .085 184 / .10);
    --accent-12:oklch(72% .085 184 / .16);
    --accent-20:oklch(72% .085 184 / .26);
    --accent-40:oklch(72% .085 184 / .44);
    --on-accent:oklch(17% .022 250);
    --warn:oklch(78% .095 85);   --warn-bg:oklch(78% .095 85 / .13);   --warn-line:oklch(78% .095 85 / .28);
    --ok:oklch(78% .095 155);    --ok-bg:oklch(78% .095 155 / .13);    --ok-line:oklch(78% .095 155 / .28);
    --info:oklch(78% .095 240);  --info-bg:oklch(78% .095 240 / .13);  --info-line:oklch(78% .095 240 / .28);
    --hold:oklch(78% .095 45);   --hold-bg:oklch(78% .095 45 / .13);   --hold-line:oklch(78% .095 45 / .28);
    --bad:oklch(78% .095 25);    --bad-bg:oklch(78% .095 25 / .13);    --bad-line:oklch(78% .095 25 / .28);
    --md:oklch(78% .095 305);    --md-bg:oklch(78% .095 305 / .13);    --md-line:oklch(78% .095 305 / .28);
    --idle:oklch(93% .015 250 / .52);
    --idle-bg:oklch(93% .015 250 / .07);
    --idle-line:oklch(93% .015 250 / .17);
    --src-meta:#8bbaff;
    --src-google:#b88100;
    --src-phone:#73f6a8;
    --src-web:#008ca1;
    --src-partner:#f8a6ff;
    --src-yelp:#ce514d;
    --lift:0 1px 2px oklch(0% 0 0 / .5), 0 8px 24px -6px oklch(0% 0 0 / .6);
  }
}

/* The explicit toggle wins over the OS in both directions. Restated rather than aliased: a
   custom property cannot be pointed at another theme's block without the indirection that
   caused the original bug. */
:root[data-theme="dark"]{
  color-scheme:dark;
  --ink:oklch(93% .015 250);
  --paper:oklch(17% .022 250);
  --panel:oklch(21% .025 250);
  --ink-1:oklch(93% .015 250 / .94);
  --ink-2:oklch(93% .015 250 / .70);
  --ink-3:oklch(93% .015 250 / .76);
  --ink-4:oklch(93% .015 250 / .34);
  --line:oklch(93% .015 250 / .13);
  --line-strong:oklch(93% .015 250 / .38);
  --tint:oklch(93% .015 250 / .05);
  --tint-2:oklch(93% .015 250 / .08);
  --accent:oklch(72% .085 184);
  --accent-hi:oklch(80% .085 184);
  --accent-08:oklch(72% .085 184 / .10);
  --accent-12:oklch(72% .085 184 / .16);
  --accent-20:oklch(72% .085 184 / .26);
  --accent-40:oklch(72% .085 184 / .44);
  --on-accent:oklch(17% .022 250);
  --warn:oklch(78% .095 85);   --warn-bg:oklch(78% .095 85 / .13);   --warn-line:oklch(78% .095 85 / .28);
  --ok:oklch(78% .095 155);    --ok-bg:oklch(78% .095 155 / .13);    --ok-line:oklch(78% .095 155 / .28);
  --info:oklch(78% .095 240);  --info-bg:oklch(78% .095 240 / .13);  --info-line:oklch(78% .095 240 / .28);
  --hold:oklch(78% .095 45);   --hold-bg:oklch(78% .095 45 / .13);   --hold-line:oklch(78% .095 45 / .28);
  --bad:oklch(78% .095 25);    --bad-bg:oklch(78% .095 25 / .13);    --bad-line:oklch(78% .095 25 / .28);
  --md:oklch(78% .095 305);    --md-bg:oklch(78% .095 305 / .13);    --md-line:oklch(78% .095 305 / .28);
  --idle:oklch(93% .015 250 / .52);
  --idle-bg:oklch(93% .015 250 / .07);
  --idle-line:oklch(93% .015 250 / .17);
  --src-meta:#8bbaff;
  --src-google:#b88100;
  --src-phone:#73f6a8;
  --src-web:#008ca1;
  --src-partner:#f8a6ff;
  --src-yelp:#ce514d;
  --lift:0 1px 2px oklch(0% 0 0 / .5), 0 8px 24px -6px oklch(0% 0 0 / .6);
}

:root{accent-color:var(--accent)}

/* ═══ base ═══════════════════════════════════════════════════════════════════════════ */

*{box-sizing:border-box}

body{
  margin:0;
  background:var(--paper);
  color:var(--ink-1);
  font-family:var(--sans);
  font-size:13px;            /* a dense tool, not a marketing page */
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  font-feature-settings:"cv05" 1;
}

/* Paper grain. 3%, tiled from a turbulence filter — it stops a large flat field from looking
   like an unpainted div, and at this opacity it is felt rather than seen. Fixed so it does not
   swim while a long table scrolls. */
body::before{
  content:"";
  position:fixed;
  inset:0;
  pointer-events:none;
  z-index:0;
  opacity:.03;
  background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNjAiIGhlaWdodD0iMTYwIj48ZmlsdGVyIGlkPSJuIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjgiIG51bU9jdGF2ZXM9IjMiLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0iMTYwIiBoZWlnaHQ9IjE2MCIgZmlsdGVyPSJ1cmwoI24pIi8+PC9zdmc+");
}
@media(prefers-reduced-motion:no-preference){}

h1,h2,h3,h4{font-weight:600;margin:0;letter-spacing:-.011em}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-hi);text-decoration:underline;text-underline-offset:2px}

/* Keyboard only. A mouse user should never see a ring; a keyboard user must never lose it. */
:focus-visible{
  outline:2px solid var(--accent-40);
  outline-offset:1px;
  border-radius:1px;
}

/* ═══ type scale — 11 / 12 / 13 / 15 / 20 / 28, and nothing between ══════════════════ */

.t-28{font-size:28px;line-height:1.14;font-weight:600;letter-spacing:-.02em}
.t-20{font-size:20px;line-height:1.25;font-weight:600;letter-spacing:-.014em}
.t-15{font-size:15px;line-height:1.4;font-weight:600;letter-spacing:-.008em}
.t-13{font-size:13px;line-height:1.5}
.t-12{font-size:12px;line-height:1.45;color:var(--ink-2)}
.t-11{font-size:11px;line-height:1.4;color:var(--ink-3)}

/* The one label treatment: column headers, field labels, section eyebrows. */
.label{
  font-size:11px;
  font-weight:600;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--ink-3);
}

/* Anything a person reads as a value rather than prose: MH refs, phone numbers, timestamps,
   IDs, and every numeral that sits in a column. Tabular so the column actually lines up. */
.mono,.num{
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1,"zero" 1;
  letter-spacing:-.01em;
}
.num{text-align:right}

/* ═══ layout ════════════════════════════════════════════════════════════════════════ */

.ops-shell{display:flex;min-height:100vh;position:relative;z-index:1}

.ops-side{
  width:var(--sidebar-w);
  flex:0 0 var(--sidebar-w);
  background:#0F1B2D;                    /* the ink, literally — a fixed surface, not tokenised */
  color:rgba(255,255,255,.82);
  display:flex;
  flex-direction:column;
  position:relative;
  overflow:hidden;
}
/* Camo rather than a grid — see app.css. Material, not pattern. */
.ops-side::before{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:0;
  background:url(camo-blue.svg);background-size:300px 300px;opacity:.30;mix-blend-mode:soft-light;
}
.ops-side > *{position:relative}

.ops-main{flex:1;min-width:0;display:flex;flex-direction:column}

.ops-bar{
  height:52px;
  flex:0 0 52px;
  border-bottom:1px solid var(--line);
  display:flex;
  align-items:center;
  gap:12px;
  padding:0 var(--gutter);
  background:var(--paper);
}
.ops-body{padding:20px var(--gutter) 64px;flex:1;min-width:0}

/* ═══ sidebar ═══════════════════════════════════════════════════════════════════════ */

.ops-brand{
  display:flex;
  align-items:center;
  gap:8px;
  padding:16px var(--gutter) 14px;
  border-bottom:1px solid rgba(255,255,255,.09);
  color:#fff;
}
.ops-brand svg{flex:0 0 24px}
.ops-brand .n{font-size:13px;font-weight:600;letter-spacing:-.01em;line-height:1.2}
.ops-brand .s{font-size:11px;color:rgba(255,255,255,.44);letter-spacing:.04em;text-transform:uppercase}

.ops-nav{padding:12px 0 24px;overflow-y:auto;flex:1}
.ops-nav .grp{
  padding:16px var(--gutter) 6px;
  font-size:11px;
  font-weight:600;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:rgba(255,255,255,.36);
}
.ops-nav a{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:8px;
  padding:6px var(--gutter);
  font-size:13px;
  color:rgba(255,255,255,.74);
  border-left:2px solid transparent;
  line-height:20px;
}
.ops-nav a:hover{background:rgba(255,255,255,.05);color:#fff;text-decoration:none}
.ops-nav a.on{
  background:rgba(255,255,255,.07);
  color:#fff;
  border-left-color:var(--accent-hi);   /* the accent's one job: say where you are */
}
.ops-nav a .ct{
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:11px;
  color:rgba(255,255,255,.40);
}
.ops-side .foot{
  padding:12px var(--gutter);
  border-top:1px solid rgba(255,255,255,.09);
  font-size:11px;
  color:rgba(255,255,255,.38);
  line-height:1.5;
}

/* ═══ stats — hierarchy, not a row of equal boxes ═══════════════════════════════════
   One figure earns the 28px treatment because it is the one somebody is actually here to
   check. The rest are a compact inline strip: still readable, visibly subordinate. Seven
   identical cards is the shape that says nobody decided what mattered. */

.stat-lead{
  display:flex;
  align-items:flex-start;
  gap:28px;
  padding:16px 0 20px;
  border-bottom:1px solid var(--line);
  margin-bottom:20px;
  flex-wrap:wrap;
}
.stat-hero .v{font-size:28px;line-height:1.1;font-weight:600;letter-spacing:-.02em;font-family:var(--mono);font-variant-numeric:tabular-nums}
.stat-hero .l{margin-top:2px}
.stat-hero .s{font-size:12px;color:var(--ink-3);margin-top:2px}
.stat-hero.alert .v{color:var(--bad)}

.stat-strip{display:flex;gap:24px;flex-wrap:wrap;padding-top:2px}
.stat-strip .it{min-width:64px}
.stat-strip .v{font-size:15px;font-weight:600;font-family:var(--mono);font-variant-numeric:tabular-nums;line-height:1.3}
.stat-strip .l{margin-top:1px}
.stat-strip .it.alert .v{color:var(--bad)}

/* ═══ table — the centre of gravity ════════════════════════════════════════════════
   No card wrapper, no gaps, no rounded rows. Hairline dividers and a tint on hover. A table
   is a grid of values; wrapping every row in its own bordered box is a list pretending to be
   a table and it costs vertical rhythm. */

.tbl{width:100%;border-collapse:collapse;font-size:13px}
.tbl thead th{
  font-size:11px;
  font-weight:600;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--ink-2);
  text-align:left;
  padding:8px 12px;
  border-bottom:1px solid var(--line-strong);
  white-space:nowrap;
  background:var(--paper);
}
.tbl tbody td{
  padding:0 12px;
  height:44px;
  border-bottom:1px solid var(--line);
  vertical-align:middle;
}
.tbl tbody tr:hover td{background:var(--tint);color:var(--ink-1,var(--ink))}
.tbl th.num,.tbl td.num{
  text-align:right;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
}
.tbl td.ref{font-family:var(--mono);font-size:12px;color:var(--ink-2);white-space:nowrap}
.tbl td.dim{color:var(--ink-3)}
.tbl .lead-cell{font-weight:500}
.tbl tbody tr:last-child td{border-bottom:1px solid var(--line)}

/* See the note in leads.html: overflow-x:auto also makes this a vertical scroll
   container and silently kills the sticky thead. Narrow screens only. */
.tbl-wrap{border-top:1px solid var(--line)}
@media(max-width:900px){.tbl-wrap{overflow-x:auto}}
.tbl-empty{padding:0 12px;height:auto}

/* ═══ badges — the state family ═════════════════════════════════════════════════════ */

.badge{
  display:inline-flex;
  align-items:center;
  gap:5px;
  height:19px;
  padding:0 6px;
  border-radius:var(--r-badge);
  font-size:11px;
  font-weight:600;
  letter-spacing:.01em;
  white-space:nowrap;
  border:1px solid transparent;
}
.badge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;flex:0 0 5px}
.badge.ok{color:var(--ok);background:var(--ok-bg);border-color:var(--ok-line)}
.badge.warn{color:var(--warn);background:var(--warn-bg);border-color:var(--warn-line)}
.badge.bad{color:var(--bad);background:var(--bad-bg);border-color:var(--bad-line)}
.badge.idle{color:var(--idle);background:var(--idle-bg);border-color:var(--idle-line)}
.badge.plain::before{display:none}

/* ═══ controls ═════════════════════════════════════════════════════════════════════ */

.btn{
  display:inline-flex;
  align-items:center;
  gap:6px;
  height:28px;
  padding:0 10px;
  border-radius:var(--r-control);
  border:1px solid var(--line-strong);
  background:var(--panel);
  color:var(--ink-1);
  font:inherit;
  font-size:12px;
  font-weight:500;
  cursor:pointer;
  white-space:nowrap;
}
.btn:hover{background:var(--tint);color:var(--ink-1,var(--ink));border-color:rgba(15,27,45,.24)}
.btn.pri{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.btn.pri:hover{background:var(--accent-hi);color:var(--on-accent,#fff);border-color:var(--accent-hi)}
.btn.danger{background:var(--bad);border-color:var(--bad);color:var(--on-accent)}
.btn.danger:hover{filter:brightness(1.06)}
.btn[disabled]{opacity:.42;cursor:not-allowed}
.btn.sm{height:24px;font-size:11px;padding:0 8px}

/* NAME COLLISION, fixed. This was `.field` — my own control primitive — while every legacy
   page uses `class="field"` for the WRAPPER around a label and its input. Both definitions
   applied: the wrapper won display and gap, and this one's border and background survived,
   so every form field rendered as a box inside a box.

   One class, one meaning. The control primitive is `.control`; `.field` is the wrapper and is
   defined once, in ops-universal.css. */
.control{
  height:36px;
  padding:0 10px;
  border-radius:var(--r-control);
  border:1px solid var(--line-strong);
  background:var(--panel);
  color:var(--ink-1);
  font:inherit;
  font-size:14px;
}
.control:focus{border-color:var(--accent);outline:none;box-shadow:0 0 0 3px var(--accent-08)}


/* ═══ panels and notices ═══════════════════════════════════════════════════════════
   A panel is flat: a border and a background shift. It is not floating, so it has no shadow.
   Only .float gets one. */

.panel{
  background:var(--panel);color:var(--ink-1,var(--ink));
  border:1px solid var(--line);
  border-radius:var(--r-control);
  padding:16px;
}
.panel > h2{font-size:15px;margin-bottom:4px}
.panel .lede{font-size:12px;color:var(--ink-2);margin:0 0 12px;max-width:74ch;line-height:1.6}
.float{border-radius:var(--r-float);box-shadow:var(--lift);background:var(--panel);color:var(--ink-1,var(--ink));border:1px solid var(--line)}

.notice{
  border:1px solid var(--line);
  border-left:2px solid var(--ink-3);
  border-radius:var(--r-control);
  padding:10px 12px;
  font-size:12.5px;
  line-height:1.55;
  background:var(--tint);color:var(--ink-1,var(--ink));
  margin-bottom:12px;
}
.notice b{display:block;font-weight:600;margin-bottom:2px}
.notice.ok{border-left-color:var(--ok);background:var(--ok-bg);color:var(--ok)}
.notice.warn{border-left-color:var(--warn);background:var(--warn-bg);color:var(--warn)}
.notice.bad{border-left-color:var(--bad);background:var(--bad-bg);color:var(--bad)}
.notice.ok b,.notice.warn b,.notice.bad b{color:inherit}

.sechead{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:12px;
  padding-bottom:8px;
  margin-bottom:0;
  border-bottom:1px solid var(--line-strong);
}

/* ═══ empty states — the only place illustration is allowed near data ══════════════ */

.empty{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  padding:40px 20px;
  text-align:center;
  color:var(--ink-3);
}
.empty svg{color:var(--ink-4)}
.empty .h{font-size:13px;font-weight:600;color:var(--ink-2)}
.empty .p{font-size:12px;max-width:44ch;line-height:1.55}

/* ═══════════════════════════════════════════════════════════════════════════════════
   MOTION

   Four properties are animatable here and no others: opacity, transform, background-color,
   border-color. Everything below obeys that.
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* ── feedback: you did something ─────────────────────────────────────────────────── */

.tbl tbody tr{transition:background-color var(--exit) linear}
.tbl tbody tr:hover{transition-duration:var(--enter)}
/* The hovered row should read as one continuous band, not a stripe with a lid: its own
   divider lightens so the eye joins the row to itself rather than to the rule below it. */
.tbl tbody tr:hover td{border-bottom-color:var(--tint-2)}

.btn{transition:background-color var(--exit),border-color var(--exit),transform 60ms}
.btn:hover{transition-duration:var(--enter)}
/* Half a pixel. You feel it more than you see it. */
.btn:active{transform:translateY(.5px);filter:brightness(.96);transition-duration:60ms}

/* Row actions rest at 65% so a long table reads calm, and arrive the instant the row is
   under the cursor or the keyboard. */
.row-actions{opacity:.65;transition:opacity var(--exit)}
tr:hover .row-actions,
tr:focus-within .row-actions,
.row-actions:focus-within{opacity:1;transition-duration:var(--enter)}

/* Checkboxes stay out of the way until wanted — then, once ANY row is selected, they pin
   visible for every row so the selection is legible without hunting. */
.sel-cell{opacity:0;transition:opacity var(--exit)}
tr:hover .sel-cell,
tr:focus-within .sel-cell,
.sel-cell:has(:checked),
.selecting .sel-cell{opacity:1;transition-duration:var(--enter)}

/* Click-to-copy: the value becomes a tick for 900ms and returns. Too small for a toast. */
.copyable{cursor:copy;border-bottom:1px dashed transparent;transition:border-color var(--exit)}
.copyable:hover{border-bottom-color:var(--line-strong)}
.copyable.copied{color:var(--ok)}

/* ── state change: something happened ────────────────────────────────────────────── */

/* Optimistic confirmation. One flash of accent at 8%, decaying over 400ms. Never repeats. */
@keyframes ops-confirm{from{background-color:var(--accent-08)}to{background-color:transparent}}
.tbl tbody tr.confirmed td{animation:ops-confirm 400ms var(--ease-out) 1}

/* A write that failed: the value is already reverted, so the row holds a red edge long enough
   to be noticed and no longer. */
@keyframes ops-reject{0%,88%{border-left-color:var(--bad)}100%{border-left-color:transparent}}
.tbl tbody tr.rejected td:first-child{border-left:2px solid var(--bad);animation:ops-reject 2s linear 1}

/* Arrived while you were reading. A 2px edge that fades over 2s — the row is simply THERE.
   Sliding it in moves the text somebody is mid-sentence on. */
@keyframes ops-arrive{from{border-left-color:var(--accent)}to{border-left-color:transparent}}
.tbl tbody tr.arrived td:first-child{border-left:2px solid transparent;animation:ops-arrive 2s var(--ease-out) 1}

/* Leaving a filtered view: fade and collapse over 180ms so you SEE which row left. Instant
   removal reads as a misclick. (Height is animated here on purpose and only here — a single
   departing row, never during a scroll.) */
@keyframes ops-exit{to{opacity:0;transform:translateX(-4px)}}
.tbl tbody tr.leaving td{animation:ops-exit 180ms var(--ease-in) forwards}

/* Refetching: keep the old data on screen at 60% and inert. Replacing a populated table with a
   spinner throws away the context the reader already has, and it is the single biggest
   perceived-speed loss available. */
.stale{opacity:.6;pointer-events:none;transition:opacity var(--exit)}

/* Spinners only after 400ms — anything quicker resolves before it would have helped, and a
   flashed spinner makes a fast action feel slow. */
.spin{opacity:0;animation:ops-spin-in 1ms linear 400ms forwards}
@keyframes ops-spin-in{to{opacity:1}}

/* Skeletons match the real row exactly: same height, same columns. Flat tinted blocks — a
   shimmer sweep is decoration, and a mismatched skeleton reflows into the real thing and reads
   as jank. */
.skel{background:var(--tint-2);border-radius:2px;height:11px}
.tbl tbody tr.skel-row td{height:44px}

/* SLA breach: ONE 600ms pulse at the moment it crosses. A looping pulse is invisible within a
   minute and irritating for the rest of the shift. */
@keyframes ops-breach{0%{background-color:var(--bad-bg)}50%{background-color:var(--bad-line)}100%{background-color:var(--bad-bg)}}
.badge.breached{animation:ops-breach 600ms var(--ease-out) 1}

/* Undo instead of "are you sure". The bar drains so the remaining time is visible rather than
   guessed at. */
.undo{
  position:fixed;left:50%;bottom:24px;z-index:80;
  transform:translateX(-50%) translateY(8px);
  opacity:0;
  display:flex;align-items:center;gap:12px;
  padding:10px 12px;
  border-radius:var(--r-float);
  background:#0F1B2D;color:rgba(255,255,255,.92);
  border:1px solid rgba(255,255,255,.12);
  box-shadow:var(--lift);
  font-size:12.5px;
  transition:opacity var(--panel-in) var(--ease-out),transform var(--panel-in) var(--ease-out);
}
.undo.show{opacity:1;transform:translateX(-50%) translateY(0)}
.undo .act{
  background:none;border:0;color:var(--accent-hi);
  font:inherit;font-weight:600;cursor:pointer;padding:2px 4px;border-radius:var(--r-control);
}
.undo .act:hover{background:rgba(255,255,255,.08)}
.undo .drain{position:absolute;left:0;bottom:0;height:2px;background:var(--accent-hi);width:100%;transform-origin:left;border-radius:0 0 var(--r-float) var(--r-float)}
.undo.show .drain{animation:ops-drain 8s linear forwards}
@keyframes ops-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}

/* ── surface and depth ──────────────────────────────────────────────────────────── */

/* The pinned header earns its separation only once there is something above it. Flush at rest
   is what makes the border MEAN "this is stuck". */
.tbl thead th{position:sticky;top:0;z-index:2}
.scrolled .tbl thead th{border-bottom-color:var(--line-strong);box-shadow:0 1px 0 rgba(15,27,45,.06)}

/* Only meaningful once the wrapper actually scrolls, i.e. under 900px. */
@media(max-width:900px){.tbl-wrap{overscroll-behavior:contain}}

/* Side-peek. Enters over 180ms, leaves over 120ms — exits slightly faster than entrances. */
.peek{
  position:fixed;top:0;right:0;bottom:0;width:min(520px,92vw);z-index:70;
  background:var(--panel);color:var(--ink-1,var(--ink));border-left:1px solid var(--line);
  box-shadow:var(--lift);
  transform:translateX(12px);opacity:0;pointer-events:none;
  transition:opacity var(--panel-out) var(--ease-in),transform var(--panel-out) var(--ease-in);
  overflow-y:auto;overscroll-behavior:contain;
}
.peek.show{transform:translateX(0);opacity:1;pointer-events:auto;transition-duration:var(--panel-in);transition-timing-function:var(--ease-out)}
.peek-back{
  position:fixed;inset:0;z-index:69;background:rgba(15,27,45,.20);
  opacity:0;pointer-events:none;transition:opacity var(--panel-out) var(--ease-in);
}
.peek-back.show{opacity:1;pointer-events:auto;transition-duration:var(--panel-in)}

/* ── tooltips: 500ms for the first, 0ms for the next one within 300ms ────────────── */
[data-tip]{position:relative}
[data-tip]:hover::after,[data-tip]:focus-visible::after{
  content:attr(data-tip);
  position:absolute;left:0;top:calc(100% + 4px);z-index:75;
  background:#0F1B2D;color:#fff;
  font-size:11px;line-height:1.4;font-weight:400;letter-spacing:0;text-transform:none;
  padding:4px 6px;border-radius:var(--r-control);
  white-space:nowrap;pointer-events:none;
  opacity:0;animation:ops-tip 1ms linear 500ms forwards;
}
/* Once the group is "warm", the next tip is immediate — scanning a row of them should not feel
   like waiting five times. */
.tips-warm [data-tip]:hover::after{animation-delay:0ms}
@keyframes ops-tip{to{opacity:1}}

/* ── typography that nobody notices and everybody feels ─────────────────────────── */
.empty .h{text-wrap:balance}
.empty .p,.panel .lede,.notice{text-wrap:pretty}
.trunc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block}

/* ═══ THE NON-NEGOTIABLE ════════════════════════════════════════════════════════════
   Reduced motion keeps opacity and kills every transform. Panel slides genuinely make some
   people ill, and in a queue tool a rep may well have this switched on all day. Nothing here
   becomes unusable without movement — the panel still appears, the row still confirms, the
   toast still shows. They simply stop moving. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:1ms !important;
    animation-iteration-count:1 !important;
    transition-duration:1ms !important;
  }
  .peek,.peek.show,.undo,.undo.show{transform:none !important}
  .btn:active{transform:none !important}
  .tbl tbody tr.leaving td{animation:none;opacity:0}
  .undo.show .drain{animation:none;transform:scaleX(0)}
  /* Opacity survives: these still fade, they just do not travel. */
  .undo{transition:opacity var(--panel-in) linear !important}
}

/* ═══════════════════════════════════════════════════════════════════════════════════
   ACCESSIBILITY

   This is a healthcare-adjacent tool used all day on Windows. The items here are the ones
   with actual exposure, not the ones that are easy.
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* WCAG 2.2 target size: 24×24 minimum. Small icon buttons in dense tables are exactly where
   this gets failed, so the floor is enforced on the primitive rather than remembered per use.
   The visual box can stay small; the HIT box cannot. */
.btn.sm,.icon-btn{position:relative;min-height:24px;min-width:24px}
.icon-btn::after{content:"";position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:max(100%,24px);height:max(100%,24px)}

/* FORCED COLORS (Windows High Contrast). Backgrounds are overridden by the OS, so anything
   communicated ONLY by a background tint disappears — which would silently erase every status
   on this page. Two defences: each badge already carries a text label, and the dot becomes a
   bordered shape so state survives as form, not just colour. */
@media (forced-colors:active){
  .badge{border:1px solid CanvasText;background:Canvas;color:CanvasText;forced-color-adjust:none}
  .badge::before{background:CanvasText;border:1px solid CanvasText}
  .badge.bad::before{border-radius:0}                 /* square  = failing  */
  .badge.warn::before{border-radius:0;rotate:45deg}   /* diamond = degraded */
  .badge.ok::before{border-radius:50%}                /* circle  = passing  */
  .tbl tbody tr:hover td{background:Highlight;color:HighlightText}
  .btn{border:1px solid ButtonText}
  .notice{border:1px solid CanvasText}
  /* A left border carrying meaning must not vanish into the OS palette. */
  .tbl tbody tr.arrived td:first-child,.tbl tbody tr.rejected td:first-child{border-left-color:CanvasText}
}

/* Validate only once somebody has actually interacted. :invalid alone turns a new-lead form
   red while the rep is still typing the first field, which trains people to ignore it. */
.field:user-invalid{border-color:var(--bad);box-shadow:0 0 0 3px var(--bad-bg)}
.field:user-valid{border-color:var(--ok-line)}

/* ── PHYSICIAN FIELDS ───────────────────────────────────────────────────────────────
   One marker for every prescriber field on the site — referring doctor, ordering doctor,
   NPI, the picker that writes them. A rep filling an intake form crosses several kinds of
   field that look identical, and the physician is the one that decides whether the claim
   pays: DOS, NPI and prescriber are what denials are made of.

   It marks the WRAPPER, not the control, so the label, the input and any confirmation text
   under it all read as one region. Density and type stay wherever they were — this rule sets
   colour and a left rule only, per the per-component scoping rule in CLAUDE.md.

   Violet (--md) is deliberately outside the status family so it never reads as a warning. */
.physician-field{
  border-left:3px solid var(--md-line);
  padding-left:var(--space-3);
}
.physician-field > label,
.physician-field .flabel{color:var(--md)}
/* Background and colour together, always — a tinted ground that inherited its text is the
   exact shape of the 2026-08-13 half-applied-theme bug. */
.physician-field input:focus,
.physician-field select:focus{
  border-color:var(--md);
  box-shadow:0 0 0 3px var(--md-bg);
  background:var(--panel);
  color:var(--ink-1);
}
/* A selected prescriber is a fact worth seeing at a glance, not a success state. */
.physician-chip{
  background:var(--md-bg);
  color:var(--md);
  border:1px solid var(--md-line);
  border-radius:var(--r-badge);
  padding:2px 6px;
  font-size:12px;
}
@media (forced-colors:active){
  .physician-field{border-left-color:CanvasText}
}

/* Sit alongside prefers-reduced-motion rather than assuming motion is the only sensitivity. */
@media (prefers-reduced-transparency:reduce){
  .peek-back{background:var(--paper);color:var(--ink-1,var(--ink))}
  .undo{background:#0F1B2D;color:#e2e8f0}
}
@media (prefers-contrast:more){
  :root{
    --line:oklch(22% .045 250 / .28);
    --line-strong:oklch(22% .045 250 / .42);
    --ink-3:oklch(22% .045 250 / .70);
    --ink-4:oklch(22% .045 250 / .55);
  }
  .badge{border-width:1px}
}

/* ═══════════════════════════════════════════════════════════════════════════════════
   PLATFORM — progressive enhancement only. Every rule here improves a page that already
   works without it; none of it is load-bearing.
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* A filter that changes the row count should not shift the whole layout sideways by the width
   of a scrollbar. */
html{scrollbar-gutter:stable}

/* Off-screen rows are skipped entirely. contain-intrinsic-size supplies the height they WOULD
   have taken so the scrollbar stays honest and the page does not jump as they render. One
   declaration instead of a virtualisation dependency — worth trying before adding one. */
.tbl tbody tr{content-visibility:auto;contain-intrinsic-size:auto 44px}

/* Optical centring. Fonts carry invisible ascent/descent, so text in a button is mathematically
   centred and visually low. Trimming the box fixes it. Firefox support is not there yet, hence
   the @supports guard — without it the existing padding is already correct. */
@supports (text-box:trim-both cap alphabetic){
  .btn,.badge{text-box:trim-both cap alphabetic}
  .btn{padding-block:6px}
  .badge{padding-block:4px;height:auto}
}

/* Selection state with no JS bookkeeping at all: the row knows it is selected, and the table
   knows something is selected. Both used to require a class toggle per row plus a counter. */
.tbl tbody tr:has(input[type=checkbox]:checked) td{background:var(--accent-08);color:var(--ink-1,var(--ink))}
.tbl:has(input[type=checkbox]:checked) .sel-cell{opacity:1}
.bulkbar{display:none}
.tbl-wrap:has(input[type=checkbox]:checked) + .bulkbar{display:flex}

/* The sticky header earns its separation from scroll position rather than from a scroll
   listener. Chrome-only today, so the .scrolled class above remains the fallback and this
   simply takes over where it is supported. */
@supports (container-type:scroll-state){
  .tbl-wrap{container-type:scroll-state}
  @container scroll-state(stuck: top){
    .tbl thead th{box-shadow:inset 0 -1px 0 var(--line-strong),0 4px 12px oklch(22% .045 250 / .10)}
  }
}

/* Popovers and dialogs animate in from display:none, which needed JS until @starting-style.
   Applies to native [popover] and <dialog>, both of which come with top-layer placement, focus
   management, focus restore and Escape handling already built — so an invoker button
   (command/commandfor) replaces a hand-rolled focus trap entirely, and hand-rolled focus traps
   are reliably buggy. */
[popover],dialog{
  border:1px solid var(--line);
  border-radius:var(--r-float);
  background:var(--panel);
  color:var(--ink-1);
  box-shadow:var(--lift);
  padding:12px;
  opacity:0;
  transition:opacity var(--panel-out) var(--ease-in),overlay var(--panel-out) allow-discrete,display var(--panel-out) allow-discrete;
}
[popover]:popover-open,dialog:open{opacity:1;transition-duration:var(--panel-in);transition-timing-function:var(--ease-out)}
@starting-style{
  [popover]:popover-open,dialog:open{opacity:0}
}
dialog::backdrop{background:oklch(22% .045 250 / .28)}
/* :open styles a <details> by state with no class plumbing. */
details:open > summary{color:var(--ink-1)}

/* Anchor positioning tethers a menu to its trigger and flips it when it would overflow, with no
   positioning library. Support is uneven, so this layers over a normally-positioned fallback —
   the menu is usable either way, it is just better placed where this lands. */
@supports (anchor-name:--a){
  .anchor{anchor-name:--anchor}
  .anchored{
    position:absolute;
    position-anchor:--anchor;
    top:anchor(bottom);left:anchor(left);
    position-try-fallbacks:flip-block,flip-inline;
    margin:4px 0 0;
  }
}

/* corner-shape goes past border-radius. A squircle on the primary button is a small "somebody
   designed this" signal, and it degrades to the plain 3px radius everywhere else. */
@supports (corner-shape:squircle){
  .btn.pri,.btn.danger{corner-shape:squircle;border-radius:6px}
}

/* View transitions suit the side-peek — one element, one clear before/after. Deliberately NOT
   applied to table rows or reordering: on a 200-row queue that is the staggered-fade-in problem
   under a new name, and it fights the reduced-motion rule below. */
@media (prefers-reduced-motion:no-preference){
  @supports (view-transition-name:none){
    .peek{view-transition-name:peek}
  }
}

/* ═══ misc ═════════════════════════════════════════════════════════════════════════ */

.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.spread{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.stack-4{display:flex;flex-direction:column;gap:4px}
.stack-8{display:flex;flex-direction:column;gap:8px}
.stack-12{display:flex;flex-direction:column;gap:12px}
.stack-20{display:flex;flex-direction:column;gap:20px}
.muted{color:var(--ink-3)}
.hr{height:1px;background:var(--line);border:0;margin:16px 0}

@media(max-width:860px){
  .ops-side{position:fixed;inset:0 auto 0 0;z-index:60;transform:translateX(-100%);transition:transform .16s}
  .ops-side.open{transform:none}
  .stat-lead{gap:20px}
}


/* ═══ BUTTONS — three tiers, and real states ═══════════════════════════════════════
   A control with exactly one state reads as static text in a box. That is the single biggest
   reason the fax row looked unclickable: no hover, no active, no focus ring, and three
   identical weights so nothing said which action was the expected next move.

   UPPERCASE IS FOR LABELS, NOT CONTROLS. The 11px/+.06em treatment names a static thing —
   a column header, a field label. All-caps micro-text on a button makes the control read as a
   caption. Buttons are sentence case at 13px, medium.

   The rule this file keeps re-learning: TYPE AND DENSITY ARE SCOPED PER COMPONENT, never
   applied app-wide. Table density broke the forms; label casing broke the buttons.

   Borders here are --line-strong, which clears 3:1 against the surface (WCAG 2.2 §1.4.11).
   Around 8-10% white on a dark ground is not "subtle", it is failing.
   ═══════════════════════════════════════════════════════════════════════════════════ */

.btn--primary,.btn--secondary,.btn--ghost{
  display:inline-flex;align-items:center;gap:6px;
  height:32px;padding:0 12px;
  border-radius:var(--r-control);
  font-family:var(--sans);font-size:13px;font-weight:500;line-height:1;
  text-transform:none;letter-spacing:0;
  cursor:pointer;white-space:nowrap;
  transition:background-color var(--exit),border-color var(--exit),color var(--exit),transform 60ms;
}
.btn--primary svg,.btn--secondary svg,.btn--ghost svg{width:14px;height:14px;flex:none}
.btn--primary:active,.btn--secondary:active,.btn--ghost:active{transform:translateY(.5px)}
.btn--primary:focus-visible,.btn--secondary:focus-visible,.btn--ghost:focus-visible{
  outline:2px solid var(--accent-40);outline-offset:1px;
}

/* One per group, maximum. The inset hairline along the top edge is what makes a filled button
   read as a physical surface rather than a coloured rectangle — one line, not skeuomorphism. */
.btn--primary{
  background:var(--accent);color:var(--on-accent);
  border:1px solid var(--accent);
  box-shadow:inset 0 1px 0 oklch(100% 0 0 / .18);
}
.btn--primary:hover{background:var(--accent-hi);color:var(--on-accent,#fff);border-color:var(--accent-hi);transition-duration:var(--enter)}

.btn--secondary{
  background:var(--panel);color:var(--ink-1);
  border:1px solid var(--line-strong);
}
.btn--secondary:hover{background:var(--tint);color:var(--ink-1,var(--ink));border-color:var(--ink-3);transition-duration:var(--enter)}

.btn--ghost{
  background:transparent;color:var(--ink-2);
  border:1px solid transparent;
}
.btn--ghost:hover{background:var(--tint);color:var(--ink-1);transition-duration:var(--enter)}

.btn--primary[disabled],.btn--secondary[disabled],.btn--ghost[disabled]{opacity:.45;cursor:not-allowed}

/* Async: the spinner appears only after 400ms, and min-width is pinned from the resting size so
   the control cannot resize mid-action. A button that changes width while you watch is worse
   feedback than none. */
.btn--busy{position:relative;color:transparent!important}
.btn--busy::after{
  content:"";position:absolute;inset:50% auto auto 50%;
  width:13px;height:13px;margin:-6.5px 0 0 -6.5px;
  border:1.5px solid currentColor;border-top-color:transparent;border-radius:50%;
  color:var(--on-accent);opacity:0;
  animation:ops-spin .7s linear infinite,ops-spin-in 1ms linear 400ms forwards;
}
.btn--secondary.btn--busy::after,.btn--ghost.btn--busy::after{color:var(--ink-1)}
@keyframes ops-spin{to{transform:rotate(360deg)}}

/* ── funnel / milestone: a label and its value are not the same thing ─────────────
   Rendered as one wall of caps, this is the least readable block on the panel — and it is the
   actual status summary for the record. Label dim and small; value brighter, sentence case,
   and COLOURED so "Not signed" and "Signed" are distinguishable without reading them. */
.milestone{display:flex;gap:9px;align-items:flex-start;padding:6px 0}
.milestone .mtxt{min-width:0}
.milestone .t{
  font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-3);line-height:1.4;
}
.milestone .d{
  font-size:13px;font-weight:400;letter-spacing:0;text-transform:none;
  color:var(--ink-1);line-height:1.45;margin-top:1px;
}
.milestone.is-done .d{color:var(--ok)}
.milestone.is-waiting .d{color:var(--warn)}
.milestone .mtick{
  width:16px;height:16px;border-radius:50%;flex:none;margin-top:2px;
  display:flex;align-items:center;justify-content:center;
  font-size:10px;font-weight:600;
  border:1px solid var(--line-strong);color:var(--ink-2);background:var(--panel);
}
.milestone.is-done .mtick{background:var(--ok);border-color:var(--ok);color:var(--on-accent)}
.milestone.is-waiting .mtick{background:var(--warn-bg);border-color:var(--warn-line);color:var(--warn)}


/* ═══ DESTINATION-CODED ACTIONS ════════════════════════════════════════════════════
   These three buttons send a patient's information to three DIFFERENT places, and that is the
   thing worth encoding: the physician by fax, MediHarbor's own billing system, and an outside
   company. Rendered in one neutral they read as three flavours of the same act, which is
   exactly the confusion you do not want on a screen that transmits PHI.

   Hue follows the destination and matches the nav, so BFlow is the same violet in the rail and
   on the button. Tinted ground with same-hue text rather than solid fills — the single teal
   primary still has to be the loudest thing in the row, because it is the step that advances
   the funnel. Both halves are set on every rule; a background without its colour is the bug
   the contrast gate exists to catch. */
.btn--bflow{
  background:oklch(72% .085 320 / .12);
  color:oklch(45% .10 320);
  border:1px solid oklch(72% .085 320 / .34);
}
.btn--bflow:hover{background:oklch(72% .085 320 / .20);color:oklch(38% .11 320);border-color:oklch(60% .10 320)}

.btn--partner{
  background:oklch(72% .085 45 / .13);
  color:oklch(45% .10 45);
  border:1px solid oklch(72% .085 45 / .34);
}
.btn--partner:hover{background:oklch(72% .085 45 / .21);color:oklch(38% .11 45);border-color:oklch(60% .10 45)}

:root[data-theme="dark"] .btn--bflow{color:oklch(82% .09 320)}
:root[data-theme="dark"] .btn--bflow:hover{color:oklch(90% .09 320)}
:root[data-theme="dark"] .btn--partner{color:oklch(82% .09 45)}
:root[data-theme="dark"] .btn--partner:hover{color:oklch(90% .09 45)}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .btn--bflow{color:oklch(82% .09 320)}
  :root:not([data-theme="light"]) .btn--bflow:hover{color:oklch(90% .09 320)}
  :root:not([data-theme="light"]) .btn--partner{color:oklch(82% .09 45)}
  :root:not([data-theme="light"]) .btn--partner:hover{color:oklch(90% .09 45)}
}
/* They share the primary's geometry and states — only the hue differs. */
.btn--bflow,.btn--partner{
  display:inline-flex;align-items:center;gap:6px;
  height:32px;padding:0 12px;border-radius:var(--r-control);
  font-family:var(--sans);font-size:13px;font-weight:500;line-height:1;
  text-transform:none;letter-spacing:0;cursor:pointer;white-space:nowrap;
  transition:background-color var(--exit),border-color var(--exit),color var(--exit),transform 60ms;
}
.btn--bflow svg,.btn--partner svg{width:14px;height:14px;flex:none}
.btn--bflow:active,.btn--partner:active{transform:translateY(.5px)}
.btn--bflow:focus-visible,.btn--partner:focus-visible{outline:2px solid var(--accent-40);outline-offset:1px}


/* ═══ DENSITY ══════════════════════════════════════════════════════════════════════
   Three settings, applied globally and remembered per browser. Not a preference toggle for its
   own sake: a rep working a queue for six hours wants compact, and somebody reading a record or
   typing a date of birth wants room. One person can be both in a day.

   Everything sizes off these two values, so a page that uses the row/control tokens follows the
   setting without knowing it exists. */
:root{
  --row-h:44px;
  --ctl-h:36px;
  --den-font:13px;
  --den-gap:var(--space-4);
}
:root[data-density="compact"]{
  --row-h:34px;
  --ctl-h:30px;
  --den-font:12.5px;
  --den-gap:var(--space-3);
}
:root[data-density="large"]{
  --row-h:56px;
  --ctl-h:42px;
  --den-font:15px;
  --den-gap:var(--space-5);
}

.tbl tbody td,tbody td{height:var(--row-h)}
input,select,textarea,.control{height:var(--ctl-h)}
.field input,.field select,.field-grid input,.field-grid select,
.form-grid input,.form-grid select,.eff input,.eff select{height:var(--ctl-h)}
.field-grid,.form-grid,.fields{gap:var(--den-gap)}
body{font-size:var(--den-font)}
.tbl,table{font-size:var(--den-font)}
:root[data-density="large"] .field{min-height:70px}
:root[data-density="compact"] .field{min-height:52px}

/* COMPACT HAS TO COMPRESS THE CONTENT, NOT JUST THE BOX. `height` on a table cell is a MINIMUM:
   the lead cell carries a name and a phone on two lines, so asking for 34px changed nothing while
   the content still needed 41. Compact tightens the line box and the secondary line as well, which
   is what actually buys the rows back. */
:root[data-density="compact"] .sub2,
:root[data-density="compact"] .srccode{font-size:10px;line-height:1.25}
:root[data-density="compact"] .nm{font-size:12.5px;line-height:1.25}
:root[data-density="compact"] tbody td{padding-top:0;padding-bottom:0;line-height:1.25}
:root[data-density="compact"] .badge,
:root[data-density="compact"] .cpill{height:17px}
:root[data-density="compact"] .fc{gap:2px}
:root[data-density="compact"] .funnel .dot{width:15px;height:15px;font-size:9px}

:root[data-density="large"] .sub2{font-size:12px}
:root[data-density="large"] .nm{font-size:14px}
:root[data-density="large"] tbody td{line-height:1.55}

/* The control itself: three segments, current one marked. */
.denswitch{display:inline-flex;border:1px solid var(--line-strong);border-radius:var(--r-control);overflow:hidden}
.denswitch button{
  background:var(--panel);color:var(--ink-2);border:0;border-right:1px solid var(--line);
  font:inherit;font-size:11px;font-weight:500;padding:0 8px;height:24px;cursor:pointer;
  min-width:24px;
}
.denswitch button:last-child{border-right:0}
.denswitch button:hover{background:var(--tint);color:var(--ink-1)}
.denswitch button[aria-pressed="true"]{background:var(--accent-12);color:var(--accent);font-weight:600}

/* ── THE PAGE HEADER, ONE SURFACE ACROSS EVERY TOOL ──────────────────────────────────────
   Twelve pages carried `header.pagebar` on an accent bar, two carried `.pagehead` as a bare
   rule under a title, and the rest opened with a naked <h1>. Three treatments, so moving
   between tools felt like moving between products, and none of them agreed with the rail on
   the left.

   The rail is the one surface this app never changes: --nav-bg is declared once and is
   deliberately fixed in BOTH themes (see the token block above). Matching the header to it
   means the top of the page and the left of the page are the same object, and a tool loaded
   in the shell reads as part of the shell rather than as a document someone embedded.

   Colours come from the rail's own family, so contrast is inherited from a set already tuned
   for a ~45-point lightness spread: --nav-active for the title, --nav-item for the subtitle,
   --nav-item-hover for links. Background and colour are set in the same rule, every time.

   `header.pagebar` (0,1,1) deliberately outranks ops-universal.css's `.pagebar` (0,1,0), which
   paints the accent bar these pages used to wear — element+class beats class, so this wins
   regardless of which sheet loads last. */
header.pagebar,
.pagehead,
.pghead{
  background:var(--nav-bg);
  color:var(--nav-active);
  border-bottom:1px solid oklch(from var(--nav-active) l c h / .14);
  padding:14px 22px;
  margin-bottom:16px;
}
header.pagebar h1,.pagehead h1,.pghead h1,
header.pagebar h2,.pagehead h2,.pghead h2,
header.pagebar .ttl,.pagehead .ttl,.pghead .ttl{color:var(--nav-active)}
/* The subtitle is the rail's resting link colour: present, clearly secondary, still 4.5:1. */
header.pagebar .sub,header.pagebar .note,header.pagebar .meta,
.pagehead .sub,.pagehead .note,.pagehead .meta,
.pghead .sub,.pghead .note,.pghead .meta{color:var(--nav-item)}
header.pagebar a,.pagehead a,.pghead a{color:var(--nav-item-hover)}
header.pagebar a:hover,.pagehead a:hover,.pghead a:hover{color:var(--nav-active)}
/* Controls that live in the bar keep their own surface, but their BORDER has to be visible
   against a dark ground — 3:1 against the adjacent surface, per the contrast floors. */
header.pagebar .btn,header.pagebar .toggle,header.pagebar button,
.pagehead .btn,.pagehead button,
.pghead .btn,.pghead button,.pghead .iconbtn{
  background:oklch(from var(--nav-active) l c h / .10);
  color:var(--nav-active);
  border:1px solid oklch(from var(--nav-active) l c h / .30);
}
header.pagebar .btn:hover,header.pagebar .toggle:hover,header.pagebar button:hover,
.pagehead .btn:hover,.pagehead button:hover,
.pghead .btn:hover,.pghead button:hover,.pghead .iconbtn:hover{
  background:oklch(from var(--nav-active) l c h / .18);
  color:var(--nav-active);
}
