/* --- DejaVu Sans — self-hosted (local) webfont, the same for every language --- */
@font-face {
  font-family: 'DejaVu Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/dejavusans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'DejaVu Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/dejavusans-700.woff2') format('woff2');
}
@font-face {
  font-family: 'DejaVu Sans';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/dejavusans-italic-400.woff2') format('woff2');
}
@font-face {
  font-family: 'DejaVu Sans';
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/dejavusans-italic-700.woff2') format('woff2');
}

/* --- Noto Sans Georgian — the second choice of UI face (2026-09-04, owner's request) ---

   Self-hosted like DejaVu: no CDN, no Google Fonts link. Four subset files, each with its own
   `unicode-range`, which is how Google itself ships it — so a Georgian page downloads 41 KB and
   fetches the Latin file only when a Latin glyph appears. All four are cut from the SAME variable
   font (four distinct files, checked - 41.5 / 24.9 / 13.7 / 1.8 KB), which is why one file per
   subset serves both weights via `font-weight: 400 700` rather than the eight files a static
   family would need. 82 KB in total, against DejaVu's 950 KB for four static faces.

   Greek, math and symbol subsets were deliberately not taken: nothing in this app renders them,
   and an unused @font-face is a rule the browser still has to match every glyph against.

   **DejaVu stays in the stack behind it.** It used to be strictly load-bearing: the sort
   indicator was the character `⇅` (U+21C5), which no Noto family carries, so DejaVu drew it.
   Since v0.229.4 the sort marks are Tabler icons and no longer depend on this. DejaVu is kept
   anyway, for the other marks this UI prints (`↑`/`↓` on a lab result, for one) - a glyph Noto
   lacks is then drawn from a face that is already self-hosted, instead of falling through to
   whatever the operating system happens to have. */
@font-face {
  font-family: 'Noto Sans Georgian';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/notosansgeorgian-georgian.woff2') format('woff2');
  unicode-range: U+0589, U+10A0-10FF, U+1C90-1CBA, U+1CBD-1CBF, U+205A, U+2D00-2D2F, U+2E31;
}
@font-face {
  font-family: 'Noto Sans Georgian';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/notosansgeorgian-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Noto Sans Georgian';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/notosansgeorgian-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Noto Sans Georgian';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/notosansgeorgian-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

/* --- Noto Sans - the Latin face of the English interface (2026-09-16, owner's request) ---

   Only on English, and only when the Noto pill is chosen: `html[data-font="noto"][lang="en"]`
   below. Georgian keeps Noto Sans Georgian in front, because that is the face its Georgian is
   drawn in and there is nothing for this file to add there.

   One subset, not the eight Google ships. `latin` alone, because Noto Sans Georgian's own
   `latin-ext` sits directly behind it in the English stack and is byte-identical in outline and
   advance (checked glyph by glyph, both are 1000 upem `wght 100-900` cuts of the same design) -
   so `₾`, `ā`, `ș` and the rest draw the same either way, out of 13.7 KB already on the page
   instead of Noto Sans' own 168 KB `latin-ext`.

   Worth knowing before this file is treated as load-bearing: on Latin text it renders IDENTICALLY
   to Noto Sans Georgian, which already served English. The only measured difference is vertical
   metrics, one unit of a thousand (ascender 1069 vs 1068). */
@font-face {
  font-family: 'Noto Sans';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url('/assets/fonts/notosans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* The UI face, switched the way a theme is: one token, stamped on <html> before first paint.
   `--font-ui` and not a second `body` rule, so anything that ever needs to name the face reads
   one place. */
html[data-font="noto"] {
  --font-ui: 'Noto Sans Georgian', 'Noto Sans', 'DejaVu Sans', Tahoma, Arial, sans-serif;
}
/* English, with the same pill chosen: Noto Sans leads, Noto Sans Georgian catches everything it
   does not carry - Georgian names and `latin-ext` - and the two system faces close the stack
   (owner, 2026-09-16). `lang` is server-rendered on <html>, so this needs no second token and
   cannot flash. */
html[data-font="noto"][lang="en"] {
  --font-ui: 'Noto Sans', 'Noto Sans Georgian', Tahoma, Arial, sans-serif;
}
:root {
  --font-ui: 'DejaVu Sans', Tahoma, Arial, sans-serif;
  --primary: #4872AB;
  --primary-dark: #375b8b;
  --primary-light: #E7EEF8;
  --primary-text: #24446B;

  --accent: #B5493F;
  --accent-dark: #94372F;
  --bg: #F4F5F6;
  --bg-panel: #FFFFFF;
  --border: #E3E5E7;
  --text-primary: #20242A;
  --text-secondary: #5C646B;
  --text-tertiary: #949CA2;
  --danger-bg: #FCEBEB;
  --danger-text: #791F1F;
  --warn-bg: #FAEEDA;
  --warn-text: #633806;
  --success-bg: #E8F3EC;
  --success-text: #1E5334;
  --info-bg: #E6F0F6;
  --info-text: #1D4D6B;
  --special-bg: #EDE9FE;
  --special-text: #5B21B6;
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;

  /* Type scale. Twelve sizes were in use (10, 11, 11.5, 12, 12.5, 13, 13.5, 14, 15, 16, 18,
     19, 20) — nobody can tell 12 from 12.5 from 13, so none of them carried meaning. Six
     roles do. Note there is no weight scale to go with it: DejaVu ships 400 and 700 only.
     Five, not six: a card title at 18px sits too close to the 20px page title for a screen
     this dense, so titles and large controls share one step. */
  --fs-micro: 11px;
  --fs-small: 12px;
  --fs-base: 13px;
  --fs-title: 15px;
  --fs-page: 20px;

  /* Spacing, named for what it separates rather than how big it is. The invariant worth
     protecting: distance between related fields < distance between sections. Today both are
     18px, which is exactly why grouping is invisible on long forms. */
  --sp-tight: 4px;
  --sp-control: 8px;
  --sp-group: 16px;
  --sp-section: 24px;
  --sp-page: 32px;

  /* Button surfaces — see "VETMAN BUTTON SYSTEM" further down for the measurements.
     --primary-text is primary-COLOURED text meant for a light ground (it pairs with
     --primary-light); on --primary itself it fails AA in all 9 themes, best case 2.85:1.
     A filled action button therefore needs its own fill/ink pair, and a control boundary
     needs its own line colour: --border is 1.24–1.90:1 against a panel in every theme,
     which is a hairline, not the 3:1 WCAG 1.4.11 wants from a control's edge. */
  --action-bg: var(--primary);
  --on-action: #FFFFFF;

  /* Hue where the table is furniture, neutral where it is data.

     The stripe and the lines sit behind and between the content, so they mix from the
     theme's own ink: a low percentage of a saturated blue is neither white nor blue, it is
     dirty blue, and it tints the data resting on it. The header band and the row hover are
     chrome and interaction, where the theme's colour belongs and reads as intent. On the
     default theme the neutral stripe spreads 2 of 255 between channels, the tinted header
     14 — both quiet, but only one of them colours the text above it.

     The steps are ordered by how much attention each deserves: a stripe you should not notice, a hover you should,
     a header above both, and the lines that hold it together.

     Not --border for the lines: that token is tinted very differently per theme — pale
     green in nordic-emerald, pale red in terracotta-exec, pale yellow in warm-amber — and
     a red hairline under every row competes with the one colour in a row that carries
     meaning, the status pill. */
  /* 6% — #F5F6F6 in the default theme. Picked by eye by the owner, who then named the exact
     mix; it is the value that was live while the header was a dark band, and the one they
     were remembering.

     Deliberately a mix and NOT `var(--bg)`, even though the page ground is almost this colour
     here. Tying the stripe to --bg would give this theme its exact grey and take the stripe
     away in half the others: measured against their own panel, the page ground is 1.034 in
     warm, 1.045 in warm-amber, 1.046 in clinical and 1.047 in nordic-emerald — at or under
     the 1.055 this file already records as "present in theory, invisible in practice". The
     mix holds 1.083 in all ten, which is quiet but never absent. */
  --tbl-zebra: color-mix(in srgb, var(--text-secondary) 6%, var(--bg-panel));
  --rule: color-mix(in srgb, var(--text-secondary) 22%, var(--bg-panel));
  --tbl-frame: color-mix(in srgb, var(--text-secondary) 50%, var(--bg-panel));

  /* Form controls. Light themes draw a white field on a white card, which is what they have
     always done; the dark theme recesses it to the page ground so the field is visible
     against its panel at all. That difference used to be an !important block, which meant
     any future change to input styling would silently not apply in dark — the same trap the
     table header sat in. */
  --field-bg: var(--bg-panel);
  /* A hint, plainly lighter than a typed value. Measured against the field fill, a placeholder
     lands at 2.79–2.96:1 in every theme while a real value sits at 9.72–17.85 — a 3.3–6.4×
     gap, so the two can never be mistaken for each other. Below 3:1 is deliberate and safe
     here: every field has a real label (v0.47.0), so the placeholder is a hint and not the
     field's name. Two themes override this token because their own tertiary ink is much
     darker (pro-slate 4.76, dark 3.75) and would read as typed text. */
  --placeholder: var(--text-tertiary);

  /* The page grid, named so the header can align with the column below it. The search field's
     left edge has to meet the content's, and that could not be expressed while 252, 20, 14 and
     26 were scattered across five rules. */
  --sidebar-w: 252px;
  --header-pad: 20px;
  --header-gap: 14px;
  --content-pad: 26px;

  /* The dropdown arrow. A token because an SVG data URI cannot read a CSS variable from
     inside itself, so the only way a theme can restyle it is to replace the whole url() —
     which is what the dark theme does below. The grey is 6.02:1 on a light field; the same
     grey measured 2.97:1 on the dark theme's, just under the 3:1 WCAG 1.4.11 asks of a
     control's indicator. */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235C646B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  /* The header band, v0.162.10 (owner instruction, #12). It was an 18% wash of --primary,
     which put a blue band over an amber or green zebra inside a category card — two hues in
     one table, and the reason the whole treatment was rejected. The band is now the ink
     colour itself, softened toward the panel: dark enough to be unmistakably a header,
     neutral so nothing in the table competes with the one thing that carries meaning.

     **There is no band any more (v0.162.13), and the reason is a trap worth writing down.**
     The band and the zebra are both a mix of the same ink into the same panel, so they compete
     for one narrow range of light greys: every step that makes the band more visible walks it
     into the stripe. Measured, a 16% band sits 1.076:1 from an 11% stripe — indistinguishable.
     Four numbers were tried (10 / 16 / 22 / 30) before it became clear the number was never
     the problem.

     Two earlier attempts, for the record: an 18% wash of --primary (the blue band that put a
     second hue over an amber zebra, rejected in #12) and --text-primary at 85% (a dark band,
     rejected as too heavy — and it could not be lightened, since white on it runs 9.72 at
     100%, 6.43 at 85%, 4.53 at 72%, 3.35 at 60%).

     So the header is separated by a rule and a quieter ink instead, which costs nothing the
     zebra needs. A useful side effect: with no fill under it the label can go back to
     --text-secondary (5.48:1 on the panel). On a light band it could not — measured 4.29:1,
     under AA — which is why the banded versions had to use the same ink as the data, and
     exactly why they stopped reading as headers. */
  --tbl-head: var(--bg-panel);

  /* The line under the header, and the only thing now marking it. Dimmed on purpose (owner's
     call) but not past the floor: --text-secondary at 75% measures 3.40:1 against the panel
     across all ten themes, and the next step down is 2.57 — under the 3:1 WCAG 1.4.11 asks of
     a non-text object that carries meaning. The same floor --btn-line exists for, and the same
     reason --border (1.24-1.90) can never carry structure by itself. Drawn 2px, so a dimmer
     colour still reads as deliberate. */
  --tbl-head-rule: color-mix(in srgb, var(--text-secondary) 75%, var(--bg-panel));
  --tbl-hover: color-mix(in srgb, var(--primary) 30%, var(--bg-panel));

  /* A muted label, which is what it was before any of this — the difference is that it now
     sits on the panel rather than on a coloured wash. Two themes used to override this token
     (dark and nordic-emerald, both forcing --text-primary because a mid-grey label lost ground
     on a --primary wash); both are deleted along with the wash they were correcting. */
  --tbl-head-ink: var(--text-secondary);
  --btn-line: #8A9299;
  --danger-line: #C67E7E;
  --success-line: #6F9C82;
  --info-line: #6E97B4;

  /* Categorizing panels by color — a meaning-carrying tone, not mere decoration */
  --cat-clinical: #2F8F82;
  --cat-clinical-bg: #EAF5F3;
  --cat-clinical-text: #1E5F56;

  --cat-billing: #B8791E;
  --cat-billing-bg: #FBF2E3;
  --cat-billing-text: #8A5A14;

  --cat-files: #6B5B95;
  --cat-files-bg: #F1EEF8;
  --cat-files-text: #4E4170;
}

/* --- Theme: Clinical Teal --- */
html[data-theme="clinical"] {
  --primary: #4872AB;
  --primary-dark: #375b8b;
  --primary-light: #CCFBF1;
  --primary-text: #0F766E;

  --accent: #0D9488;
  --accent-dark: #0F766E;
  --bg: #F8FAFC;
  --bg-panel: #FFFFFF;
  --border: #CBD5E1;
  --text-primary: #0F172A;
  --text-secondary: #475569;
}

/* --- Theme: Nordic Clean --- */

html[data-theme="nordic"] {
  --primary: #2563EB;
  --primary-dark: #1D4ED8;
  --primary-light: #EFF6FF;
  --primary-text: #1E3A8A;

  --accent: #10B981;
  --accent-dark: #047857;
  --bg: #F1F5F9;
  --bg-panel: #FFFFFF;
  --border: #CBD5E1;
  --text-primary: #0F172A;
  --text-secondary: #475569;
}

/* --- Theme: Warm Pet Care --- */
html[data-theme="warm"] {
  --primary: #4872AB;
  --primary-dark: #375b8b;
  --primary-light: #FEF3C7;
  --primary-text: #92400E;

  --accent: #F59E0B;
  --accent-dark: #D97706;
  --bg: #FDFBF7;
  --bg-panel: #FFFFFF;
  --border: #E5E7EB;
  --text-primary: #1F2937;
  /* #6B7280 was the weakest secondary of the nine (4.83:1 on a panel) and fell to 4.12:1
     on the table header band. Darkened just enough to clear AA there. */
  --text-secondary: #5D646D;
}

/* --- Theme: Modern Dark Mode --- */
html[data-theme="dark"] {
  --primary: #60A5FA;
  --primary-dark: #3B82F6;
  --primary-light: #1E293B;
  --primary-text: #93C5FD;

  --accent: #FB7185;
  --accent-dark: #E11D48;
  --bg: #0F172A;
  --bg-panel: #1E293B;
  --border: #334155;
  --text-primary: #F8FAFC;
  --text-secondary: #94A3B8;
  --text-tertiary: #64748B;

  --danger-bg: #451A1A;
  --danger-text: #FCA5A5;
  --warn-bg: #422006;
  --warn-text: #FDE68A;
  --success-bg: #14321F;
  --success-text: #86EFAC;
  --info-bg: #0F2C3E;
  --info-text: #93C5FD;
  --special-bg: #2E1065;
  --special-text: #C4B5FD;

  /* The only theme whose --primary is lighter than its ground, so the action button
     inverts here: the light blue becomes the fill and the page ground becomes the ink
     (7.02:1). --primary-dark + white would be 3.68:1 and fail. */
  --action-bg: var(--primary);
  --on-action: var(--bg);
  --btn-line: #6B7A90;
  --danger-line: #A85C5C;
  --success-line: #4E8A66;
  --info-line: #4A7EA8;
  --field-bg: var(--bg);
  --placeholder: color-mix(in srgb, var(--text-tertiary) 80%, var(--field-bg));
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2394A3B8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");  /* 6.96:1 */

  --cat-clinical: #5FBBAA;
  --cat-clinical-bg: #16211F;
  --cat-clinical-text: #8FD9C9;

  --cat-billing: #D9A054;
  --cat-billing-bg: #241D12;
  --cat-billing-text: #E8BE82;

  --cat-files: #A594CC;
  --cat-files-bg: #1E1B29;
  --cat-files-text: #C6B8E6;
}

/* The knock-out filter suits the monochrome VetMan.ge wordmark. An organization's own logo is
   its brand and may well be coloured — inverting it would render it as a white silhouette, so
   .app-header-logo-org is excluded from this and from the pro-slate rule below. */
html[data-theme="dark"] .app-header-logo img:not(.app-header-logo-org),
html[data-theme="dark"] .login-logo-img {
  filter: brightness(0) invert(1);
}

/* The dark theme used to set these three with !important, which changed nothing (the values
   were its own tokens) while blocking every future change to form controls in that theme —
   the same trap the table header sat in. Token-driven and theme-independent now.

   Deliberately not scoped to .form-group: the search box, the filter inputs and the datepicker
   all live outside it, and the old override covered them. Scoping this rule would have left
   those three white-on-dark. */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
select,
textarea {
  /* background-COLOR: the shorthand would reset background-image, and `input[type="text"]`
     (0-1-1) outweighs `.ss-chevron-bg` (0-1-0) — which is how the searchable field lost the
     little arrow that says it opens. */
  background-color: var(--field-bg);
  color: var(--text-primary);

  /* The border, the radius and the padding are HERE, on the element, and not only on
     `.form-group input`, for the same reason the chevron below is: a field whose shape depends
     on its parent renders as a raw browser control the moment it is used anywhere else — in a
     table cell, in a filter bar, in a toolbar. That failure is silent. Nothing errors, no test
     sees it, and it reaches the user as a row of grey rectangles with square corners.

     It had already happened twice: /settings/finance pasted the five declarations inline to
     work around it, and the clinical-codes table shipped without them at all.

     `.form-group input` (0-1-1, later in this file) still wins where it differs, and
     `.filter-search input[type="text"]` (0-2-1) keeps its merged-with-the-button shape. */
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 9px 12px;
  font-family: inherit;
}

/* One arrow for every dropdown.

   Until now the look depended on whether the markup remembered a class: 40 selects carried
   `.ss-plain-select`/`.ss-chevron-bg` and got the app's chevron, the other 209 got the
   operating system's — which is why /contacts/create showed two different arrows on one form.
   A rule on the element cannot be forgotten the way a class can.

   `[multiple]` and `[size]` are list boxes, not dropdowns: they have no arrow to replace. */
select:not([multiple]):not([size]) {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
}

/* `.form-group select` (0-1-1) sets padding on all four sides, which would put the text back
   under the arrow. Stated separately rather than folded into the shared field rule, because
   that rule also covers inputs and textareas, which have no arrow to clear. */
.form-group select,
select.field-control {
  padding-right: 28px;
}

/* ONE appearance for every closed dropdown — a native <select>, the `.ss-plain-select` variant
   and the searchable field's `.ss-input`. They should differ in exactly one way: the searchable
   one can be typed into.

   Five things had drifted apart: the arrow sat at 12px on one and 10px on the other, the text
   cleared 30px on one and 28px on the other, one hard-coded 13px while the others took
   --fs-base, two were 38px tall while a plain select computed to about 36 — and
   `.ss-plain-select` filled with --bg-panel while everything else used --field-bg, which are
   the SAME colour in the light themes and different in dark. That last one is why this is a
   shared rule and not three tidied copies. */
input.ss-input,
.ss-plain-select,
select:not([multiple]):not([size]) {
  height: 38px;
  padding: 9px 28px 9px 12px;
  font-size: var(--fs-base);
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}

/* And the fields beside them, so a row does not stagger. Checkboxes and radios keep their own
   size, and a textarea is not a single-line control. */
.form-group input:not([type="checkbox"]):not([type="radio"]),
input.field-control:not([type="checkbox"]):not([type="radio"]) {
  height: 38px;
}

/* Removed in v0.162.10: an unterminated comment ("Only the ink differs here — the band,")
   had swallowed a `html[data-theme="dark"] table.tbl th { color: --text-secondary }` rule and
   everything down to the next `*/`, so that rule had never once applied. Nothing looked wrong,
   because the dark block's own --tbl-head-ink was setting the same thing — which is exactly
   how a dead rule survives. The header ink is one token now and needs neither.

   Also removed earlier: a repeat of dark's own --border (#334155) as a hard-coded !important,
   which changed nothing while blocking every future change to the row rule. */

/* Both dark themes: the row sits on the page ground rather than the panel, so it separates
   from the card it is inside. Written with the tokens rather than each theme's literals —
   the two hex values here were dark's own --bg and --border spelled out again, which is the
   hard-coded-component trap the comment above already describes. */
html[data-theme="dark"] .medication-row-card,
html[data-theme="terminal"] .medication-row-card {
  background-color: var(--bg) !important;
  border-color: var(--border) !important;
}

/* --- Theme: Clinical Pro / Pro Slate (medium contrast — an easy-on-the-eyes dark sidebar & header) --- */
html[data-theme="pro-slate"] {
  --placeholder: color-mix(in srgb, var(--text-tertiary) 75%, var(--field-bg));
  --primary: #4872AB;
  --primary-dark: #375B8B;
  --primary-light: #E0EBF8;
  --primary-text: #24446B;

  --accent: #B5493F;
  --accent-dark: #94372F;

  --bg: #EAEFF5;
  --bg-panel: #FFFFFF;
  --border: #CBD5E1;

  --text-primary: #0F172A;
  --text-secondary: #475569;
  --text-tertiary: #64748B;
}

html[data-theme="pro-slate"] .app-header {
  background-color: #0F172A !important;
  border-bottom: 1px solid #1E293B !important;
  color: #FFFFFF !important;
}

html[data-theme="pro-slate"] .app-header-logo img:not(.app-header-logo-org) {
  filter: brightness(0) invert(1);
}

html[data-theme="pro-slate"] .app-header .mobile-nav-toggle {
  color: #FFFFFF !important;
}

html[data-theme="pro-slate"] .app-header .topbar-user {
  color: #E2E8F0 !important;
}

html[data-theme="pro-slate"] .sidebar {
  background-color: #1E293B !important;
  border-right: 1px solid #334155 !important;
}

html[data-theme="pro-slate"] .sidebar .sb-section {
  color: #94A3B8 !important;
  font-weight: 700;
}

html[data-theme="pro-slate"] .sidebar .sb-item {
  color: #CBD5E1 !important;
}

html[data-theme="pro-slate"] .sidebar .sb-item:hover {
  background-color: #334155 !important;
  color: #FFFFFF !important;
}

html[data-theme="pro-slate"] .sidebar .sb-item.active {
  background-color: #4872AB !important;
  color: #FFFFFF !important;
  border-right: none;
  font-weight: 700;
}

html[data-theme="pro-slate"] .sidebar .sb-footer {
  border-top-color: #334155 !important;
  color: #64748B !important;
}

/* Removed in v0.162.13: the second unterminated comment in this file ("Same story as dark:
   this theme wanted a heavier header and hard-coded it,") had swallowed a pro-slate
   `table.tbl th { border-bottom-width: 2px }` rule, which therefore never applied. Every
   theme draws the 2px rule now, so there is nothing left for it to say. Both dead rules found
   this way were on the table header — see the note near the dark theme block. */

/* --- Theme: აფთიაქი / Pharmacy enamel (2026-09-03, owner's choice) ---

   Four themes were removed in the same change — `clinical-brand` (Royal Blue),
   `nordic-emerald`, `terracotta-exec` and `warm-amber` — on the owner's instruction. Ten
   themes was a menu nobody was choosing from, and two of the four were the reason
   `--action-bg` and `--tbl-head-ink` needed per-theme exceptions at all.

   This one is the old pharmacy enamel sign: bottle green, cream, and the oxblood red of the
   lettering. It was picked off a rendered comparison of five retro directions, and it needed no
   token exception: the green is dark enough to carry white on the action button (7.61:1) and
   the header label sits on the panel at 6.12:1. Everything else here is the ordinary token
   block — no component is hard-coded, which is the rule a theme must not break. */
html[data-theme="pharmacy"] {
  --primary: #1F5E4A;
  --primary-dark: #164437;
  --primary-light: #DDE8E1;
  --primary-text: #123A2E;

  --accent: #8C2F2A;
  --accent-dark: #6F2420;

  --bg: #EFEBE0;
  --bg-panel: #FAF7EF;
  --border: #DAD2C0;

  --text-primary: #1D211E;
  --text-secondary: #55605A;

  /* The four control edges are literal per theme, and the base values are tuned for a WHITE
     panel — on this theme's cream they measure 2.90–2.95 against the 3:1 WCAG 1.4.11 asks of
     a control's edge. `check_contrast.php` caught all four the first time the theme was run
     through it, which is exactly what that script is for. Darkened to clear 3:1 with room to
     spare, and tinted rather than neutral grey, because a neutral hairline on a warm ground
     reads as dirt. */
  --btn-line: #7C8A83;
  --danger-line: #B06A6A;
  --success-line: #5E8C72;
  --info-line: #5E87A4;
}

html[data-theme="pharmacy"] .app-header {
  background-color: #164437 !important;
  border-bottom: 1px solid #0E2C24 !important;
  color: #FFFFFF !important;
}

html[data-theme="pharmacy"] .app-header-logo img {
  filter: brightness(0) invert(1);
}

html[data-theme="pharmacy"] .app-header .mobile-nav-toggle,
html[data-theme="pharmacy"] .app-header .topbar-user {
  color: #FFFFFF !important;
}

html[data-theme="pharmacy"] .sidebar {
  background-color: #122B24 !important;
  border-right: 1px solid #0B1D18 !important;
}

html[data-theme="pharmacy"] .sidebar .sb-section {
  color: #A9C7B9 !important;
  font-weight: 700;
}

html[data-theme="pharmacy"] .sidebar .sb-item {
  color: #E3EFE8 !important;
}

html[data-theme="pharmacy"] .sidebar .sb-item:hover {
  background-color: #1F5E4A !important;
  color: #FFFFFF !important;
}

html[data-theme="pharmacy"] .sidebar .sb-item.active {
  background-color: #8C2F2A !important;
  color: #FFFFFF !important;
  border-right: none;
  font-weight: 700;
}

html[data-theme="pharmacy"] .sidebar .sb-footer {
  border-top-color: #0B1D18 !important;
  color: #A9C7B9 !important;
}

/* --- Theme: ტერმინალი / Amber phosphor (2026-09-03, owner's choice) ---

   The second dark theme, and a dark theme is never a token block on its own: `dark` sets 40
   tokens because everything a light theme inherits from `:root` — the semantic tint pairs, the
   field fill, the placeholder, the chevron's own stroke colour, the three category triples —
   is wrong on a dark ground and fails silently rather than visibly. This one sets the same
   set. If you add a third dark theme, copy the LIST from here, not the values.

   `--action-bg` inverts exactly as `dark` does: the amber is lighter than its ground, so it
   becomes the fill and the page ground becomes the ink (8.55:1). */
html[data-theme="terminal"] {
  --primary: #E0A32E;
  --primary-dark: #B8811F;
  --primary-light: #3A2E14;
  --primary-text: #F2C86A;

  --accent: #E2604F;
  --accent-dark: #C24634;
  --bg: #12100C;
  --bg-panel: #1B1811;
  --border: #33291A;
  --text-primary: #F1E2C4;
  --text-secondary: #B8A47E;
  --text-tertiary: #8A7A5A;

  --danger-bg: #3A1D18;
  --danger-text: #F0937F;
  --warn-bg: #3A2E14;
  --warn-text: #EFC46F;
  --success-bg: #16261A;
  --success-text: #97D3A0;
  --info-bg: #16242B;
  --info-text: #93C5D8;
  --special-bg: #24192E;
  --special-text: #C9AEDD;

  --action-bg: var(--primary);
  --on-action: var(--bg);
  --btn-line: #7A6A4A;
  --danger-line: #A86355;
  --success-line: #5C8A62;
  --info-line: #5A8092;
  --field-bg: var(--bg);
  --placeholder: color-mix(in srgb, var(--text-tertiary) 80%, var(--field-bg));
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23B8A47E' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  --cat-clinical: #6FBFA0;
  --cat-clinical-bg: #16211D;
  --cat-clinical-text: #9BD9BE;

  --cat-billing: #E0A32E;
  --cat-billing-bg: #241D12;
  --cat-billing-text: #EFC46F;

  --cat-files: #B098CC;
  --cat-files-bg: #201B29;
  --cat-files-text: #CDBBE6;
}

html[data-theme="terminal"] .app-header-logo img:not(.app-header-logo-org),
html[data-theme="terminal"] .login-logo-img {
  filter: brightness(0) invert(1);
}

/* --- Theme: ემალი / Institutional blue (2026-09-03, owner's choice) ---

   Dark institutional blue with a tomato accent, and the closest of the five retro candidates
   to what the app looked like before. No token exception: the blue carries white on the action
   button at 8.66:1 and the label sits on the panel at 5.91:1.

   It was drafted WITH a `--tbl-head-ink` override, because the comparison it came from still
   assumed the 18% table-head wash. That wash is gone (`--tbl-head: var(--bg-panel)`, see the
   token block at the top), so the override corrected a band that no longer exists and only
   made this theme's header ink darker than every other theme's. Check what a token currently
   DOES before copying an exception written for it. */
html[data-theme="enamel"] {
  --primary: #1F4E79;
  --primary-dark: #163A5A;
  --primary-light: #DCE6F0;
  --primary-text: #17395A;

  --accent: #C2452D;
  --accent-dark: #9E3623;

  --bg: #E9EDF0;
  --bg-panel: #FBFCFD;
  --border: #CBD6E0;

  --text-primary: #1B2430;
  --text-secondary: #566472;
}

html[data-theme="enamel"] .app-header {
  background-color: #163A5A !important;
  border-bottom: 1px solid #0E2740 !important;
  color: #FFFFFF !important;
}

html[data-theme="enamel"] .app-header-logo img {
  filter: brightness(0) invert(1);
}

html[data-theme="enamel"] .app-header .mobile-nav-toggle,
html[data-theme="enamel"] .app-header .topbar-user {
  color: #FFFFFF !important;
}

html[data-theme="enamel"] .sidebar {
  background-color: #12283D !important;
  border-right: 1px solid #0A1B2B !important;
}

html[data-theme="enamel"] .sidebar .sb-section {
  color: #A9C2D8 !important;
  font-weight: 700;
}

html[data-theme="enamel"] .sidebar .sb-item {
  color: #E1EAF3 !important;
}

html[data-theme="enamel"] .sidebar .sb-item:hover {
  background-color: #1F4E79 !important;
  color: #FFFFFF !important;
}

html[data-theme="enamel"] .sidebar .sb-item.active {
  background-color: #C2452D !important;
  color: #FFFFFF !important;
  border-right: none;
  font-weight: 700;
}

html[data-theme="enamel"] .sidebar .sb-footer {
  border-top-color: #0A1B2B !important;
  color: #A9C2D8 !important;
}

* {
  box-sizing: border-box;
}

html, body {
  /* A protective barrier — an overflow from any single element (header controls, dropdown menu,
     etc.) no longer makes the whole page horizontally scrollable; that element itself
     must be contained (overflow-x:auto within its own bounds), not the whole body. */
  overflow-x: hidden;
  /* …but `hidden` on the root makes the document a scroll container, and a scroll container
     is where `position: sticky` stops working — silently, everywhere on the site. `clip`
     is the same barrier without that side effect. Declared second so a browser that does
     not know it keeps the `hidden` above and simply loses sticky, rather than the guard. */
  overflow-x: clip;
}

body {
  margin: 0;
  /* DejaVu Sans is the default for every language (100% Georgian glyph coverage — confirmed via
     langcover.txt/fc-query). `--font-ui` is what makes the choice switchable; see the
     `html[data-font]` block at the top of this file. */
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--text-primary);
  font-size: 14px;
}

a {
  color: inherit;
  text-decoration: none;
}

/* A link inside a sentence. `a` above is unstyled on purpose - every row, card and menu item in
   this app is an `<a>` - so a link in prose has nothing saying it is one, and inside a dialog or an
   empty state there is no other cue at all (owner, 2026-09-16: „აქ ბმული არ აღიქმება"). Underlined
   at rest, not only on hover. */
.link {
  color: var(--primary-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.link:hover,
.link:focus-visible {
  text-decoration-thickness: 2px;
}

/* A link that leads to a MODULE is a quiet button carrying that module's icon (`module_link()`,
   owner's pick 2026-09-16). It is `.btn.btn-muted.btn-sm`; this class only places it. In an empty
   state or a dialog it is the way out of the state, so it stands on its own line under the sentence. */
.module-link {
  vertical-align: middle;
}

.empty-state .module-link,
.modal-body .module-link {
  margin-top: 10px;
}


.app-shell {
  display: flex;
  min-height: calc(100vh - 56px);
}

/* --- Unified full-width header (logo + topbar controls, one continuous bar) --- */
.app-header {
  position: sticky;
  top: 0;
  z-index: 70;
  height: 56px;
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 var(--header-pad);
  gap: var(--header-gap);
  box-sizing: border-box;
}

/* Fixed to the sidebar's width — including its 1px border — minus the header's own padding,
   so the logo block ends exactly where the sidebar does. Left free, the box grew with whatever
   logo an organization uploaded and everything after it landed at an arbitrary x. */
.app-header-logo {
  display: flex;
  align-items: center;
  flex: none;
  width: calc(var(--sidebar-w) + 1px - var(--header-pad));
  overflow: hidden;
}

.app-header-logo img {
  height: 32px;
  width: auto;
  max-width: 100%;
  display: block;
}

/* The platform wordmark has known proportions; a clinic's uploaded lockup does not, so cap
   its width — an extreme aspect ratio would otherwise push the header controls off screen. */
.app-header-logo img.app-header-logo-org {
  max-width: 220px;
  object-fit: contain;
}

.superadmin-logo-mark {
  background-color: #FDF3C8;
}
html:not([data-theme]) .superadmin-logo-mark {
  background-color: #B8860B;
}

.mobile-nav-toggle {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 6px;
  margin: 0;
  color: var(--text-primary);
}

.mobile-nav-toggle i {
  font-size: 22px;
}

.sidebar-backdrop {
  display: none;
}

/* --- Sidebar --- */
.sidebar {
  width: var(--sidebar-w);
  background: var(--bg-panel);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  min-height: calc(100vh - 56px);
  align-self: stretch;
}

.sb-footer {
  margin-top: auto;
  padding: 10px 16px;
  font-size: 11px;
  color: var(--text-tertiary);
  border-top: 1px solid var(--border);
}

/* System Switchers & Upward Popups */
.sb-system-switcher {
  position: relative;
  width: 100%;
}

.sb-system-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg-panel);
  font-size: 12px;
  font-weight: 700;
  color: var(--text-primary);
  cursor: pointer;
  transition: all 0.15s ease;
}

.sb-system-trigger:hover {
  border-color: var(--primary);
}

.sb-system-dropdown {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  width: 100% !important;
  min-width: 0 !important;
  box-sizing: border-box !important;
  z-index: 250;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  max-height: 220px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 4px;
}

.theme-switcher .sb-system-dropdown {
  bottom: calc(100% + 4px);
  top: auto;
  /* A fixed list of themes opening upward into the whole sidebar: the 220px cap above cut
     the sixth row off behind a scrollbar. The organization list keeps the cap — its length
     is however many clinics a person belongs to. */
  max-height: none;
}

.theme-switcher.open .sb-system-dropdown {
  display: block;
}

/* Which way a switcher's list opens is decided on open by `placeSwitcherMenu()` in
   `theme-switcher.js`, from the room above and below the trigger. The two rules above are only
   the placement before any script runs. */
.theme-switcher.drop-up .sb-system-dropdown,
.theme-switcher.drop-up .theme-switcher-menu {
  top: auto;
  bottom: calc(100% + 4px);
}

.theme-switcher.drop-down .sb-system-dropdown,
.theme-switcher.drop-down .theme-switcher-menu {
  top: calc(100% + 4px);
  bottom: auto;
}

.sb-system-dropdown .theme-switcher-option {
  padding: 6px 8px;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sb-system-dropdown .theme-switcher-option-text {
  min-width: 0;
  flex: 1;
  overflow: hidden;
}

.sb-system-dropdown .theme-switcher-option-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
}

/* --- Sidebar system card — the dark-sidebar themes (pro-slate, pharmacy) --- they set a
   dark .sidebar background with !important,
   but .sb-system-trigger/.sb-system-lang-pills was still using var(--bg-panel) (light) —
   sharp white buttons on a dark sidebar. Reworked into a unified, translucent glassmorphism card. */
html[data-theme="pro-slate"] .sb-system-trigger,
html[data-theme="pharmacy"] .sb-system-trigger,
html[data-theme="enamel"] .sb-system-trigger {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.14);
  color: #F1F5F9;
}

html[data-theme="pro-slate"] .sb-system-trigger:hover,
html[data-theme="pharmacy"] .sb-system-trigger:hover,
html[data-theme="enamel"] .sb-system-trigger:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.28);
}

html[data-theme="pro-slate"] .theme-switcher-label,
html[data-theme="pharmacy"] .theme-switcher-label,
html[data-theme="enamel"] .theme-switcher-label {
  color: #CBD5E1;
}

html[data-theme="pro-slate"] .sb-system-dropdown,
html[data-theme="pharmacy"] .sb-system-dropdown,
html[data-theme="enamel"] .sb-system-dropdown {
  background: #1E293B;
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

html[data-theme="pro-slate"] .sb-system-dropdown .theme-switcher-option,
html[data-theme="pharmacy"] .sb-system-dropdown .theme-switcher-option,
html[data-theme="enamel"] .sb-system-dropdown .theme-switcher-option {
  color: #F1F5F9;
  border-radius: var(--radius-sm);
}

html[data-theme="pro-slate"] .sb-system-dropdown .theme-switcher-option:hover,
html[data-theme="pharmacy"] .sb-system-dropdown .theme-switcher-option:hover,
html[data-theme="enamel"] .sb-system-dropdown .theme-switcher-option:hover {
  background: rgba(255, 255, 255, 0.08);
}

html[data-theme="pro-slate"] .sb-system-dropdown .theme-switcher-option-sub,
html[data-theme="pharmacy"] .sb-system-dropdown .theme-switcher-option-sub,
html[data-theme="enamel"] .sb-system-dropdown .theme-switcher-option-sub {
  color: #94A3B8;
}

.sb-section {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.5px;
  color: var(--text-tertiary);
  padding: 14px 16px 4px;
  border-top: 1px solid var(--border);
  margin-top: 6px;
}

.sidebar .sb-section:first-child {
  border-top: none;
  margin-top: 0;
}

/* The menu no longer opens with a heading (the dashboard row is first), and that heading was
   what held the list off the header. */
.sidebar > .sb-item:first-child {
  margin-top: 10px;
}

.sb-section-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  color: inherit;
}

.sb-chevron {
  font-size: 12px;
  transition: transform 0.15s ease;
  flex-shrink: 0;
}

.sb-group {
  font-size: 14px;
}

.sb-group.collapsed .sb-chevron {
  transform: rotate(-90deg);
}

.sb-group.collapsed .sb-group-items {
  display: none;
}

.sb-updated {
  margin-left: auto;
  padding-left: 6px;
  font-size: 10px;
  font-weight: 400;
  color: var(--text-tertiary, var(--text-secondary));
  white-space: nowrap;
  opacity: 0.75;
}

.sb-divider {
  border-top: 1px solid var(--border);
  margin: 6px 16px 6px;
}

.sb-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  font-size: 14px;
  letter-spacing: 0.5px;
  color: var(--text-secondary);
  cursor: pointer;
}

.sb-item i.ti {
  font-size: 17px;
  width: 17px;
  text-align: center;
  flex-shrink: 0;
}

.sb-item:hover {
  background: var(--bg);
  color: var(--text-primary);
}

.sb-item.active {
  background: var(--primary-light);
  color: var(--primary-text);
  font-weight: 700;
  border-right: 3px solid var(--primary);
}

.sb-item.disabled {
  color: var(--text-tertiary);
  cursor: not-allowed;
}

.sb-item .badge {
  margin-left: auto;
  font-size: 10px;
  background: var(--accent);
  color: #fff;
  border-radius: 10px;
  padding: 1px 6px;
  font-weight: 700;
}

.sb-item .soon {
  margin-left: auto;
  font-size: 9.5px;
  color: var(--text-tertiary);
  background: var(--bg);
  border-radius: 8px;
  padding: 1px 6px;
}

/* --- Main --- */
.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.topbar-user {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--text-secondary);
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--primary-light);
  color: var(--primary-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  font-weight: 700;
  flex-shrink: 0;
  overflow: hidden;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

.content {
  flex: 1;
  padding: 22px var(--content-pad);
}

/* Pulls a block out of .content's own padding so it bleeds flush against the sidebar, the
   header, and the right edge, instead of sitting inset like every other card. Negative
   margins matching .content's padding exactly (both here and at the 900px breakpoint below,
   where .content's own padding also changes) — a decorative hero banner is the only
   thing that should ever use this, ordinary page content stays inset. */
.content-bleed-top {
  margin: -22px -26px 16px -26px;
}

/* A module's header banner — the illustration behind the page title.

   The image is named at the CALL SITE through `--banner`, so this rule is written once
   and giving a new module its own banner costs one line in that module's own view.

   Dark themes veil the image with their own ground rather than loading a second file.
   88% is measured, not chosen: the illustrations are drawn pale (mean luminance ~0.76),
   so light ink over one unveiled is 1.2-1.4:1 — invisible. `terminal` needs at least
   83.9% to clear 4.5:1 and `dark` reaches 6.6:1 at the same figure. Because the veil is
   `var(--bg)`, a dark theme added later is covered without touching this.

   NEVER use `filter` here. A filtered element is a containing block for its
   `position: fixed` descendants, so every dropdown opened inside the header would fall
   back to its stylesheet placement and be clipped — the v0.165.5 trap. */
.module-banner {
  background-image: var(--banner);
  background-size: cover;
  background-position: center;
  padding: 14px var(--content-pad) 0;
}

/* The band runs from the top edge of the content down to the tab row inclusive (owner's
   choice of three rendered options, 2026-09-10). That means it holds the breadcrumbs, the
   page header and the section nav — so the view renders its own breadcrumbs inside it and
   the controller passes `'breadcrumbs' => []` to silence the layout's copy.

   Inside the band the header is NOT a card. `.page-header` is a surface everywhere else,
   and keeping its panel fill here covered the illustration the banner exists to show —
   which is exactly how this looked when the banner wrapped an opaque header. */
.module-banner .page-header {
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
  margin-bottom: 10px;
}

/* The tabs close the band: their own bottom rule becomes its edge, so the section margin
   that normally follows them would open a gap inside the picture. */
.module-banner .section-nav,
.module-banner .section-nav-row {
  margin-bottom: 0;
}

/* Contacts has no section tabs, but its view control occupies the same closing row. Match
   the tabs' 38px visual height so the button is centred with breathing room above the rule. */
.module-banner .view-row {
  align-items: center;
  min-height: 38px;
  margin-bottom: 0;
  border-bottom: 1px solid var(--rule);
}

/* Controls over the artwork: a calm lane behind them, and the controls lifted off it
   (owner's pick of rendered options, 2026-09-14 — "1", the lightest lane, with shadows).

   Why not glass, which was asked for first: four glass variants were tried on the real header
   and none read. A translucent field on a pale picture is white on white — its edge measured
   ~1.3:1 against the artwork, against the 3:1 a control's edge needs (`--btn-line` exists for
   that), and translucency can only lower it. The problem was never the material: the controls
   sat on the busiest part of the picture. So the picture steps back behind them instead.

   The lane is `.page-header::before`, not a banner pseudo, for two reasons. `::after` is
   already the rollout header's bottom rule. And it must paint ABOVE the artwork but BELOW the
   controls: every banner is `isolation: isolate` (the illustration-header rule further down),
   the trail sits at z -2 and the art viewport at -1, and a -1 layer later in tree order paints
   over the art while everything in flow still paints over it. Its containing block is the
   banner, so it covers the whole band. It is `var(--bg-panel)`, the banners' own ground, so
   every theme gets its own lane with nothing written per theme.

   The shadows are an exception to UI_GRAMMAR_SPEC §70, recorded there: on a picture, a
   control needs something to separate it from what is drawn behind it, which a flat card
   never does.

   NEVER `backdrop-filter` here — like `filter`, it makes the element a containing block for
   its `position: fixed` descendants and the header's dropdowns would be clipped (v0.165.5). */
.module-banner .page-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(90deg,
    transparent 0%, transparent 50%,
    color-mix(in srgb, var(--bg-panel) 55%, transparent) 70%,
    color-mix(in srgb, var(--bg-panel) 55%, transparent) 100%);
}
/* Below 900px the controls wrap under the title, so the lane runs downwards instead. */
@media (max-width: 900px) {
  .module-banner .page-header::before {
    background: linear-gradient(180deg,
      transparent 0%, transparent 30%,
      color-mix(in srgb, var(--bg-panel) 55%, transparent) 60%,
      color-mix(in srgb, var(--bg-panel) 55%, transparent) 100%);
  }
}

/* Every field and button in a banner's search form: the panel's fill, the 3:1 edge, the lift.
   `.ss-input` is named because a searchable select renders a text input of its own — the
   category picker on /services is one. */
.module-banner .search-box :is(input[type="text"], .ss-input),
.module-banner .search-box > .btn {
  background: var(--bg-panel);
  border-color: var(--btn-line);
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.10);
}
/* One height for the row. Measured on /services: field 36, category picker 38, `.btn-sm` 28 —
   invisible while they were flat, obvious once each one carries its own lift. */
.module-banner .search-box :is(input[type="text"], .ss-input) {
  box-sizing: border-box;
  height: 38px;
}
.module-banner .search-box > .btn {
  min-height: 38px;
}
.module-banner .search-box :is(input[type="text"], .ss-input):focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

/* The SIMPLE search — one field and its Search button — becomes one control with one edge:
   on a picture, two boxes a gap apart read as two things floating, one joined bar reads as the
   search. Only that shape. The first version joined every text input in the form, and on
   /services, where a category picker sits between the field and the button, it glued the two
   fields together with a missing edge and a rounded corner in the middle (owner, 2026-09-14).
   A form that carries a select keeps its gap and each control keeps its own full edge. */
.module-banner .search-box:not(:has(select)) { gap: 0; }
.module-banner .search-box:not(:has(select)) input[type="text"] {
  border-right-width: 0;
  border-radius: var(--radius-md) 0 0 var(--radius-md);
}
.module-banner .search-box:not(:has(select)) > .btn {
  align-self: stretch;
  min-height: 0;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}

/* Every other utility button on the band: the panel's own fill, the 3:1 edge, the lift.
   Menu items are left alone — a dropdown is already elevated and has its own surface. */
.module-banner :is(.page-actions, .view-row, .watercolor-header-footer, .watercolor-bottom-actions, .ambulatory-bottom-actions, .surgery-bottom-actions)
  :is(.btn-tool, .btn-muted):not(.search-box > .btn):not(.menu *) {
  background: var(--bg-panel);
  border-color: var(--btn-line);
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.10);
}
/* The rest state is now what `.btn-tool:hover` used to paint, so hover needs its own step. */
.module-banner :is(.page-actions, .view-row, .watercolor-header-footer, .watercolor-bottom-actions, .ambulatory-bottom-actions, .surgery-bottom-actions)
  :is(.btn-tool, .btn-muted):not(.menu *):hover,
.module-banner .search-box > .btn:hover {
  background: var(--bg);
}
.module-banner .btn-action:not(.menu *) {
  box-shadow: 0 3px 12px rgba(15, 23, 42, 0.22);
}

/* `pharmacy` gets its OWN illustration rather than a veil. Its ground is warm sand and its
   ink is dark, so the cool blue set reads as a foreign object on it while the veil — which
   exists to rescue light ink over a light picture — has nothing to rescue. A second file is
   worth it here precisely because the theme is light: swapping the artwork is the only lever,
   where a dark theme can simply have the picture dimmed under it. */
html[data-theme="pharmacy"] .module-banner {
  background-image: var(--banner-pharmacy, var(--banner));
}

/* Both dark themes now take a DRAWN dark illustration rather than a dimmed light one.
   One set serves them both: the artwork measures 0.023-0.052 mean luminance, which leaves
   `dark`'s ink at 9.9-13.8:1 and `terminal`'s at 8.1-11.3:1 — far above the veil's 5.3-6.2.

   The veil stays as the FALLBACK inside `var()`, so a page whose module has no dark twin
   still dims its light picture instead of showing it at full strength under light ink. That
   fallback is the whole comma-separated list after the first comma. */
html[data-theme="dark"] .module-banner {
  background-image: var(--banner-dark,
    linear-gradient(color-mix(in srgb, var(--bg) 88%, transparent),
                    color-mix(in srgb, var(--bg) 88%, transparent)),
    var(--banner));
}

/* `terminal` is not a second navy theme: its ground is warm near-black and its ink is cream,
   so the navy set sat on it as a cold foreign object (owner, 2026-09-10). Its own amber
   artwork measures 0.014-0.023 and leaves that cream ink at 11.2-12.9:1.
   The chain falls back through the navy twin, then the veil, then the plain picture — each
   step worse than the last but none of them broken. */
html[data-theme="terminal"] .module-banner {
  background-image: var(--banner-terminal, var(--banner-dark,
    linear-gradient(color-mix(in srgb, var(--bg) 88%, transparent),
                    color-mix(in srgb, var(--bg) 88%, transparent)),
    var(--banner)));
}

/* debug bar (views/partials/debug_bar.php) is position:fixed at the bottom — reserves no
   space of its own, so without this it covers whatever's at the bottom of the page. */
.has-debug-bar {
  padding-bottom: 48px;
}

/* --- Common components --- */
/* A surface, not floating text. The header answers "where am I, what am I looking at, what
   can I do here" — giving it a defined edge makes that a region the eye returns to instead
   of three things that happen to sit near the top.

   Centred, not top-aligned: the identity block runs to two lines and the actions read as
   belonging to the whole header rather than hanging off the title. */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-group);
  flex-wrap: wrap;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px 18px;
  margin-bottom: var(--sp-section);
}

/* A page header that sits inside a card would be a box in a box — the printable shelter
   report does exactly that, so there it stays plain. */
.card > .page-header {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
}

.page-title {
  font-size: var(--fs-page);
  font-weight: 700;
}

/* The module's name above the page's own, chosen out of ten candidates (v0.209.2). The pair is
   wrapped so the two lines sit at their own spacing rather than at `.page-identity`'s, which is
   set for a title and a control under it. `App\Core\ModuleSections::heading()` writes both. */
.page-heading {
  min-width: 0;
}

.page-module {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.6px;
  color: var(--text-tertiary);
  margin-bottom: 2px;
}

/* =========================================================
   VETMAN BUTTON SYSTEM
   ---------------------------------------------------------
   Position, icon and label carry the meaning; colour only reinforces it.
   Five appearances, three sizes, one square-icon modifier — and the appearance
   is never chosen from the label alone, always from what the button does here.

   Pick one, first match wins:
     escapes / navigates away ....... .btn-nav      Back, Cancel, Close
     destroys something ............. .btn-danger   Delete, Revoke
     THE operation this screen
       exists to perform ............ .btn-action   Save, Add, Confirm — one per context
     a utility, not the record ...... .btn-tool     Print, Export, Filter, History, Copy
     anything else that acts ........ .btn-muted    Edit, Open, View

   Measured across all 9 themes (see .CHANGELOG.md for the full table):
     .btn-action  fill + ink ......... 5.02–7.38:1   AA text
     .btn-danger  fill + ink ......... 7.82–8.98:1   AA text
     .btn-muted   --btn-line edge .... 3.07–3.35:1   AA non-text
     .btn-danger  --danger-line edge . 3.03–3.14:1   AA non-text
   Height comes from min-height, never from vertical padding, so a button keeps its
   size whatever the label wraps to and rows of them always line up.
   ========================================================= */

/* --- 1. Base --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 36px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  color: var(--text-primary);
  font-family: inherit;
  font-size: 13px;
  /* DejaVu Sans ships 400 and 700 only, so the two in-between values resolve in opposite
     directions: CSS searches upward first for anything above 500, so 600 renders as 700
     (bold), while 500 falls back to 400 (regular). Write 400 or 700 and mean it. */
  font-weight: 400;
  line-height: 1;
  text-align: center;
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

/* --- 2. Semantic appearances --- */
.btn-action {
  background: var(--action-bg);
  border-color: var(--action-bg);
  color: var(--on-action);
  font-weight: 700;
}

/* A softer edge than --btn-line. That token is the 3:1 a boundary needs when the boundary
   is the only thing identifying the control — true for an empty box, not for these: the
   glyph inside sits at 13–17:1 against the panel and does the identifying on its own. At
   full strength the edge was louder than the icon it framed, which is backwards.
   Focus and hover still bring the full-strength line back. */
.btn-muted {
  background: var(--bg-panel);
  border-color: color-mix(in srgb, var(--btn-line) 60%, var(--bg-panel));
  color: var(--text-primary);
}

/* Recessed rather than raised: a tinted ground and a softer edge read as chrome
   around the record, not as an operation on it. */
.btn-tool {
  background: var(--bg);
  border-color: var(--border);
  color: var(--text-secondary);
}

/* No fill and no edge at rest — a way out of the screen, not an action on it.
   The label always carries the meaning here, which is why this one may go unbounded. */
.btn-nav {
  background: none;
  border-color: transparent;
  color: var(--text-secondary);
}

/* Softened for the same reason as .btn-muted: the tinted fill and the red glyph identify
   this button on their own, so the edge does not have to carry the full 3:1 as well. It
   comes back at full strength on hover and focus. */
.btn-danger {
  background: var(--danger-bg);
  border-color: color-mix(in srgb, var(--danger-line) 60%, var(--danger-bg));
  color: var(--danger-text);
}

/* --- 3. Sizes --- */
.btn-lg {
  min-height: 44px;
  padding: 0 20px;
  font-size: 15px;
}

.btn-sm {
  min-height: 28px;
  padding: 0 10px;
  gap: 5px;
  font-size: 12px;
}

/* A button sitting in a row with form fields. `.btn` is 36px and a field is 38px — two pixels
   is invisible on its own and unmistakable side by side. */
.btn-field {
  min-height: 38px;
  height: 38px;
}

.btn-icon.btn-field {
  width: 38px;
  min-width: 38px;
  flex: 0 0 38px;
}

/* --- 4. Icon-only (square) --- */
.btn-icon {
  width: 36px;
  min-width: 36px;
  flex: 0 0 36px;
  padding: 0;
  gap: 0;
}

.btn-icon.btn-sm {
  width: 28px;
  min-width: 28px;
  flex-basis: 28px;
}

.btn-icon.btn-lg {
  width: 44px;
  min-width: 44px;
  flex-basis: 44px;
}

/* The icon always outsizes the label — at the button's own font-size it renders
   as a thin 12–13px mark and stops being the thing you recognise. */
.btn .ti {
  font-size: 16px;
  line-height: 1;
}

.btn-sm .ti {
  font-size: 14px;
}

.btn-lg .ti {
  font-size: 18px;
}

/* --- 5. States --- */
/* brightness() rather than a second fill token: it darkens correctly against both a
   dark fill (8 themes) and the light fill the dark theme uses, without a 10th colour. */
.btn-action:hover,
.btn-danger:hover {
  filter: brightness(.93);
}

.btn-danger:hover {
  border-color: var(--danger-line);
}

.btn-muted:hover {
  background: var(--bg);
  border-color: var(--btn-line);
}

.btn-tool:hover {
  background: var(--bg-panel);
  border-color: var(--btn-line);
}

.btn-nav:hover {
  background: var(--bg);
  color: var(--text-primary);
}

.btn:active {
  transform: translateY(1px);
}

.btn:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

/* Not opacity: a washed-out label loses contrast on every theme at once. A distinct
   flat surface plus tertiary ink stays legible and still reads as unavailable. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--bg);
  border-color: var(--border);
  color: var(--text-tertiary);
  cursor: not-allowed;
  filter: none;
  transform: none;
}

/* --- 6. Spatial grammar --- */
/* Where a button sits is the first thing that tells the user what it is, so the
   ordering lives in the container and not in each call site. */
.page-actions,
.row-actions,
.form-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Utilities first, the one action last — the eye lands where the work ends. */
.page-actions {
  justify-content: flex-end;
  flex-wrap: wrap;
}

/* Edit, then utilities, then delete. Never wraps: a row action that drops to a second
   line breaks the column the user is scanning down. */
.row-actions {
  gap: 6px;
  justify-content: flex-end;
  flex-wrap: nowrap;
}

/* Cancel, then save. */
.form-actions {
  justify-content: flex-end;
  gap: 10px;
  margin-top: 8px;
  flex-wrap: wrap;
}


/* `.btn-primary` and `.btn-secondary` are gone as of v0.46.0 — every one of their 625
   call sites was reclassified by context. Do not reintroduce them: "not primary" is a
   non-meaning, and it is what let one class absorb 453 unrelated buttons. */

.search-box {
  display: flex;
  align-items: center;
  gap: 6px;
}

.search-box input {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 8px 12px;
  font-size: 13px;
  min-width: 240px;
}

/* The search field in list pages' filter bar — input + button merged
   into one visual block (input open on the right, button open on the left — the impression of one "search bar"). */
/* The ward map's own search. A magnifier inside the field and no button, because nothing is
   submitted — the map narrows as the letters land. */
.ward-search {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.ward-search > i {
  position: absolute;
  left: 10px;
  color: var(--text-tertiary);
  pointer-events: none;
}

.ward-search > input {
  padding-left: 30px;
  min-width: 200px;
  font-size: var(--fs-base);
}

/* The browser's own clear cross, which only Chromium draws, and only for type=search. */
.ward-search > input::-webkit-search-cancel-button {
  cursor: pointer;
}

.filter-search {
  display: inline-flex;
  align-items: stretch;
}

.filter-search input[type="text"] {
  border: 1px solid var(--border);
  border-right: none;
  border-radius: var(--radius-md) 0 0 var(--radius-md);
  padding: 9px 12px;
  font-size: var(--fs-base);
  min-width: 220px;
}

/* --action-bg / --on-action, not --primary + white: this button is a filled action surface
   and needs the same pair the action button uses. --primary under white is 3.19:1 in
   warm-amber and 3.74:1 in nordic-emerald, both below AA.

   Keep the label. The merged bar is the point, but an icon-only trigger makes the user
   work out what the magnifier does, and a search you have to decode is a search you
   retype into instead. */
.filter-search .filter-search-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: var(--action-bg);
  border: none;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: 8px 14px;
  cursor: pointer;
  color: var(--on-action);
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  transition: filter 0.15s ease;
}

.filter-search .filter-search-btn:hover {
  filter: brightness(.93);
}

.filter-search .filter-search-btn:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

.stat-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
}

.stat-label {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.stat-val {
  font-size: 24px;
  font-weight: 700;
}

/* `.card` and `.panel` were two names for one identical rule — 511 call sites between them.
   The markup finished moving in v0.75.0 (351 class tokens across 82 views) and the `.panel`
   half of every selector went with it: nothing rendered differently, because the two names had
   shared one declaration since v0.48.0. See the VETMAN UI SYSTEM section for the anatomy. */
.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* A token, not a number, because a full-bleed child has to cancel exactly this much — see
     `.jfeed`. The journal's day bands were hard-coded to -18px and the card they sit in is
     14px inside a tab panel, so every band stuck 4px out past the card's own frame. */
  --card-pad: 18px;
  padding: var(--card-pad);
  margin-bottom: var(--sp-section);
}

.card:last-child {
  margin-bottom: 0;
}

.card-title {
  font-size: var(--fs-title);
  font-weight: 700;
  margin-bottom: 12px;
}

/* Categorizing panels by color — only the title bar gets colored, the panel body stays white.
   Note: we deliberately don't have overflow:hidden on the panel here — it was clipping the searchable-select's
   dropdown list when the select is near the bottom of the panel (e.g. the stationary services add form). */
.card[data-cat] > .card-title {
  /* The second full-bleed child in this sheet, and it carried the same hard-coded 18px the
     journal's day bands did. Where it still draws — a `data-cat` card outside a tab panel — the
     card really is 18px, so nothing was visibly wrong; it would have overhung the moment such a
     card appeared anywhere else. Read from the card, like `.jfeed`. */
  margin: calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad)) 14px;
  padding: 12px var(--card-pad);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.card[data-cat="clinical"] > .card-title { background: var(--cat-clinical-bg); color: var(--cat-clinical-text); }

.card[data-cat="clinical"] > .card-title i { color: var(--cat-clinical-text); }

.card[data-cat="billing"] > .card-title { background: var(--cat-billing-bg); color: var(--cat-billing-text); }

.card[data-cat="billing"] > .card-title i { color: var(--cat-billing-text); }

.card[data-cat="files"] > .card-title { background: var(--cat-files-bg); color: var(--cat-files-text); }

.card[data-cat="files"] > .card-title i { color: var(--cat-files-text); }

/* Alternating rows. 4% measured 1.055:1 against white — technically present,
invisible in
   practice. 7% is 1.099:1: enough to follow a row across a wide table,
still quiet. */
/* One rule, and two decisions inside it.
   **`of :not(.tbl-subrow)`** — several tables put a hidden edit/history <tr> after each record
   (14 of them across ten views), so counting rows in the DOM counts the invisible ones too:
   every VISIBLE row came out odd and the whole stripe landed on rows nobody can see. That is
   why the rate history under a stay looked like it had no zebra at all — it had one, on the
   hidden half. `of S` counts only the rows matching S, so a companion row drops out of the
   count instead of consuming it.
   **`odd` and not `even`** (owner instruction, v0.162.20) — the first data row is shaded, so
   a table opens on the stripe rather than on the panel.

   A plain `:nth-child(even)` fallback sat above this until v0.162.20 and was removed as
   theatre: `--tbl-zebra` is itself a `color-mix()`, so a browser too old for `of S` cannot
   resolve the colour either and the fallback painted nothing. Both features shipped in
   Chrome 111; `of S` has been in Safari since 9. */
.tbl tbody tr:nth-child(odd of :not(.tbl-subrow)) td {
  background: var(--tbl-zebra);
}

/* A companion row is part of the record above it, not a row of its own — so it is never
   striped, including on the fallback path and including while it is open. */
.tbl tbody tr.tbl-subrow td {
  background: none;
}

/* The green and amber stripes are gone (v0.162.10). Measured, the amber one sat 1.094:1 from
   the panel and a neutral stripe sits 1.099 — identical separation. So the colour was buying
   nothing a reader could use, while putting a second hue under a header that had a first one.
   The card's icon still says which category it is. */

/* --rule, the same grey as the lines between the rows (owner instruction, v0.162.18). A
   stronger frame was tried and read as too dark against them: on a list page the table is
   white on the grey page ground, so the FILL is what separates it and the border only has to
   close the shape. It is a different job from the border on a card inside a tab panel, which
   was flattened to transparent and therefore has nothing but its edge — that one keeps
   --tbl-frame. Not --border (1.24-1.90): matching the inner lines is the point, and --border
   is not what they are drawn in. */
.table-wrap {
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  /* Scrolls sideways at every width, not only below 900px. `hidden` (kept for the rounded
     corners) cut off every column past the edge with no scrollbar on a narrowed desktop
     window: /services, /stationary/list and the visit register lost their action buttons
     even at 1280px (owner report, 2026-09-14). `auto` clips to the radius just the same.
     `overflow-y` stays hidden so a hover effect never grows a vertical bar.
     `position: relative` makes it the containing block of anything absolute inside: a header's
     `.sr-only` label otherwise escaped the scroll area and made /feedback scroll sideways. */
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
}

/* ვიწრო შიგთავსის ცხრილი — ლაბორატორიული შედეგები და მისთანანი, სადაც რიგი მხოლოდ
   სახელი + რიცხვი + ერთეული + ნორმაა. სრულ სიგანეზე გაწელვისას პარამეტრსა და მის
   მნიშვნელობას შორის ეკრანის ნახევარი ცარიელი რჩება და თვალი ხაზს კარგავს —
   29-რიგიან სისხლის საერთოზე ეს კითხვას რეალურად ურთულებს.
   მრავალსვეტიან ცხრილს (მაგ. /lab/compare) არ ეხება — იქ სიგანე შიგთავსია. */
.table-wrap.tbl-narrow {
  max-width: 820px;
}

/* შიგთავსზე მორგებული სიგანე — ცხრილი, რომლის სვეტების რაოდენობაც ცვალებადია
   (მაგ. /lab/compare — სვეტი თითო ანალიზზე). ფიქსირებული max-width აქ არ გამოდგება:
   ორ სვეტზე ისევ გაწელილი დარჩებოდა, ექვსზე კი მოჭრიდა. `width:auto` ბრაუზერს
   შიგთავსით ალაგებინებს, `max-width:100%` კი ჰორიზონტალურ გადადინებას აჩერებს. */
.table-wrap.tbl-fit {
  display: inline-block;
  max-width: 100%;
  overflow-x: auto;
}

.table-wrap.tbl-fit table.tbl {
  width: auto;
}

table.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

/* A very light wash of the theme's own colour rather than grey, so the header belongs to
   the theme instead of sitting on top of it. Mixed from --primary, not --primary-light:
   that token equals --bg-panel exactly in the dark theme, so the band would vanish there.
   At 10% the label still measures 4.82–8.74:1 in every theme, and the tint separates the
   band from the neutral zebra stripe by hue rather than only by lightness.

   The band is also the tallest thing in the header, because Georgian has no uppercase —
   text-transform is not available as a hierarchy device here, so height and letter-spacing
   carry it instead. */
table.tbl th {
  background: var(--tbl-head);
  /* 9px, owner's choice out of five rendered heights (feedback #24, 2026-09-14): 17px made the
     header read as too tall. The header row is now ~36px against a plain data row's ~41px, so
     it is identified by the weight and the 2px rule beneath, not by height. Do not raise it
     back to "taller than a row" without asking. */
  padding: 9px 16px;
  text-align: left;
  color: var(--tbl-head-ink);
  font-weight: 700;
  /* Georgian has no uppercase, so the usual "make it a header" move is unavailable and
     letter-spacing has to do that work alone. It is what separates a header row from a
     bold data row here — without it the band reads as just another stripe with dark text. */
  letter-spacing: .04em;
  /* 2px and its own token, because this line is now the entire boundary between the header
     and the first row — --rule is the hairline every row already has, so reusing it would
     leave the header marked by nothing at all. */
  border-bottom: 2px solid var(--tbl-head-rule);
  font-size: var(--fs-small);
}

table.tbl td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
}

table.tbl tr:last-child td {
  border-bottom: none;
}

/* The hover has to outrank the stripe, and since v0.162.15 that takes counting.
   `:nth-child(An+B of S)` carries the specificity of the pseudo-class PLUS the most specific
   selector in S, so `.tbl tbody tr:nth-child(odd of :not(.tbl-subrow)) td` is (0,3,3) — one
   class more than the old `table.tbl tr:hover td` at (0,2,3). The stripe therefore won on
   exactly the striped rows, and only the white ones lit up under the pointer.
   `table.tbl tbody tr:hover:not(.tbl-subrow) td` is (0,3,4) — level on classes, ahead on the
   element count, which is the component that breaks the tie. So it wins on its own arithmetic
   rather than on source order, and the `:not()` earns its place — a companion edit row is
   part of the record above it and should not light up as a row of its own. */
table.tbl tbody tr:hover:not(.tbl-subrow) td {
  background: var(--tbl-hover);
}

/* §29 — დასაწკაპუნებელი მწკრივი კლავიატურითაც უნდა იხსნებოდეს.
   `<tr onclick>` არავითარ ტაბ-რიგში არ დგება: თაგვის გარეშე ჩანაწერი მიუწვდომელია.
   ამიტომ ნამდვილი <a> ზის იდენტობის უჯრაში — ის იღებს ფოკუსს, კონტექსტურ მენიუს და
   Ctrl+დაწკაპუნებას. მწკრივის onclick მხოლოდ მოხერხებულობაა და თვითონ ამოწმებს,
   ინტერაქტიულ ელემენტზე ხომ არ დააწკაპუნეს (data-row-href, tbl-rowlink.js). */
table.tbl a.row-link {
  display: block;
  color: inherit;
  text-decoration: none;
}
table.tbl a.row-link:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* §37 — ფილტრის მოქმედებები (გაფილტვრა/გასუფთავება). §123: გასუფთავება
   უტილიტაა და არა წაშლა, ე.ი. .btn-tool და არა .btn-danger. */
.filter-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-control);
}

.pill {
  display: inline-block;
  font-size: 11px;
  padding: 2px 9px;
  border-radius: 10px;
  font-weight: 700;
}

.pill-ok {
  background: var(--primary-light);
  color: var(--primary-text);
}

.pill-warn {
  background: var(--warn-bg);
  color: var(--warn-text);
}

.pill-danger {
  background: var(--danger-bg);
  color: var(--danger-text);
}

.pill-gray {
  background: var(--bg);
  color: var(--text-secondary);
}

/* A contact's positive balance (advance/credit) — deliberately a different color from the debt pill-danger, so it's not confused. */
.pill-credit {
  background: var(--info-bg);
  color: var(--info-text);
}

/* .pill-ok ლურჯია და ასე რჩება: 70 გამოყენებიდან 5 „გაუქმებულია" და 2 „დაბრუნებულია",
   ე.ი. ის საინფორმაციო ჭდეა და არა წარმატების. ნამდვილი მწვანე ცალკე ცხოვრობს. */
.pill-success {
  background: var(--success-bg);
  color: var(--success-text);
}

/* პროცედურის მწკრივი — ჭდის იგივე ტონი, ოღონდ ძლიერ განზავებული */
.row-special {
  background: color-mix(in srgb, var(--special-text) 6%, transparent);
}

/* „პროცედურა" წამალთან შედარებით — კატეგორიაა და არა მდგომარეობა, ამიტომ საკუთარი ტონი */
.pill-special {
  background: var(--special-bg);
  color: var(--special-text);
}

.avatar-sm {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: 700;
  margin-right: 8px;
  vertical-align: middle;
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: var(--text-secondary);
}

/* `.card` is defined once, higher up in the file. The duplicate that stood here was
   commented out during the v0.75.0 `.panel` → `.card` consolidation — but the comment
   was never closed, so it ran on to the next one at „Forms" and took `.badge` with it.
   Three views used `.badge` and rendered as unstyled text the whole time;
   `check_dead_css.php` could not see it, because a commented-out rule is not a defined
   class. */


.badge {
  display: inline-block;
  font-size: 11px;
  padding: 2px 9px;
  border-radius: 10px;
  font-weight: 700;
}

/* --- Forms --- */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 14px;
  min-width: 0;
}

.form-group.full {
  grid-column: 1 / -1;
}

.form-group label {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-secondary);
}

/* A typed search keeps a × so clearing it is one click and not select-all-then-delete. The
   searchable dropdown already had this as `.ss-clear`; this is the same gesture and the same
   look on an ordinary search field, so one action does not have two appearances. The wrapper
   is added by JS around the input alone, which leaves `.search-box`/`.filter-search` still
   laying out an input next to its button. */
.search-clear-wrap {
  position: relative;
  display: inline-flex;
  align-items: stretch;
}

.search-clear {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary);
  opacity: .45;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.search-clear:hover,
.search-clear:focus-visible {
  opacity: 1;
  background: var(--border);
}

/* Room for the button only while it is there — an empty field keeps its full width. */
.search-clear-wrap > input.has-clear {
  padding-right: 32px;
}

/* There was no ::placeholder rule at all, so the browser default applied — around #757575,
   close enough to a typed value that the two were hard to tell apart. One rule, one token.
   opacity:1 is not decorative: Firefox dims placeholders on its own, which would push the
   measured contrast below what was planned. */
::placeholder {
  color: var(--placeholder);
  opacity: 1;
}

/* `.field-control` is the same field OUTSIDE a .form-group — in a table cell, in a filter bar,
   anywhere the stacked label/field/help block would be wrong. It exists because without it the
   only ways to get a bordered input in a <td> were to fake a .form-group (which carries margins
   meant for stacked forms) or to paste the five declarations inline, and /settings/finance had
   already done the second. A field that renders as a raw browser input is the single most
   visible way this UI drifts, and it drifts silently — nothing errors, it just looks unfinished.

   UI_GRAMMAR_SPEC §46 has named this class all along; it simply had no rule behind it. */
.form-group input,
.form-group select,
.form-group textarea,
.field-control {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 9px 12px;
  font-size: var(--fs-base);
  font-family: inherit;
  width: 100%;
  /* Stated rather than left to the browser default, which is white — the reason the dark
     theme needed an override in the first place.

     background-COLOR, never the `background` shorthand. The shorthand resets background-image
     to none, and this rule (0-1-1) outweighs `.ss-chevron-bg` (0-1-0) — so between v0.50.0 and
     v0.61.0 every dropdown inside a .form-group silently lost its chevron. */
  background-color: var(--field-bg);
  color: var(--text-primary);
}

/* checkbox/radio inputs must never inherit the 100% text-field width above —
   otherwise they act as invisible spacers inside flex rows and push label
   text to the far edge of the container. */
input[type="checkbox"],
input[type="radio"] {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  /* Without this the browser paints its own blue, which is ordinary on a light theme and the
     single brightest thing on the page on a dark one — found while checking the `terminal`
     theme's form controls, and it was `dark`'s too for as long as that theme has existed.
     The browser picks the tick's own colour against whatever fill this sets. Does not touch
     `.switch`, which draws its own track. */
  accent-color: var(--primary);
}

.form-group textarea {
  resize: vertical;
  min-height: 70px;
}

/* .form-actions now lives in the VETMAN BUTTON SYSTEM section above, with the rest of
   the spatial grammar — two definitions in one stylesheet is how the ordering drifts. */

/* --- Searchable select (progressive enhancement over <select class="searchable-select">) --- */
.ss-chevron-bg {
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
}

/* The multi-picker's chosen values. Chips rather than a comma-joined string in the field: a
   visit with five coded terms has to show which five and let one of them go without retyping
   the rest, and a text field can do neither. */
.ss-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 5px;
}

.ss-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 6px 2px 9px;
  font-size: var(--fs-micro);
  line-height: 1.7;
  background: var(--primary-light);
  color: var(--primary-text);
  border: 1px solid var(--btn-line);
  border-radius: 999px;
}

.ss-chip-x {
  border: 0;
  background: none;
  padding: 0 2px;
  cursor: pointer;
  color: inherit;
  font-size: 14px;
  line-height: 1;
}

/* A row already chosen stays in the list, dimmed and ticked. Removing it would make the list
   jump under the cursor — the same reason the two-pane picker dims instead of hiding. */
/* The "none" row. Dim, because it is the absence of a choice rather than one of them —
   and it has to be visible: for a plain (non-searchable) select it is the ONLY way back to
   empty, the × being drawn for searchable ones alone. */
.ss-item.ss-item-blank {
  color: var(--text-tertiary);
  font-style: italic;
}

.ss-item.ss-picked {
  opacity: 0.62;
}

.ss-tick {
  float: right;
  font-weight: 700;
  color: var(--primary-text);
}

.ss-wrap {
  position: relative;
  min-width: 0;
}

/* Only what is specific to the searchable field: it is an <input>, so it needs the frame the
   shared field rule gives a select, and a long value has to end in an ellipsis rather than
   scroll. Size, padding, fill and arrow all come from the shared dropdown rule above. */
.ss-wrap .ss-input {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: inherit;
  width: 100%;
  min-width: 0;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  box-sizing: border-box;
}

.ss-wrap.ss-open .ss-input {
  border-color: var(--primary);
}

/* The original <select>, kept in the DOM so the form still posts its value and still works
   with JS off. It must contribute NO layout — and `!important` is here because an inline
   `style="width:100%"` at the call site beat `width: 0` and left a full-width invisible select
   inside `.ss-wrap`. On an ordinary page nothing clips it so nothing shows; inside a
   `.modal-body` (whose `overflow-y: auto` makes `overflow-x` compute to `auto`) it drew a
   horizontal scrollbar across the whole dialog. Same reasoning as the utility layer's
   `!important`: this rule replaces inline style, so it has to outrank it. */
/* The one frame before `searchable-select.js` runs. The script is deferred, so the native
   control is parsed, painted and only then replaced — a long list that appears and disappears
   on every load. `html.js` is stamped before first paint (see layout/app.php), so this hides
   nothing at all when JavaScript is off, which is the whole reason the native select is there.
   The measurements are the same as `.ss-native` below, so the swap costs no layout shift.

   Both classes the app marks deliberately are covered — `ss-plain-select` is enhanced too, it
   simply cannot be typed into. The script's third selector, a bare `.form-group select`, is
   NOT: it is a catch-all, and a page whose script failed to load would lose every select on
   it rather than one. */
html.js select.searchable-select:not(.ss-native),
html.js select.ss-plain-select:not(.ss-native) {
  position: absolute;
  opacity: 0;
  height: 0;
  width: 0;
  min-width: 0;
  padding: 0;
  border: 0;
  pointer-events: none;
}

select.ss-native {
  position: absolute !important;
  opacity: 0;
  height: 0 !important;
  width: 0 !important;
  min-width: 0 !important;
  padding: 0 !important;
  border: 0;
  pointer-events: none;
}

.ss-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  /* No max-height and no flex here on purpose. The height belongs to `.ss-options`, which is
     what scrolls; `.ss-create` then simply follows it and is always visible. An earlier attempt
     made this a flex column with `.ss-options { flex: 1 }` — and `flex: 1` means
     `flex-basis: 0%`, so in a container with no height of its own the options box grew from
     zero into zero free space and the dropdown showed nothing but its footer. */
  overflow: hidden;
}

.ss-options {
  max-height: 220px;
  overflow-y: auto;
}

/* The last row that is not an option — "new contact". Pinned, so it is still there after
   scrolling four hundred names; that is the whole reason it sits inside the list rather than
   next to the field. */
.ss-create {
  display: flex;
  align-items: center;
  gap: 7px;
  flex: none;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  /* Its own ground, and a boundary you can see (v0.162.25, owner instruction #18… #19).
     The row used to share the list's fill and be marked only by a --rule hairline (1.36:1) and
     a coloured icon, which meant it read as one more contact — and a row that reads as an
     option is a row nobody presses. The fill is what fixes that: it stops the eye counting
     this as an entry and makes it the foot of the list. --bg inverts sensibly in the dark
     theme too, where it is darker than the panel rather than lighter.

     The label is deliberately NOT touched. The ask was "a bit more noticeable, not very
     pronounced", and the fill alone answers it; tinting or thickening the text as well is the
     step past what was asked. */
  border-top: 1px solid var(--tbl-frame);
  background: var(--bg);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--fs-small);
  font-weight: 400;
  text-align: left;
  cursor: pointer;
}

/* The icon carries the accent on its own — no chip behind it. It is the only coloured thing
   in the row, which is enough to say "this is an action" without tinting a list. */
.ss-create .ti {
  flex: none;
  font-size: 16px;
  color: var(--primary-text);
}

/* The same hover as every other row in this list, and that is the change: the resting state
   now carries the distinction, so the hover no longer has to. It used to be --tbl-zebra, which
   sat a hair from the --bg the row rests on and would have left the two states telling each
   other apart by 1.02:1 — the same trap the table header and its stripe fell into.
   `--primary-text` on `--primary-light` is a measured pair („ბრენდის ტექსტი ღია ფონზე"). */
.ss-create:hover,
.ss-create:focus-visible {
  background: var(--primary-light);
  color: var(--primary-text);
}

.ss-item {
  padding: 8px 12px;
  font-size: 13px;
  cursor: pointer;
}

.ss-item:hover,
.ss-item.ss-active {
  background: var(--primary-light);
  color: var(--primary-text);
}

.ss-empty {
  padding: 8px 12px;
  font-size: 13px;
  color: var(--text-secondary);
}

/* An <optgroup> heading inside the searchable list. Not selectable and not hoverable — it is a
   label, and giving it the same padding as an item would let it read as one. */
.ss-group {
  padding: 7px 12px 3px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  pointer-events: none;
}
.ss-group + .ss-item {
  border-top: none;
}

.ss-plain-select {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: inherit;
  background-color: var(--field-bg);
  color: var(--text-primary);
}

.flash {
  padding: 10px 16px;
  border-radius: var(--radius-md);
  font-size: 13px;
  margin-bottom: 16px;
}

.flash-success {
  background: var(--primary-light);
  color: var(--primary-text);
}

.flash-error {
  background: var(--danger-bg);
  color: var(--danger-text);
}

.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  align-items: start;
}

/* The pet profile's top region: patient card, contacts, related animals.

   Its own class rather than `.two-col`, which 30-odd other pages share — three across is a
   decision about THIS page's three cards, not about every two-column layout in the app.

   Three columns only once each one can hold its content. The content column is the viewport
   less the 252px sidebar and 2x26px of padding; three tracks with two 18px gaps then give
   (100vw - 340) / 3, which reaches ~380px — the width the patient card needs beside its 140px
   photo — at about 1480px. Below that the animals stack under the contacts they read with,
   and `.two-col`'s own arrangement is what the page had before. */
.pet-top {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  align-items: start;
}

.pet-top-stack {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* `gap` owns the spacing between these two cards, so the card's own stacking margin has to be
   cancelled — a flex/grid gap is added to a margin, never used instead of it. Measured before
   this: the declared 18px rendered as 42px, because `.card` carries `margin-bottom: 24px` for
   ordinary flow. The rule also holds at =>1500px, where the stack is `display: contents` and
   these cards become grid items of `.pet-top`: `display: contents` changes the box tree, not
   which selectors match, so their DOM parent is still the stack. */
.pet-top-stack > .card {
  margin-bottom: 0;
}

/* One row of the contacts / related-animals lists. This is a class rather than the inline style
   it replaces for one reason: the LAST row must not draw a separator, and an inline
   `border-bottom` cannot be overridden without `!important`. A rule under the final row divides
   nothing and leaves the list looking cut off above the card's own padding — the same reason
   `.board-slot`, `.med-slot` and `.jline` each drop theirs, and like them the padding stays. */
.pet-link-row {
  padding: 6px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
}

.pet-link-row:last-child {
  border-bottom: 0;
}

/* Chronic conditions on the pet profile (PetCondition). The name carries the weight a contact's
   link does in the card above; a ruled-out or cured condition stays listed, muted, because what
   became of a positive FIV test is part of the record — but it must not read like a live one. */
.pet-condition-name {
  font-weight: 700;
  margin-right: 4px;
}

.pet-condition-closed .pet-condition-name {
  font-weight: 600;
  color: var(--text-secondary);
}

/* The recommendation to verify a single kind of positive test, and the note that a ruled-out
   condition came back. Warn ink on the panel, not a filled box: the pill beside the name is
   already the thing that catches the eye; this line is what it means. */
.pet-condition-hint {
  color: var(--warn-text);
  margin-top: 2px;
}

@media (min-width: 1500px) {
  .pet-top {
    grid-template-columns: 1fr 1fr 1fr;
  }

  /* The wrapper stops being a box and hands its two cards to the grid, so all three sit on one
     row. `display: contents` is what makes one markup shape serve both arrangements — the
     alternative is printing the cards twice and hiding one set. */
  .pet-top-stack {
    display: contents;
  }

  /* A fourth card would wrap alone into the first column of a second row. It takes the whole
     row instead, so the three cards the owner asked to sit side by side still do. */
  .pet-conditions-card {
    grid-column: 1 / -1;
  }
}

@media (max-width: 900px) {
  .pet-top {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 900px) {

  .two-col,
  .form-grid {
    grid-template-columns: 1fr;
  }

  .mobile-nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .sidebar {
    position: fixed;
    left: 0;
    top: 56px;
    height: calc(100vh - 56px);
    width: 260px;
    transform: translateX(-100%);
    transition: transform .2s ease;
    z-index: 60;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  .sidebar.sb-open {
    transform: translateX(0);
    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.2);
  }

  .sidebar-backdrop {
    position: fixed;
    inset: 56px 0 0 0;
    background: rgba(20, 24, 29, 0.45);
    z-index: 55;
  }

  .sidebar-backdrop.sb-open {
    display: block;
  }

  .content {
    padding: 16px;
  }

  .content-bleed-top {
    margin: -16px -16px 16px -16px;
  }

  /* No background here. Two panel-to-transparent gradients used to sit on this rule, and the
     `background` shorthand they were written in also reset the panel colour to transparent: the
     unstriped rows showed the grey page through, the sticky first column painted white over it,
     and the right-hand gradient drew a white fade down the edge (owner report on /pets,
     2026-09-14). The base `.table-wrap` background applies at every width now. */
  .table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    position: relative;
  }

  table.tbl {
    font-size: 12.5px;
  }

  table.tbl td {
    padding: 12px 10px;
  }

  /* Same height as the desktop header (9px, feedback #24); only the sides narrow. */
  table.tbl th {
    padding: 9px 10px;
  }

  table.tbl th:first-child,
  table.tbl td:first-child {
    position: sticky;
    left: 0;
    background: var(--bg-panel);
    z-index: 1;
  }

  table.tbl th:first-child {
    background: var(--tbl-head);
  }

  /* Same parity and the same exclusion as the stripe itself — this only repaints the sticky
     first column, so if the two disagree that column stripes against the rest of its row. */
  table.tbl tbody tr:nth-child(odd of :not(.tbl-subrow)) td:first-child {
    background: var(--tbl-zebra);
  }

  table.tbl tbody tr:hover:not(.tbl-subrow) td:first-child {
    background: var(--tbl-hover);
  }

  /* Touch targets: every size steps up, and the two smaller ones converge. That is
     correct — on a phone the hierarchy is carried by fill and icon, not by size. */
  .btn {
    min-height: 44px;
  }

  .btn-lg {
    min-height: 48px;
  }

  .btn-sm {
    min-height: 36px;
  }

  .btn-icon {
    width: 44px;
    min-width: 44px;
    flex-basis: 44px;
  }

  .btn-icon.btn-sm {
    width: 36px;
    min-width: 36px;
    flex-basis: 36px;
  }

  .btn-icon.btn-lg {
    width: 48px;
    min-width: 48px;
    flex-basis: 48px;
  }

  .search-box input {
    min-width: 0;
    flex: 1;
  }

  .page-header {
    gap: 10px;
  }

  .app-header {
    padding: 0 12px;
    gap: 10px;
  }

  .app-header-logo {
    flex: none;
  }

  .app-header-logo img {
    height: 26px;
  }

  

  

  /* debug bar (views/partials/debug_bar.php) — position:fixed, nothing had given up
     space for it, so it was covering buttons/content located at the bottom of the page; it's
     worse on mobile, since the summary row wraps into 2-3 lines via flex-wrap on a narrow screen. */
  .has-debug-bar {
    padding-bottom: 90px;
  }

  #debugBarSummary {
    font-size: 11px;
    gap: 10px;
    padding: 5px 10px;
  }

  #debugBarSummary .dbg-extra {
    display: none;
  }

  .sb-item {
    padding: 11px 16px;
  }
}

/* "Card" restack for opted-in tables (add data-label="..." to each <td> — see views/pets/index.php).
   `.tbl-cards` opts a table into this; other .tbl tables keep the horizontal-scroll behavior above. */
@media (max-width: 600px) {
  /* The same visual language that views/ambulatory/board.php's cards have (border+radius+
     bg-panel, avatar+name header, thin gray meta lines) — not a separate "broken-down
     version of a table row". */
  table.tbl.tbl-cards thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }

  table.tbl.tbl-cards tbody tr {
    display: block;
    position: relative;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    margin-bottom: 10px;
    padding: 10px 40px 10px 10px;
  }

  table.tbl.tbl-cards tbody tr:last-child {
    margin-bottom: 0;
  }

  /* In card mode the table stops being a table: every row is a framed card, so the wrapper's own
     frame becomes a box behind a stack of boxes. The owner saw exactly that („ეს რაღაც უკან
     ბოქსის კიდეები რატომ ჩანს?") — its 1px edge ran one pixel outside each card's, doubling the
     line, and showed through the 10px gaps between cards with a 12px corner behind their 8px ones.

     This is NOT an exception to „a table always carries its own frame" (owner rule, 2026-08-31):
     above 600px the wrapper is untouched. Below it there is no table to frame — the cards are the
     frames. `/pets` and `/contacts` both wrap theirs in `.card.card-table` (the same since 2026-09-14);
     other lists use `.table-wrap` for the same job, so both are listed; an empty list shows no table and keeps its frame.
     `overflow` is left alone on purpose: a list whose cards are wider than the screen
     should stay clipped here rather than drag the page sideways. Descendant `:has()`, not `>`,
     so a wrapper div between the two cannot quietly break it. */
  .table-wrap:has(table.tbl-cards),
  .card-table:has(table.tbl-cards) {
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
  }

  /* In card mode the ROW is the surface and a cell never paints — so the reset carries
     `!important`, on purpose. Three table rules paint cells and each one out-ranked the last fix:
     the zebra `.tbl tbody tr:nth-child(odd of :not(.tbl-subrow)) td` is (0,3,3), its 900px
     first-column twin `…td:first-child` is (0,4,4), and the row hover is (0,3,3). Against a plain
     (0,2,4) they won, and every other card carried a grey block that stopped 40px short of its
     right edge, where the row's padding begins. Chasing that with a higher number would lose to
     the next paint rule anyone adds; there is no cell background card mode wants. */
  table.tbl.tbl-cards tbody tr td {
    background: transparent !important;
  }

  /* `tbody tr td`, not `td`: (0,2,4) outranks `table.tbl.table-compact td` (0,2,2) whatever the
     order. Both lists took `.table-compact` after this block was written, and that rule sits
     further down the file, so at equal weight it won — each „label: value" line became a 36px box
     with 6px padding. On a table cell `height` is a minimum; on a cell turned `display: block` it
     is a fixed height, so it is reset here too.

     `position: static` lives HERE, at (0,2,4): enough to undo the 900px sticky first column
     (0,2,2), and deliberately NOT enough to beat `td.tbl-card-actions` (0,3,2), whose
     `position: absolute` is what keeps the buttons in the card's top corner. */
  table.tbl.tbl-cards tbody tr td {
    display: block;
    position: static;
    height: auto;
    padding: 0;
    border-bottom: none;
    font-size: 11.5px;
    color: var(--text-secondary);
    line-height: 1.6;
  }

  table.tbl.tbl-cards td[data-label]::before {
    content: attr(data-label) ": ";
  }

  /* In card mode a cell is a "label: value" line, not a column — right-aligning one of them
     leaves it hanging off the edge while every sibling stays left. The tabular figures stay;
     only the alignment is dropped. */
  table.tbl.tbl-cards td.num {
    text-align: left;
  }

  table.tbl.tbl-cards td.tbl-card-lead {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 4px;
    font-size: 13px;
    font-weight: 700;
    color: var(--text-primary);
  }

  table.tbl.tbl-cards td.tbl-card-actions {
    position: absolute;
    top: 8px;
    right: 8px;
    padding: 0;
  }

  /* 80px, the same photo the card view draws (`.ecard-avatar`, owner's choice 2026-09-14). The
     tile's icon or initials scale with it; the list above 600px keeps its 40px. */
  table.tbl.tbl-cards td.tbl-card-lead .avatar-sm {
    width: 80px;
    height: 80px;
    font-size: 34px;
    font-weight: 400;
  }
}

/* --- Login --- */
/* Theme-matched abstract artwork with a solid fallback while the image loads. */
.login-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--bg);
  background-image: url(/assets/img/dashboard-illustration-light-abstract-v1.webp);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  padding: 20px;
}

/* Each supported palette uses its corresponding abstract artwork. */
html[data-theme="pharmacy"] .login-shell {
  background-image: url(/assets/img/dashboard-illustration-pharmacy-abstract-v1.webp);
}

html[data-theme="dark"] .login-shell {
  background-image: url(/assets/img/dashboard-illustration-dark-abstract-v1.webp);
}

html[data-theme="terminal"] .login-shell {
  background-image: url(/assets/img/dashboard-illustration-terminal-abstract-v1.webp);
}

.login-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 36px 34px;
  width: 100%;
  max-width: 400px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}

.login-logo {
  display: flex;
  align-items: center;
  margin-bottom: 22px;
}

.login-logo-img {
  height: 38px;
  width: auto;
  display: block;
}

.login-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text-primary);
}

.login-sub {
  font-size: 12.5px;
  color: var(--text-secondary);
  margin-bottom: 20px;
}

/* --- Flatpickr (datepicker.js) — restyled with the app's design variables, instead of the default CDN theme --- */
.flatpickr-calendar {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  font-family: inherit;
  overflow: hidden;
  padding-bottom: 6px;
}
.flatpickr-calendar.arrowTop:before,
.flatpickr-calendar.arrowTop:after {
  display: none;
}

/* Month/year — same background as the calendar body (var(--bg-panel)), noticeable as a "selectable" pill */
.flatpickr-months {
  background: var(--bg-panel);
  padding: 10px 4px;
  border-bottom: 1px solid var(--border);
}
.flatpickr-months .flatpickr-month {
  color: var(--text-primary);
  fill: var(--text-secondary);
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.flatpickr-current-month {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text-primary);
  padding: 5px 12px;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.flatpickr-current-month span.cur-month {
  cursor: default;
}
.flatpickr-current-month:focus-within {
  border-color: var(--primary-light);
  background: var(--primary-light);
}
.flatpickr-current-month .flatpickr-monthDropdown-months,
.flatpickr-current-month input.cur-year {
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text-primary);
  background: transparent;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: none;
  box-shadow: none;
  cursor: pointer;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:focus,
.flatpickr-current-month input.cur-year:focus {
  outline: none;
}
.flatpickr-current-month .flatpickr-monthDropdown-months option {
  color: var(--text-primary);
  background: var(--bg-panel);
}
/* Changing the year via scroll/text only — the default small arrows aren't needed */
.numInputWrapper span.arrowUp,
.numInputWrapper span.arrowDown {
  display: none;
}
.numInputWrapper {
  width: auto !important;
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
  fill: var(--text-secondary);
  padding: 8px;
  border-radius: var(--radius-md);
}
.flatpickr-months .flatpickr-prev-month:hover,
.flatpickr-months .flatpickr-next-month:hover {
  fill: var(--primary);
  background: var(--primary-light);
}

.flatpickr-weekdays {
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
span.flatpickr-weekday {
  color: var(--text-tertiary);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  background: transparent;
}
.flatpickr-days {
  padding: 6px 8px 2px;
}
.flatpickr-day {
  color: var(--text-primary);
  border-radius: var(--radius-md);
  font-size: 13px;
  font-weight: 400;
}
.flatpickr-day.today {
  border-color: var(--primary);
  font-weight: 700;
}
.flatpickr-day:hover,
.flatpickr-day:focus {
  background: var(--primary-light);
  border-color: var(--primary-light);
  color: var(--primary-text);
}
.flatpickr-day.selected,
.flatpickr-day.selected:hover,
.flatpickr-day.selected:focus,
.flatpickr-day.today.selected {
  background: var(--primary-light);
  border-color: var(--primary-light);
  color: var(--primary-text);
  font-weight: 700;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay {
  color: var(--text-tertiary);
  opacity: 0.45;
}

.flatpickr-time {
  border-top: 1px solid var(--border);
  height: 42px;
}
.flatpickr-time input,
.flatpickr-time .flatpickr-time-separator,
.flatpickr-time .flatpickr-am-pm {
  color: var(--text-primary);
  font-weight: 700;
}
.flatpickr-time input:hover,
.flatpickr-time input:focus,
.flatpickr-time .flatpickr-am-pm:hover,
.flatpickr-time .flatpickr-am-pm:focus {
  background: var(--primary-light);
}

input.flatpickr-input,
input.flatpickr-alt-input {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 9px 12px;
  font-size: 13px;
  font-family: inherit;
  background: var(--bg-panel);
  color: var(--text-primary);
  transition: border-color 0.15s ease;
}
input.flatpickr-input:focus,
input.flatpickr-alt-input:focus {
  border-color: var(--primary);
  outline: none;
  box-shadow: 0 0 0 3px var(--primary-light);
}

/* --- Tabs (tabs.js): folder-style variant — the active tab shares the content's
   background (--bg-panel) and border, so it reads as attached to the panel below it;
   inactive tabs sit at the page's own background (--bg) so they recede. --- */
/* The nav overlaps the panel's top border by 1px and paints above it, so the active tab can
   erase the line beneath itself. The 1px padding is what keeps that overhang inside the
   nav's own clip box — `overflow-x: auto` also clips vertically, which would otherwise cut
   it off and leave the border showing under the active tab. */
.tabs-nav {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  margin-bottom: -1px;
  padding-bottom: 1px;
  position: relative;
  z-index: 1;
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs-nav::-webkit-scrollbar {
  display: none;
}
.tab-btn {
  appearance: none;
  border: 1px solid transparent;
  /* An inactive tab carries the panel's top line itself — the nav sits over that border,
     so without this its background would punch a gap in the line. */
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 700;
  font-family: inherit;
  padding: 10px 16px;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  cursor: pointer;
  margin-bottom: -1px;
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: 7px;
  transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}
.tab-btn i {
  font-size: 16px;
}
.tab-btn:hover {
  color: var(--primary-hover, var(--primary));
}
.tab-btn.active {
  background: var(--bg-panel);
  border-color: var(--border);
  border-bottom-color: var(--bg-panel);
  color: var(--primary-text);
  font-weight: 700;
}
.care-type-btn.active {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}
.tab-btn .badge-count {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  background: var(--bg-subtle, var(--bg));
  color: var(--text-muted, var(--text-secondary));
}
.tab-btn.active .badge-count {
  background: color-mix(in srgb, var(--primary) 20%, transparent);
  color: var(--primary);
}
.tab-panel {
  display: none;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 0 var(--radius-md) var(--radius-md) var(--radius-md);
  padding: 20px;
  margin-bottom: 18px;
}
.tab-panel.active {
  display: block;
}

/* §68 — ჩანართის შიგნით სექცია ჩარჩოიანი ბლოკია, არა უწყვეტი ზედაპირის ნაწილი.
   აქამდე ისინი ბრტყელდებოდა და ერთმანეთისგან მხოლოდ თმისხელა ხაზით გამოიყოფოდა.
   ორ-სამ მოკლე სექციაზე ეს მუშაობს; „მკურნალობის" ჩანართში შვიდი სექციაა და
   თითოეულს თავისი ცხრილი, ფორმა ან გრაფიკი აქვს — ერთნაირი ვერტიკალური რიტმით
   სათაურის ზემოთ და ქვემოთ თვალი ვეღარ ჯგუფავს, სად იწყება და სად მთავრდება ერთი.
   ფონს არ იცვლის (პანელი უკვე თეთრია, მეორე თეთრი არაფერს იტყოდა) — მას საზღვარი
   და საკუთარი შევსება აქვს, ზუსტად ისე, როგორც `.card-nested`-ს.
   ბარათი ბარათში თავისას ინარჩუნებს — სექცია ჩარჩოიანია, სექციის შიგნით ყუთი კი არა.
   2026-08-31-მდე ეს პირდაპირი შვილით იყო დაწერილი, რაც სხვას ნიშნავდა: სტაციონარის
   „მკურნალობის" ბარათი `<div id="stayMedications">`-ში ზის (AJAX-ის განახლების არეა)
   და სწორედ ამ ერთი შუალედური div-ის გამო რჩებოდა თეთრი და სხვა ჩარჩოთი, გვერდით
   მდგარ ლაბორატორიასა და ჟურნალთან შედარებით. მთელ აპში ეს ერთადერთი ასეთი ბარათი
   იყო (დათვლილია) — ანუ შუალედური არე იმალება, ბარათი ბარათში კი ქვემოთ გამოირიცხება. */
/* --tbl-frame and not --border on these three rules (v0.162.16). A card on an ordinary page
   is white on the grey page ground, so its fill does the separating and a 1.24-1.90:1 hairline
   costs nothing. Inside a panel the fill was taken away — the cards went transparent in
   v0.162.7 so every section would look alike — which leaves the edge as the ONLY thing saying
   where the box is, and at --border there was effectively nothing there. That is why a table
   in the billing section read as hanging in mid-air: at the time the table's own frame was
   stripped inside a card, so the card's edge was the only box and it was invisible. Tables
   carry their own frame again since 2026-08-31 (see below), but this token stays: the card
   still has to say where the SECTION begins, and two nested edges of different weight is
   exactly how a section holding several tables reads correctly. */
.tab-panel .card {
  background: transparent;
  border: 1px solid var(--tbl-frame);
  border-radius: var(--radius-md);
  --card-pad: 14px;
  padding: var(--card-pad);
  margin-bottom: 14px;
}

.tab-panel > .card:last-child {
  margin-bottom: 0;
}

/* §68 — ერთადერთ სექციას საზღვარი არ სჭირდება: პანელი თავად არის ის ყუთი, და ორი
   ერთნაირი ჩარჩო ერთმანეთში არაფერს ამბობს. */
.tab-panel > .card:only-child {
  border: none;
  border-radius: 0;
  padding: 0;
}

/* The exception, and the only one: a card INSIDE a card is not a section of the panel. The
   two ward lanes had their own copy of the rule above until 2026-08-31 (`.tab-panel >
   .ward-body > .ward-work > .card` and the same for `.ward-side`), written as direct children
   so that a card nested deeper would keep its own fill. The rule above is descendant now, so
   that intent is stated once, here, and any wrapper — a refresh region, a form, a lane added
   later — stops mattering. */
.tab-panel .card .card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 18px;
}

/* --- Theme switcher ---
   `.material-symbols-outlined` lived here until v0.141.0, when Google Fonts was dropped: the
   twelve icons it drew are all in Tabler, which was already loaded. The sizing it used to set
   is now the icon font's own. */
.theme-switcher {
  position: relative;
}
.theme-switcher-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  font-weight: 700;
  padding: 6px 12px;
  border-radius: var(--radius-md);
  border: 1.5px solid var(--border);
  background: var(--bg-panel);
  color: var(--text-primary);
  cursor: pointer;
  font-family: inherit;
  transition: all 0.15s ease;
}
.theme-switcher-trigger:hover {
  border-color: var(--primary);
  background: var(--primary-light);
}
.theme-switcher-trigger .ti {
  font-size: 18px;
}
.theme-switcher-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 240px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  padding: 4px;
  z-index: 200;
  /* `placeSwitcherMenu()` caps the height when neither side has room for the whole list. */
  overflow-y: auto;
}
.theme-switcher.open .theme-switcher-menu {
  display: block;
}
.theme-switcher-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: 12.5px;
  color: var(--text-primary);
}
.theme-switcher-option:hover {
  background: var(--bg);
}
.theme-switcher-option.active {
  background: var(--primary-light);
  font-weight: 700;
}

/* The selected row's ink, and it has to be set on the TEXT rather than on the row.
   Five themes give the sidebar a dark dropdown and paint every option `#F1F5F9`
   (`html[data-theme="…"] .sb-system-dropdown .theme-switcher-option`, specificity (0,3,1)).
   The active row then puts `--primary-light` — a LIGHT fill — under that near-white ink, so
   the theme you are actually using was the one you could not read. Any rule on the row itself
   would have to out-specify a (0,3,1) selector repeated five times; a rule on the child wins
   without that, because a declared value always beats an inherited one no matter how specific
   the ancestor's was.
   `--primary-text` on `--primary-light` is a pair check_contrast.php already measures
   („ბრენდის ტექსტი ღია ფონზე"), so this needs no new token and no new deviation. */
.theme-switcher-option.active .theme-switcher-option-name,
.theme-switcher-option.active .theme-switcher-option-sub {
  color: var(--primary-text);
}

/* The subtitle needs one class more. The three dark-dropdown themes grey every subtitle with
   `html[data-theme="…"] .sb-system-dropdown .…-option-sub` (0,3,1), which outranks the rule
   above (0,3,0) — the selected row's subtitle read #94A3B8 on a light fill, about 2:1. */
.sb-system-dropdown .theme-switcher-option.active .theme-switcher-option-sub {
  color: var(--primary-text);
}
.theme-switcher-option .ti {
  font-size: 18px;
}

/* Two modifiers for the same control outside the sidebar (v0.209.5). A card's own top row is a
   small row — the language pills beside it are 11px — and a menu hung from the right edge of a
   trigger that sits at the LEFT of a 400px card would run off it. */
.theme-switcher-sm .theme-switcher-trigger {
  font-size: 11.5px;
  padding: 4px 9px;
  gap: 6px;
}

.theme-switcher-sm .theme-switcher-trigger .ti {
  font-size: 15px;
}

.theme-switcher-left .theme-switcher-menu {
  right: auto;
  left: 0;
}

/* The language pair, wearing the theme trigger's clothes (owner, v0.209.5): the same border,
   panel fill, radius and hover — but no dropdown, because two options do not need one hidden
   behind a click. The chosen one is lit the way the theme menu lights its current row, so the
   two controls beside each other read as one family and not as two ideas. */
.lang-seg {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px 5px 3px 8px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  transition: border-color .15s ease;
}

.lang-seg:hover {
  border-color: var(--primary);
}

.lang-seg > .ti {
  font-size: 15px;
  color: var(--primary);
  margin-right: 2px;
}

.lang-seg > a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--text-secondary);
  text-decoration: none;
  transition: background-color .15s ease, color .15s ease;
}

.lang-seg > a:hover {
  background: var(--bg);
  color: var(--text-primary);
}

/* `--primary-text` on `--primary-light` is the pair `check_contrast.php` already measures
   („ბრენდის ტექსტი ღია ფონზე"), so this needs no token of its own. */
.lang-seg > a[aria-current="page"] {
  background: var(--primary-light);
  color: var(--primary-text);
}
.theme-switcher-option-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}
.theme-switcher-option-sub {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-secondary);
}

/* The trigger's label had no rule of its own, which is the whole of the second bug: with no
   `flex`/`min-width` it could not shrink, and with no `nowrap` a long name („ზურმუხტისფერი
   მწვანე") wrapped onto two lines — which the button's `align-items: center` then centred,
   squeezing the icon and the caret to the edges. It takes the free space, stays on one line
   and truncates instead. Left-aligned explicitly: the row reads icon → name → caret, and a
   centred name in a left-aligned row is the thing that looked wrong even before it wrapped. */
.theme-switcher-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

/* --- Organization rows (the user menu's organization list, feedback #25) --- */
.org-switcher-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  color: var(--text-primary);
}
.org-switcher-option:hover {
  background: var(--bg);
}
.org-switcher-option .ti:first-child {
  font-size: 20px;
  color: var(--text-secondary);
}
.org-switcher-option-logo {
  width: 24px;
  height: 24px;
  object-fit: contain;
  border-radius: 4px;
  background: #fff;
  border: 1px solid var(--border);
}
.org-switcher-option.active {
  background: var(--primary-light);
}
.org-switcher-option.active .ti:first-child {
  color: var(--primary);
}
.org-switcher-option-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
  flex: 1;
}
.org-switcher-option-name {
  font-size: 13.5px;
  font-weight: 700;
}
.org-switcher-option.active .org-switcher-option-name {
  color: var(--primary-text);
}
.org-switcher-option-sub {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.org-switcher-check {
  font-size: 18px;
  color: var(--primary);
}

/* --- Settings hub: categorized grid cards --- */
.settings-category {
  margin-bottom: 26px;
}
.settings-category-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 10px;
}
.settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.settings-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.settings-card:hover {
  border-color: var(--primary);
  box-shadow: 0 4px 14px rgba(0,0,0,0.08);
  transform: translateY(-1px);
}
.settings-card-icon {
  width: 40px;
  height: 40px;
  min-width: 40px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  background: var(--primary-light);
  color: var(--primary);
}
.settings-card-title {
  font-size: 14px;
  font-weight: 700;
  margin-bottom: 3px;
}
.settings-card-desc {
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.4;
}
.settings-back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-secondary);
  text-decoration: none;
  margin-bottom: 16px;
}
.settings-back-link:hover {
  color: var(--primary);
}

/* A field the catalogue filled in, not the vet. Tinted rather than badged: the value is a
   suggestion the prescriber signs for, so it has to be visibly not-yet-theirs while staying
   an ordinary editable input. The tint clears on the first keystroke. */
.form-group input.autofilled,
input.autofilled {
  background-color: #ecfdf5;
  border-color: #10b981;
}

/* Clear button for a searchable free-text input (SearchableInput). Faint until hovered —
   it is a convenience, not an action to be drawn towards, and it sits on top of a field
   whose value is the thing being read. */
.ss-wrap .ss-clear {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary);
  opacity: 0.45;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.ss-wrap .ss-clear:hover,
.ss-wrap .ss-clear:focus-visible {
  opacity: 1;
  background: var(--border);
}

/* Room for the button, only while it is there — an empty field keeps its full width. */
.ss-wrap input.ss-has-clear {
  padding-right: 32px;
}

/* A SearchableSelect field keeps its dropdown chevron alongside the clear button —
   shift the button left of the chevron instead of on top of it, and widen the
   reserved space to fit both. */
.ss-wrap .ss-clear.ss-clear-chevron {
  right: 26px;
}

/* Room for the arrow. `.ss-input` reserves it in its own padding; a free-text field enhanced
   in place inherits .form-group's 12px and would run its text under the arrow. The .ss-has-clear
   rule below is more specific and still wins when both are showing. */
.ss-wrap input.ss-chevron-bg {
  padding-right: 28px;
}

.ss-wrap input.ss-chevron-bg.ss-has-clear {
  padding-right: 52px;
}

/* --- Prescription row risk marking ---------------------------------------------------
   The safety panel names a finding; these put it back on the row the vet is editing.
   `border-left-color` needs !important because the row card sets its left border inline
   (procedure rows are purple), and a class cannot otherwise win against that.
   Colours match the panel's severity styles exactly — one severity, one colour, both
   places, or the pairing stops being readable at a glance. */
.medication-row-card.risk-contraindicated { border-left-color: #dc2626 !important; }
.medication-row-card.risk-major           { border-left-color: #ea580c !important; }
.medication-row-card.risk-minor           { border-left-color: #ca8a04 !important; }

.row-risk-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 700;
  line-height: 1.2;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
}
.risk-contraindicated .row-risk-flag { background: #fee2e2; color: #7f1d1d; }
.risk-major .row-risk-flag           { background: #ffedd5; color: #7c2d12; }
.risk-minor .row-risk-flag           { background: #fef9c3; color: #713f12; }

/* The tint is what makes a contraindicated row findable in a list of six without reading
   any of them. Kept to the card only — the fields inside stay on the normal surface, so
   text contrast is unchanged and the row is still ordinary to type into. */
.medication-row-card.risk-contraindicated { background: #fef2f2 !important; }
html[data-theme="dark"] .medication-row-card.risk-contraindicated { background: #2a1215 !important; }
/* Warmer than dark's, to sit on this theme's brown-black rather than its blue-black. */
html[data-theme="terminal"] .medication-row-card.risk-contraindicated { background: #2A1712 !important; }

/* A category heading inside a table, instead of a category column that repeats itself.
   The lab catalogue's 111 parameters carry only 5 distinct categories, so the column was
   ~106 redundant cells; as a group row the value is stated once and its width goes to the
   columns that need it. Any grouped .tbl can use this — it assumes nothing lab-specific. */
tr.tbl-group-row td {
  background: var(--bg-subtle, var(--bg));
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 7px 10px;
}
tr.tbl-group-row .tgr-name {
  font-weight: 700;
  font-size: 12.5px;
  letter-spacing: 0.3px;
}
tr.tbl-group-row .tgr-count {
  margin-left: 8px;
  font-size: 11.5px;
  color: var(--text-tertiary);
}
html[data-theme="dark"] tr.tbl-group-row td,
html[data-theme="terminal"] tr.tbl-group-row td { background: rgba(255, 255, 255, 0.04); }

/* In card mode every tbody tr becomes a bordered card (see .tbl-cards below). A group row
   holds no record, so it must read as a heading between cards rather than as an empty one. */
@media (max-width: 600px) {
  table.tbl.tbl-cards tbody tr.tbl-group-row {
    display: block;
    background: transparent;
    border: none;
    border-radius: 0;
    padding: 6px 0 2px;
    margin-bottom: 4px;
  }
  table.tbl.tbl-cards tbody tr.tbl-group-row td {
    display: block;
    background: transparent;
    border: none;
    padding: 0;
  }
}

/* =========================================================
   VETMAN UI SYSTEM
   ---------------------------------------------------------
   Page → filter region → content → actions. The button system above already covers
   actions; this covers everything they sit inside. Two rules run through all of it:

     A container never repeats its own frame. `.table-wrap` is itself a card — same
     background, border and radius — and it sat inside a `.panel` in 135 places, two
     identical boxes with 18px of padding wasted between them. `.card-table` is the fix.

     Density is a property of a region, not of a page. A patient record is roomy at the
     top and dense inside its embedded history tables, so `.dense` goes on the table.
   ========================================================= */

/* --- Page header --- */
.page-identity {
  display: flex;
  flex-direction: column;
  /* 2px was right when the second line was a context sentence — part of the heading. A
     control belongs to itself and needs to sit apart from the title, or it reads as a
     subtitle that happens to have a switch in it. */
  gap: var(--sp-control);
  min-width: 0;
}

/* None of the 135 page headers said what you were looking at. One line — a count, a filter
   state, an owner — answers it without opening anything. */
.page-context {
  font-size: var(--fs-small);
  color: var(--text-secondary);
  margin: 0;
}

/* The context line can carry a link — the owner of a patient, say. Coloured so it reads as
   clickable, underlined only on hover so a quiet second line stays quiet. */
.page-context a {
  color: var(--primary-text);
  text-decoration: none;
}

.page-context a:hover,
.page-context a:focus-visible {
  text-decoration: underline;
}

/* An identity with a leading icon. Put the icon in its own column and the title and its
   context line share one left edge — otherwise the context starts under the ICON while the
   name starts after it, and the two lines read as belonging to different things. */
.page-identity.has-icon {
  flex-direction: row;
  align-items: flex-start;
  gap: 10px;
}

/* §25 — სათაურში პაციენტის სახელი ბმულია (მოდულის ჩანაწერიდან პროფაილზე), მაგრამ
   სათაურად რჩება: შვიდი ხედი ამას `style="color:inherit"`-ით წერდა. */
.page-title-link {
  color: inherit;
  text-decoration: none;
}
.page-title-link:hover { text-decoration: underline; }

/* ჭდე, რომელიც ბმულია — ზომა ჭდისაა, ქცევა ბმულის */
.pill-link {
  text-decoration: none;
  font-size: var(--fs-small);
  padding: 4px 10px;
}

/* §25 — იმავე ცხოველის სხვა ჩანაწერები იმავე მოდულში, ერთ ხაზად სათაურის ქვეშ.
   ვიწრო ეკრანზე ჰორიზონტალურად გადაიხვევა და არა გვერდს — იგივე წესი, რაც
   `.table-wrap`-ს აქვს. */
.record-siblings {
  display: flex;
  align-items: center;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
  margin-bottom: var(--sp-group);
}
.record-siblings::-webkit-scrollbar { display: none; }
.record-siblings-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--text-secondary);
  margin-right: 2px;
}
.record-sibling {
  flex: none;
  /* §73 — 28px, იგივე, რაც `.btn-sm`-ს: ეს თითით დასაწკაპუნებელი სამიზნეა */
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  padding: 0 10px;
  border: 1px solid var(--btn-line);
  border-radius: 999px;
  background: var(--bg-panel);
  font-size: var(--fs-small);
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
}
.record-sibling:hover {
  color: var(--text-primary);
  background: var(--tbl-zebra);
}
.record-sibling:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}
.record-sibling.is-current {
  background: var(--action-bg);
  border-color: var(--action-bg);
  color: var(--on-action);
  font-weight: 700;
}
.record-siblings-all {
  flex: none;
  font-size: var(--fs-small);
  color: var(--primary-text);
  white-space: nowrap;
  margin-left: 2px;
}

/* §60/§68 — პაციენტის კლინიკური გაფრთხილებები სათაურის ქვეშ, ერთ ბლოკად */
.pet-alerts {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--sp-group);
}

.page-identity.has-icon > .ti {
  flex: none;
  font-size: 22px;
  /* Optically centred on the title's cap height rather than its line box. */
  line-height: 1.2;
  color: var(--text-secondary);
}

.page-identity.has-icon > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}




.card-footer {
  margin-top: var(--sp-group);
  padding-top: var(--sp-group);
  border-top: 1px solid var(--border);
}

/* A card whose whole body is a table: the table IS the card, so the card gives up its
   padding instead of drawing a second frame around one. */
/* Same grey as the row lines, and now at the same strength too — an outer border that is
   darker than the lines inside it is a seam you notice before you notice the data, which is
   exactly how it read at --tbl-frame. */
.card-table,
.table-wrap {
  border-color: var(--rule);
}

.card-table {
  padding: 0;
  /* Same as .table-wrap: a table wider than its card scrolls instead of being cut off. */
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
}

/* A table always carries its own frame — owner instruction, 2026-08-31, and it replaces the
   rule that used to live here.

   That rule stripped `.table-wrap`'s border wherever it sat inside a `.card`: `.table-wrap` is
   itself a card (same background, border and radius), so a table in a card drew two identical
   boxes, one inside the other, in 49 files. Removing the inner one was right where the card
   held nothing but that table; it was wrong everywhere else, and the stationary billing
   section is why. That card is a ledger, a total, a form, a hint AND the rate history — the
   card's frame is around the section, so the tables inside it had no boundary at all.

   Two narrower rules were tried on the way and both were dropped: flattening every table in a
   card (13 more places gained the defect above), then flattening only single-table cards (the
   answer then depended on what else happened to be in the box, so the same table looked
   different on two screens). The instruction is the simpler rule and it is the one that holds:
   the table is the box, every time, however deep it sits and whatever shares the card with it.

   So there is no rule here now — `.table-wrap`'s own declaration is all of it. A card that
   genuinely should not draw a second frame around its table takes `.card-table` (padding 0) at
   the call site; that is what the class is for. */





/* Every cell gets tabular figures, not just `.num`. A left-aligned date column is still a
   column of digits — `10 აგვ 2026` over `08 ივლ 2026` only reads straight down if 1 and 0
   occupy the same width as 8. `.num` needed this for money and got it; a date needs it for
   exactly the same reason and was never given it. Applying it to the whole cell also covers
   ids, weights and doses sitting in ordinary columns. */
table.tbl td,
table.tbl th {
  font-variant-numeric: tabular-nums;
}

/* Right-aligned was already the intent in 149 inline styles. What they could not do is line
   the digits up: proportional figures make 1 narrower than 8, so columns of numbers drift. */
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* `table.tbl th`-ს სპეციფიკურობა 0-1-2-ია და `.num`-ს (0-1-0) სჯობნის,
ე.ი. სათაური
   მარცხნივ რჩებოდა,
უჯრა კი მარჯვნივ — ერთი სვეტი ორად გახლეჩილი. `.ta-r`-ს ეს არ
   ემართებოდა,
რადგან უტილიტების ფენას `!important` აქვს; `.num` კომპონენტია და მას
   არა. ამიტომ სელექტორი აქ ისწორებს ბალანსს,
და არა `!important`. (§125) */
table.tbl th.num,
table.tbl td.num {
  text-align: right;
}

.tbl .ident {
  font-weight: 700;
  color: var(--text-primary);
}

.tbl .avatar-sm {
  margin-right: var(--sp-control);
}

/* The name stands beside its avatar as a block of its own. Inline, a name that wrapped in a
   narrow column put its second word under the avatar (owner report, 2026-09-14). */
.tbl td.ident > .row-link:has(> .avatar-sm) {
  display: inline-flex;
  align-items: center;
}

/* Nothing inside a cell may decide the row's height — a 40px tile in one column sets the
   height of every other column with it. */
.tbl td > .ti {
  color: var(--text-tertiary);
  vertical-align: -1px;
}

/* A sortable header is a link, but it must not read as body copy that happens to be blue —
   it inherits the header's own colour and announces itself by the direction mark beside it. */
/* Deliberately plain inline, not inline-flex. A flex box inside the cell lays out on
   different rules than the bare text in a non-sortable header, so the two kinds of header
   sat differently in the same row. Inline makes `<th><a>label ⇅</a></th>` render exactly
   like `<th>label</th>` — same baseline, same box, same band. */
.th-sort {
  color: inherit;
  text-decoration: none;
}

.th-sort:hover {
  color: var(--text-primary);
}

/* The direction mark. It was an inline opacity:0.3, which put the unsorted arrows below
   every contrast floor at once and made the header look faded rather than quiet. Colour
   carries "inactive" instead, and the active one takes the header's own ink.

   A Tabler icon since v0.229.4, not a character: `⇅ ↑ ↓` are in no Noto family, so the UI
   font's stack decided how they looked. Sized in `px` and not `--fs-micro` (11px): an icon
   is drawn on its own em box with padding built in, so it reads a size smaller than a
   letter of the same declared size. `vertical-align` sits it on the label's baseline -
   without it the icon rides high enough to lift the header's line box. */
.th-sort-mark,
.sort-arrow {
  font-size: 14px;
  line-height: 1;
  vertical-align: -2px;
  color: var(--text-tertiary);
  letter-spacing: 0;
  margin-left: var(--sp-tight);
}

.th-sort-mark.is-active,
.sort-arrow.is-active {
  color: var(--text-primary);
}

.th-sort:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}





/* --- Form sections --- */



.form-grid {
  display: grid;
  gap: 0 var(--sp-group);
}

/* Two columns is the ceiling. Pair fields that belong together — species and breed, date and
   time, quantity and unit — never two unrelated fields that happen to fit side by side, since
   proximity is read as relationship whether or not one was meant. */
.form-grid-2 {
  grid-template-columns: 1fr 1fr;
}


/* --- Filter region --- */
/* --bg-panel, not --bg: the page ground is already --bg, so a --bg bar would have no fill
   contrast at all and the whole boundary would rest on a 1px --border, which measures
   1.24–1.90:1 against a panel in every theme. */
/* A layout row and nothing else. It carried a panel background and a border, which put a
   second empty box above every list for the sake of holding two controls. The controls
   inside carry their own edges. */
.filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-group);
  flex-wrap: wrap;
  margin-bottom: var(--sp-section);
}

/* --- Section navigation --- */
/* Peer sections of one module, where one of them is always the page you are on: the five
   reference catalogues, the two template kinds. Distinct from the overflow menu, which
   holds things that are NOT peers of this page — hiding a peer would hide where you are.

   Underlined rather than filled: these are places, and the button system's fills are for
   actions. The current one is marked by weight, ink and the rule beneath it, so the state
   survives without colour. */
.section-nav {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-section);
}

.section-nav > a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 0;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  font-size: var(--fs-base);
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease;
}

.section-nav > a:hover {
  color: var(--text-primary);
}

.section-nav > a[aria-current="page"] {
  color: var(--text-primary);
  font-weight: 700;
  border-bottom-color: var(--action-bg);
}

.section-nav > a:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 3px;
}

@media (max-width: 900px) {
  .section-nav {
    gap: 16px;
    overflow-x: auto;
    scrollbar-width: none;
  }
}

/* --- Split action and overflow menu --- */
/* One action with two ways to start it: order a test, or record a result that came back
   from outside. They were two separate buttons in the header, which said they were two
   unrelated things. The chevron says "same action, other route"; the ⋮ beside it says
   "other things entirely" — two different promises, so two different controls.

   The ⋮ sits last, after the primary action, against the "utilities first" rule the button
   system otherwise follows. An overflow affordance at the far right edge is a strong enough
   convention to be worth the exception, and it is an icon rather than a button-shaped thing,
   so it does not compete with the action beside it. */
.split {
  position: relative;
  display: inline-flex;
}

.split > .btn:first-child {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.split-toggle {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  padding: 0 9px;
  min-width: 0;
  /* Derived from the ink, not a hardcoded white: the dark theme fills this button with a
     light colour and a white hairline on it would be invisible. */
  border-left: 1px solid color-mix(in srgb, var(--on-action) 35%, transparent);
}

.has-menu {
  position: relative;
}

.menu {
  display: none;
  position: absolute;
  right: 0;
  top: calc(100% + 5px);
  min-width: 215px;
  padding: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 20px rgba(0, 0, 0, .12);
  z-index: 30;
}

.menu.is-open {
  display: block;
}

.menu a,
.menu button,
.menu label {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--fs-base);
  color: var(--text-primary);
  text-decoration: none;
  text-align: left;
  white-space: nowrap;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.menu a:hover,
.menu button:hover,
.menu label:hover {
  background: var(--tbl-zebra);
}

.menu a:focus-visible,
.menu button:focus-visible,
.menu label:focus-within {
  outline: 2px solid var(--action-bg);
  outline-offset: -2px;
}

.menu .menu-sep {
  height: 1px;
  margin: 4px 2px;
  background: var(--rule);
}

/* A menu item that cannot be undone. It reads as danger without being loud about it — the
   menu is already the quiet place, and shouting inside it would only make the item easier
   to hit by mistake. */
.menu .menu-danger {
  color: var(--danger-text);
}

.menu .menu-danger:hover {
  background: var(--danger-bg);
}

/* A form inside a menu is just a wrapper for its one submit button. */
.menu form {
  margin: 0;
}

/* The chevron on a menu trigger is an affordance, not a second icon — it says the button
   opens something. Smaller and quieter so it does not read as equal to the icon before it.
   The !important overrides `.btn .ti { font-size: 16px }`, which is the rule that gives every
   button icon its size; a plain 14px there would lose to it. */
.menu-caret {
  font-size: 14px !important;
  color: var(--text-tertiary);
  margin-left: -2px;
}

[aria-expanded="true"] > .menu-caret {
  transform: rotate(180deg);
}

/* ── The user menu (feedback #23, v0.221.0) ─────────────────────────────────────────────────
   `partials/user-menu.php`: who is signed in, the profile, theme, language, font, sign out.
   An ordinary `.menu`, so the item rules above reach inside it — the rules below undo them
   where a control is not a list item (the theme grid, the two segmented choices). They sit
   after `.menu` on purpose: at equal specificity the later rule wins. */
.user-menu-trigger {
  padding: 3px 6px 3px 3px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  font-family: inherit;
  cursor: pointer;
}

/* The header is dark in some themes and light in others, so the edge is mixed from the
   trigger's own ink rather than taken from a panel token. */
.user-menu-trigger:hover,
.user-menu-trigger[aria-expanded="true"] {
  border-color: color-mix(in srgb, currentColor 40%, transparent);
}

.user-menu-trigger .menu-caret {
  color: inherit;
  margin-left: 0;
}
/* The name and, under it, the organization being worked in (feedback #25, placement „ა"). The
   second line is the only place the active clinic is named once the switcher left the sidebar,
   so it must not be dropped to save width; a long name is cut with an ellipsis instead. */
.user-menu-id {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  line-height: 1.25;
  text-align: left;
}

.user-menu-trigger .user-menu-name {
  white-space: nowrap;
}

.user-menu-org {
  max-width: 220px;
  overflow: hidden;
  font-size: 11px;
  opacity: .8;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.menu.user-menu .user-menu-org-option {
  padding: 7px 8px;
}
/* On a phone the two lines are capped, or the unwrapped name widened the trigger by 52px and
   squeezed the logo beside it from 118px to 66px (measured at 390px). 90px brings the trigger
   back to its old width; the name is cut first, and the whole name is in the open menu. */
@media (max-width: 600px) {
  .user-menu-id {
    max-width: 90px;
  }

  .user-menu-trigger .user-menu-name,
  .user-menu-org {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

.menu.user-menu {
  width: 340px;
  max-width: calc(100vw - 16px);
  max-height: calc(100vh - 72px);
  overflow-y: auto;
  padding: 6px;
}

.user-menu-who {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px 8px;
}

.avatar.user-menu-avatar {
  width: 36px;
  height: 36px;
  font-size: 18px;
}

.user-menu-who-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.user-menu-who-name {
  font-weight: 700;
  color: var(--text-primary);
}

.user-menu-who-role,
.user-menu-heading {
  font-size: 12px;
  color: var(--text-secondary);
}

.user-menu-heading {
  font-weight: 700;
  padding: 6px 8px 4px;
}

.user-menu-themes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
}

.menu .user-menu-themes .theme-switcher-option {
  padding: 6px 8px;
  font-size: 12.5px;
  white-space: normal;
}

.menu .user-menu-themes .theme-switcher-option.active {
  background: var(--primary-light);
}

.user-menu-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 8px;
}

.user-menu-row-label {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-primary);
}

.user-menu-row-label .ti {
  font-size: 16px;
  color: var(--text-secondary);
}

.menu .lang-seg {
  padding: 3px;
}

.menu .lang-seg > a,
.menu .lang-seg > button {
  display: inline-flex;
  width: auto;
  min-width: 24px;
  justify-content: center;
  padding: 2px 8px;
  border: 0;
  background: none;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--text-secondary);
}

.menu .lang-seg > a:hover,
.menu .lang-seg > button:hover {
  background: var(--bg);
  color: var(--text-primary);
}

.menu .lang-seg > [aria-current="page"],
.menu .lang-seg > [aria-pressed="true"] {
  background: var(--primary-light);
  color: var(--primary-text);
}

@media (max-width: 900px) {
  .menu {
    min-width: 0;
    width: max-content;
    max-width: calc(100vw - 32px);
  }
}

/* A filter is a condition you switch on, not a place you navigate to. The switch says so
   with its own shape, and it is the one control here nobody has to be taught.

   A real <input type="checkbox"> underneath: it keeps the keyboard, the screen reader and
   the form submit, and the track is only its appearance. Hidden with clip rather than
   display:none, because a display:none input is not focusable and the whole thing would
   stop working for anyone not using a mouse. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: var(--fs-base);
  color: var(--text-secondary);
  cursor: pointer;
  padding: 4px 0;
  user-select: none;
}

/* Two toggles on one line are two separate settings, not one control with two halves —
   whitespace between inline-flex elements is about 4px, which reads as the second belonging
   to the first. `--sp-group` is the token for "these are different things".
   Only ever matches genuine neighbours: the three on the shop item form sit in separate
   sections, so nothing there moves. */
.switch + .switch {
  margin-left: var(--sp-group);
}

.switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.switch-track {
  position: relative;
  flex: none;
  width: 34px;
  height: 20px;
  border-radius: 20px;
  /* --btn-line, not --rule: this track IS the control, so it needs the 3:1 a control's
     boundary requires (3.16–3.35:1 across the themes). --rule measures 1.36–1.51:1, which
     is right for a hairline between table rows and leaves an off switch nearly invisible. */
  background: var(--btn-line);
  transition: background-color .15s ease;
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--bg-panel);
  transition: transform .15s ease;
}

.switch:hover .switch-track {
  background: var(--text-secondary);
}

.switch input:checked ~ .switch-track {
  background: var(--action-bg);
}

.switch input:checked ~ .switch-track::after {
  transform: translateX(14px);
}

.switch input:checked ~ .switch-label {
  color: var(--text-primary);
  font-weight: 700;
}

.switch input:focus-visible ~ .switch-track {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

.switch-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Declared here rather than inherited from .switch. Inside a .form-group the caption rule
     `.form-group label` (0-1-1) outweighs `.switch` (0-1-0) and lands on the wrapping <label>,
     so the toggle's text would inherit 12.5px/700 and read as a field caption instead of a
     control. This span is targeted by nothing else, so stating the three properties settles it
     without an !important or a deeper selector. */
  font-size: var(--fs-base);
  font-weight: 400;
  color: var(--text-secondary);
}

@media (max-width: 900px) {
  .switch {
    padding: 8px 0;
  }
}

/* Mutually exclusive views — "all" against "debtors only". Joining them into one block says
   they are alternatives; two separate buttons say they are two things you can do. Only one
   can be on, so only one is filled.

   `aria-current="page"` on the selected link, not just the fill: the state has to survive
   without colour, and a screen reader has nothing to go on otherwise. */
.filter-seg {
  display: inline-flex;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.filter-seg > a,
.filter-seg > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 14px;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 400;
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}

.filter-seg > * + * {
  border-left: 1px solid var(--rule);
}

.filter-seg > :hover {
  background: var(--tbl-zebra);
  color: var(--text-primary);
}

.filter-seg > [aria-current="page"],
.filter-seg > [aria-current="page"]:hover {
  background: var(--action-bg);
  color: var(--on-action);
  font-weight: 700;
}

.filter-seg > :focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: -2px;
}

@media (max-width: 900px) {
  .filter-seg > a,
  .filter-seg > button {
    min-height: 42px;
  }

  /* `overflow: hidden` clips the children to the rounded corners, which is right — but on a
     phone it also CLIPS the ones that no longer fit, so the last filters simply cannot be
     reached. Measured at 390px on the pet profile's records filter: five elements sat past
     the right edge with no way to scroll to them. `overflow-x: auto` keeps the clip on the
     vertical axis and gives the horizontal one a scroll, exactly like `.tabs-nav`. */
  .filter-seg {
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .filter-seg::-webkit-scrollbar { display: none; }
  .filter-seg > a,
  .filter-seg > button {
    flex: none;
  }
}

.filter-main {
  display: flex;
  align-items: center;
  gap: var(--sp-control);
  flex-wrap: wrap;
}

/* A filtered list that does not say so reads as an empty list. */
.filter-note {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}





/* --- Modal --- */
/* Native <dialog> so focus trapping, Escape and the top layer come from the browser rather
   than from 63 hand-rolled display:none boxes, none of which did any of the three. */
dialog.modal {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-panel);
  color: var(--text-primary);
  padding: 0;
  width: min(480px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .18);
  overflow: hidden;
}

/* A column, so a tall body scrolls between a header and a footer that stay put. Without this
   `max-height` merely clipped: `.modal-body`'s overflow-y never engaged (nothing bounded its
   height) and the footer — the confirm button — was pushed out of the dialog entirely. Latent
   until a form long enough to need it went in. */
dialog.modal[open] {
  display: flex;
  flex-direction: column;
}

/* A <form> wrapping the body and footer has to pass the constraint through, or the body's
   height is unbounded again and the column buys nothing. */
dialog.modal > form {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.modal-header,
.modal-footer {
  flex: none;
}

/* Wider, for a form that is a table rather than a few fields — payment allocation, mainly. */
dialog.modal.modal-lg {
  width: min(780px, calc(100vw - 32px));
}

dialog.modal::backdrop {
  background: rgba(0, 0, 0, .45);
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-control);
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}

/* The header is the drag handle (modal.js): a modal makes the page behind it inert, so being
   able to push it aside is the only way to read the record a half-filled form is about.
   `touch-action: none` because a pointer drag and the browser's own touch gesture cannot both
   own the same press — and it is why this is not behind a min-width query. It applies on a
   phone as well: the dialog is nearly the full width there and barely moves sideways, but its
   height is its content's, and pushing a short form up or down is the whole point on the screen
   where the least of the record shows. `.btn` keeps its own pointer cursor, so the close button
   in the same header still reads as a button rather than as part of the handle. */
dialog.modal .modal-header {
  cursor: move;
  touch-action: none;
}

dialog.modal.is-dragging {
  user-select: none;
}

.modal-title {
  font-size: var(--fs-title);
  font-weight: 700;
  margin: 0;
}

.modal-body {
  padding: var(--sp-group);
  font-size: var(--fs-base);
  overflow-y: auto;
  /* A flex item will not shrink below its content without this, which is what actually lets
     overflow-y do anything. */
  min-height: 0;
}

.modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-control);
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

/* The viewer's actions, as far from the close button as the dialog allows.
   `.modal-footer` right-aligns, which would put the destructive button back under the reader's
   hand — so this one starts at the left, and delete is pushed to the opposite edge from the two
   that cannot lose anything. */
.lightbox-actions {
  justify-content: flex-start;
}

.lightbox-actions .lightbox-delete {
  margin-left: auto;
}

/* The `hidden` attribute, made to actually hide.
   `hidden` works through the browser's own `[hidden] { display: none }`, which ANY author rule
   setting `display` outweighs — so `.form-group[hidden]` (flex) and `.search-clear[hidden]`
   (flex) both stayed on screen: an empty error box read as a pink bar, and the search field's
   clear button showed with nothing to clear. `!important` is right here precisely because the
   attribute is meant to win over presentation. */
[hidden] {
  display: none !important;
}

/* --- Global search (header) --- */
/* Borrows the dropdown's look on purpose: the app already has one list shape and a second one
   would only teach the same reading twice. */
.gsearch {
  position: relative;
  flex: 0 1 440px;
  min-width: 0;
  /* Flush with the sidebar's right edge, not with the content column's text. The logo box
     already ends exactly there; this cancels the header's flex gap so the field starts on that
     same line rather than a gap's width past it. */
  margin-left: calc(-1 * var(--header-gap));
}

.gsearch input {
  width: 100%;
  height: 40px;
  padding: 0 62px 0 36px;
  border: 1px solid var(--border);
  /* The sidebar's system card is the shape being matched — --radius-lg, the app's softest
     corner, which is what a card gets and a form field does not. */
  border-radius: var(--radius-lg);
  background-color: var(--field-bg);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--fs-base);
}

.gsearch-icon {
  position: absolute;
  top: 50%;
  left: 13px;
  transform: translateY(-50%);
  font-size: 15px;
  color: var(--text-tertiary);
  pointer-events: none;
}

/* The shortcut, stated rather than hidden in documentation nobody reads — but quietly. A
   bordered box inside a 34px field read as a second control sitting in the first one. */
.gsearch-kbd {
  position: absolute;
  top: 50%;
  right: 14px;
  transform: translateY(-50%);
  font-family: inherit;
  font-size: 10px;
  letter-spacing: .04em;
  color: var(--text-tertiary);
  opacity: .7;
  pointer-events: none;
  white-space: nowrap;
}

/* Out of the way once the field is in use — the hint has done its job by then. */
.gsearch input:focus + .gsearch-kbd,
.gsearch input:not(:placeholder-shown) + .gsearch-kbd {
  display: none;
}

.gsearch-results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 60;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
}

.gsearch-group {
  padding: 8px 12px 4px;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--text-secondary);
}

.gsearch-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 12px;
  color: var(--text-primary);
  text-decoration: none;
}

.gsearch-row .ti {
  flex: none;
  font-size: 16px;
  color: var(--text-secondary);
}

.gsearch-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.gsearch-title {
  font-size: var(--fs-base);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gsearch-text small {
  font-size: var(--fs-micro);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* What is attached now, above the control that replaces it. A replacement is a decision made
   while looking at the thing being replaced, not at its file name alone. */
.doc-current {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  margin-bottom: 12px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  background: var(--bg);
}

/* Every file of the study, one of them selected. The selected one is what a replacement
   lands on, so the marking has to be unmistakable rather than decorative. */
.doc-current-files {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  flex: none;
  max-width: 60%;
}

.doc-current-file {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--bg-panel);
  overflow: hidden;
  cursor: pointer;
}

.doc-current-file img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.doc-current-file.is-selected {
  border-color: var(--action-bg);
}

.doc-current-file:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

.doc-current-icon {
  font-size: 28px;
  color: var(--text-secondary);
}

.doc-current-body {
  min-width: 0;
}

.doc-current-name {
  font-weight: 700;
  font-size: var(--fs-small);
  word-break: break-all;
}

/* The replacement chooser and the button that sends it, on one line with the button on the
   field's baseline. Its own form, so it needs its own spacing — the editor's `.form-group`
   rhythm does not reach across two forms. */
.doc-replace-row {
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

.doc-replace-row > .field-grow {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 0;
}

/* Every block inside a dialog body keeps the same gap under it, whichever element it is —
   a form-group, a switch, or a row that belongs to another form. */
.modal-body > .form-group,
.modal-body > .form-row,
.modal-body > .doc-replace-row,
.modal-body > .doc-current {
  margin-bottom: 14px;
}

.modal-body > .switch {
  margin-top: 4px;
}


/* A result and its own action, side by side. The row keeps the whole width it had; the
   action only takes what it needs, so the two-line contact rows do not reflow. */
.gsearch-line {
  display: flex;
  align-items: stretch;
}

.gsearch-line > .gsearch-row {
  flex: 1;
  min-width: 0;
}

/* "Add a patient to THIS owner." Quiet until the row is under the cursor — it is a shortcut
   off a result, not a second result. */
.gsearch-row-add {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: none;
  padding: 0 12px;
  color: var(--text-tertiary);
  text-decoration: none;
  opacity: 0;
  transition: opacity .12s ease, color .12s ease;
}

.gsearch-line:hover > .gsearch-row-add,
.gsearch-row-add:focus-visible {
  opacity: 1;
}

.gsearch-row-add:hover {
  color: var(--primary-text);
}

.gsearch-row-add .ti {
  font-size: 15px;
}

.gsearch-row-add .gsearch-row-add-plus {
  font-size: 11px;
}

/* Touch has no hover, so the shortcut would never appear at all. */
@media (hover: none) {
  .gsearch-row-add { opacity: 1; }
}

/* Hover and keyboard selection are the same state — one highlight, whichever hand is driving. */
.gsearch-row:hover,
.gsearch-row.is-active,
.gsearch-all:hover,
.gsearch-all.is-active {
  background: var(--tbl-zebra);
}

.gsearch-all {
  display: block;
  padding: 7px 12px;
  border-top: 1px solid var(--rule);
  color: var(--primary-text);
  font-size: var(--fs-small);
  text-decoration: none;
}

.gsearch-empty {
  padding: 18px 12px;
  text-align: center;
  font-size: var(--fs-small);
  color: var(--text-tertiary);
}

/* The two create screens, reachable from the search that failed to find them.
   A strip rather than more rows: these are not results, and drawing them like results would
   mean the list's last two entries are things that do not exist yet. */
.gsearch-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  padding: 8px 12px;
  border-top: 1px solid var(--rule);
  background: var(--bg);
}

.gsearch-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  color: var(--text-primary);
  font-size: var(--fs-small);
  text-decoration: none;
  white-space: nowrap;
}

.gsearch-action:hover,
.gsearch-action.is-active {
  background: var(--action-bg);
  border-color: var(--action-bg);
  color: var(--on-action);
}

/* The five themes that paint the header dark or branded. A light `--field-bg` box on a navy
   band is the same mismatch the sidebar buttons had before they became a glass card — so the
   field borrows exactly that card's treatment rather than inventing a second one.

   Measured on each theme's own header colour: white label 6.42–15.31:1, the dimmed placeholder
   4.18–8.59:1. Both hold in all five. */
html[data-theme="pro-slate"] .gsearch input,
html[data-theme="pharmacy"] .gsearch input,
html[data-theme="enamel"] .gsearch input {
  background-color: rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-lg);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
  color: #FFFFFF;
}

html[data-theme="pro-slate"] .gsearch input::placeholder,
html[data-theme="pharmacy"] .gsearch input::placeholder,
html[data-theme="enamel"] .gsearch input::placeholder {
  color: rgba(255, 255, 255, 0.72);
  opacity: 1;
}

html[data-theme="pro-slate"] .gsearch-icon,
html[data-theme="pro-slate"] .gsearch-kbd,
html[data-theme="pharmacy"] .gsearch-kbd,
html[data-theme="enamel"] .gsearch-kbd {
  color: rgba(255, 255, 255, 0.6);
}

/* Below this the header has no room for it; the module pages keep their own search. */
@media (max-width: 900px) {
  .gsearch {
    display: none;
  }

  /* The fixed logo box exists only to line the header up with the sidebar column. Below this
     the sidebar is a drawer and the menu button takes the space, so holding 233px would push
     the user block off a phone screen. */
  .app-header-logo {
    flex: 0 1 auto;
    width: auto;
  }
}

/* --- Panel picker (lab panel form) --- */
/* Picking 15 parameters out of 100+ is a search, not a choice. The form used to print every
   checkbox at once: ~85% of the screen was noise and nothing showed WHICH ones were picked.
   Two panes — catalogue on the left, the panel being built on the right — plus a category
   filter, ready-made sets and drag-to-reorder. `.pp-fallback` is the plain checkbox list and
   stays in the DOM: with no JS the form still works exactly as it did. */
.pp-panes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-group);
  align-items: start;
}

.pp-pane {
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.pp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-control);
  padding: 10px 13px;
  background: var(--tbl-head);
  color: var(--tbl-head-ink);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: .03em;
}

.pp-head .pp-count {
  font-weight: 400;
  font-size: var(--fs-micro);
  opacity: .85;
}

.pp-tools {
  display: flex;
  flex-direction: column;
  gap: var(--sp-control);
  padding: 9px 11px;
  border-bottom: 1px solid var(--rule);
}

.pp-body {
  height: 330px;
  overflow-y: auto;
}

/* .search-box's input carries min-width:240px for a page header. Inside a half-width pane that
   overflows, so here it gives up its floor and just fills. */
.pp-tools .search-box input {
  flex: 1;
  min-width: 0;
}

.pp-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-control);
  padding: 8px 11px;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}

/* Category filter. Rounded because these are filters, not the segmented control that says
   which page you are on — different job, different shape. */
.pp-segs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.pp-seg {
  font-family: inherit;
  font-size: 11.5px;
  padding: 4px 10px;
  border-radius: 999px;
  cursor: pointer;
  border: 1px solid var(--btn-line);
  background: var(--bg-panel);
  color: var(--text-secondary);
}

.pp-seg:hover {
  background: var(--tbl-hover);
}

.pp-seg[aria-pressed="true"] {
  background: var(--action-bg);
  border-color: var(--action-bg);
  color: var(--on-action);
  font-weight: 700;
}

.pp-seg .pp-n {
  opacity: .75;
  font-weight: 400;
}

/* Left row: one catalogue parameter, click to add. */
.pp-row {
  display: flex;
  align-items: center;
  gap: var(--sp-control);
  width: 100%;
  padding: 6px 11px;
  border: 0;
  border-bottom: 1px solid var(--tbl-zebra);
  background: none;
  font-family: inherit;
  font-size: var(--fs-small);
  text-align: left;
  color: var(--text-primary);
  cursor: pointer;
}

.pp-row:last-child {
  border-bottom: 0;
}

.pp-row:hover {
  background: var(--tbl-hover);
}

/* Already picked: dimmed, not removed. Removing it would make the list jump under the cursor
   and leave the vet unsure which row they just hit. */
.pp-row[disabled] {
  opacity: .45;
  cursor: default;
}

.pp-row[disabled] .pp-add {
  visibility: hidden;
}

.pp-add {
  flex: none;
  font-size: 15px;
  color: var(--primary-text);
}

.pp-ab {
  flex: none;
  min-width: 58px;
  font-weight: 700;
}

.pp-nm {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-secondary);
}

.pp-un,
.pp-tag {
  flex: none;
  font-size: var(--fs-micro);
  color: var(--text-tertiary);
}

/* Right row: one parameter on the panel, draggable. */
.pp-crow {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 9px;
  border-bottom: 1px solid var(--tbl-zebra);
  background: var(--bg-panel);
  font-size: var(--fs-small);
  cursor: grab;
}

.pp-crow:hover {
  background: var(--tbl-hover);
}

.pp-crow.is-dragging {
  opacity: .4;
}

.pp-crow.is-over {
  box-shadow: inset 0 2px 0 var(--action-bg);
}

.pp-grip {
  flex: none;
  font-size: 15px;
  color: var(--text-tertiary);
}

.pp-ord {
  flex: none;
  min-width: 16px;
  text-align: right;
  font-size: var(--fs-micro);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

.pp-move,
.pp-remove {
  display: flex;
  padding: 2px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--text-tertiary);
  font-size: 13px;
  cursor: pointer;
}

.pp-move:hover {
  background: var(--action-bg);
  color: var(--on-action);
}

.pp-move[disabled] {
  opacity: .25;
  cursor: default;
}

.pp-move[disabled]:hover {
  background: none;
  color: var(--text-tertiary);
}

.pp-remove:hover {
  color: var(--danger-text);
}

.pp-empty {
  padding: 26px 14px;
  text-align: center;
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--text-tertiary);
}

/* Ready-made sets: the panels this clinic already has. */
.pp-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-bottom: var(--sp-group);
}

.pp-preset {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
}

.pp-preset:hover {
  background: var(--tbl-hover);
}

.pp-preset .ti {
  font-size: 15px;
  color: var(--primary-text);
}

.pp-preset b {
  font-weight: 700;
}

.pp-preset .pp-n {
  font-size: var(--fs-micro);
  color: var(--text-tertiary);
}

.pp-label {
  margin-bottom: 7px;
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--text-secondary);
}

.pp-link {
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--fs-micro);
  color: var(--primary-text);
  text-decoration: underline;
  cursor: pointer;
}

@media (max-width: 820px) {
  .pp-panes {
    grid-template-columns: 1fr;
  }

  .pp-body {
    height: 250px;
  }
}

/* --- Confirm dialog (replaces native confirm()) --- */
/* A <dialog>, not a positioned overlay. An open dialog lives in the browser's top layer, and
   no z-index can reach that — so an overlay div raised from inside an open modal rendered
   *behind* it. It borrows dialog.modal for its frame and only narrows the width, so it must
   sit after that rule: the two selectors weigh the same and the later one wins. */
dialog.cm-dialog {
  width: min(380px, calc(100vw - 32px));
}

.cm-message {
  padding: var(--sp-section);
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--text-primary);
}

@media (max-width: 900px) {
  .filter-bar {
    align-items: stretch;
  }

  .form-grid-2 {
    grid-template-columns: 1fr;
  }

}

/* ── განმეორებადი განლაგება ────────────────────────────────────────────────
   ეს ოთხი ნიმუში views-ში ათობით ადგილას სიტყვასიტყვით ერთი და იგივე იყო.
   მნიშვნელობები განზრახ ზუსტად იგივეა, რაც ინლაინ ეწერა — ეს გადატანაა და
   არა გადაკეთება; იერი არსად იცვლება. ლიტერალები ტოკენებით ჩავანაცვლე იქ,
   სადაც ტოკენს ზუსტად იგივე მნიშვნელობა აქვს (--fs-title 15px და ა.შ.).
   ────────────────────────────────────────────────────────────────────────── */
.section-heading {
  font-size: var(--fs-title);
  font-weight: 700;
  margin: 0 0 var(--sp-group) 0;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--sp-control);
}
/* იგივე სათაური ხაზის გარეშე */
.section-heading.plain {
  border-bottom: 0;
  padding-bottom: 0;
}
/* ველი, რომელიც მწკრივში დანარჩენ ადგილს იკავებს */
.field-grow {
  margin: 0;
  flex: 1;
  min-width: 160px;
}
/* ორსვეტიანი ბადე */
.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
/* ორ კიდეზე გაწეული მწკრივი — სათაური მარცხნივ, მოქმედება მარჯვნივ */
.row-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
/* სტატისტიკის ციფრი (.fs-12.text-secondary წარწერასთან წყვილში) */
.stat-value {
  font-size: 28px;
  font-weight: 700;
}
/* ველების მწკრივი — ქვედა კიდეზე სწორდება, რომ ღილაკი და input ერთ ხაზზე დადგეს */
.form-row {
  display: flex;
  gap: var(--sp-control);
  flex-wrap: wrap;
  align-items: flex-end;
}

/* §46 — იგივე ერთხაზიანი რიგი, ოღონდ მაშინ, როცა რომელიმე ველს ქვეშ დამაზუსტებელი
   მოსანიშნი აქვს („სავარაუდო თარიღი"). `flex-end` ასეთ რიგში იარლიყებს აცდენს:
   უფრო მაღალი ჯგუფი მთლიანად ზემოთ იწევს — გაზომილია /pets/{id}-ის ვაქცინაციის
   ფორმაზე, სადაც „თარიღი" დანარჩენ იარლიყებზე 21px-ით მაღლა იდგა. აქ ბაზისი
   ზემოთაა: ყველა იარლიყი ერთ ხაზზე, დამაზუსტებელი კი თავის ველს ქვემოდან რჩება. */
.form-row-top {
  display: flex;
  gap: var(--sp-control);
  flex-wrap: wrap;
  align-items: flex-start;
}
/* იარლიყი (15px) + `.form-group`-ის gap (6px): ღილაკი ველების ბაზისზე დგება და არა
   იარლიყისაზე. გაზომილია, არა შერჩეული. */
.form-row-top > .btn,
.form-row-top > button.btn { margin-top: 21px; }

/* ── უტილიტები ─────────────────────────────────────────────────────────────
   ინლაინ style-ის ჩასანაცვლებელი მხოლოდ იმ დეკლარაციებისთვის, რომლებიც
   ათობით ადგილას ერთი და იგივეა. ერთჯერადი განლაგება ადგილზე რჩება — მისი
   კლასად გატანა კომპონენტს არ ქმნის, უბრალოდ სახელს ცვლის.

   !important განზრახია: ესენი ინლაინ style-ს ანაცვლებენ, რომელიც ყველა წესს
   ჯობნიდა. მის გარეშე `.card .card-title` ტიპის ორკლასიანი სელექტორი
   უტილიტს გადაფარავდა და ჩანაცვლება იერს შეცვლიდა.
   ────────────────────────────────────────────────────────────────────────── */
.m-0 { margin: 0 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-8 { margin-bottom: 8px !important; }
.mb-16 { margin-bottom: 16px !important; }
.ml-4 { margin-left: 4px !important; }
.ta-r { text-align: right !important; }
.ta-c { text-align: center !important; }
.nowrap { white-space: nowrap !important; }
.d-inline { display: inline !important; }
.d-none { display: none !important; }
.flex-1 { flex: 1 !important; }
.cursor-pointer { cursor: pointer !important; }
.fs-10 { font-size: 10px !important; }
.fs-11 { font-size: 11px !important; }
.fs-12 { font-size: 12px !important; }
.fs-13 { font-size: 13px !important; }
/* DejaVu Sans-ს მხოლოდ 400 და 700 აქვს — 600 ბრაუზერის სინთეზირებულ ცხიმს იძლევა */
.fw-bold { font-weight: 700 !important; }
.text-danger { color: var(--danger-text) !important; }
.text-secondary { color: var(--text-secondary) !important; }
.text-tertiary { color: var(--text-tertiary) !important; }

/* Visible only to a screen reader. Two views (`prophylaxis/index`, `feedback/index`) name
   this class on a <label> that a sighted user reads from the control beside it — until now
   it was undefined, so those labels simply rendered in full. No !important: it defines a
   box, it does not override one. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* =========================================================
   TABLE SYSTEM — §23–36
   ---------------------------------------------------------
   §24-ის ანატომია არსებულ პრიმიტივებზე აეწყო, არ დაუბრუნებია:
     .table-shell   → არ დაიწერა. .card უკვე ეს არის (488 გამოყენება),
                      და §24 ამბობს „use only where useful. Avoid
                      unnecessary wrapper nesting."
     .table-wrap    → უკვე არსებობს (102), სპეციფიკაციის სახელი იგივეა.
     .table         → `table.tbl` (104). სახელი განსხვავდება, სტრუქტურა არა.
     .table-actions → `.row-actions` (38). §77 სტრუქტურულ სახელს ითხოვს და
                      `row-actions` უფრო ზუსტია — ეს ერთი მწკრივის
                      მოქმედებებია, არა ცხრილისა. მიზეზი დაფიქსირებულია §11-ის
                      მიხედვით.
   ახალია მხოლოდ ის, რაც მართლა აკლდა: toolbar, footer, compact.
   ========================================================= */

/* §24-ის toolbar და footer, და §37-ის .filter-actions განზრახ არ დაწერილა.
   პილოტს არცერთი არ დასჭირდა: ძებნა გვერდის სათაურშია, ჯამებსა და გვერდებს
   pagination-ის partial გამოაქვს. დაიწერება, როცა პირველი ცხრილი მართლა
   მოითხოვს. იგივე მიზეზი: პილოტს არ დასჭირდა — ძებნა §37-ის
   ფილტრის ზოლშია — და კლასი, რომელსაც არავინ იყენებს, სწორედ ის შეცდომაა,
   რომელმაც v0.48.0-ის 14 კლასი დატოვა სამ თვეს მკვდარი. დაიწეროს მაშინ,
   როცა პირველი ცხრილი მართლა მოითხოვს. */


/* §16 / §30 — WORKSPACE სიმკვრივე. 44px → 36px.
   §31: min-height და არა height — გრძელი შიგთავსი მწკრივს მაინც ზრდის,
   კლინიკური ტექსტი არ იჭრება.

   `th` აღარაა ამ სელექტორში (v0.162.19). სიმკვრივე მონაცემების თვისებაა და არა სათაურის:
   44 ცხრილი ატარებს `.table-compact`-ს და მათში ჰედერი 6px-ზე იკუმშებოდა, ე.ი. სწორედ ის
   მაღალი ზოლი ქრებოდა, რომელიც v0.162.18-მა დაამატა. სპეციფიკურობით ეს წესი (0,2,2)
   `table.tbl th`-ს (0,1,2) სჯობნიდა, ამიტომ `/pets`-ზე ჰედერი 17px-ის ნაცვლად 6px-ით
   იხატებოდა — ხოლო იმავე გვერდის მწკრივები ავატარებისა და ღილაკების გამო 54px-ია, ე.ი.
   სხვაობა თვალშისაცემი იყო. ზოლის გაქრობის შემდეგ სიმაღლე ჰედერის სამი ნიშნიდან ერთია
   (წონა, ქვედა ხაზი, სიმაღლე) და მოდიფიკატორმა ის აღარ უნდა წაართვას. */
table.tbl.table-compact td {
  padding-top: 6px;
  padding-bottom: 6px;
}
/* `min-height` <td>-ზე უგულებელყოფილია — გაზომილია: `/shop/sales`-ის მწკრივი 30px-ზე
   იდგა, `min-height: 36px`-ის მიუხედავად. ცხრილის უჯრაზე `height` მოქმედებს როგორც
   მინიმუმი და არა როგორც ფიქსირებული სიმაღლე, ე.ი. გრძელი შიგთავსი მწკრივს მაინც
   ზრდის — სწორედ ის, რასაც §30 ითხოვს და §31 იცავს. */
table.tbl.table-compact td {
  height: 36px;
}

/* §35 — ცარიელი მდგომარეობა მოქმედებით. .empty-state უკვე არსებობს;
   ეს მისი შიგთავსის იერარქიაა (§96: ყველაფერი ერთი წონისა არ არის). */
.empty-state-icon {
  font-size: 42px;
  color: var(--text-tertiary);
  display: block;
}
.empty-state-title {
  font-size: var(--fs-title);
  font-weight: 700;
  color: var(--text-primary);
  margin-top: 10px;
}
.empty-state-action {
  margin-top: 16px;
}

/* §35 — სექციის ცარიელი მდგომარეობა.
   ცენტრში აწყობილი, 40px-ით შევსებული ბლოკი მაშინაა სწორი, როცა ცარიელია მთელი
   ეკრანი — სიის გვერდი, სადაც ის ერთადერთი შინაარსია. ერთი სექციისთვის ის სამ
   სხვადასხვა გასწორებას აჩენს ერთ ბლოკში: სათაური მარცხნივ, შეტყობინება ცენტრში,
   ჩამატების ფორმა ისევ მარცხნივ. /pets/{id}-ზე ექვსმა ასეთმა ბლოკმა დაახლოებით
   800px სიცარიელე დაიკავა და გვერდი დაშლილად წაიკითხა. */
.empty-state.empty-state-inline {
  text-align: left;
  padding: 0 0 4px;
}


/* პილოტის ორი დომენური დეტალი (§110: სტრუქტურა გენერიკულია, ესენი არა) */
.item-sku { font-family: monospace; }

/* =========================================================
   FORM SYSTEM — §41–53
   ---------------------------------------------------------
   .form-group, .form-grid და .form-actions უკვე არსებობდა და
   მუშაობს — აქ მხოლოდ ის ემატება, რაც პილოტს დასჭირდა.
   ========================================================= */

/* §42/§106 — სემანტიკური ჯგუფი. <fieldset> ბრაუზერის ნაგულისხმევ ჩარჩოს
   ატარებს, რაც §68-ის „ჩარჩო ჩარჩოში" იქნებოდა — ამიტომ იშლება. */
.form-section {
  border: 0;
  padding: 0;
  margin: 0 0 var(--sp-section);
  min-width: 0;
}

/* §137 — ერთი სათაური, ერთი იერი. ფორმის სექციის სათაური ზუსტად ისეთია, როგორიც
   გვერდის სექციისა (`.section-heading`): 15px ცხიმიანი ხაზქვეშ. ორი განსხვავებული
   მძიმე სათაური ერთსა და იმავე აპლიკაციაში თვითონ იყო შეუსაბამობა. */
.form-legend {
  padding: 0;
  font-size: var(--fs-title);
  font-weight: 700;
  margin: 0 0 var(--sp-group) 0;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--sp-control);
  width: 100%;
}

/* §73 — მონიშვნის label. მთელი ხაზი დასაწკაპუნებელია, არა მხოლოდ კვადრატი:
   ხელთათმანიანი თითისთვის 13px-იანი checkbox სამიზნე ძალიან პატარაა. */
.choice-label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-weight: 400;
  min-height: 28px;
}
.choice-label.mt-8 { margin-top: 8px; }
.choice-label.mt-20 { margin-top: 20px; }
.choice-label.mb-10 { margin-bottom: 10px; }
/* §106 — a label that names SEVERAL controls is not a label: it has no `for=` and wraps
   nothing, so a screen reader reads the group's name to nobody. `<fieldset>`/`<legend>` is
   the element for it. These two make one look exactly like a `.form-group` and its label —
   the browser's own fieldset comes with a border, padding and margins that would otherwise
   make every converted group look different from the fields around it. */
.field-group {
  border: 0;
  padding: 0;
  margin: 0 0 14px;
  min-width: 0;
}
.field-group > .field-legend {
  padding: 0;
  margin-bottom: 6px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-secondary);
}

/* =========================================================
   DIAGNOSTIC FILES — §110
   ---------------------------------------------------------
   `views/partials/pet-documents.php` ერთადერთია, სადაც ინლაინ სტილი მწკრივზე
   მრავლდებოდა და არა ერთჯერადი იყო: 34 დოკუმენტი × თითოზე ათამდე დეკლარაცია
   გამოსახულ DOM-ში 421 `style=` ატრიბუტს იძლეოდა. §82 ერთჯერადის გატანას
   კრძალავს — ეს პირიქითაა, ესენი ზუსტად მეორდება ყოველ მწკრივზე.
   ========================================================= */
.doc-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  vertical-align: middle;
}
.doc-file-icon {
  font-size: 20px;
  vertical-align: middle;
}
/* §31 — კომენტარი არ იჭრება; ხაზის გადატანები ისე რჩება, როგორც აკრიფეს */
.doc-comment {
  max-width: 280px;
  white-space: pre-wrap;
}
/* ფოტოდან წაკითხვის ლოდინი — ღილაკის სქოლიო, არა ცალკე ჩანაწერი.
   სრულ სიგანეზე გაჭიმული ზოლი ცხრილის მწკრივს ჰგავდა და ორ ფაილს შუაზე ჭრიდა:
   ზედა ხაზი იშლება, რომ წინა მწკრივს მიეკრას, და მარჯვნივ სწორდება იმ ღილაკის ქვეშ,
   რომელმაც გამოიწვია. */
.doc-wait {
  /* მშობელ უჯრას `nowrap` აქვს — წინადადება უნდა გადავიდეს ხაზზე, არა გააგანიეროს სვეტი. */
  white-space: normal;
  max-width: 420px;
  margin: 10px 0 0 auto;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--text-tertiary);
}
.doc-wait .ti {
  font-size: 12px;
  vertical-align: -1px;
}

.cell-sunken { background: var(--bg); }
.textarea-full {
  width: 100%;
  resize: vertical;
}
.th-rail { width: 110px; }

/* =========================================================
   CARD SYSTEM — §54–59
   ---------------------------------------------------------
   .card და .card-title უკვე არსებობდა. აქ ის ემატება, რაც
   ბარათებიან ეკრანს დასჭირდა — და მხოლოდ ის.
   ========================================================= */

/* §55 — ბარათის თავი: იდენტობა მარცხნივ, მდგომარეობა/მეტამონაცემი მარჯვნივ.
   ეს კლასი v0.83.0-ში წაიშალა, რადგან სამ თვეში არავის დასჭირდა. ახლა
   სტაციონარის გალიის ბლოკს სჭირდება — ხელით აწყობილი ბარათი იყო, სათაურის
   მწკრივითა და სხეულით. ბრუნდება იმიტომ, რომ მოთხოვნა გაჩნდა, არა იმიტომ,
   რომ სპეციფიკაციაში წერია. */
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-control);
  margin-bottom: 6px;
}

/* §68 — ბარათი ბარათში მაშინ, როცა იერარქია ნამდვილია: ოთახი შეიცავს გალიას.
   მსუბუქი ჩარჩო და --radius-md (§69), რომ გარე ბარათს არ დაემსგავსოს. */
.card-nested {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px;
}

/* §54 — თანაბარი ობიექტების ბადე */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px;
}

/* §111 — პაციენტის იდენტობის ზოლი. §110 გენერიკული სტრუქტურისთვის დომენურ
   სახელს კრძალავს, მაგრამ §111 ამ კონკრეტულ ნიმუშს („patient identity strip")
   პირდაპირ ასახელებს დასაშვებად: ფოტო/სახეობის იკონი, სახელი, მდგომარეობა.
   §56 — სახელი ვიზუალურად დომინირებს, ჭდეები მის ქვეშ დგება. */
.patient-strip {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: inherit;
}
.patient-strip:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}
.patient-strip-avatar {
  flex-shrink: 0;
  width: 60px;
  height: 60px;
  border-radius: var(--radius-md);
  object-fit: cover;
  border: 1px solid var(--border);
}
.patient-strip-avatar-fallback {
  flex-shrink: 0;
  width: 60px;
  height: 60px;
  border-radius: var(--radius-md);
  background: var(--primary-light);
  color: var(--primary-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
}
.patient-strip-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* §12 — 700, არა 600; §56 — იდენტობა ჭდეზე მძიმეა */
.patient-strip-name {
  font-size: var(--fs-base);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.patient-strip-state {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  align-items: center;
}

/* How long this patient has been in, beside the state pills. Deliberately NOT a pill: a pill
   on this board means a state that wants something (severity, an alert), and a day count wants
   nothing — a third one would compete with the two that do. */
.patient-strip-days {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: var(--fs-micro);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* ── The ward's standing orders: „არ აჭამო", „ნიკაპი" (ux-audit K4) ────────
   `views/partials/ward-care.php` draws these in three places — the chart, the shift board and
   the map — and the only difference between them is how much room there is. The chart gets the
   whole sentence in a block; the two cards get one line, the note truncated with its full text
   in `title` and the fasting flag never truncated, because it is four words and it is the one
   that changes what somebody does in the next ten minutes. */
.ward-care {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--sp-group);
  padding: 10px 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--rule);
  background: var(--bg);
  font-size: 13px;
}
/* Amber only while the order actually stands. An ended fast is still shown — nothing clears
   the flag and somebody has to decide — but it is no longer something to act on, so it must
   not carry the same colour as one that is. */
.ward-care.is-fasting {
  background: var(--warn-bg);
  color: var(--warn-text);
  border-color: color-mix(in srgb, var(--warn-text) 30%, transparent);
}
.ward-care-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.care-chips {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}
.care-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}
.care-chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* On the map it is its own line under the state pills (the body is a column); on the board it
   follows the patient's name on the same line. */
.patient-strip .care-chips { margin-top: 2px; }
.board-group-head .care-chips { display: inline-flex; margin-left: 8px; vertical-align: middle; }

/* სტაციონარის დაფის სამი დომენური დეტალი (§111) */
.summary-bar {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  align-items: center;
}
/* A prose cell in a table of short ones — the admission reason. One line, the whole of it in
   the cell's own `title`. Without a width a table cell simply grows, and one long sentence sets
   the height of every row beside it. */
.cell-clip {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.room-note { margin: -8px 0 12px; }
.cage-note { margin-bottom: 6px; }
.occupant-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* §29/§105 — ნავიგაციური სტატისტიკის ბარათი. `onclick`-იანი <div> ტაბ-რიგში არ
   დგება; <a> იღებს ფოკუსს, Enter-ს და Ctrl+დაწკაპუნებას უფასოდ. */
.stat-card-link {
  display: block;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
.stat-card-link:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}

/* =========================================================
   READ-ONLY INFORMATION — §60
   ---------------------------------------------------------
   `<strong>იარლიყი:</strong> მნიშვნელობა` წყვილი ორ რამეს აფუჭებს:
   იარლიყი მნიშვნელობაზე მძიმეა (§60 პირიქით ითხოვს), და ერთი ვიწრო
   სვეტი ბარათის სიგანეს არ იყენებს. ბადე თითოეულ წყვილს დამოუკიდებელ
   უჯრად აქცევს, იარლიყი მეორეხარისხოვანია, მნიშვნელობა — მთავარი.
   ეს კლასები v0.48.0-ში დაიწერა, v0.83.0-ში წაიშალა (არავის ეხმარებოდა)
   და ახლა ბრუნდება, რადგან ცხოველის ბარათს ნამდვილად სჭირდება.
   ========================================================= */
.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px var(--sp-group);
}
.info-item { min-width: 0; }
/* §60 — იარლიყი მეორეხარისხოვანი: ნორმალური წონა, დამხმარე ფერი */
.info-label {
  display: block;
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--text-secondary);
  margin-bottom: 2px;
}
/* §31 — კლინიკური მნიშვნელობა არასოდეს იჭრება: გადადის ახალ ხაზზე */
.info-value {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.info-value .pill { vertical-align: middle; }

/* ფოტო და ბადე გვერდიგვერდ; ტელეფონზე ეს 140px-ს და ერთ ვიწრო სვეტს ტოვებდა, სადაც
   „სტერილიზებული/კასტრირებული" ორ ხაზად ტყდებოდა. 600px-ის ქვემოთ ერთმანეთის ქვეშ დგება. */
.pet-card-body {
  display: flex;
  gap: var(--sp-group);
  align-items: flex-start;
}
@media (max-width: 600px) {
  /* `align-items: flex-start` has to go with it: stacked, it makes the grid shrink to its
     content instead of using the card's width — measured 172px inside a 390px card. */
  .pet-card-body {
    flex-direction: column;
    align-items: stretch;
  }
}
/* დამაზუსტებელი ხაზი მნიშვნელობის ქვეშ (თარიღი, კლინიკა, შენიშვნა) */
.info-note {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 400;
  color: var(--text-secondary);
  line-height: 1.4;
}

/* §14 / WCAG 2.4.7 — ფოკუსის რგოლი მუქ ქრომზე.
   გვერდითა პანელს (#1E293B) და თავსართს (#0F172A) ყველა თემაზე ერთი და იგივე მუქი ფონი
   აქვს, ბრაუზერის ნაგულისხმევი `outline: auto` კი currentColor-ს იღებს — გაზომილია:
   გამოსვლის ღილაკზე ის rgb(121,31,31)-ია, ანუ 1.41:1 პანელის ფონზე. პრაქტიკულად უხილავი.
   45 ფოკუსირებადი ელემენტია ამ ორ ზოლში; ყველა ღია რგოლს იღებს. */
.sidebar :focus-visible,
.app-header :focus-visible {
  outline: 2px solid #93C5FD;
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* §25 — ვისმა ლაბორატორიამ შეასრულა და ვისი ინტერვალით შეფასდა. ბეჭდვასა და ელფოსტაში
   იგივე წინადადება საკუთარი ინლაინ სტილით მიდის (ის დოკუმენტები `app.css`-ს არ ტვირთავენ),
   ამიტომ ეს კლასი მხოლოდ ეკრანისაა. */
.lab-provenance {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 12px;
  margin-bottom: var(--sp-group);
  border-left: 3px solid var(--info-line, var(--rule));
  background: var(--info-bg, var(--bg));
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: var(--fs-small);
  color: var(--info-text, var(--text-secondary));
}
.lab-provenance > .ti {
  flex: none;
  font-size: 16px;
  line-height: 1.4;
}

/* §25 — ჩანაწერის სამი მომენტი: შეკვეთა, შესრულება, დადასტურება. თითო ხაზი სამი ველია, და
   ისინი ერთმანეთის ქვეშ უნდა დადგეს — სახელები სხვადასხვა სიგრძისაა, თავისუფალ ნაკადში კი
   თარიღები დაკიბულდებოდა. grid ამას აკეთებს, ცხრილის მარკირების გარეშე: ეს სია არაა
   მონაცემთა ცხრილი, სამი წინადადებაა ჩანაწერის შესახებ. */
.lab-trail {
  display: grid;
  grid-template-columns: auto auto 1fr;
  gap: 2px 14px;
  align-items: baseline;
  margin-top: var(--sp-group);
  padding-top: 10px;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-small);
}
.lab-trail-step {
  display: contents;
}
.lab-trail-label {
  color: var(--text-secondary);
}
.lab-trail-who {
  font-weight: 700;
}
.lab-trail-when {
  color: var(--text-secondary);
  white-space: nowrap;
}
/* ტელეფონზე სამი სვეტი 390px-ში არ ეტევა — ხაზი ორ რიგად იშლება, ეტიკეტი თავზე. */
@media (max-width: 600px) {
  .lab-trail {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .lab-trail-step {
    display: block;
    padding: 4px 0;
  }
  .lab-trail-step + .lab-trail-step {
    border-top: 1px solid var(--rule);
  }
  .lab-trail-when {
    margin-left: 8px;
  }
}

/* A long file comment in the documents table. The cell shows one line and a button; the rest
   lives in the panel's dialog. Without the clamp a 39-line ultrasound conclusion rendered in
   full inside a 280px column and pushed every other row apart. */
.doc-note-preview {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-bottom: 4px;
}
/* pre-wrap, not <pre>: the line breaks and blank lines ARE the structure of a radiology
   conclusion, but the text stays in the page's own font rather than turning into code. */
.doc-note-full {
  white-space: pre-wrap;
  line-height: 1.55;
  font-size: 13px;
  max-height: 65vh;
  overflow-y: auto;
}

/* The session's own warning, not a page's. Fixed bottom-centre so it does not sit where a
   flash message does — this one is about the clock, not about what was just saved, and the
   two arriving in the same place would read as one. */
.session-warning {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  font-size: 13px;
  background: var(--warn-bg);
  color: var(--warn-text);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  max-width: calc(100vw - 24px);
}

.session-warning i {
  font-size: 18px;
  flex: 0 0 auto;
}

/* ── Calendar board (v0.149.0, generalised v0.151.0) ───────────────────────────
   One day board and one week board, shared by appointments, surgery and grooming.
   Grid and tokens only — every one of the nine themes redefines those, so the board
   follows them without a line of its own per theme.

   The day view is NOT a cell per half hour. Each column is one tall box and every
   entry is positioned inside it by the minute, which is the only way a two-hour
   operation can be drawn twice the height of a one-hour one. */

.cal-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.cal-nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cal-range { font-size: var(--fs-title); }
.cal-filters { display: flex; gap: 10px; align-items: center; }

/* The rule between „who" and „how the day is drawn": they are different kinds of control and
   without it the toolbar reads as one undifferentiated row of settings. */
.cal-filters-rule {
  width: 1px;
  align-self: stretch;
  min-height: 20px;
  background: var(--border);
  flex: none;
}

/* The board scrolls inside its own box. A clinic with eleven staff would otherwise
   push the page itself sideways and take the sidebar with it. */
.cal-scroll { overflow-x: auto; }

.cal-day-grid {
  display: grid;
  min-width: 100%;
  border-top: 1px solid var(--border);
  border-left: 1px solid var(--border);
}

.cal-corner, .cal-col-head {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg-panel);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 8px 10px;
  font-size: var(--fs-small);
  font-weight: 700;
}

.cal-rail { border-right: 1px solid var(--border); }

.cal-rail-slot {
  border-bottom: 1px solid var(--border);
  padding: 2px 8px;
  font-size: var(--fs-micro);
  color: var(--text-tertiary);
  text-align: right;
  box-sizing: border-box;
}

/* Only the top of the hour is labelled and ruled darker: a strong line on every half
   hour turns the board into graph paper and the eye stops finding the hour at all. */
.cal-rail-hour { color: var(--text-secondary); border-top: 1px solid var(--border); }

.cal-col {
  position: relative;
  border-right: 1px solid var(--border);
  box-sizing: border-box;
}

/* The empty half of the board is clickable on purpose — booking starts from a gap. */
.cal-slot {
  display: block;
  border-bottom: 1px solid var(--border);
  box-sizing: border-box;
}

.cal-slot:hover { background: var(--primary-light); }

/* A positioned entry. Sits above the slot links so a booked block is not a booking link. */
.cal-block {
  position: absolute;
  left: 2px;
  right: 2px;
  z-index: 2;
  overflow: hidden;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-micro);
  line-height: 1.3;
  background: var(--info-bg);
  color: var(--info-text);
  border-left: 3px solid var(--info-text);
  text-decoration: none;
  box-sizing: border-box;
}

.cal-chip {
  display: block;
  padding: 3px 6px;
  margin-bottom: 2px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-micro);
  line-height: 1.35;
  background: var(--info-bg);
  color: var(--info-text);
  border-left: 3px solid var(--info-text);
  text-decoration: none;
}

.cal-chip:hover, .cal-block:hover { filter: brightness(0.96); }
.cal-chip strong, .cal-block strong { display: block; font-size: var(--fs-small); }
.cal-chip-time { font-variant-numeric: tabular-nums; opacity: 0.85; }
.cal-chip-sub { display: block; opacity: 0.8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Status is the colour, following each module's own pill map — appointments,
   surgery and grooming share this board and name their states differently.
   `cancelled` is muted rather than red: two of the three modules pill it grey, and a
   board full of red cancellations hides the one thing that is actually urgent. It keeps
   the strike-through, because a cancelled booking must not read as a live one. */
.cal-chip-confirmed, .cal-chip-completed { background: var(--success-bg); color: var(--success-text); border-left-color: var(--success-text); }
.cal-chip-in_progress, .cal-chip-preparing, .cal-chip-recovery { background: var(--warn-bg); color: var(--warn-text); border-left-color: var(--warn-text); }
.cal-chip-in_surgery, .cal-chip-no_show { background: var(--danger-bg); color: var(--danger-text); border-left-color: var(--danger-text); }
.cal-chip-cancelled { background: var(--bg); color: var(--text-secondary); border-left-color: var(--text-tertiary); text-decoration: line-through; }

.cal-untimed { display: flex; flex-wrap: wrap; gap: 8px; }
.cal-untimed .cal-chip { min-width: 180px; margin-bottom: 0; }

.cal-week {
  display: grid;
  grid-template-columns: repeat(7, minmax(150px, 1fr));
  gap: 8px;
  min-width: 100%;
}

.cal-day { border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
.cal-day-today { border-color: var(--primary); }

.cal-day-head {
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-small);
  font-weight: 700;
}

.cal-day-head a { display: flex; justify-content: space-between; padding: 6px 8px; text-decoration: none; color: inherit; }
.cal-day-today .cal-day-head { background: var(--primary-light); color: var(--primary-text); }
.cal-day-count { color: var(--text-tertiary); font-weight: 400; }
.cal-day-body { padding: 6px; min-height: 90px; }

.cal-month {
  display: grid;
  grid-template-columns: repeat(7, minmax(96px, 1fr));
  gap: 6px;
  min-width: 100%;
}

.cal-weekday {
  font-size: var(--fs-micro);
  font-weight: 700;
  color: var(--text-secondary);
  text-align: center;
  padding-bottom: 2px;
}

/* A neighbouring month's days are drawn, because a booking in one is real and hiding it
   would make the cell lie — but dimmed, so the month keeps its shape. */
.cal-day-outside { opacity: 0.55; }
.cal-month .cal-day-body { min-height: 56px; padding: 4px; }
.cal-month .cal-day-name { font-variant-numeric: tabular-nums; }
.cal-month .cal-chip { margin-bottom: 1px; }

.cal-slot-add {
  display: block;
  text-align: center;
  padding: 2px;
  border-radius: var(--radius-sm);
  color: var(--text-tertiary);
  font-size: var(--fs-small);
  text-decoration: none;
}

.cal-slot-add:hover { background: var(--primary-light); color: var(--primary-text); }

@media (max-width: 900px) {
  .cal-toolbar { align-items: stretch; }
  .cal-filters { width: 100%; }
  .cal-filters select { flex: 1; }
}

/* ── Dose calculator result (v0.150.0) ─────────────────────────────────────────
   Two figures side by side: what to give in mg, and what that is in ml. The
   volume is the one a vet draws up, so it is the one that carries the accent. */

.dose-result { display: flex; flex-wrap: wrap; gap: 12px; }

.dose-result-figure {
  flex: 1 1 220px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
}

.dose-result-primary { background: var(--success-bg); border-color: var(--success-text); }
.dose-result-primary strong { color: var(--success-text); }
.dose-result-label { display: block; font-size: var(--fs-micro); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
.dose-result-figure strong { display: block; font-size: var(--fs-page); font-variant-numeric: tabular-nums; margin: 2px 0; }
.dose-result-sub { display: block; font-size: var(--fs-micro); color: var(--text-secondary); }

/* ── Staff functions (v0.152.0) ────────────────────────────────────────────────
   A multi-select of what a person does. Checkboxes and not `.switch` toggles: the
   standing rule is that a yes/no SETTING is a toggle, while picking several out of a
   list stays a checkbox row (CLAUDE.md, 2026-08-13). */

.staff-functions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
}

.staff-function {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  font-size: var(--fs-small);
  cursor: pointer;
  margin: 0;
}

/* On a list row: what someone does, after their name. */
.staff-function-tags { display: flex; flex-wrap: wrap; gap: 4px; }

/* ── Shift board (v0.156.0) ────────────────────────────────────────────────
   A work list, not a table. It is read standing up, on a phone as often as on a
   desk, so the row never scrolls sideways — below 600px it folds into a card and
   keeps its one important control at thumb size. */

.board-section > summary.board-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: var(--fs-title);
  font-weight: 600;
  list-style: none;
}
.board-section > summary.board-section-head::-webkit-details-marker { display: none; }
.board-section > summary.board-section-head::after {
  content: '';
  margin-left: auto;
  width: 7px; height: 7px;
  border-right: 2px solid var(--text-tertiary);
  border-bottom: 2px solid var(--text-tertiary);
  transform: rotate(45deg);
  transition: transform .15s ease;
}
.board-section[open] > summary.board-section-head::after { transform: rotate(-135deg); }

.board-section-overdue > summary.board-section-head,
.board-section-stale > summary.board-section-head { color: var(--danger-text); }
.board-section-due > summary.board-section-head { color: var(--primary-text); }

.board-group { margin-top: 14px; }
.board-group-head {
  /* Sticky so a long section never leaves you asking whose dose you are looking at. */
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-panel);
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.board-group-head a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: var(--text-primary);
}
.board-group-head a:hover strong { text-decoration: underline; }

.board-slots { list-style: none; margin: 0; padding: 0; }

.board-slot {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  grid-template-areas: 'time body actions';
  gap: 10px;
  align-items: center;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}
.board-slot:last-child { border-bottom: 0; }

.board-slot-time {
  grid-area: time;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  /* No nowrap: the column is fixed, so a date that does not fit has to wrap rather than run
     across the row beside it. The day and the delay are their own lines for the same reason. */
  line-height: 1.25;
}
.board-slot.is-late .board-slot-time { color: var(--danger-text); }
.board-slot-day,
.board-slot-late {
  display: block;
  font-size: var(--fs-micro);
  font-style: normal;
  font-weight: 400;
}
.board-slot-day { color: var(--text-secondary); }

.board-slot-body { grid-area: body; min-width: 0; }
.board-slot-title { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.board-slot-detail {
  display: block;
  font-size: var(--fs-micro);
  color: var(--text-secondary);
  margin-top: 2px;
}

/* The urgent stripe is doubled by the pill beside the name — colour alone never carries it. */
.board-slot.is-urgent { border-left: 3px solid var(--accent); padding-left: 8px; }

.board-slot-actions {
  grid-area: actions;
  display: flex;
  align-items: center;
  gap: 6px;
}
.board-slot-actions form { display: inline-flex; }

@media (max-width: 600px) {
  .board-slot {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      'time actions'
      'body actions';
    gap: 2px 10px;
    align-items: center;
    padding: 12px 0;
  }
  .board-slot-actions { align-self: center; }
  /* A control used with a thumb, mid-round, while holding an animal. */
  .board-slot-actions .btn { min-width: 44px; min-height: 44px; }
  .board-slot-day,
  .board-slot-late { display: inline; margin-left: 6px; }
}

/* ── Route chips on a prescription row (v0.158.0) ──────────────────────────
   A route can be more than one — "IV or SC" is one clinical thought — so the
   control is a set of toggles, not a box. They sit beside the dose because that
   is where the decision is made. */

.route-chips { display: flex; flex-wrap: wrap; gap: 4px; }

.route-chip {
  cursor: pointer;
  user-select: none;
  line-height: 1;
}
.route-chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.route-chip span {
  display: inline-block;
  padding: 4px 8px;
  font-size: var(--fs-micro);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-panel);
  color: var(--text-secondary);
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.route-chip:hover span { border-color: var(--primary); }
.route-chip input:checked + span {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-action);
}
/* Keyboard focus has to be visible on a control whose own input is off-screen. */
.route-chip input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Suggested by the catalogue, not yet confirmed by the vet — the same statement
   `.autofilled` makes on a text field. */
.route-picker.autofilled-routes .route-chip input:checked + span {
  background: var(--primary-light);
  border-color: var(--primary);
  color: var(--primary-text);
  border-style: dashed;
}

/* ── "Add to this prescription" bar (v0.158.0) ─────────────────────────────
   Four of the form's six cards answer questions most prescriptions do not ask.
   They start folded and come back from here, so the two that always matter —
   the weight and the medications — are no longer three screens apart. */

.rx-add-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  padding: 10px 14px;
}

.rx-add-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px;
  font: inherit;
  font-size: var(--fs-small);
  color: var(--text-secondary);
  background: var(--bg-panel);
  border: 1px dashed var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.rx-add-chip:hover { border-color: var(--primary); color: var(--primary-text); }
.rx-add-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Open: solid, because the section is now part of this prescription rather than
   something on offer. */
.rx-add-chip.is-on {
  border-style: solid;
  border-color: var(--primary);
  background: var(--primary-light);
  color: var(--primary-text);
}

@media (max-width: 600px) {
  /* Four chips and a label do not sit on one line on a phone; the label takes
     its own row rather than squeezing the chips into two characters each. */
  .rx-add-bar > span { flex: 1 0 100%; }
}

/* ── Simple mode (v0.222.14, ux-audit §6 B11) ──────────────────────────────
   One form drawn two ways. The mode is a single class on the <form>; nothing is
   removed, so a field hidden here is still named, still submitted and still what
   the record is built from. `.rx-keep` is written by the form's own script onto
   anything that already holds a value — that is what stops a mode from hiding
   something a person typed, whether they typed it a moment ago or last month.

   `!important` is needed because several of these carry an inline `display`
   (the row's flag line is a flex row, the labels inside it inline-flex), and an
   inline style outranks any rule here. */

.rx-simple [data-rx-simple-hide]:not(.rx-keep) { display: none !important; }

/* A manipulation has no dose and no route — but only simple mode acts on that. */
.rx-simple .medication-row-card.is-procedure [data-rx-med-only]:not(.rx-keep) { display: none !important; }

/* ── Reordering a medication row (v0.222.14) ───────────────────────────────
   The order of a prescription is read off the sheet top to bottom, and until
   now the only way to change it was to retype the rows. The grip drags; the
   arrows by the bin do the same from the keyboard. DOM order is submit order,
   so the row IS the line — see `reindexRows()` for the one thing that is not
   carried by position and has to be restamped. */

.rx-row-grip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 28px;
  padding-top: 22px;
}

.row-num-badge {
  font-weight: 700;
  font-size: 12px;
  color: var(--accent);
}

.rx-drag-handle {
  display: inline-flex;
  padding: 2px;
  color: var(--text-tertiary);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: grab;
  line-height: 1;
}
.rx-drag-handle:hover { color: var(--text-primary); background: var(--bg-panel); }
.rx-drag-handle:active { cursor: grabbing; }

.medication-row-card.is-dragging { opacity: .45; }

/* Where it would land. A line rather than a fill: the row underneath is a form, and
   tinting eleven fields to say „here" says it about the wrong thing. */
.medication-row-card.is-drop-above { box-shadow: 0 -3px 0 -1px var(--primary); }
.medication-row-card.is-drop-below { box-shadow: 0 3px 0 -1px var(--primary); }

/* A card names what it holds, and in simple mode it holds less. */
.rx-only-simple { display: none; }
.rx-simple .rx-only-simple { display: inline; }

/* The mode switch: one frame, two states, and NOTHING that changes size when it is pressed.
   Built as a pair of `btn-action`/`btn-tool` pills first — those differ in font-weight and in
   border as well as in fill, so both labels re-measured on every press and the control shifted
   under the cursor. The owner reported it as jumping about. Same shape as `.lang-seg`. */

.rx-mode-switch {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px 5px 3px 9px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  transition: border-color .15s ease;
}

.rx-mode-switch:hover { border-color: var(--primary); }

.rx-mode-label {
  font-size: 11px;
  color: var(--text-secondary);
  margin-right: 3px;
}

.rx-mode-switch > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 3px 10px;
  font: inherit;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--text-secondary);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}

.rx-mode-switch > button:hover { background: var(--bg); color: var(--text-primary); }
.rx-mode-switch > button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* `--primary-text` on `--primary-light` is the pair `check_contrast.php` already measures. */
.rx-mode-switch > button[aria-pressed="true"] {
  background: var(--primary-light);
  color: var(--primary-text);
}

/* ── Medication chart, grouped by course ───────────────────────────────────
   One block per course, its doses a single banded list running edge to edge.
   Separate rounded tiles with gaps read as a stack of controls; a banded list
   reads as a chart, which is what this is. Follows .docs/harness's mockup
   (artifact "გალიის ბარათი") one rule at a time, in app tokens. */

.med-course {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-bottom: 10px;
  /* So the slot band's fill reaches the rounded corners. */
  overflow: hidden;
}

.med-course:last-of-type { margin-bottom: 0; }

/* Something is overdue on this course. A stripe, not a fill: filling the block put the same
   colour behind the one row that is actually late, so the signal pointed at the course
   instead of at the dose. */
.med-course.is-late { border-left: 3px solid var(--danger-line); }
.med-course.is-done { opacity: .72; }

.med-course-head { display: flex; align-items: flex-start; gap: 8px; padding: 9px 12px 8px; }
.med-course-id { flex: 1; min-width: 0; }
.med-course-id b { display: block; font-size: 13.5px; line-height: 1.3; }
.med-course-id span { font-size: 11.5px; color: var(--text-secondary); }

.med-course-count {
  flex: none;
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  padding-top: 2px;
}

.med-course-count.is-done { color: var(--success-text); }
.med-course-menu { flex: none; }

/* Today's doses: one band, rows divided by a rule rather than by space. */
.med-course-due { border-top: 1px solid var(--border); background: var(--bg); }

.med-slot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  font-size: 12px;
  border-bottom: 1px solid var(--border);
}

.med-slot:last-child { border-bottom: none; }

.med-slot-when {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  width: 52px;
  flex: none;
}

/* What happened, in words — the part that makes this a record rather than a form. */
.med-slot-said {
  flex: 1;
  min-width: 0;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.med-slot.is-late { background: var(--danger-bg); }
.med-slot.is-late .med-slot-when,
.med-slot.is-late .med-slot-said { color: var(--danger-text); font-weight: 600; }

.med-slot.is-due { background: var(--primary-light); }
.med-slot.is-due .med-slot-said { color: var(--primary-text); font-weight: 600; }

.med-slot.is-done .med-slot-when { color: var(--success-text); font-weight: 400; }

.med-course-quiet {
  padding: 7px 12px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  font-size: 12px;
  color: var(--text-secondary);
}

.med-course-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 7px 12px;
  border-top: 1px solid var(--border);
  font-size: 11.5px;
  color: var(--text-secondary);
}

/* Pushes the schedule link and the money to the right, as in the mockup. */
.med-course-foot > .med-course-spacer { flex: 1; }
.med-course-foot b { color: var(--text-primary); font-weight: 600; }
.med-course-rx { display: inline-flex; align-items: center; gap: 4px; }

@media (max-width: 700px) {
  .med-slot-said { flex: 1 1 100%; order: 3; }
}
/* ══ Ward chart: two columns, journal feed, sidebar widgets (v0.161.0) ══════
   Left is what gets written during a shift, right is what gets read off it.
   A stack of equal-weight cards loses that distinction, which is what made
   this page hard to scan. Structure only — colours come from the tokens. */

.ward-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 16px;
  align-items: start;
}

.ward-work { min-width: 0; }
/* Deliberately NOT sticky. Nothing in this column has to stay in view while the chart is
   being read — an admission date and a room number are read once. Pinning it was solving
   an empty-space problem by pretending the content was urgent. */
.ward-side { min-width: 0; }
.ward-side .card { margin-bottom: 12px; }
.ward-side .card:last-child { margin-bottom: 0; }

/* Category tint off in every section of a record, not only in the chart: the page stacks
   clinical and billing cards under one strip, and alternating green and amber headers read as
   decoration rather than as meaning. The icon still carries the category.
   Extended from `.ward-body` to `.tab-panel` in v0.162.9 — until then the treatment section
   drew flat headers and the billing section three tinted ones, which is exactly the split the
   section strip was moved up to remove. `.tab-panel` carries a data-cat card on this page
   only, so nothing else is touched. */
.ward-body .card[data-cat] > .card-title,
.tab-panel > .card[data-cat] > .card-title {
  margin: 0 0 12px;
  padding: 0 0 8px;
  background: none;
  border-radius: 0;
  color: inherit;
  border-bottom: 1px solid var(--border);
}

.ward-body .card[data-cat="clinical"] > .card-title i,
.tab-panel > .card[data-cat="clinical"] > .card-title i { color: var(--cat-clinical); }
.ward-body .card[data-cat="billing"] > .card-title i,
.tab-panel > .card[data-cat="billing"] > .card-title i { color: var(--cat-billing); }

/* ── sidebar widgets ── */

.kv {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 4px 0;
  font-size: 12.5px;
  border-bottom: 1px solid var(--border);
}

.kv:last-of-type { border-bottom: none; }
.kv > span:first-child { color: var(--text-secondary); flex: none; }
.kv > span:last-child { font-weight: 600; text-align: right; min-width: 0; }

/* A row whose value is prose rather than a figure: the label keeps its line, the text gets the
   full width under it. `.kv` is a two-column row, so in a 320px rail the value lives in
   whatever the label leaves — an admission reason, which is a sentence or two every time, was
   rendering as a column three or four words wide. Normal weight and left-aligned, because at
   this length it is a paragraph and not a value; `pre-line` keeps the line breaks that were
   typed into the textarea. Declared after `.kv`'s own rules, which it matches in specificity. */
/* A second line under a value, in the weight the value is not. `.kv > span:last-child` is
   bold — right for a figure, wrong for everything a row might want to add to it, which is how
   the stay's duration ended up printing „10 თვე და 28 დღე (სულ: 333 დღე)" at one weight from
   end to end. */
.kv-sub {
  display: block;
  margin-top: 2px;
  font-weight: 400;
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}

.kv-stack { display: block; }
.kv-stack > span:last-child {
  display: block;
  margin-top: 3px;
  text-align: left;
  font-weight: 400;
  line-height: 1.45;
  white-space: pre-line;
}

.btn-linky {
  border: none;
  background: none;
  padding: 0 0 0 6px;
  font: inherit;
  font-size: 11px;
  color: var(--primary-text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.btn-linky:hover { text-decoration: underline; }

.trend { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.trend + .trend { border-top: 1px solid var(--border); }

.trend-label {
  font-size: 11.5px;
  color: var(--text-secondary);
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.spark { width: 74px; height: 26px; flex: none; }
.spark polyline { fill: none; stroke: var(--primary); stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; }
.spark circle { fill: var(--primary); }
.spark line { stroke: var(--border); stroke-width: 1; }

.trend-value { margin-left: auto; text-align: right; }
.trend-value > b { display: block; font-size: 14px; font-variant-numeric: tabular-nums; line-height: 1.2; }
.trend-value > span { font-size: 10.5px; color: var(--text-secondary); }

.ward-money {
  font-size: 26px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  text-align: center;
  color: var(--text-secondary);
}

/* Neutral until it is actually owed — a permanently red number stops signalling. */
.ward-money.is-owed { color: var(--danger-text); }

/* The bill at a glance in a record page's header (visit, v0.222.1): two labelled figures between
   the state pill and the actions menu, framed by hairlines so they read as a fact, not a button.
   The balance turns red only when something is owed, as the ward's figure does. */
.visit-bill {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 2px 12px;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
}
.visit-bill .info-value { white-space: nowrap; }
.visit-bill .info-value.is-owed { color: var(--danger-text); }

/* A SOAP section written in place: the field grows with the text, the save sits at its foot on
   the right (§22 - Save last), and the row never reflows under a narrow tab. */
.visit-note-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: end;
}
.visit-note-form textarea { width: 100%; resize: vertical; }

/* The new-client form's two columns (owner | animal): side by side on a wide screen, stacked into
   one column on a narrow one (v0.222.5, ux-audit-2026-09-13 §6 B9). The owner asked for exactly
   this — layout „ბ" on the desk, layout „ა" on a phone — so the breakpoint is the one place the
   choice lives, not two separate views. */
.nc-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: start;
}
@media (max-width: 880px) {
  .nc-cols { grid-template-columns: 1fr; }
}
.ward-money-label { font-size: 11.5px; color: var(--text-secondary); text-align: center; margin: 2px 0 10px; }
.ward-side-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; }

/* The bill at a glance in a record page's header (v0.222.1, visit first): two labelled figures
   between the state pill and the actions, ruled off on both sides so they read as a fact of the
   record rather than as a control. Owed money in the same ink the ward's money block uses;
   neutral otherwise, for the same reason — a permanently red number stops signalling. */
.visit-bill {
  display: flex;
  gap: 16px;
  align-items: center;
  padding: 2px 12px;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
}
.visit-bill .info-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
.visit-bill .info-value.is-owed { color: var(--danger-text); }

/* ── journal feed ── */

.jfilter { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 10px; }

.jchip {
  font-size: 11.5px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  border-radius: 99px;
  padding: 2px 11px;
  cursor: pointer;
  font-family: inherit;
}

.jchip:hover { background: var(--bg); }
.jchip.is-on { background: var(--primary-light); border-color: var(--primary); color: var(--primary-text); font-weight: 600; }

/* A filled band, not a bare line: it separates days at a glance while scrolling, which a
   1px rule does not. Runs to the card's edges — the feed is flush inside the card. */
.jday {
  background: var(--bg);
  /* 4px until 2026-09-04; the owner asked for more room. The band separates one day from the
     next and was reading as a rule rather than as a heading. */
  padding: 9px 12px;
  font-size: 10.5px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-secondary);
  font-weight: 600;
}

.jline { display: flex; gap: 10px; padding: 7px 12px; font-size: 12.5px; border-bottom: 1px solid var(--border); }
.jline:last-child { border-bottom: none; }

/* A deleted vital stays in the record — struck through, not removed. */
.jline.is-deleted { opacity: .55; text-decoration: line-through; }

.jline-time {
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  font-size: 11.5px;
  width: 42px;
  flex: none;
  padding-top: 1px;
}

.jline-icon { width: 20px; flex: none; text-align: center; color: var(--text-secondary); }
.jline-body { flex: 1; min-width: 0; }
.jline-who { font-size: 11px; color: var(--text-secondary); margin-left: 6px; }

.jchips { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 3px; }

.jchips .k {
  font-size: 11px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 99px;
  padding: 0 8px;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 1000px) {
  .ward-body { grid-template-columns: minmax(0, 1fr); }
}

/* A journal note can be an entire AI extraction. The feed is a timeline, not a reader:
   two lines here, the whole text on hover and still in the DOM for find-in-page. */
.jline-note { display: block; }

.jline-note.is-clamped {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}


/* The course-level menu sits with the progress count, not with the dose rows: it acts on the
   whole chain, and the destructive item in it must not be next to a button being tapped fast. */
.med-course-menu { flex: none; margin-left: 4px; }

/* The journal feed is flush inside its card: negative margins pull it out to the card's own
   padding so the day bands and row rules reach the edges, as the mockup has them. */
/* Full-bleed inside the card: the day bands and the row rules run to its edges. The inset is
   read from the card rather than repeated as a number — the two disagreed for as long as the
   journal sat in a tab panel, and the band overhung the frame by the difference. */
.jfeed { margin: 0 calc(-1 * var(--card-pad, 18px)) calc(-1 * var(--card-pad, 18px)); }
.jfeed > .empty-state { margin: 0 var(--card-pad, 18px) var(--card-pad, 18px); }

.med-course-icon { flex: none; color: var(--text-secondary); padding-top: 2px; }
.card-title-actions { display: inline-flex; gap: 6px; flex-wrap: wrap; }

/* The prescriptions behind the courses above: one line each, at the foot of the treatment card.
   They were a card of their own listing documents beside the work those documents ordered. */
.rx-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 12px;
}

.rx-strip-label { color: var(--text-secondary); display: inline-flex; align-items: center; gap: 5px; }
.rx-strip-empty { color: var(--text-secondary); font-style: italic; }

.rx-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 99px;
  padding: 2px 11px;
  background: var(--bg);
}

.rx-chip:hover { border-color: var(--primary); color: var(--primary-text); }
.rx-chip small { color: var(--text-secondary); font-size: 11px; }

/* A named division inside the treatment card: the scheduled courses above, the single acts
   below. Not a nested card — a box inside a box reads as two things, which is what folding
   the panel in was undoing. */
.med-section { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }

.med-section-head {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 10px;
}

/* ==========================================================================
   FEEDBACK WIDGET (v0.162.0) — a note left by clicking the thing it is about

   Development-only: `views/partials/feedback_widget.php` renders nothing unless
   `APP_FEEDBACK` is on, so on any other environment none of this is ever reached.
   The one rule that matters here is that the launcher must be findable and must never
   be in the way — hence a small circle in the corner at rest, and a mode that has to be
   switched on before a single click behaves differently.
   ========================================================================== */

.fb-launcher {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 300;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--btn-line);
  background: var(--bg-panel);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .14);
}

.fb-launcher i { font-size: 18px; }
.fb-launcher:hover { color: var(--primary-text); border-color: var(--primary); }

/* Pressed is a state, not a hover: the mode stays on across the whole page. */
.fb-launcher[aria-pressed="true"] {
  background: var(--action-bg);
  border-color: var(--action-bg);
  color: var(--on-action);
}

/* The debug bar owns the bottom edge where it is enabled. */
.fb-launcher-raised { bottom: 52px; }

body.fb-picking, body.fb-picking * { cursor: crosshair !important; }

/* Nothing on the page may act while the mode is on — the click is being taken by
   feedback.js, and a control that still lights up under the pointer says otherwise. */
body.fb-picking a:hover,
body.fb-picking button:hover { text-decoration: none; }

.fb-hint {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 300;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--text-primary);
  color: var(--bg-panel);
  font-size: var(--fs-small);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .2);
}

.fb-hint kbd {
  border: 1px solid rgba(255, 255, 255, .45);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
  font-size: var(--fs-micro);
}

/* Outline only, never a fill: a tinted box over a table cell hides the value the note is
   probably about. `pointer-events: none` is what keeps the element underneath pickable. */
.fb-highlight {
  position: fixed;
  z-index: 299;
  pointer-events: none;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.fb-dialog { max-width: 520px; }

.fb-dialog .fb-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-control);
}

.fb-dialog .fb-row .form-group { margin-bottom: var(--sp-group); }

/* What was clicked, quoted back. Monospace-free on purpose — the user picked a button, not
   a selector, and the selector is stored rather than shown. */
.fb-target {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: var(--sp-group);
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

.fb-target i { color: var(--accent); font-size: 14px; margin-top: 1px; }
.fb-target-text { word-break: break-word; }

.fb-error {
  margin-top: var(--sp-control);
  padding: 8px 10px;
  border-radius: var(--radius-md);
  background: var(--danger-bg);
  color: var(--danger-text);
  font-size: var(--fs-small);
}

.fb-toast {
  position: fixed;
  right: 18px;
  bottom: 70px;
  z-index: 300;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  background: var(--success-bg);
  color: var(--success-text);
  border: 1px solid color-mix(in srgb, var(--success-text) 30%, transparent);
  font-size: var(--fs-small);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .14);
  transition: opacity .3s ease;
}

.fb-toast-out { opacity: 0; }

/* Arrived here from a note: the element it was left on, marked for four seconds. Outline
   rather than a background, for the same reason the picking highlight is one. */
.fb-recall {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* The list page's own two pieces: the note text is the identity cell and wraps, and the
   place it was left is printed under it rather than in a column of its own — a URL column
   is unreadable at table widths and this one is context, not an axis. */
.fb-note-body { display: block; max-width: 46ch; }
.fb-note-place { display: block; margin-top: 2px; font-size: var(--fs-micro); color: var(--text-secondary); word-break: break-all; }

/* The counter beside the sidebar link. Only ever shows `new`, so an empty one is not
   rendered at all rather than drawn as a zero. */
.sb-count {
  margin-left: auto;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  /* The danger pill's own pair, not white-on---accent: `--accent` is amber in two themes and
     emerald in two more, where white ink measures 2.1–2.4:1. This pair is checked in all ten
     (`check_contrast.php`, "საშიშროების ჭდე" — 7.82 worst case). */
  background: var(--danger-bg);
  color: var(--danger-text);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* A write that succeeded and still needs reading — over-capacity, a fallback that was used,
   anything the record now carries that the user did not ask for. Green would hide it; red
   would say the save failed, which is the mistake this class exists to correct. */
.flash-warn {
  background: var(--warn-bg);
  color: var(--warn-text);
  border: 1px solid color-mix(in srgb, var(--warn-text) 30%, transparent);
}

/* ── A shared spot on the ward map (v0.163.8) ──────────────────────────────
   One spot holding several patients used to stack them, so five made a ~390px
   card. Worse, `.card-grid` stretches a row to its tallest item, so the
   single-patient cards beside it grew to match and stood mostly empty. */

/* Short cards stop growing to meet the tall one. This alone removes the empty
   space; the rules below remove the height that caused it. */
.card-grid { align-items: start; }

/* Two columns' worth of width, so the patients can sit side by side. `auto-fill`
   already collapses this to one column on a narrow screen, so no media query is
   needed — the span simply has nowhere to go and the card behaves as before. */
.cage-shared { grid-column: span 2; }

.cage-shared .occupant-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 8px;
}

/* Compact inside a shared spot only: the 60px portrait is worth its space when a
   spot holds one animal and is the whole card, and is not when it is one of six. */
.cage-shared .patient-strip-avatar,
.cage-shared .patient-strip-avatar-fallback {
  width: 34px;
  height: 34px;
}
.cage-shared .patient-strip { gap: 7px; }
.cage-shared .patient-strip-name { font-size: var(--fs-small); }
.cage-shared .patient-strip-state { gap: 4px; flex-wrap: wrap; }

@media (max-width: 600px) {
  /* One column at this width, so a span of two would overflow the grid. */
  .cage-shared { grid-column: auto; }
}

/* Rearranging the spots in a room (v0.164.0). The header is the handle: a patient
   inside a spot is a link, and a draggable ancestor swallows the click that opens it. */
.card-header[data-cage-handle] { cursor: grab; }
.card-header[data-cage-handle]:active { cursor: grabbing; }

/* The box being carried is faded, and the gap it will land in is the box itself —
   it is moved in the DOM as the pointer passes, so the preview IS the result. */
.card-nested.is-dragging {
  opacity: 0.5;
  outline: 2px dashed var(--primary);
  outline-offset: -2px;
}

/* =========================================================
   LIGHTBOX — §166
   ---------------------------------------------------------
   ფაილი გვერდის შიგნით იხსნება, ახალ ჩანართში კი არა. მარკირება
   `assets/js/lightbox.js`-ს შეაქვს, ამიტომ აქ მხოლოდ ის კლასებია,
   რომლებსაც ის ქმნის — ხედში არცერთი მათგანი არ იწერება.
   ========================================================= */
dialog.modal.lightbox {
  width: min(1100px, 96vw);
  max-width: 96vw;
}
.lightbox .lightbox-counter {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.lightbox-body {
  display: flex;
  align-items: center;
  gap: 8px;
}
.lightbox-stage {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lightbox-media {
  max-width: 100%;
  /* ეკრანის სიმაღლეს ეყრდნობა და არა სურათისას: ვერტიკალური რენტგენი
     სხვაგვარად დიალოგს ეკრანზე მაღალს ხდის და ზედა კიდე იკარგება. */
  max-height: 72vh;
  display: block;
  border-radius: var(--radius-sm);
}
.lightbox-frame {
  width: 100%;
  height: 72vh;
  border: 1px solid var(--border);
  background: var(--bg);
}
/* ისარი გვერდზე დგას და არა სურათზე — გამოსახულების დაფარვა ზუსტად ის
   არის, რასაც კითხვა უშლის ხელს. */
.lightbox-nav {
  flex: 0 0 auto;
}
.lightbox-caption {
  padding: 8px 16px 14px;
  font-size: 12px;
  color: var(--text-secondary);
  white-space: pre-wrap;
  word-break: break-word;
}
@media (max-width: 600px) {
  .lightbox-media,
  .lightbox-frame {
    max-height: 60vh;
  }
  .lightbox-frame {
    height: 60vh;
  }
}

/* =========================================================
   DIAGNOSTIC FILES — ერთად ატვირთულის ჯგუფი (§166)
   ---------------------------------------------------------
   ჯგუფის პირველ მწკრივს თარიღი, ტიპი, კომენტარი და ავტორი უწერია,
   დანარჩენებს — მხოლოდ ფაილი და მოქმედებები. საზღვარი ხაზით ჩანს,
   თორემ ცარიელი უჯრები ეკრანზე უბრალოდ დაკარგულ მონაცემად იკითხება.
   ========================================================= */
/* კვლევა ერთი მწკრივია — დამატებითი მწკრივები აღარ იხატება, ე.ი. ზებრაც და
   ხაზებიც ისევ ცხრილის ჩვეულებრივი წესით მუშაობს (v0.166.0-ის შესწორება:
   პირველი ვარიანტი ცარიელ მწკრივებს ტოვებდა და ორივეს არღვევდა). */
.doc-thumb-wrap {
  position: relative;
  display: inline-block;
  line-height: 0;
}
/* ერთ ფაილზე მოქმედება მის თავზეა და არა ცალკე მწკრივში: ჩანაცვლება — ყოველთვის,
   წაშლა — მხოლოდ კვლევის შიგნით (ცალკე მდგომს მწკრივის თავისი წაშლა აქვს). */
.doc-thumb-tools {
  position: absolute;
  top: 2px;
  right: 2px;
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.doc-thumb-wrap:hover .doc-thumb-tools,
.doc-thumb-tools:focus-within {
  opacity: 1;
}
.doc-thumb-tools .btn {
  width: 18px;
  min-width: 18px;
  height: 18px;
  min-height: 18px;
  padding: 0;
  /* The app's own corner, not a circle: every other icon button in the interface is a rounded
     square, and one round one reads as a different kind of control. */
  border-radius: var(--radius-sm);
}
.doc-thumb-tools .btn i {
  font-size: 12px;
}

/* შეხებით მართვისას hover არ არსებობს — ღილაკები მუდამ ჩანს. */
@media (hover: none) {
  .doc-thumb-tools {
    opacity: 1;
  }
}
/* ერთი კვლევის ფაილები ერთ ზოლად — რამდენიც არ უნდა იყოს, მწკრივი ერთი რჩება. */
.doc-strip {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
}

/* =========================================================
   SOURCE SHEET BESIDE THE RECORD — §166
   ---------------------------------------------------------
   Recording from a photograph is a comparison between two things:
   the sheet, and what was read off it. A tab cannot serve that —
   switching to it takes the grid off screen, so the comparison is
   made from memory, at exactly the point where 44 and 4.4 have to
   be told apart.
   The sheet therefore stands as a column beside the record and is
   `sticky`: it stays put while the grid scrolls, because that is
   when the checking happens.
   On a narrow screen the column drops below and `sticky` is turned
   off — a split phone shows neither of them rather than both.
   ========================================================= */
.src-split {
  display: grid;
  /* A third, not a half: the right-hand side of the comparison is the results
     grid — five columns — and squeezing it is precisely the cost the comparison
     was meant to avoid. A third leaves an A4 sheet at a size a figure reads at. */
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: 14px;
  align-items: start;
}

.src-media {
  position: sticky;
  /* The 56px header bar, plus room to breathe. */
  top: 68px;
}

.src-media-frame {
  display: block;
  width: 100%;
  /* Bounded by the screen and not by the sheet — the same reason the lightbox
     has it: a portrait sheet makes the column taller than the viewport, and
     sticking it then means nothing, since its top edge is already gone. */
  max-height: calc(100vh - 150px);
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: var(--bg);
}

a.src-media-link {
  display: block;
  cursor: zoom-in;
}

iframe.src-media-frame {
  height: calc(100vh - 150px);
  border: 1px solid var(--border);
}

@media (max-width: 900px) {

  .src-split {
    grid-template-columns: 1fr;
  }

  .src-media {
    position: static;
  }

  .src-media-frame {
    max-height: 60vh;
  }

  iframe.src-media-frame {
    height: 60vh;
  }
}

/* ── The two timings on a recorded dose (v0.189.0) ─────────────────────────
   „+2 სთ გეგმიდან · 24 სთ წინა დოზიდან". Quiet by default: on a chart that is
   read far more often than it is written to, the reading only has to be there
   when someone looks for it. `.is-off` is the one case that has to catch the
   eye — the interval between doses, not the deviation from the plan, is what
   makes it. */
.med-slot-timing {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 400;
  color: var(--text-tertiary);
  line-height: 1.4;
}

.med-slot-timing.is-off {
  color: var(--warn-text);
  font-weight: 600;
}

/* The "this course has drifted" offer, under a course's slots (v0.189.0). Quiet — it is a
   suggestion about the plan, not an alarm about the animal. */
.med-course-drift {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding: 8px 12px;
  border-radius: var(--radius-md);
  background: var(--warn-bg);
  color: var(--warn-text);
  font-size: var(--fs-micro);
  line-height: 1.5;
}

/* ── Medication by episode on the pet profile (v0.190.0) ────────────────────
   One line per visit or stay: what it was, when, how much, and the first few
   drugs. A link, because the detail lives on the episode's own page. */
.episode-meds { display: flex; flex-direction: column; gap: 2px; }

.episode-med {
  display: block;
  padding: 8px 10px;
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
}

.episode-med:hover { background: var(--bg); }

.episode-med-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 13px;
}

.episode-med-date { color: var(--text-secondary); }

.episode-med-count {
  margin-left: auto;
  font-size: var(--fs-micro);
  color: var(--text-tertiary);
}

.episode-med-names {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-micro);
  color: var(--text-secondary);
  line-height: 1.5;
}

.episode-med-rest { color: var(--text-tertiary); }

/* --- The ambulatory work board (v0.200.0) -----------------------------------------------
   One chain of columns: booked → arrived → with the vet → at the till → done. Five columns
   is a desk's width and not a law — `repeat(5, ...)` cut the last of them off the moment the
   window narrowed, which is how the owner found this. `auto-fit` lays out as many columns as
   the width actually holds and wraps the rest onto the next row; below 600px the board becomes
   five stacked sections, which is the only shape a phone can read. The board never scrolls
   sideways: a column you have to find by swiping is a column nobody looks at. */
.wboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  align-items: start;
}

.wboard-col { padding: 10px; min-height: 120px; }

.wboard-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.wboard-head-title { font-weight: 600; font-size: 13px; }

.wboard-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 8px 10px;
  margin-bottom: 8px;
  background: var(--bg-panel);
}

.wboard-card:last-child { margin-bottom: 0; }

/* The name takes the row, the actions menu keeps the corner. */
.wboard-card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 6px;
  margin-bottom: 6px;
}

.wboard-card-top .wboard-pet { margin-bottom: 0; flex: 1; min-width: 0; }
.wboard-card-top .has-menu { flex: none; }

.wboard-pet {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: inherit;
  margin-bottom: 6px;
}

.wboard-pet-name { font-weight: 600; font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
.wboard-meta { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; overflow-wrap: anywhere; }

/* Wraps rather than pushes: the time and the button are two things, not one line. */
.wboard-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 6px;
}

.wboard-pills { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
.wboard-btn { padding: 2px 8px; font-size: 11px; }
.wboard-empty { font-size: 12px; padding: 14px 6px; }

@media (max-width: 600px) {
  /* One column per row, and an empty one collapses to its own heading. Five stacked boxes each
     saying „ცარიელია" is most of a phone screen spent on nothing; the count in the heading says
     the same thing in one line. `:last-child` does it without :has() — with no cards under it,
     the heading is the column's last child. */
  .wboard { grid-template-columns: minmax(0, 1fr); }
  .wboard-col { min-height: 0; }
  .wboard-col > .wboard-empty { display: none; }
  .wboard-head:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }
}

/* --- The working-hours editor (v0.203.0) ------------------------------------------------
   Seven rows, each a day: the switch that says whether it is worked, then the hours. The day
   name sits inside the switch's label so the whole thing is one target. */
.sched-day {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.sched-day:last-of-type { border-bottom: 0; }
.sched-day-toggle { min-width: 190px; }
.sched-day-name { font-size: 13px; }

.sched-day-times { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }

.sched-time { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: var(--text-secondary); }
.sched-time input { width: 120px; }

.sched-picker { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }

.sched-exception-form {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

@media (max-width: 700px) {
  /* The switch keeps the first line to itself; the hours wrap under it in pairs. */
  .sched-day { align-items: flex-start; flex-direction: column; gap: 8px; }
  .sched-day-times { width: 100%; }
  .sched-time { flex: 1 1 45%; }
  .sched-time input { width: 100%; }
}

/* --- Free hours, offered under a date field (v0.204.0) ---------------------------------- */
.slot-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: 8px;
}

.slot-picker:empty { margin-top: 0; }
.slot-picker-label,
.slot-picker-empty { font-size: 11px; color: var(--text-secondary); }

.slot-chip {
  font: inherit;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 99px;
  background: var(--bg-panel);
  color: var(--text);
  cursor: pointer;
}

.slot-chip:hover { border-color: var(--primary); }

/* The hour that was taken, so the row still says which one after the field is filled. */
.slot-chip.is-chosen {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

/* --- The month grid (v0.205.0) ------------------------------------------------------------
   One person's month as it will actually be worked. The week is the rule; a cell that carries
   an amendment says so with its own left edge rather than a different colour, so „closed" and
   „amended" can both be true at once and still be read. */
.sched-month-nav { display: flex; align-items: center; gap: 8px; }
.sched-month-name { font-weight: 600; font-size: 14px; min-width: 150px; text-align: center; }

.sched-month-head,
.sched-month {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
}

.sched-month-head {
  margin-bottom: 6px;
  font-size: 11px;
  color: var(--text-secondary);
  text-align: center;
}

.sched-cell {
  font: inherit;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 68px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  color: var(--text);
  cursor: pointer;
}

.sched-cell-day { font-size: 13px; font-weight: 600; }

.sched-cell-hours {
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.sched-cell.is-outside { opacity: 0.45; }
.sched-cell.is-closed { background: var(--bg); }
.sched-cell.is-closed .sched-cell-hours { color: var(--text-tertiary); }
.sched-cell.is-amended { border-left: 3px solid var(--warn-text, var(--primary)); }
.sched-cell.is-today .sched-cell-day { color: var(--primary); }

.sched-cell.is-chosen {
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
}

.sched-month-tools,
.sched-month-apply {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.sched-month-apply { margin-top: 0; padding-top: 0; border-top: 0; }
.sched-month-hours { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
.sched-month-count { font-size: 12px; color: var(--text-secondary); margin-left: auto; }

@media (max-width: 700px) {
  /* Seven columns still, because a month that is not a week wide is not a month — the cells
     simply get shorter and drop their hours to one line. */
  .sched-cell { min-height: 52px; padding: 4px; }
  .sched-cell-hours { font-size: 10px; }
  .sched-month-count { margin-left: 0; }
}


/* --- Where a page says which shape it is drawn in (v0.206.0) ------------------------------
   The section row carries places on the left and this on the right: „ხედი: [ბარათები]". One
   button naming the shape you would move to, labelled because on a row of place names an
   unlabelled button reads as another place.

   Chosen from five candidates the owner looked at side by side. What survived the comparison is
   the split: list-versus-cards is one question and day/week/month is another, so they are two
   controls beside two different things and never one control with four values. */
.section-nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.section-nav-row > .section-nav { flex: 1; min-width: 0; }
.section-nav-aside { flex: none; }

.view-control { display: inline-flex; align-items: center; gap: 8px; }
.view-control-label { font-size: 12px; color: var(--text-secondary); }

/* A page with no section row of its own puts the control on a line under the header. */
.view-row { display: flex; justify-content: flex-end; margin: 0 0 12px; }

/* The second question, where a page has one: day / week / month, beside the date walk it
   changes rather than among the filters — and as plain links, because on that row they are read
   more often than pressed and bordered pills beside „‹ დღეს ›" made it a control panel. */
.vlinks { display: flex; gap: 10px; align-items: center; font-size: 13px; flex-wrap: wrap; }
.vlinks > a { text-decoration: none; color: var(--text-secondary); }
.vlinks > a:hover { color: var(--text-primary); }
.vlinks > a[aria-current="true"] { color: var(--text-primary); font-weight: 700; }
.vlinks .sep { color: var(--text-tertiary); }

@media (max-width: 700px) {
  .section-nav-aside { width: 100%; }
}

/* --- A list drawn as cards (v0.206.0) -----------------------------------------------------
   The table compares a column; this one is for scanning faces and names. Same records, same
   order, same actions — only the arrangement differs, which is why it is a view and not a
   second page. `auto-fill` rather than a fixed count: the grid is as wide as the window allows
   and collapses to one column on a phone without a media query. */
.ecards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 10px;
}

/* The card view's card IS the phone list's card (owner instruction, 2026-09-14): the table under
   600px restacks into exactly this — `table.tbl.tbl-cards` in the 600px block. Every value below
   mirrors a declaration there (border, radius, 10px padding with 40px kept clear for the corner
   actions, a 13px bold name row, 11.5px secondary lines at 1.6). Change one, change the other. */
.ecard {
  position: relative;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px;
}

.ecard-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text-primary);
  text-decoration: none;
}

.ecard-avatar {
  /* 80px, owner's choice (2026-09-14). The phone card's lead photo is the same size. */
  width: 80px;
  height: 80px;
  border-radius: var(--radius-md);
  object-fit: cover;
  flex: none;
}

.ecard-avatar-blank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 34px;
  font-weight: 400;
}

.ecard-name {
  overflow-wrap: anywhere;
}

.ecard-lines {
  font-size: 11.5px;
  color: var(--text-secondary);
  line-height: 1.6;
}


/* Today · tomorrow · this week … above a booking list (v0.208.2). Links rather than buttons,
   the same family the calendar's day/week/month row uses. */
/* The filter card holds two things: what narrows the list, and which stretch of days it covers.
   One box with the first on the left and the second on the right — the shape the calendar's own
   toolbar has, which is where the owner pointed. */
.filters-bar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.filters-bar { align-items: center; }
.filters-bar > form { flex: 1; min-width: 0; }
.filters-form { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }

/* The label sits BEFORE the control, not above it: one filter and a row of links do not need
   two lines, and the card is shorter for it. */
.filter-inline { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-secondary); }
.filter-inline > select { min-width: 180px; }
/* A label with two date inputs is 404px wide and did not wrap, so on a phone the „to" date was cut
   off by html's `overflow-x: clip` (surgery, visit and ward registers; pass-2 audit). */
@media (max-width: 600px) {
  .filter-inline { flex-wrap: wrap; max-width: 100%; }
  .filter-inline > select { min-width: 0; max-width: 100%; }
}

/* `flex: 0 1 auto` + `min-width: 0`, not `flex: none`: a box that may not shrink keeps its one-line
   width, so the `.vlinks` inside never wraps and on a phone „ყველა" was cut off by html's
   `overflow-x: clip` - unreachable on five list pages (pass-2 audit). On a wide screen nothing moves. */
/* One purchase line: item, quantity, cost, batch, expiry, remove. Six columns on a phone left
   quantity and cost 29px wide (pass-2 audit), so below 600px it becomes two: the item takes the
   row, the rest pair up. */
.purchase-row-grid {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1.2fr 1.2fr 40px;
  gap: 10px;
  align-items: end;
}
@media (max-width: 600px) {
  .purchase-row-grid { grid-template-columns: 1fr 1fr; }
  .purchase-row-grid > :first-child { grid-column: 1 / -1; }
  .purchase-row-grid > :last-child { grid-column: 1 / -1; justify-self: end; }
}

.period-links {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  flex: 0 1 auto;
  min-width: 0;
}

/* Reinstalled Illustration family: fit complete artwork by height and keep menus in flow. */
.module-banner.illustration-header {
  background-color: var(--bg-panel);
  background-size: auto 100%;
  background-position: right center;
  background-repeat: no-repeat;
}
.illustration-header:not(.content-bleed-top) {
  margin-bottom: 16px;
  padding-bottom: 10px;
}
.illustration-header:has(> .section-nav),
.illustration-header:has(> .section-nav-row),
.illustration-header:has(> .view-row) {
  padding-bottom: 0;
}
@media (max-width: 700px) {
  .module-banner.illustration-header { background-size: auto 72%; }
}

/* Ambulatory artwork uses its intrinsic bounds: the edge mask must shrink with
   the image, not with a letterboxed background container. */
html .module-banner.illustration-header:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) {
  position: relative;
  isolation: isolate;
  background-image: none;
  background-color: var(--bg-panel);
  padding-bottom: 0;
}
.module-banner:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: url(/assets/img/ambulatory-watercolor-trail-v1.webp) center / 100% 100% no-repeat;
  -webkit-mask-image: linear-gradient(to right, black 65%, transparent);
  mask-image: linear-gradient(to right, black 65%, transparent);
}
/* Dedicated palette-specific textures render in their native colors. */
html[data-theme="pharmacy"] :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header)::before {
  background-image: url(/assets/img/watercolor-trail-pharmacy-v1.webp);
}
html[data-theme="terminal"] :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header)::before {
  background-image: url(/assets/img/watercolor-trail-terminal-v1.webp);
}
/* Reuse the same watercolor contours in each dark palette. Filter only this
   decorative layer, never the banner containing menus and other controls. */
html[data-theme="dark"] :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header)::before {
  mix-blend-mode: screen;
  opacity: 0.65;
  -webkit-mask-image: linear-gradient(to right, black 65%, transparent),
                      linear-gradient(to bottom, transparent, black 20%, black 80%, transparent);
  mask-image: linear-gradient(to right, black 65%, transparent),
              linear-gradient(to bottom, transparent, black 20%, black 80%, transparent);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
html[data-theme="dark"] :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header)::before {
  filter: invert(1) hue-rotate(180deg) saturate(0.7) brightness(0.8) contrast(1.15);
}
/* Clip only the decorative layer, keeping header menus free to open outside. */
:is(.ambulatory-art-viewport, .surgery-art-viewport, .watercolor-art-viewport) {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: -1;
  pointer-events: none;
}
:is(.ambulatory-header-art, .surgery-header-art, .watercolor-header-art) {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  z-index: -1;
  pointer-events: none;
  /* Fade the actual image edges, including the top/bottom on narrow headers.
     Separate masks intersect, leaving the physical objects fully visible. */
  -webkit-mask-image: linear-gradient(to right, transparent, black 38%),
                      linear-gradient(to bottom, transparent, black 12%, black 85%, transparent);
  mask-image: linear-gradient(to right, transparent, black 38%),
              linear-gradient(to bottom, transparent, black 12%, black 85%, transparent);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
:is(.ambulatory-header-art--dark, .surgery-header-art--dark, .watercolor-header-art--dark),
:is(.ambulatory-header-art--pharmacy, .surgery-header-art--pharmacy, .watercolor-header-art--pharmacy),
:is(.ambulatory-header-art--terminal, .surgery-header-art--terminal, .watercolor-header-art--terminal) { display: none; }
html[data-theme="dark"] :is(.ambulatory-header-art--light, .surgery-header-art--light, .watercolor-header-art--light),
html[data-theme="pharmacy"] :is(.ambulatory-header-art--light, .surgery-header-art--light, .watercolor-header-art--light),
html[data-theme="terminal"] :is(.ambulatory-header-art--light, .surgery-header-art--light, .watercolor-header-art--light) { display: none; }
html[data-theme="dark"] :is(.ambulatory-header-art--dark, .surgery-header-art--dark, .watercolor-header-art--dark),
html[data-theme="pharmacy"] :is(.ambulatory-header-art--pharmacy, .surgery-header-art--pharmacy, .watercolor-header-art--pharmacy),
html[data-theme="terminal"] :is(.ambulatory-header-art--terminal, .surgery-header-art--terminal, .watercolor-header-art--terminal) { display: block; }
:is(.ambulatory-bottom-actions, .surgery-bottom-actions, .watercolor-bottom-actions) {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
}
:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .section-nav-row { border-bottom: 1px solid var(--rule); }
:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .section-nav-row > .section-nav { border-bottom: 0; }
@media (min-width: 1201px) {
  :is(.ambulatory-header-art, .surgery-header-art, .watercolor-header-art) {
    transform: translateY(-50%) scale(1.4);
    transform-origin: right center;
  }
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > :is(.ambulatory-bottom-actions, .surgery-bottom-actions, .watercolor-bottom-actions) {
    position: absolute;
    right: var(--content-pad);
    bottom: 15px;
    max-width: 48%;
  }
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .section-nav-row {
    justify-content: flex-start;
    padding-right: var(--header-actions-space, 460px);
  }
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .section-nav-row > .section-nav { flex: 0 1 auto; }
}
@media (max-width: 1200px) {
  /* Start the contained artwork at the breadcrumb edge instead of letterboxing
     above it. Keep the bottom fade where the image meets the watercolor trail. */
  :is(.ambulatory-header-art, .surgery-header-art, .watercolor-header-art) {
    top: 0;
    transform: none;
    -webkit-mask-image: linear-gradient(to right, transparent, black 38%),
                        linear-gradient(to bottom, black 85%, transparent);
    mask-image: linear-gradient(to right, transparent, black 38%),
                linear-gradient(to bottom, black 85%, transparent);
  }
  .module-banner:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) { display: flex; flex-direction: column; }
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > :is(.ambulatory-bottom-actions, .surgery-bottom-actions, .watercolor-bottom-actions) {
    order: 1;
    align-self: flex-end;
    max-width: 100%;
    margin: 0 0 15px;
  }
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .section-nav-row { order: 2; width: 100%; }
}
@media (max-width: 480px) {
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > :is(.ambulatory-bottom-actions, .surgery-bottom-actions, .watercolor-bottom-actions) { align-self: stretch; gap: 6px; }
  :is(.ambulatory-bottom-actions, .surgery-bottom-actions, .watercolor-bottom-actions) > a {
    flex: 1 1 145px;
    justify-content: center;
    white-space: normal;
    text-align: center;
  }
}

.surgery-flow-header { --header-actions-space: 240px; }

/* Register headers also close on a view-control row when they have no tabs. */
.watercolor-flow-header { --header-actions-space: 240px; }
@media (min-width: 1201px) {
  .watercolor-flow-header > .view-row { padding-right: var(--header-actions-space); }
}
@media (max-width: 1200px) {
  .watercolor-flow-header > .view-row { order: 2; width: 100%; }
}


/* Remaining module headers: content-sized rows prevent wrapped controls overlapping tabs. */
.module-banner.watercolor-rollout-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto);
  grid-template-rows: auto 1fr auto;
  min-height: 190px;
  column-gap: 16px;
  padding-bottom: 0;
}
.watercolor-rollout-header > .page-header {
  grid-column: 1 / -1;
  grid-row: 2;
  align-self: start;
  min-width: 0;
  margin-bottom: 30px;
}
.watercolor-rollout-header > :not(.page-header):not(.watercolor-bottom-actions):not(.watercolor-header-footer):not(.watercolor-art-viewport) {
  grid-column: 1 / -1;
}
.module-banner.watercolor-rollout-header > .watercolor-bottom-actions {
  position: static;
  grid-column: 2;
  grid-row: 3;
  align-self: end;
  max-width: 650px;
  margin: 0 0 15px;
  min-width: 0;
}
.watercolor-rollout-header > .watercolor-header-footer {
  grid-column: 1;
  grid-row: 3;
  align-self: end;
  min-width: 0;
}
.watercolor-rollout-header:not(:has(> .watercolor-bottom-actions)) > .watercolor-header-footer { grid-column: 1 / -1; }
.watercolor-rollout-header::after {
  content: "";
  position: absolute;
  left: var(--content-pad, 16px);
  right: var(--content-pad, 16px);
  bottom: 0;
  border-bottom: 1px solid var(--rule);
  pointer-events: none;
}
.watercolor-header-footer .section-nav,
.watercolor-header-footer .section-nav-row { border-bottom: 0; margin-bottom: 0; flex-wrap: wrap; }
.watercolor-header-footer > div[style] { flex-wrap: wrap; margin-bottom: 0 !important; }
.watercolor-rollout-header .page-actions,
.watercolor-rollout-header .search-box { min-width: 0; max-width: 100%; }
.watercolor-rollout-header .search-box input { min-width: 0; }
.watercolor-rollout-header .watercolor-bottom-actions > :is(a, button) { white-space: normal; text-align: center; }
@media (max-width: 1200px) {
  .module-banner.watercolor-rollout-header { display: flex; flex-direction: column; min-height: 190px; }
  .watercolor-rollout-header > .page-header { width: 100%; margin-bottom: 24px; }
  .module-banner.watercolor-rollout-header > .watercolor-bottom-actions { max-width: 100%; margin-top: auto; }
  .watercolor-rollout-header > .watercolor-header-footer { order: 2; width: 100%; margin-top: auto; }
  .watercolor-rollout-header:has(> .watercolor-bottom-actions) > .watercolor-header-footer { margin-top: 0; }
}
@media (max-width: 600px) {
  .watercolor-rollout-header > .page-header { flex-wrap: wrap; gap: 10px; }
  .watercolor-rollout-header .page-actions { width: 100%; }
}


/* Native abstract waves extend the clinical artwork through the left title area. */
html:not([data-header-style="illustration"]) .module-banner.abstract-flow-header::before {
  background: url(/assets/img/lab-abstract-waves-light-v1.webp) center / 100% 100% no-repeat;
  filter: none;
  mix-blend-mode: normal;
  opacity: 1;
  -webkit-mask-image: linear-gradient(to right, black 40%, transparent 95%);
  mask-image: linear-gradient(to right, black 40%, transparent 95%);
  -webkit-mask-composite: source-over;
  mask-composite: add;
}
html:not([data-header-style="illustration"])[data-theme="dark"] .module-banner.abstract-flow-header::before {
  background-image: url(/assets/img/lab-abstract-waves-dark-v1.webp);
  filter: saturate(0.55) brightness(0.85);
  opacity: 0.6;
}
html:not([data-header-style="illustration"])[data-theme="pharmacy"] .module-banner.abstract-flow-header::before {
  background-image: url(/assets/img/lab-abstract-waves-pharmacy-v1.webp);
  filter: saturate(0.8);
  opacity: 0.8;
}
html:not([data-header-style="illustration"])[data-theme="terminal"] .module-banner.abstract-flow-header::before {
  background-image: url(/assets/img/lab-abstract-waves-terminal-v1.webp);
  filter: saturate(0.55) brightness(0.85);
  opacity: 0.6;
}


/* Approved abstract clinical artwork: enlarge inside the decorative viewport. */
@media (min-width: 1201px) {
  html:not([data-header-style="illustration"]) .abstract-flow-header :is(.watercolor-header-art, .ambulatory-header-art, .surgery-header-art) {
    transform: translateY(-50%) scale(1.5);
    transform-origin: right center;
  }
}

/* Approved softening for the dark clinical palettes. */
html:not([data-header-style="illustration"])[data-theme="dark"] .abstract-flow-header :is(.watercolor-header-art--dark, .ambulatory-header-art--dark, .surgery-header-art--dark),
html:not([data-header-style="illustration"])[data-theme="terminal"] .abstract-flow-header :is(.watercolor-header-art--terminal, .ambulatory-header-art--terminal, .surgery-header-art--terminal) {
  filter: saturate(0.55) brightness(0.85);
  opacity: 0.8;
}

/* Keep pharmacy's ivory ground bright, with only mild color softening. */
html:not([data-header-style="illustration"])[data-theme="pharmacy"] .abstract-flow-header :is(.watercolor-header-art--pharmacy, .ambulatory-header-art--pharmacy, .surgery-header-art--pharmacy) {
  filter: saturate(0.8);
  opacity: 0.9;
}

/* Original stock/shop illustrations keep upper-edge objects inside the header. */
@media (min-width: 1201px) {
  html[data-header-style="illustration"] .watercolor-rollout-header:is([data-header-art="stock"], [data-header-art="shop"]) .watercolor-header-art {
    transform: translateY(-50%);
  }
}
.header-style-row { flex-wrap: wrap; }

/* Dashboard art stays in the title band, above the statistics and work lists. */
html .module-banner.dashboard-flow-header {
  min-height: 200px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding-top: 14px;
  padding-bottom: 20px;
}
.dashboard-flow-header > .page-header { margin: 0; }
/* No action controls in this banner: keep the artwork's native contrast. */
.module-banner.dashboard-flow-header .page-header::before { content: none; }
@media (max-width: 900px) {
  html .module-banner.dashboard-flow-header { min-height: 170px; }
}
@media (max-width: 600px) {
  html .module-banner.dashboard-flow-header {
    min-height: 160px;
    justify-content: flex-start;
  }
}

/* Module search controls stay next to their lists. */
.list-search-toolbar { display:flex; gap:12px; flex-wrap:wrap; align-items:center; margin:0 0 16px; }
.list-search-toolbar .search-box { display:flex; gap:0; flex-wrap:wrap; max-width:100%; min-width:0; }
.list-search-toolbar .search-box input[type="text"] { width:clamp(220px,30vw,380px); max-width:100%; min-width:0; height:38px; background:var(--field-bg); border-radius:var(--radius-md) 0 0 var(--radius-md); }
.list-search-toolbar .search-box > button[type="submit"] { min-height:38px; border-radius:0 var(--radius-md) var(--radius-md) 0; }
.list-search-toolbar .search-box:has(select) { gap:8px; }
.list-search-toolbar .search-box:has(select) > input[type="text"],
.list-search-toolbar .search-box:has(select) > button { border-radius:var(--radius-md); }
@media (max-width:600px) {
  .list-search-toolbar .search-box { width:100%; flex-wrap:wrap; }
  .list-search-toolbar .search-box input[type="text"] { flex:1; width:0; }
  .list-search-toolbar .search-box:has(select) input[type="text"] { flex-basis:100%; width:100%; }
  /* The two rules above were written for an input sitting directly in the box. The JS × wrapper
     (`.search-clear-wrap`) now stands between them and does not grow, so on a phone the input kept
     its width:0 and every list's search was a 26px square (pass-2 audit). The wrapper takes the row. */
  .list-search-toolbar .search-box > .search-clear-wrap { flex:1; min-width:0; }
  .list-search-toolbar .search-box > .search-clear-wrap > input[type="text"] { flex:1; width:0; }
  .list-search-toolbar .search-box:has(select) > .search-clear-wrap { flex-basis:100%; }
}

.list-controls { display:flex; align-items:flex-start; gap:12px; flex-wrap:wrap; margin-bottom:16px; }
.list-controls > .list-search-toolbar { margin:0; padding-top:12px; }
.list-controls > .filters-bar { flex:1; min-width:min(100%,400px); margin-bottom:0; }
@media (max-width:900px) { .list-controls > .list-search-toolbar { width:100%; padding-top:0; } }

/* ── Staff messages and the clinic board (v0.222.16, ux-audit §6 D19) ──────
   Two lists and a thread. No chat furniture: a handover note is written, read
   and kept as written, so there is nothing here to animate. */

.msg-person {
  display: block;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
}
.msg-person:last-child { border-bottom: 0; }
.msg-person:hover { background: var(--bg); }
.msg-person.is-open { background: var(--primary-light); }

.msg-person-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  font-size: 13px;
}

/* The last line of the conversation, clipped to one line: it is a reminder of which
   conversation this is, not the conversation. */
.msg-person-last {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.msg-bubble {
  padding: 9px 12px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  font-size: 13px;
}

/* Mine is marked by its edge, not by being pushed to the other side of the page: at this
   width a right-aligned column would leave half the card empty on every other line. */
.msg-bubble.is-mine {
  border-left-color: var(--primary);
  background: var(--bg-panel);
}

.msg-layout {
  display: grid;
  grid-template-columns: minmax(220px, 300px) 1fr;
  gap: 16px;
  align-items: start;
}

@media (max-width: 760px) {
  /* The people list stops being a column and becomes the block above the thread. */
  .msg-layout { grid-template-columns: 1fr; }
  .msg-person-last { display: none; }
}

/* A card with nothing on it yet (v0.225.2). The ward's task card is on every chart and empty on
   almost all of them; muted, it stays reachable without competing with the chart beside it. */
.card.card-quiet {
  padding-block: 10px;
  background: var(--bg);
}
.card.card-quiet .card-title { margin-bottom: 6px; }
