/* --- 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;
}

/* 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', 'DejaVu Sans', Tahoma, Arial, sans-serif;
}
/* Noto Sans Georgian already carries the same Latin outlines, so English uses the same face and
   avoids a redundant Latin font request. */
html[data-font="noto"][lang="en"] {
  --font-ui: 'Noto Sans Georgian', Tahoma, Arial, sans-serif;
}
:root {
  /* Noto Sans Georgian after DejaVu, for what DejaVu does not draw (2026-10-01): the lari sign
     U+20BE and Georgian Mtavruli. Without it „540,00 ₾" and an uppercase avatar letter depended on
     whatever system font the device had, and a box was drawn where it had none. The Noto faces are
     split by `unicode-range`, so only the subset a missing glyph falls in is ever downloaded. */
  --font-ui: 'DejaVu Sans', 'Noto Sans Georgian', 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 platform wordmark needs its own dark-theme knock-out. Organization logos are excluded
   here because one shared rule beside their base dimensions handles every named theme. */
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;
}

/* A touch screen gets 16px in every typed field. iOS Safari zooms the whole page into a field
   set smaller than that when it takes focus and leaves it zoomed; the fields were 13px
   (--fs-base) everywhere, 29 of them on /contacts/new-with-pet alone (mobile audit 2026-10-01).
   `!important` because a dozen stronger selectors (the searchable select, the banner search,
   the filter bars, inline sizes) set a field's size, and each would reopen the zoom.
   A mouse never matches this query, so a desktop window, narrowed or not, keeps 13px. */
@media (hover: none) and (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="hidden"]),
  select,
  textarea {
    font-size: 16px !important;
  }
}

/* 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,
html[data-theme="pro-slate"] .app-header .app-header-org {
  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,
html[data-theme="pharmacy"] .app-header .app-header-org {
  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,
html[data-theme="enamel"] .app-header .app-header-org {
  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;
  gap: 8px;
  flex: none;
  width: calc(var(--sidebar-w) + 1px - var(--header-pad));
  overflow: hidden;
}

/* The environment mark (DEV / DEMO) and the version under it. They were inline styles on the
   layout; a phone has to hide one of them, which a style attribute cannot be told to do. */
.app-env { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; flex: none; }
.app-env-badge { font-size: 10px; font-weight: 800; letter-spacing: .05em; color: #fff; background: var(--accent); border-radius: 4px; padding: 2px 6px; line-height: 1; text-transform: uppercase; }
.app-env-version { font-size: 10px; color: var(--text-secondary); line-height: 1; }

/* The clinic's name under the platform logo - a phone only, see the 600px block below. */
.app-header-org { display: none; color: var(--text-secondary); }
/* The platform's symbol beside that name - a phone only as well. */
.app-header-logo > img.app-header-symbol { display: none; }

.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;
}

/* An uploaded company lockup keeps its original colours only on Clean Clinical. That palette
   is the bare :root state (no data-theme attribute); every named theme has a coloured/dark
   header and uses the logo as a white knock-out. Limit this to the main header: the organization
   picker, profile preview and printed letterhead sit on light paper and retain the source art. */
html[data-theme] .app-header-logo img.app-header-logo-org {
  filter: brightness(0) invert(1);
}

.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);
}

/* The sidebar's trigger only: unscoped, this pale grey also reached the login card's trigger,
   where on a white card it looked switched off (public-site audit, 2026-10-01). */
html[data-theme="pro-slate"] .sb-system-trigger .theme-switcher-label,
html[data-theme="pharmacy"] .sb-system-trigger .theme-switcher-label,
html[data-theme="enamel"] .sb-system-trigger .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-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;
}

/* Organization section icons: a dark neutral between the two text tones (#383E44 on the
   default theme), built from theme variables so dark themes get a light one. */
.sb-item i.ti.sb-icon-org {
  color: color-mix(in srgb, var(--text-primary) 60%, var(--text-secondary));
}

/* These themes paint the sidebar dark without changing the text variables - the icon
   follows the item's own text colour there. */
html[data-theme="pro-slate"] .sidebar .sb-item i.ti.sb-icon-org,
html[data-theme="pharmacy"] .sidebar .sb-item i.ti.sb-icon-org,
html[data-theme="enamel"] .sidebar .sb-item i.ti.sb-icon-org {
  color: inherit;
}

.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. */
/* A breadcrumb link. The hover was an inline onmouseover/onmouseout pair, which the
   Content-Security-Policy no longer runs. */
.crumb-link {
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 0.15s;
}
.crumb-link:hover {
  color: var(--primary);
}

.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;
}

/* 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, .breadcrumb-view-control, .module-header-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, .breadcrumb-view-control, .module-header-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. A patient photo is the one
   exception — see the `.has-photo` override just below. */
.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 patient photo makes `.page-identity` two lines beside a tall avatar, and the owner wants
   the actions off that band entirely — not beside it at any vertical position, but on their
   own line under it, in the header's bottom-right corner (owner instruction, 2026-09-24).
   `flex-basis: 100%` claims the whole row for the identity, which pushes `.page-actions` onto
   the header's next flex line (the header's own `flex-wrap: wrap` already allows this); the
   `margin-left: auto` is what actually pins the actions to the right on that second line — with
   only one item on it, `.page-header`'s `justify-content: space-between` has nothing to
   distribute and would otherwise leave it flush left. The header's own `gap` (16px, meant for
   identity-beside-actions spacing) reads as a lot of empty band between two lines that belong
   to the same header, so it is narrowed here to the space a heading keeps above a paragraph. */
/* Every header in a module banner takes the same shape (owner, 2026-09-29, header audit §1):
   the actions stand on their own line in the banner's bottom-right corner, never beside the
   title. Beside it they were centred on the header, 48-67px above the banner's edge, while list
   pages and record pages put theirs 15px above it. With the trail line fixed at 28px (below) the
   stacked header outgrows the banner's 150px floor, so the 15px comes from the header's own
   padding and border on every page. `.page-header-stacked` asks for it outside a banner. */
.page-header:is(.module-banner .page-header, :has(> .page-identity.has-photo), .page-header-stacked) {
  row-gap: 6px;
}
.page-header:is(.module-banner .page-header, :has(> .page-identity.has-photo), .page-header-stacked) > .page-identity {
  flex-basis: 100%;
}
.page-header:is(.module-banner .page-header, :has(> .page-identity.has-photo), .page-header-stacked) > .page-actions {
  margin-left: auto;
  /* Bottom edges on one line: the visit's two-line bill beside the buttons lifted them 3px. */
  align-items: flex-end;
}

/* 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 {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.6px;
  color: var(--text-tertiary);
  margin-bottom: 2px;
}

/* Larger than the label it sits beside - the button system's rule (icon always bigger than
   its text) holds here too, or the glyph reads as a thin mark at 11px. */
.page-module i {
  font-size: 13px;
}

/* =========================================================
   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;
}

/* --- 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;
}
/* Anchors have no native disabled state. The server still guards writes; this makes the visible read-only control inert. */
a.readonly-control[aria-disabled="true"] { pointer-events:none; cursor:not-allowed; color:var(--text-tertiary); text-decoration: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;
}

/* A single title treatment for categorized cards, including guide cards.
   Category is conveyed by the icon where applicable. */
.card[data-cat] > .card-title {
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.card[data-cat="clinical"] > .card-title > span:first-child > i { color: var(--cat-clinical); }
.card[data-cat="billing"] > .card-title > span:first-child > i { color: var(--cat-billing); }
.card[data-cat="files"] > .card-title > span:first-child > i { color: var(--cat-files); }

/* 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.
   Companion edit/history rows and filtered or paged-out rows must not consume a
   stripe. Counting every DOM row would shade hidden rows and make later pages
   start with the wrong stripe. The of-selector counts only visible data rows.
   **`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, [hidden])) 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,
.form-grid > .full {
  grid-column: 1 / -1;
}

/* A form's fields in groups (owner's pick „C", 2026-09-23): two columns, a row holds only
   fields that belong together (start and end, quantity and amount, before and after), the main
   field and the note span the row, the note comes last, and groups are parted by a hairline -
   not by a heading, which made the small dialogs taller than their content. */
/* Years, months, days of an approximate age: three equal dropdowns in one row. */
.pet-age-fields {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-control);
  max-width: 360px;
}

/* A group separator means something only between grid rows; a field set drawn into a one-line
   row elsewhere must not grow a line through its middle. */
.form-sep {
  display: none;
}

/* In the grid the grid owns the rhythm: a field partial drawn with `.m-0` for a one-line row
   (the service's variant and lot fields) still keeps the row gap when it lands in a dialog. */
.form-grid > .form-group {
  margin-bottom: 14px !important;
}

.form-grid > .form-sep {
  display: block;
  grid-column: 1 / -1;
  height: 0;
  border-top: 1px solid var(--border);
  margin: 0 0 14px;
}

.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);
}

/* A required dropdown left empty: the native control is hidden, so the enhanced field carries
   the error itself (searchable-select.js, 'invalid'). */
.ss-wrap.ss-invalid .ss-input {
  border-color: var(--danger-line);
  box-shadow: 0 0 0 1px var(--danger-line);
}

.ss-error {
  margin-top: 4px;
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--danger-text);
}

/* 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. */
/* The card on the left, contacts and related animals stacked on the right, half and half (owner,
   2026-09-30). Three equal columns above 1500px left a 575px card beside two short ones and a
   white field under them (2026-09-29); two thirds then spread the card's facts across ~860px. */
.pet-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 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. */
.pet-top-stack > .card {
  margin-bottom: 0;
}

/* An AI interpretation drawn from its markdown (v0.259.5): the answer box's look, with the
   document's headings, paragraphs and lists spaced like a note rather than a page. */
.ai-md {
  border-left: 3px solid var(--primary);
  padding: 10px 14px;
  background: var(--bg);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  color: var(--text-primary);
  font-size: 13.5px;
  line-height: 1.65;
}
.ai-md h2,
.ai-md h3,
.ai-md h4 {
  font-size: 14px;
  margin: 14px 0 6px;
}
.ai-md > :first-child {
  margin-top: 0;
}
.ai-md p,
.ai-md ul,
.ai-md ol,
.ai-md table {
  margin: 0 0 8px;
}
.ai-md ul,
.ai-md ol {
  padding-left: 20px;
}
.ai-full > summary .ai-caret {
  display: inline-block;
  transition: transform 0.15s;
}
.ai-full[open] > summary .ai-caret {
  transform: rotate(90deg);
}

/* The medical tab's jumps to its sections (v0.259.5). The cards stop below the sticky header
   when jumped to. */
.med-jump {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}
[data-tab-panel="medical"] > .card {
  scroll-margin-top: 80px;
}

/* 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;
}

/* The chronic conditions, one more `.si-team` fact of the patient card under the others, across
   both columns (owner, 2026-09-30). The icon sits by the label, not in the middle of a long list,
   and the conditions carry no rules between them, as the facts above carry none. */
.pet-card .pet-conditions {
  grid-template-columns: minmax(0, 1fr);
}
.pet-card .pet-conditions > div {
  align-items: flex-start;
}
.pet-card .pet-conditions > div > i {
  margin-top: 6px;
}
.pet-conditions .pet-link-row {
  border-bottom: 0;
  padding: 3px 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 (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;
  }

  /* The corner is 40px - the row's right padding - which is an icon button's width. A labelled
     button („ნახვა", 75px, on the visit register and the protocols) overhung it, and the card's
     first line ran underneath: „თარიღი/დრო: დღეს, 16 სექ 2026 · 18" cut off by the button on the
     owner's phone. A button that carries words takes the card's last line instead, right-aligned,
     and gives the corner back so no line has to make room for it. */
  table.tbl.tbl-cards td.tbl-card-actions:has(.btn:not(.btn-icon)) {
    position: static;
    margin-top: 8px;
    text-align: right;
  }
  table.tbl.tbl-cards tbody tr:has(> td.tbl-card-actions .btn:not(.btn-icon)) {
    padding-right: 10px;
  }

  /* 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,
html[data-theme="pharmacy"] .signup-page {
  background-image: url(/assets/img/dashboard-illustration-pharmacy-abstract-v1.webp);
}

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

html[data-theme="terminal"] .login-shell,
html[data-theme="terminal"] .signup-page {
  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;
  /* Centred: the card's own content is centred and on the demo the wordmark is the page's only
     heading (owner, 2026-09-18). */
  justify-content: 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;
}
/* A fade on whichever edge still hides a tab (`tabs.js` toggles these on scroll/resize) — the
   only cue this row gives that it scrolls at all, since the scrollbar itself is hidden above. */
.tabs-nav.has-overflow-left {
  -webkit-mask-image: linear-gradient(to right, transparent, black 24px);
  mask-image: linear-gradient(to right, transparent, black 24px);
}
.tabs-nav.has-overflow-right {
  -webkit-mask-image: linear-gradient(to left, transparent, black 24px);
  mask-image: linear-gradient(to left, transparent, black 24px);
}
.tabs-nav.has-overflow-left.has-overflow-right {
  -webkit-mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent);
  mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent);
}
/* „მეტი" (tabs.js, owner's choice 2026-09-28 over edge arrows): the tabs that do not fit are
   taken out of the row and listed in this one menu at its end. It is drawn as a tab itself -
   the shared rules below list it beside `.tab-btn`. The open tab is never among them, so the
   button always reads „მეტი". */
.tab-btn.is-overflow {
  display: none;
}
.tabs-more {
  display: none;
}
.tabs-more.is-shown {
  display: flex;
}
.menu .badge-count {
  margin-left: auto;
}
.tab-btn,
.tabs-more-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,
.tabs-more-btn i {
  font-size: 16px;
}
.tab-btn:hover,
.tabs-more-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,
.tabs-more-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;
}

/* Provenance for a prescription hint inherited from the active substance rather than
   stated by the selected finished product. */
.ref-med-dose-origin {
  display: block;
  margin-top: 2px;
  color: var(--text-secondary);
  line-height: 1.35;
}

/* 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;
}

/* §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;
}

/* On the ward chart the patient is recognised visually as often as by name. The photo takes
   the species icon's leading position; both identity lines keep one shared edge beside it. */
.page-identity.has-photo {
  flex-direction: row;
  align-items: center;
  gap: 12px;
}

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

.page-pet-avatar {
  width: 56px;
  height: 56px;
  flex: 0 0 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 19px;
  font-weight: 700;
  text-decoration: none;
}

.page-pet-avatar img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.page-pet-avatar-fallback { background: var(--bg-subtle, var(--bg)); }

@media (max-width: 600px) {
  .page-identity.has-photo { gap: 9px; }
  .page-pet-avatar { width: 44px; height: 44px; flex-basis: 44px; font-size: 16px; }
}




.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;
}

/* Source-heavy clinical dose rows need to stay usable inside narrow reference panels.
   The two-column layout keeps every field in view; on phones the regimen gets enough room
   to preserve one-line dose units without introducing a distant horizontal scrollbar. */
@media (max-width: 600px) {
  table.tbl.clinical-dose-table col:first-child { width: 58% !important; }
  table.tbl.clinical-dose-table col:last-child { width: 42% !important; }
  table.tbl.clinical-dose-table th,
  table.tbl.clinical-dose-table td { padding-left: 8px; padding-right: 8px; }
}

/* 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);
}

/* Animal-record lists use one compact search and type selector. The same controls
   sit in the profile heading and inside the expandable related-records card. */
.record-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 16px;
}

.record-list-heading {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
}

.record-list-total,
.record-list-results {
  color: var(--text-secondary);
  font-size: var(--fs-small);
  font-weight: 400;
}

.record-list-filter {
  justify-content: flex-end;
  gap: 8px;
  margin-bottom: 0;
  font-weight: 400;
}

.record-list-filter input[type="search"] {
  width: 190px;
  min-width: 0;
  font-size: var(--fs-base);
}

.record-list-filter select {
  min-width: 155px;
  max-width: 245px;
}

.pet-other-records-filter {
  justify-content: flex-start;
  margin-bottom: 12px;
}

/* Search, type and the upper paginator share one line, without becoming a second box
   around the table. The lower paginator stays equally light, so neither edge feels heavier. */
.record-list-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 32px;
  padding-bottom: 10px;
  container: record-list-toolbar / inline-size;
}

.record-list-toolbar .pet-other-records-filter {
  flex: 0 1 auto;
  margin: 0;
}

.record-list-toolbar > .list-search-toolbar {
  /* Do not let the parent flex algorithm make this box narrower than the controls it
     contains. Its form can wrap its own controls on genuinely narrow screens; at intermediate
     widths the paginator must wrap as a unit instead of being positioned over the filter. */
  flex: 0 0 auto;
  max-width: 100%;
  margin: 0;
}

.record-list-toolbar .pagination-bar-toolbar {
  position: relative;
  flex: 1 1 430px;
  min-width: 0;
}

/* Two neighbouring theme tones create a quiet recessed divider. `--border` gives
   it definition and `--bg-panel` supplies the light edge; the 32px gap leaves 16px
   of breathing room on either side. */
.record-list-toolbar .pagination-bar-toolbar::before {
  content: "";
  position: absolute;
  top: 50%;
  inset-inline-start: -16px;
  width: 2px;
  height: 28px;
  background: linear-gradient(to right, var(--border) 0 50%, var(--bg-panel) 50% 100%);
  transform: translate(-50%, -50%);
}

.record-list-save-error {
  width: 100%;
  color: var(--danger-text);
  font-size: var(--fs-small);
}

@media (max-width: 700px) {
  .record-list-toolbar {
    align-items: stretch;
  }

  .record-list-toolbar .pagination-bar-toolbar {
    flex-basis: 100%;
  }

  .record-list-toolbar .pagination-bar-toolbar::before {
    display: none;
  }

  .record-list-toolbar > .list-search-toolbar {
    width: 100%;
  }

  .record-list-filter {
    width: 100%;
    justify-content: flex-start;
  }
  .record-list-filter input[type="search"],
  .record-list-filter select {
    flex: 1 1 180px;
    max-width: 100%;
  }
}

@container record-list-toolbar (max-width: 940px) {
  .record-list-toolbar .pagination-bar-toolbar {
    flex-basis: 100%;
  }

  .record-list-toolbar .pagination-bar-toolbar::before {
    display: none;
  }
}

/* --- 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;
}

/* Sets a utility link (a calculator, not a catalogue) apart from the rows before it, without
   claiming a second row for it — a heading would cost as much space as the tabs themselves. */
.section-nav-sep {
  align-self: stretch;
  width: 1px;
  margin: 4px 0;
  background: var(--rule);
}

@media (max-width: 900px) {
  /* `nowrap` is what makes the scroll happen. The base rule's `flex-wrap: wrap` stayed in force
     here, so `overflow-x: auto` never had anything to scroll: on a phone the four /pets tabs broke
     into two rows with the current tab's underline on the first only, and the view control dropped
     to a third (owner's Pixel 8, 412px, 2026-09-17). One row that slides, like every tab strip. */
  .section-nav {
    gap: 16px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  /* The edge that still hides a link fades, as on `.tabs-nav` (tabs.js sets the classes). A cut
     word alone - „მკურნალობ", „ჰოსპიტალიზ" - did not say the row moves, and a segment cut
     exactly at a border said nothing at all (mobile audit 2026-10-01). */
  :is(.section-nav, .filter-seg).has-overflow-left {
    -webkit-mask-image: linear-gradient(to right, transparent, black 24px);
    mask-image: linear-gradient(to right, transparent, black 24px);
  }
  :is(.section-nav, .filter-seg).has-overflow-right {
    -webkit-mask-image: linear-gradient(to left, transparent, black 24px);
    mask-image: linear-gradient(to left, transparent, black 24px);
  }
  :is(.section-nav, .filter-seg).has-overflow-left.has-overflow-right {
    -webkit-mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent);
  }
}

/* --- 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 bolt menu beside it says
   "other things entirely" — two different promises, so two different controls.

   The bolt menu 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);
}

/* A quiet split action uses the same hairline as its outlined halves; the default divider above
   is derived from filled-action ink and becomes too bright against a panel-coloured button. */
.split > .btn-muted.split-toggle {
  border-left-color: color-mix(in srgb, var(--btn-line) 60%, var(--bg-panel));
}

.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;
  }
}

/* A phone's header (owner, 2026-09-17, option „გ" of four rendered at 412px). The user block was
   171px and the logo box shrank to make room for it, so its DEMO/DEV mark was cut to a red sliver
   and the name read „ლევან გოგი…". Now:
   - the user block is its icon and caret; the person's name is in the open menu;
   - the environment mark stays, its version line goes;
   - the clinic stays named, because the rule above (feedback #25) holds: the trigger's second line
     was the only place the active clinic was named. Under the platform logo its name takes a
     line of its own; a clinic with its own logo is named by the logo and prints nothing more.
   Measured at 412px: logo + mark 159px, user block 66px, the name 132px for „VetMan • დემო კლინიკა". */
@media (max-width: 600px) {
  .user-menu-trigger .user-menu-id { display: none; }
  .app-env-version { display: none; }
  .app-env { justify-content: center; }

  /* Under the platform logo the name used to take a second line: the wordmark shrank to 22px and
     a 10.5px name sat squeezed under it, lined up with the box rather than the logo. Since
     2026-10-05 (owner, option „ა" of three) it is one line: the platform's symbol, the name at
     reading size, then the DEV / DEMO mark. A long name ends in an ellipsis. */
  .app-header-logo:has(> .app-header-org) {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }
  .app-header-logo:has(> .app-header-org) > img:not(.app-header-symbol) { display: none; }
  .app-header .app-header-logo > img.app-header-symbol[src] {
    display: block;
    order: -1;
    flex: none;
    width: 26px;
    height: 26px;
    max-width: none;
  }
  .app-header-logo:has(> .app-header-org) > .app-env { order: 1; }
  .app-header-org {
    display: block;
    min-width: 0;
    font-size: 16px;
    font-weight: 600;
    line-height: 1.2;
    color: var(--text-primary);
    white-space: nowrap;
    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);
}

/* The header's own toggles (/pets: debtors, hide deceased) sit in a bare <form>. On a phone the
   second wraps under the first and kept the margin above, so it stood 16px to the right of the one
   it lines up with. A flex gap spaces them side by side and adds nothing at the start of a line. */
.page-identity > form:has(> .switch) {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--sp-group);
}
.page-identity > form > .switch + .switch {
  margin-left: 0;
}

.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 flex item will not shrink below its content unless told, so `.filter-seg`'s own
     `max-width: 100%` measured against a row already as wide as the segments and never
     scrolled: on /prophylaxis at 390px „ყველა" sat at 435px, out of reach (2026-10-01). */
  min-width: 0;
  max-width: 100%;
}





/* --- 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;
  /* Above the field: the coloured themes give the input a backdrop-filter, which makes it a
     layer of its own painted after this icon, and its tinted fill hid the magnifier. */
  z-index: 1;
}

/* 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;
}

/* The phone's search strip continues the header, so it takes each coloured header's own colour. */
html[data-theme="pro-slate"] .gsearch-strip { background-color: #0F172A; border-bottom-color: #1E293B; }
html[data-theme="pharmacy"] .gsearch-strip { background-color: #164437; border-bottom-color: #0E2C24; }
html[data-theme="enamel"] .gsearch-strip { background-color: #163A5A; border-bottom-color: #0E2740; }

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);
}

/* „Did you mean?" under a list (partials/search-suggestions): guesses, so a card of plain links
   apart from the table, never rows inside it. Row shape borrowed from the header search. */
.search-suggest {
  margin-top: var(--sp-group);
}

.search-suggest-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 2px var(--sp-group);
  margin: 0;
  padding: 0;
  list-style: none;
}

.search-suggest-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 8px;
  border-radius: var(--radius-md);
  color: var(--text-primary);
  text-decoration: none;
}

.search-suggest-row:hover,
.search-suggest-row:focus-visible {
  background: var(--tbl-zebra);
}

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

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

.search-suggest-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

/* The phone's search strip and the header button that brings it back - see the layout. Both
   exist only below 900px; above it the field is in the header and neither is drawn. */
.gsearch-strip,
.gsearch-open {
  display: none;
}

/* Below this the header has no room for the field, so it sits in a strip under the header
   (owner, 2026-10-04, variant 5 of five). The strip is in the page's flow and scrolls away under
   the sticky header; once it is gone `.gsearch-open` appears there. Tapping that pins the strip
   under the header until the search is left. */
@media (max-width: 900px) {
  .app-header > .gsearch {
    display: none;
  }

  .gsearch-strip {
    display: block;
    padding: 0 12px 10px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
  }

  .gsearch-strip:empty {
    display: none;
  }

  .gsearch-strip.is-pinned {
    position: sticky;
    top: 56px;
    z-index: 69;
  }

  .gsearch-strip .gsearch {
    flex: none;
    margin-left: 0;
  }

  /* No keyboard to press it with. */
  .gsearch-strip .gsearch-kbd {
    display: none;
  }

  .gsearch-strip .gsearch input {
    padding-right: 12px;
  }

  /* Out of the header the field lost `.app-header :focus-visible` and fell back to the browser's
     own ring, whose top edge ran under the header and showed on a phone as a dark line over the
     field (owner, 2026-10-05). Drawn inside the field, so nothing can cover it; light blue on
     the dark headers as in the header itself, the accent on the white one. */
  .gsearch-strip .gsearch input:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
  }

  html[data-theme] .gsearch-strip .gsearch input:focus-visible {
    outline-color: #93C5FD;
  }

  /* One band with the header while the strip is on screen; the header's line comes back when the
     strip has gone under it. `html` and :has() outweigh the themes' own !important borders. */
  html .app-header:has(+ .gsearch-strip > .gsearch):not(.gsearch-away) {
    border-bottom-color: transparent !important;
  }

  /* Always takes its room and only turns visible, so the logo beside it does not jump as the
     strip scrolls in and out. */
  .gsearch-open {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 40px;
    height: 40px;
    margin-right: -6px;
    padding: 0;
    border: none;
    background: none;
    color: inherit;
    cursor: pointer;
    visibility: hidden;
    opacity: 0;
    transition: opacity .15s, visibility .15s;
  }

  .gsearch-open i {
    font-size: 21px;
  }

  .app-header.gsearch-away .gsearch-open {
    visibility: visible;
    opacity: 1;
  }

  /* The search button costs the logo box 50px. A clinic's own wide logo gives that up itself
     (object-fit keeps its shape) instead of the box cutting off the DEV / DEMO mark beside it. */
  .app-header-logo > img.app-header-logo-org {
    min-width: 0;
    flex-shrink: 1;
  }

  /* 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;
}
/* ლაბ. ჩანაწერის წყარო, როცა ერთ ატვირთვაში რამდენიმე ფაილია (views/lab/show.php):
   თითო ფაილი თავის მწკრივზე, ფოტო მინიატურით, დანარჩენი ხატულით. */
.lab-source-files {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.lab-source-file {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.lab-source-file .link {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* §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;
}

/* The hotel's week chart. Seven day columns under a fixed layout shared whatever width there
   was: at 390px each got about 25px, the month names ran into each other and a booking showed
   one letter (mobile audit 2026-10-01). Below this width `.table-wrap` scrolls it instead, and
   the mobile table rule keeps the unit column in place. Wider screens never reach the floor. */
table.hotel-week {
  min-width: 640px;
}
.hotel-week-head {
  flex-wrap: wrap;
  gap: 8px;
}

/* The clinic dashboard (BL-015, v0.270.0): quick actions, finance, today's schedule, module
   tiles. Every link-shaped block here is an <a>, never an `onclick` <div>: an <a> takes focus,
   Enter and Ctrl+click for free (§29/§105 - `.stat-card-link`, which this replaced). */
.dash-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}
/* Critical results on a phone: the first three and „show all (N)" (dashboard-critical.js).
   A wider screen lists every one and never shows the button. `!important` because each row
   carries an inline `display:flex`. */
.dash-critical-all { display: none; }
@media (max-width: 900px) {
  .dash-critical:not(.is-expanded) > .dash-critical-row:nth-child(n+4 of .dash-critical-row) {
    display: none !important;
  }
  .dash-critical:not(.is-expanded) > .dash-critical-all {
    display: inline-flex;
    margin-top: 6px;
  }
}
/* „Install the app" on the dashboard (v0.300.1, pwa.js): a phone only. `hidden` keeps it out
   until the script knows installing is possible; above 900px it never shows. */
.pwa-banner { display: none; }
@media (max-width: 900px) {
  .pwa-banner { display: flex; align-items: center; gap: 10px; }
}
.pwa-banner-icon { font-size: 22px; flex-shrink: 0; }
.pwa-banner-text { flex: 1; min-width: 0; line-height: 1.4; }
.pwa-banner-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* The iPhone's steps (partials/user-menu, #pwaIosHelp). */
.pwa-ios-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.pwa-ios-steps li { display: flex; align-items: flex-start; gap: 10px; line-height: 1.5; }
.pwa-ios-steps .ti { font-size: 20px; color: var(--primary); flex-shrink: 0; }
.dash-finance { margin-bottom: 18px; }
.dash-finance.is-folded .card-title { margin-bottom: 0; }
.dash-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
}
.dash-stat,
.dash-tile {
  display: block;
  padding: 12px 14px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
}
.dash-stat:hover,
.dash-tile:hover { border-color: var(--btn-line); }
.dash-stat:focus-visible,
.dash-tile:focus-visible,
a.dash-sched-row:focus-visible {
  outline: 2px solid var(--action-bg);
  outline-offset: 2px;
}
.dash-stat-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-secondary);
}
.dash-stat-val {
  margin-top: 4px;
  font-size: 22px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.dash-stat-sub {
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-secondary);
}
.dash-up { color: var(--success-text); font-weight: 700; }
.dash-down { color: var(--danger-text); font-weight: 700; }
.dash-bars-title {
  margin-top: 14px;
  font-size: 12px;
  color: var(--text-secondary);
}
.dash-bars {
  display: flex;
  gap: 4px;
}
/* One column per day: the bar, then its date and weekday. The track's top padding is
   the room the hover amount needs above a full-height bar, so it never covers the cards above. */
.dash-bar {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  color: var(--text-secondary);
  text-decoration: none;
  border-radius: var(--radius-sm);
}
.dash-bar-track {
  display: flex;
  align-items: flex-end;
  box-sizing: border-box;
  height: 90px;
  padding-top: 26px;
}
.dash-bar-fill {
  position: relative;
  flex: 1;
  min-height: 3px;
  background: var(--primary-light);
  border-radius: 3px 3px 0 0;
}
.dash-bar.is-today .dash-bar-fill,
.dash-bar:hover .dash-bar-fill,
.dash-bar:focus .dash-bar-fill { background: var(--primary); }
.dash-bar-val {
  display: none;
  position: absolute;
  bottom: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--text-primary);
  color: var(--bg-panel);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  white-space: nowrap;
  pointer-events: none;
}
/* The outer columns anchor their amount to the card's edge instead of centring it past it. */
.dash-bar:nth-child(-n+2) .dash-bar-val { left: 0; transform: none; }
.dash-bar:nth-last-child(-n+2) .dash-bar-val { left: auto; right: 0; transform: none; }
.dash-bar:hover .dash-bar-val,
.dash-bar:focus .dash-bar-val { display: block; }
.dash-bar:focus { outline: none; }
.dash-bar:focus-visible { outline: 2px solid var(--action-bg); outline-offset: 2px; }
.dash-bar-val-date { display: none; }
.dash-bar-day,
.dash-bar-dow {
  overflow: hidden;
  text-align: center;
  white-space: nowrap;
  line-height: 1.3;
}
.dash-bar-day {
  margin-top: 4px;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.dash-bar-dow { font-size: 10px; }
.dash-bar.is-today .dash-bar-day,
.dash-bar.is-today .dash-bar-dow { color: var(--primary); }
.dash-two {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.dash-wide > .card + .card { margin-top: 16px; }
.dash-chips { display: flex; gap: 6px; }
.dash-sched-row {
  display: grid;
  grid-template-columns: 46px 20px minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  color: inherit;
  text-decoration: none;
}
.dash-sched-row:last-of-type { border-bottom: 0; }
.dash-sched-row[hidden] { display: none; }
.dash-sched-row.is-done { opacity: 0.6; }
.dash-time {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.dash-sched-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.dash-sched-sub {
  font-size: 12px;
  color: var(--text-secondary);
}
.dash-now {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0;
  font-size: 11px;
  font-weight: 700;
  color: var(--danger-text);
}
.dash-now::after {
  content: "";
  flex: 1;
  height: 2px;
  background: currentColor;
  opacity: 0.6;
}
.dash-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.dash-wide .dash-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dash-tile-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 700;
}
.dash-tile-figs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin-top: 6px;
}
.dash-fig b {
  display: block;
  font-size: 18px;
  font-variant-numeric: tabular-nums;
}
.dash-fig small {
  font-size: 11px;
  color: var(--text-secondary);
}
.dash-schedule.is-empty .card-title { margin-bottom: 4px; }
@media (max-width: 1100px) {
  .dash-wide .dash-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .dash-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-two { grid-template-columns: minmax(0, 1fr); }
  .dash-wide .dash-tiles,
  .dash-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .dash-actions .btn {
    justify-content: flex-start;
    font-size: 13px;
  }
  .dash-stat-val { font-size: 18px; }
}
@media (max-width: 700px) {
  .dash-bars { gap: 2px; }
  .dash-bar-mon,
  .dash-bar-dow { display: none; }
  .dash-bar-val-date { display: inline; }
}

/* Dashboard refresh buttons (v1.12.4, dashboard-refresh.js): a button resting after too many
   clicks looks switched off; the redrawn figures fade in so even a quick answer is seen. */
.dash-refresh.is-resting { opacity: 0.45; cursor: not-allowed; }
@keyframes dash-refreshed { from { opacity: 0.3; } to { opacity: 1; } }
.dash-refreshed { animation: dash-refreshed 0.45s ease-out; }
/* With the refresh button the finance title holds three buttons: on a phone they move under the title. */
@media (max-width: 600px) {
  .dash-finance .card-title.row-between { flex-wrap: wrap; row-gap: 8px; }
}

/* =========================================================
   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-ის ქვემოთ ერთმანეთის ქვეშ დგება. */
/* The patient card in the contact card's shape (owner, 2026-09-30): `.si-team` rows, icon, label
   over value, two columns. Beside the photo the six identity facts run DOWN the two columns -
   species, breed, sex, then age, colour, weight - in the owner's order; a fact the animal lacks is
   left out, so a column may be short. */
.pet-card .pet-si-top {
  grid-template-rows: repeat(3, auto);
  grid-auto-flow: column;
  margin-bottom: 0;
}
.pet-card-body {
  margin-bottom: 14px;
}
/* A standard fact not recorded yet: in its place, all of it grey, with a dash (owner,
   2026-09-30) - the pet card and the contact card alike. */
.si-team > .si-empty i,
.si-team > .si-empty .si-label,
.si-team > .si-empty .si-value {
  color: var(--text-tertiary);
  font-weight: 400;
}
/* The facts under the photo keep the columns of the facts beside it: the first column is the
   photo, its gap and the top grid's first column together, so the second starts exactly where
   the top grid's second does (owner, 2026-09-30). 16px is `.si-team`'s column gap. */
.pet-card .pet-si-more {
  grid-template-columns:
    calc(120px + var(--sp-group) + (100% - 120px - var(--sp-group) - 16px) / 2)
    minmax(0, 1fr);
}
/* One column when the card itself is narrow - a phone, and a laptop, where half the page left two
   ~120px columns and a pill broke mid-word. A column flow would add a column for every fourth
   fact, so the top grid goes back to rows. */
@container pet-card (max-width: 480px) {
  .pet-card .si-team {
    grid-template-columns: minmax(0, 1fr);
  }
  .pet-card .pet-si-top {
    grid-template-rows: none;
    grid-auto-flow: row;
  }
}
.pet-card-body {
  display: flex;
  gap: var(--sp-group);
  align-items: flex-start;
}
/* The photo goes above the facts when the card itself is narrow - a phone, and a laptop, where
   half the page left the facts ~135px beside the photo (2026-09-30). `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 {
  container: pet-card / inline-size;
}
@container pet-card (max-width: 480px) {
  .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;
}

/* 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;
}
/* A file comment pasted as markdown, drawn from it (MarkdownLite::noteToHtml): its headings,
   rules and nested lists carry the structure, so the source's blank lines no longer have to.
   One look wherever such a note is shown in full: the documents panel's dialog, the lightbox
   caption, an imaging report's files. The share page carries the same rules in print.css. */
.note-md {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-primary);
}
.doc-note-full.note-md,
.lightbox-caption.note-md {
  white-space: normal;
}
.note-md > :first-child {
  margin-top: 0;
}
.note-md > :last-child {
  margin-bottom: 0;
}
.note-md h2,
.note-md h3,
.note-md h4,
.note-md h5 {
  font-size: 14px;
  font-weight: 600;
  margin: 16px 0 6px;
}
.note-md p,
.note-md ul,
.note-md ol {
  margin: 0 0 8px;
}
.note-md ul,
.note-md ol {
  padding-left: 20px;
}
.note-md li + li {
  margin-top: 4px;
}
.note-md li > ul,
.note-md li > ol {
  margin: 4px 0 8px;
}
.note-md li > ul > li + li,
.note-md li > ol > li + li {
  margin-top: 2px;
}
.note-md hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 12px 0;
}

/* 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; }

/* A phone with several people on the day (BL-049): three 160px columns made a 549px board in a
   250px box, and the third person was simply cut off. There each person's column is the width of
   the box less a strip, so the next one shows at the edge and says „swipe"; a swipe stops on a
   column, and the hour rail and the corner stay put while the columns move under them. One
   person keeps the ordinary layout, which already fits. */
@media (max-width: 640px) {
  .cal-scroll:has(> .cal-day-grid-multi) {
    container-type: inline-size;
    scroll-snap-type: x mandatory;
    scroll-padding-inline-start: 53px; /* the rail and the grid's left border */
  }
  .cal-day-grid.cal-day-grid-multi {
    grid-template-columns: 52px repeat(var(--cal-cols), calc(100cqi - 52px - 28px));
    width: max-content; /* the grid's own top border must run under every column */
  }
  .cal-day-grid-multi .cal-col { scroll-snap-align: start; }
  .cal-day-grid-multi :is(.cal-corner, .cal-rail) {
    position: sticky;
    left: 0;
    background: var(--bg-panel);
  }
  .cal-day-grid-multi .cal-corner { z-index: 5; }
  .cal-day-grid-multi .cal-rail { z-index: 4; }
  .cal-day-grid-multi .cal-rail-slot { padding-inline: 4px; }
}

.cal-day-grid {
  display: grid;
  grid-template-columns: 68px repeat(var(--cal-cols, 1), minmax(160px, 1fr));
  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-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; }

/* ── Three kinds of line on the prescription form (v1.57.0) ────────────────
   A manipulation and an infusion set the medication's dose and route aside in
   both modes on the prescription form (the treatments panel keeps its own
   row as it was), and each kind shows its own block. A field holding a value
   stays (`.rx-keep`), as everywhere on this form. */
#prescriptionForm .medication-row-card.is-procedure [data-rx-med-only]:not(.rx-keep),
#prescriptionForm .medication-row-card.is-infusion [data-rx-med-only]:not(.rx-keep) { display: none !important; }
.medication-row-card:not(.is-procedure) [data-rx-proc-only],
.medication-row-card:not(.is-infusion) [data-rx-infusion-only],
.medication-row-card:not(.is-procedure):not(.is-infusion) [data-rx-components] { display: none !important; }
.rx-kind-box { margin: 10px 0 0 38px; padding-left: 10px; border-left: 2px solid var(--border); }
.medication-row-card.is-procedure .rx-kind-box { border-left-color: var(--special-text); }
.medication-row-card.is-infusion .rx-kind-box { border-left-color: var(--info-text); }
.rx-inf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px 10px; }
.rx-inf-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.rx-inf-readout { color: var(--info-text); font-weight: 600; }
.rx-inf-calc { margin-top: 8px; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.rx-calc-result { margin-top: 6px; }
.rx-component { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.rx-component .ss-wrap, .rx-component > .rx-component-name { flex: 2 1 0; min-width: 0; }
.rx-component .ss-wrap > .rx-component-name { width: 100%; }
.rx-component .rx-component-amount { flex: 1 1 0; min-width: 0; }
.rx-component-step { min-width: 18px; text-align: right; }
@media (max-width: 560px) {
    .rx-kind-box { margin-left: 0; }
    .rx-component { flex-wrap: wrap; }
    .rx-component .ss-wrap, .rx-component .rx-component-name { flex-basis: 100%; }
}

/* ── 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-count.is-warn { color: var(--warn-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; }

/* ── 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; }
/* Where a line came from when it is not the ward: „ამბულატორია" on the admission day's visit (v0.287.6). */
.jline-source { margin-left: 6px; font-size: 10px; vertical-align: 1px; }

.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;
}

.med-section-head-schedule { margin-top: 2px; }

.med-course-prn {
  margin-left: 5px;
  vertical-align: 1px;
  font-size: 10px;
}

.schedule-day {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 8px;
}

.schedule-day > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 11px;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
}

.schedule-day > summary::-webkit-details-marker { display: none; }
.schedule-day[open] > summary { border-bottom: 1px solid var(--border); }
.schedule-day.has-overdue > summary { color: var(--danger-text); }
.schedule-table { margin: 0; }
.schedule-table th { font-size: 10px; color: var(--text-secondary); }
.schedule-table td { vertical-align: middle; }
.schedule-time-col, .schedule-time { width: 88px; }
.schedule-time { font-variant-numeric: tabular-nums; font-weight: 650; }
.schedule-frequency { display: block; margin-top: 2px; color: var(--text-secondary); font-size: 10.5px; }
.schedule-row-late .schedule-time { color: var(--danger-text); }
.schedule-actions { justify-content: flex-end; }
.schedule-course-detail { margin: 8px 0; }

.performed-day {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 8px;
}

.performed-day > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 11px;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
}

.performed-day > summary::-webkit-details-marker { display: none; }
.performed-day[open] > summary { border-bottom: 1px solid var(--border); }
.performed-day-list { background: var(--bg-panel); }

.performed-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}

.performed-row:last-child { border-bottom: 0; }
.performed-time { width: 42px; flex: none; color: var(--text-secondary); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.performed-icon { width: 18px; flex: none; text-align: center; color: var(--text-secondary); }
.performed-body { min-width: 0; flex: 1; }
.performed-detail, .performed-note, .performed-who { display: block; margin-top: 2px; font-size: 11px; color: var(--text-secondary); }
.performed-note { color: var(--text-primary); }
.performed-manage { margin-top: 3px; }

.performed-list { border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; margin-bottom: 8px; }
.performed-when { width: 172px; white-space: nowrap; }

/* The course's dates and, when it changed, its dose history (2026-09-27). */
/* „სრული განრიგი" keeps a margin inside the card: the table carries its own frame. */
.med-course-all { border-top: 1px solid var(--border); padding: 10px 12px 12px; }
/* The first unrecorded dose, reached from the card's warning (med-schedule-filter.js). */
.med-course-all tr.is-flash > td { animation: med-row-flash 2.4s ease-out; }
@keyframes med-row-flash { 0%, 35% { background: var(--danger-bg); box-shadow: inset 0 0 0 9999px var(--danger-bg); } 100% { background: transparent; box-shadow: none; } }
@media (prefers-reduced-motion: reduce) { .med-course-all tr.is-flash > td { animation: none; background: var(--danger-bg); } }
.med-course-dose-runs { display: block; margin-top: 2px; font-size: 11px; color: var(--text-secondary); }
.med-course-dose-runs .ti { color: var(--warn-text); }

.med-course-missed { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 12px; border-top: 1px solid var(--border); background: var(--danger-bg); color: var(--danger-text); font-size: 12px; }
.med-course-missed .btn { margin-left: auto; }

/* A course's doses, one square each, coloured as the status pill says. */
.dose-strip { display: flex; flex-wrap: wrap; gap: 3px; padding: 7px 12px 8px; border-top: 1px solid var(--border); }
.dose-dot { width: 11px; height: 11px; border-radius: 3px; border: 1px solid transparent; background: var(--btn-line); }
.dose-dot.is-pill-ok { background: var(--primary); }
.dose-dot.is-pill-warn { background: color-mix(in srgb, var(--warn-text) 45%, var(--warn-bg)); }
.dose-dot.is-pill-danger { background: var(--danger-line); }
.dose-dot.is-open { background: transparent; border-color: var(--btn-line); }
.dose-dot.is-late { background: transparent; border-color: var(--danger-line); }

@media (max-width: 700px) {
  .performed-when { width: 100%; white-space: normal; }
}

@media (max-width: 700px) {
  .performed-row { align-items: flex-start; flex-wrap: wrap; }
  .performed-body { flex-basis: calc(100% - 78px); }
  .performed-row > .pill { margin-left: 69px; }
}

/* ==========================================================================
   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; }

/* 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; }

/* 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);
}
/* Zoom (BL-050): the picture is the preview until the reader asks for the original, which is
   then drawn at its own pixel size inside a stage that scrolls both ways. */
img.lightbox-media {
  cursor: zoom-in;
}
.lightbox-stage.is-zoomed {
  display: block;
  max-height: 72vh;
  overflow: auto;
}
.lightbox-stage.is-zoomed .lightbox-media {
  max-width: none;
  max-height: none;
  cursor: zoom-out;
}
.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;
  /* The caption scrolls inside itself rather than growing the dialog. It could not shrink, so a
     2000-character ultrasound protocol squeezed the video to a strip and pushed the footer
     (original, replace, delete) out of the bottom of the dialog. */
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
}
/* A long caption (lightbox.js sets the class: a drawn markdown report, or text past a few
   lines) gets a share of the height, and the file gives up that share: the media's limit is the
   screen less the header, footer, paddings and the caption's quarter. Both stay in view. */
.lightbox.has-long-caption .lightbox-body {
  flex-shrink: 0;
}
.lightbox.has-long-caption .lightbox-caption {
  max-height: 25vh;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}
.lightbox.has-long-caption .lightbox-media,
.lightbox.has-long-caption .lightbox-stage.is-zoomed {
  max-height: min(72vh, calc(75vh - 240px));
}
.lightbox.has-long-caption .lightbox-frame {
  height: min(72vh, calc(75vh - 240px));
  max-height: none;
}
@media (max-width: 600px) {
  .lightbox-media,
  .lightbox-frame {
    max-height: 60vh;
  }
  .lightbox-frame {
    height: 60vh;
  }
  .lightbox-stage.is-zoomed {
    max-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;
}

/* More than one file in the sheet's upload group (v1.56.55): one button per file, the one on
   screen pressed. Names are often long, so a button shrinks with an ellipsis and the row wraps. */
.src-media-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.src-media-switch .btn {
  min-width: 0;
  max-width: 100%;
}
.src-media-switch .btn span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

iframe.src-media-frame {
  height: calc(100vh - 150px);
  border: 1px solid var(--border);
}

@media (max-width: 900px) {

  .src-split {
    /* minmax(0, …), not a bare 1fr: 1fr's minimum is the content's min-content width,
       so the results grid and the readings table (nowrap cells) set the column to
       ~525px and the photo, the table and the apply button ran off a phone screen
       (owner, 2026-10-22, lab 994). The wide tables scroll inside their own .table-wrap. */
    grid-template-columns: minmax(0, 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); }

/* An attached file's markdown report on an imaging record, drawn (`.note-md`) under its line. */
.episode-med-note { margin-top: 6px; font-size: 13px; }

/* --- 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.244.23) -----------------------------
   The page-level List/Cards or List/Calendar question belongs at the breadcrumb row's right.
   Day/week/month is a local calendar question and remains beside the date walk. */
.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); }
/* On a phone the button lands on the banner's drawing, and the bare label on the busy part of it
   could hardly be read (mobile audit, BL-049). The label keeps its words and gets the panel's
   surface behind it, the way the button beside it already has. */
@media (max-width: 640px) {
  .module-banner .view-control-label {
    padding: 3px 8px;
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--bg-panel) 88%, transparent);
  }
}

.breadcrumb-actions-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
  margin-bottom: 12px;
}
/* One height for the trail line in a banner, whatever stands at its right end (header audit §2).
   A bare trail was 17px, with the settings link 25px, with the view control 28px, so the title
   moved 3px between two tabs of one module. */
.module-banner > nav[aria-label="breadcrumb"],
.module-banner > .breadcrumb-actions-row {
  min-height: 28px;
}
.module-banner > nav[aria-label="breadcrumb"] {
  display: flex;
  align-items: center;
}
.breadcrumb-actions-row > nav {
  flex: 1 1 auto;
  min-width: 0;
  margin-bottom: 0 !important;
}
.breadcrumb-view-control {
  flex: none;
  margin-inline-start: auto;
}
@media (max-width: 600px) {
  .breadcrumb-actions-row { align-items: flex-start; flex-wrap: wrap; }
  .breadcrumb-view-control { margin-inline-start: auto; }
}

/* 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); }
/* A dot and the link after it are one unbreakable piece. As separate flex items a wrap could end a
   line on the dot - „დღეს · გუშინ · ამ კვირაში · ამ თვეში ·" with „ყველა" alone below it, on the
   owner's phone. Same 10px between dot and word as between pieces, so nothing moves on a wide screen. */
.vlinks > .vlink { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }

@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-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;
}

/* Pet and contact cards lead with the record name; the visual and facts form the second row. */
.pet-ecard-title,
.contact-ecard-title {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  margin-bottom: 8px;
  color: var(--text-primary);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.35;
  text-decoration: none;
}

.pet-ecard-body,
.contact-ecard-body {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr);
  gap: 10px;
  align-items: start;
}

.pet-ecard-photo,
.contact-ecard-avatar {
  display: block;
  width: 80px;
  height: 80px;
  border-radius: var(--radius-md);
  text-decoration: none;
}

.pet-ecard .ecard-lines,
.contact-ecard .ecard-lines { min-width: 0; }

.contact-photo-control {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.contact-photo-preview {
  width: 80px;
  height: 80px;
  flex: none;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  object-fit: cover;
}

.contact-photo-preview-empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 30px;
}

.contact-photo-hint { margin-top: 4px; }
.contact-photo-remove { display: inline-flex; align-items: center; gap: 6px; margin-top: 7px; font-size: 12px; font-weight: 400; }

.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; }
/* `flex: 1 1 440px`, not `flex: 1`. With a zero basis the form never made the row wrap: beside the
   period links it simply shrank, and between about 1000px and 1600px of window the visit and ward
   registers drew their selects 42px wide with the links laid over them (measured, 2026-09-18). 440px
   is the widest filter row - a label and two dates - plus its gaps. Narrower than that, the links
   drop under the filters; wider, the two sit side by side exactly as before. */
.filters-bar > form { flex: 1 1 440px; 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; flex-wrap: wrap; max-width: 100%; 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%; }

  /* Label before control stays (the owner's shape above), but each row sized itself to its own
     word, so on a phone „სტატუსი", „ვეტერინარი" and „თარიღი" started their fields at three different
     x positions, and the second date wrapped under the label with nothing to say what it was. One
     label column, every control filling the rest: the fields form one edge, and the two dates share
     a row. 80px holds the longest label these filters carry. */
  .filters-form { flex-direction: column; align-items: stretch; gap: 8px; }
  .filters-form > .filter-inline { display: flex; flex-wrap: nowrap; width: 100%; }
  .filters-form > .filter-inline > span:first-child { flex: 0 0 80px; }
  .filters-form > .filter-inline > :is(.ss-wrap, select, input:not([type="hidden"])) { flex: 1 1 0; min-width: 0; width: auto; }
}

/* `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) {
  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. */
/* `isolation: isolate` is what keeps the artwork's own negative z-indexes (the viewport at -1,
   the trail at -2) from sinking behind the page. But it also makes the banner a stacking
   context, so the header's ⚡ menu — `position: fixed; z-index: 30`, but still a DOM descendant
   — only competes INSIDE the banner. Outside it, the whole banner stacked at `auto`, below
   `.tabs-nav`'s `z-index: 1`, and the tab row painted straight through any menu tall enough
   to reach it (owner, 2026-09-24). `z-index: 2` lifts the banner as a whole above the tabs;
   nothing else sits beside it at a higher level, so nothing else moves. */
html .module-banner.illustration-header:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) {
  position: relative;
  min-height: 150px;
  isolation: isolate;
  z-index: 2;
  background-image: none;
  background-color: var(--bg-panel);
  padding-bottom: 0;
}
/* The 150px floor is sized for a header with no tab row below it — breadcrumb + title alone
   never reaches it. A list page's tabs (`.section-nav-row`) add enough of their own height
   that the floor was left unfilled, and since the artwork viewport is `inset: 0` on this same
   box, that leftover band painted the tinted background past the tabs' own bottom rule, which
   is supposed to be where the illustrated area ends (owner, 2026-09-24). Dropping the floor
   here lets the box shrink to exactly breadcrumb + title + tabs, so the art viewport does too. */
html .module-banner.illustration-header:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header):has(> .section-nav-row) {
  min-height: 0;
}
/* One banner height on every page, and the title in its middle (owner, 2026-09-29: „ყველა
   170px", picked from 150-192px rendered side by side; then „page-title ყოველთვის უნდა იყოს ამ
   170px ჰედერის შუაში ვერტიკალურად. თუ რამე ჩნდება ზემოთ ან ქვემოთ, ეგ არ უნდა ცვლიდეს
   page-title-ის პოზიციას").

   - The band is 170px (it still grows where a wrapped tab row cannot fit).
   - The identity is laid over the band's first 170px and centres its content there, so the
     `.page-title` line stands at 85px on every page. It takes no clicks itself (the trail and the
     buttons lie under it); its children do.
   - What sits above or below the title takes no height in that centring: the module's name
     (`.page-module`) is a zero-height line drawn upwards, the second line (`.page-context`) a
     zero-height line drawn downwards, and so are a list's switches. A patient photo stands
     beside the title and is centred on it.
   - The actions stand in the bottom-right corner, 15px above the edge, as the lists' do.
   - NOT `transform` for any of this: it would make the header a containing block for the
     position: fixed menus inside it (the v0.165.5 trap).

   Only on a wide screen; on a phone the header stacks as before. The trail's own
   `margin-bottom: 12px` is inline in `partials/breadcrumbs`, hence the !important. */
@media (min-width: 901px) {
  html .module-banner.illustration-header:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header),
  html .module-banner.illustration-header:is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header):has(> .section-nav-row) {
    min-height: 170px;
    display: flex;
    flex-direction: column;
    padding-top: 8px;
  }
  .module-banner > nav[aria-label="breadcrumb"],
  .module-banner > .breadcrumb-actions-row {
    margin-bottom: 4px !important;
  }
  /* The header holds the room between the trail and a tab row; its content is placed below. */
  .module-banner > .page-header {
    flex: 1 0 auto;
    display: block;
    min-height: 76px;
    padding: 0;
    margin-bottom: 0;
  }
  .module-banner:has(> .section-nav-row, > .section-nav) > .page-header {
    margin-bottom: 4px;
  }
  .module-banner > .page-header > .page-identity {
    position: absolute;
    top: 0;
    left: var(--content-pad);
    right: var(--content-pad);
    height: 170px;
    margin: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    gap: 0;
    pointer-events: none;
  }
  .module-banner > .page-header > .page-identity > * {
    pointer-events: auto;
  }
  /* Beside the title and centred on it: a patient photo, a leading icon. */
  .module-banner > .page-header > .page-identity:is(.has-photo, .has-icon) {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: center;
    column-gap: var(--sp-group);
  }
  .module-banner > .page-header > .page-identity.has-photo {
    column-gap: 12px;
  }
  .module-banner > .page-header > .page-identity.has-icon {
    column-gap: 10px;
  }
  .module-banner .page-identity .page-module {
    display: flex;
    align-items: flex-end;
    height: 0;
    margin: 0;
    position: relative;
    top: -2px;
    white-space: nowrap;
  }
  .module-banner .page-identity .page-context {
    height: 0;
    margin: 0;
    position: relative;
    top: 2px;
    overflow: visible;
    white-space: nowrap;
  }
  /* A list's switches (/contacts, /pets, suppliers) stand under the title, like a second line,
     and like it take no height, so the title keeps its place (owner, 2026-09-29). */
  .module-banner .page-identity > form:has(> .switch) {
    height: 0;
    position: relative;
    top: 1px;
    overflow: visible;
    align-items: flex-start;
  }
  /* Smaller in the header than in a form: they read as a note under the title (owner). */
  .module-banner .page-identity > form > .switch {
    gap: 7px;
    padding: 0;
    font-size: 12px;
  }
  .module-banner .page-identity > form > .switch .switch-track {
    width: 28px;
    height: 16px;
  }
  .module-banner .page-identity > form > .switch .switch-track::after {
    width: 12px;
    height: 12px;
  }
  .module-banner .page-identity > form > .switch input:checked ~ .switch-track::after {
    transform: translateX(12px);
  }
  .module-banner > .page-header > .page-actions {
    position: absolute;
    right: var(--content-pad);
    bottom: 15px;
    max-width: 60%;
    margin: 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; }
.module-header-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) > .module-header-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;
  }
  /* Room for the corner buttons only where there are any: on the dose calculator, with none,
     the reserved 240px folded the reference tabs onto a second line (header audit, 2026-09-29). */
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header):has(> .module-header-actions) > .section-nav-row {
    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) > .module-header-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%; }
}
/* A record's page keeps its module's tabs (owner, 2026-10-01), but its buttons live inside the
   header, in the corner the tabs also reach: below 1200px they covered the open tab. They step up
   over the tab row here, where a list's buttons stand at this width (band 206px, buttons at
   120px, as high as they are on a wide screen). The header's 127px is the 76px it always keeps
   plus the 36px buttons and the 15px under them; 11px of that is the buttons' own margin, the
   other 4px the header's. On a phone the header already stacks. */
@media (min-width: 901px) and (max-width: 1200px) {
  .module-banner:has(> .section-nav-row.is-record) > .page-header:has(> .page-actions) {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-end;
    min-height: 127px;
  }
  .module-banner:has(> .section-nav-row.is-record) > .page-header > .page-actions {
    position: static;
    max-width: 100%;
    margin: 0 0 11px;
  }
}
/* Five tabs (the shelter's) reach under the buttons a little wider still: at 1201px the first
   button covered „ანგარიში" and the two stood under 20px apart until ~1225px. */
@media (min-width: 1201px) and (max-width: 1260px) {
  .module-banner:has(> .section-nav-row.is-record > .section-nav > a:nth-child(5)) > .page-header:has(> .page-actions) {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-end;
    min-height: 127px;
  }
  .module-banner:has(> .section-nav-row.is-record > .section-nav > a:nth-child(5)) > .page-header > .page-actions {
    position: static;
    max-width: 100%;
    margin: 0 0 11px;
  }
}
@media (max-width: 480px) {
  :is(.ambulatory-flow-header, .surgery-flow-header, .watercolor-flow-header) > .module-header-actions { align-self: stretch; gap: 6px; }
  .module-header-actions > a {
    flex: 1 1 145px;
    justify-content: center;
    white-space: normal;
    text-align: center;
  }
}

.surgery-flow-header { --header-actions-space: 240px; }

/* Animal-register headers reserve the action area beside their section tabs. */
.watercolor-flow-header { --header-actions-space: 240px; }


/* Every module banner now follows the same natural-height flow: breadcrumbs, identity,
   section navigation and an optional action group. */

/* 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;
}


.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) {
  /* 128px, the owner's pick of four heights rendered side by side at 412px (2026-09-17: 160 / 128 /
     104 top / 104 centred). At 160 the title stood over a 75px band of artwork before the first
     figure; 128 halves it and keeps the curves visible under the title. */
  html .module-banner.dashboard-flow-header {
    min-height: 128px;
    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:nowrap; max-width:100%; min-width:0; }
.list-search-toolbar .search-box input[type="text"]:not(.ss-input) { 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-field-submit { display:flex; flex:0 1 auto; min-width:0; max-width:100%; }
.list-search-toolbar .search-field-submit > .search-clear-wrap { flex:1 1 auto; min-width:0; }
.list-search-toolbar .search-field-submit > .search-clear-wrap > input[type="text"] { border-radius:var(--radius-md) 0 0 var(--radius-md); }
.list-search-toolbar .search-field-submit > button[type="submit"] { flex:0 0 auto; min-height:38px; margin-inline-start:-1px; border-radius:0 var(--radius-md) var(--radius-md) 0; }
.list-search-toolbar .search-box:has(select) { gap:8px; }
.list-search-toolbar .search-box > .ss-wrap {
  /* The enhanced select contains a width:100% input. Without a definite wrapper width its
     flex contribution is measured as zero, even though the input is visibly about 220px wide;
     the neighbouring paginator can then be laid out on top of it. Give filter pickers a real
     flex size so the shared toolbar either fits honestly or moves the paginator to its own row. */
  flex:0 1 220px;
  width:220px;
  max-width:100%;
}
.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:760px) {
  .list-search-toolbar .search-box { flex-wrap:wrap; }
}
@media (max-width:600px) {
  .list-search-toolbar .search-box { width:100%; flex-wrap:wrap; }
  .list-search-toolbar .search-field-submit { flex:1 1 100%; width:100%; }
  .list-search-toolbar .search-box input[type="text"]:not(.ss-input) { flex:1; width:0; }
  .list-search-toolbar .search-box:has(select) input[type="text"]:not(.ss-input) { 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-search-toolbar .search-box > .ss-wrap { flex:1 1 180px; width:auto; }
}

.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; } }

/* One catalogue service can carry several complexity prices. The editor stays tabular so the
   eye compares price/cost/duration vertically; the list uses compact lines inside one cell so
   variants never turn back into duplicate service rows. */
.service-price-variants :is(th, td),
.service-price-adjustments :is(th, td) { vertical-align:middle; }
.service-price-variants input[type="checkbox"],
.service-price-variants input[type="radio"] { display:block; margin-inline:auto; }
.service-price-variant-line { display:flex; align-items:center; gap:6px; flex-wrap:wrap; min-height:24px; }
.service-price-variant-line + .service-price-variant-line { margin-top:3px; }
.service-price-adjustment-line { display:flex; align-items:center; gap:5px; margin-top:4px; padding-left:4px; }

/* ── 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; }

/* The public demo's login page: one door per tariff instead of a form (App\Core\Demo::entries(),
   views/partials/demo-doors.php). Chosen by the owner on 2026-09-17 from ten styles rendered on the
   real page, then refined: a narrow card, one door per line on a tinted row with the tariff's icon
   in its module colour, every door closed to begin with and only one open at a time, the button
   only inside the open one, and inside it the description set apart from the module names by a
   rule and a label. */
.login-shell:has(.login-card-demo) {
  /* Anchored to the top, not centred: a centred card grows up and down at once when a door opens,
     and the whole page jumps. Anchored, it only grows downwards. */
  align-items: flex-start;
  padding-top: max(20px, 6vh);
}
.login-card.login-card-demo { max-width: 540px; }
/* The choice leads the page and the launch news follows the doors (public-site audit, 2026-10-01). */
.demo-choose-title {
  margin: 0 0 4px; color: var(--text-primary);
  font-size: 18px; line-height: 1.35; font-weight: 700;
}
.demo-choose-note { margin: 0; color: var(--text-secondary); font-size: 12.5px; line-height: 1.55; }
.demo-choose-note .ti { color: var(--primary); }
.demo-launch-notice {
  margin: 18px 0 0; padding: 12px 14px; text-align: start;
  border: 1px solid color-mix(in srgb, var(--primary) 34%, var(--border));
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--primary) 7%, var(--bg-panel));
}
.demo-launch-notice h2 {
  margin: 0 0 6px; color: var(--text-primary);
  font-size: 14px; line-height: 1.35; font-weight: 700;
}
.demo-launch-notice p {
  margin: 4px 0 0; color: var(--text-secondary);
  font-size: 12.5px; line-height: 1.55;
}
.demo-launch-notice p strong { color: var(--text-primary); }
.demo-accordion { margin-top: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.demo-accordion details {
  border: 1px solid var(--border); border-radius: var(--radius-md); padding: 9px 12px;
  /* Not white: white rows on the white card read as nothing to press. */
  background: var(--bg);
  transition: border-color .15s ease, background-color .15s ease;
}
.demo-accordion details:not([open]):hover { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 9%, var(--bg-panel)); }
.demo-accordion details:not([open]):hover .demo-entry-chevron,
.demo-accordion details:not([open]):hover .demo-entry-name { color: var(--primary-text); }
.demo-accordion details[open] { border-color: var(--primary); background: var(--primary-light); }
.demo-accordion summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.demo-accordion summary::-webkit-details-marker { display: none; }
.demo-entry-title { display: inline-flex; align-items: center; gap: 9px; flex: 1; min-width: 0; }
.demo-entry-icon { font-size: 19px; line-height: 1; }
.demo-entry-name { font-weight: 600; font-size: 14px; line-height: 1.3; }
/* A closed door's first line of description, so eight closed rows already say how they differ.
   The open door shows the whole description below, as the owner chose it, so the line goes. */
.demo-entry-text { display: grid; min-width: 0; }
.demo-entry-teaser {
  font-size: 12px; line-height: 1.4; color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.demo-accordion details[open] .demo-entry-teaser { display: none; }
.demo-entry-chevron { flex: none; margin-left: 8px; color: var(--text-secondary); transition: transform .15s ease; }
.demo-accordion details[open] .demo-entry-chevron { transform: rotate(180deg); }
.demo-entry-description { font-size: 13px; line-height: 1.5; color: var(--text-primary); padding: 6px 0 0 28px; }
.demo-module-label {
  margin: 9px 0 0 28px; padding-top: 7px; border-top: 1px solid var(--border);
  font-size: 11px; font-weight: 700; color: var(--text-secondary);
}
.demo-module-grid {
  list-style: none; margin: 8px 0 4px; padding: 0 0 0 28px;
  /* Rows spaced out (the owner, 2026-09-17): at 4px the two columns read as one block of words. */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); gap: 10px 12px;
  font-size: 12px; font-weight: 600; line-height: 1.4; color: var(--text-primary);
}
.demo-module-grid li { display: flex; align-items: flex-start; gap: 6px; min-width: 0; }
.demo-module-grid .ti { font-size: 15px; flex: none; margin-top: -1px; }
.demo-accordion form { margin: 0; }
.demo-wide-button { width: 100%; justify-content: center; margin-top: 10px; }

/* The strip above every demo page (views/partials/demo-bar.php, 2026-10-01): which version this
   is, that it saves nothing, another door, and the plans. */
.demo-bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px 18px;
  padding: 6px 16px; font-size: 12.5px; line-height: 1.4;
  background: var(--primary-light); color: var(--primary-text);
  border-bottom: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
}
.demo-bar-text { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0 6px; }
.demo-bar-text .ti { font-size: 15px; }
.demo-bar-dot { opacity: .5; }
.demo-bar-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
.demo-bar-link {
  display: inline-flex; align-items: center; gap: 5px; padding: 2px 0;
  border: 0; background: none; color: var(--primary-text); font: inherit; font-weight: 700;
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.demo-bar-link:hover { color: var(--primary); }
.demo-bar-link .ti { font-size: 15px; }

/* ==========================================================================================
   REGISTRATION - /register, /register/sent, /register/confirm, /register/confirmed (v0.238.0)
   ==========================================================================================
   The owner's approved mockup (2026-09-17) in the app's own parts: a top bar with the wordmark,
   two steps in one form, the plan cards, „Customize the plan" with its two tabs, and the summary
   beside the choices. Structure only; every colour is a theme token, with two documented
   exceptions:

   - **The wordmark's two colours are the brand, not a theme**: lowercase bold „vet" #b5493f and
     „man" #4872ab, in every theme (owner, 2026-09-17 - the public site follows the same rule).
   - **The summary is the one emphasised surface**, a dark panel as in the mockup. It is built from
     tokens that already are dark-on-light ink in every light theme (`--primary-text` as the ground,
     `--bg-panel` as the ink - 10:1 in the default theme); the two dark themes, where that pair would
     invert into a light slab, take a primary-tinted panel instead. Four local custom properties carry
     the choice so the rules below it are written once. */

.signup-body { background: var(--bg); }
/* The same ground as the login card (`.login-shell`): registering and signing in are two doors
   into the same building, and a flat page beside an illustrated one looked like another site
   (owner, 2026-09-18). The theme blocks above swap the artwork; this only has to place it. */
.signup-page {
  min-height: 100vh; display: flex; flex-direction: column; padding: 26px 20px 40px;
  background-color: var(--bg);
  background-image: url(/assets/img/dashboard-illustration-light-abstract-v1.webp);
  background-size: cover; background-position: center top; background-repeat: no-repeat; background-attachment: fixed;
}

/* One framed window on the artwork, as the approved mockup has it: the bar on top, the work
   below, nothing floating loose on the illustration. */
.signup-window {
  width: 100%; max-width: 1160px; margin: 0 auto;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-lg);
  /* `clip`, never `hidden`: a scroll container is where `position: sticky` silently stops
     working, and the summary beside the choices is sticky (the same trap html/body fell into). */
  overflow: clip; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07);
}
.signup-header {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px 16px;
  padding: 14px 24px; background: var(--bg-panel); border-bottom: 1px solid var(--border);
}
.signup-brand { display: inline-flex; align-items: center; text-decoration: none; }
/* The application s own wordmark (brand-v2). The global rules further down give it the lighter
   duotone under the dark themes, so this only has to say how big it is. */
.signup-brand-img { width: 168px; max-width: 46vw; height: auto; display: block; }
.signup-header-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }

.signup-main { width: 100%; margin: 0 auto; padding: 24px 28px 40px; flex: 1; }
.signup-form { margin: 0; }

.signup-steps ol { list-style: none; display: flex; margin: 0 0 22px; padding: 0; border-bottom: 1px solid var(--border); }
.signup-steps li { flex: 1; min-width: 0; }
.signup-step {
  width: 100%; min-height: 44px; display: flex; align-items: center; gap: 8px; padding: 10px 6px;
  background: none; border: 0; border-bottom: 3px solid transparent; margin-bottom: -1px;
  font: inherit; font-size: var(--fs-base); color: var(--text-secondary); text-align: start; cursor: pointer;
}
.signup-step[aria-current="step"] { border-bottom-color: var(--primary); color: var(--text-primary); font-weight: 700; }
.signup-step:disabled { cursor: default; color: var(--text-tertiary); }
.signup-step:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.signup-step-no {
  display: inline-grid; place-items: center; flex: none; width: 24px; height: 24px; border-radius: 50%;
  background: var(--primary-light); color: var(--primary-text); font-size: var(--fs-small); font-weight: 700;
}

.signup-notice { display: flex; align-items: flex-start; gap: 8px; }

.signup-columns { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 28px; align-items: start; }
.signup-content { min-width: 0; }
.signup-aside { position: sticky; top: 16px; }

.signup-eyebrow { font-size: var(--fs-micro); font-weight: 700; color: var(--text-secondary); margin-bottom: 6px; }
.signup-title { margin: 0; font-size: 24px; line-height: 1.35; font-weight: 700; color: var(--text-primary); }
.signup-title:focus { outline: none; }
.signup-lead { margin: 6px 0 20px; font-size: 14px; line-height: 1.6; color: var(--text-secondary); }

.signup-plans, .signup-modules, .signup-levels, .signup-period { border: 0; margin: 0; padding: 0; min-width: 0; }

/* A card or a chip is the control; the radio inside stays in the tab order, only unseen. */
.signup-choice-input,
.signup-level input,
.signup-period-option input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }

.signup-plan-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.signup-plan-card,
.signup-plan-chip,
.signup-level {
  position: relative; border: 1px solid var(--btn-line); border-radius: var(--radius-md);
  background: var(--bg-panel); color: var(--text-primary); cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease;
}
.signup-plan-card { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 18px 16px; min-width: 0; }
.signup-plan-card:hover,
.signup-plan-chip:hover,
.signup-level:not(.is-below):hover { border-color: var(--primary); }
.signup-plan-card:has(input:checked),
.signup-plan-chip:has(input:checked),
.signup-level:has(input:checked) { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); background: var(--primary-light); }
.signup-plan-card:has(input:focus-visible),
.signup-plan-chip:has(input:focus-visible),
.signup-level:has(input:focus-visible),
.signup-module:has(input:focus-visible),
.signup-period-option:has(input:focus-visible) span { outline: 2px solid var(--primary); outline-offset: 2px; }
.signup-plan-icon { font-size: 24px; line-height: 1; }
.signup-plan-name { font-size: var(--fs-title); font-weight: 700; line-height: 1.35; overflow-wrap: anywhere; }
.signup-plan-price { font-size: 18px; font-weight: 700; }
.signup-plan-limits { font-size: var(--fs-small); line-height: 1.6; color: var(--text-secondary); }
.signup-plan-check { display: none; position: absolute; top: 12px; inset-inline-end: 12px; font-size: 20px; color: var(--primary); }
.signup-plan-card:has(input:checked) .signup-plan-check { display: block; }

/* Icon and plan name on one line (owner, 2026-10-25), as phones already show it; price and limits go under. */
@media (min-width: 641px) {
  .signup-plan-card { flex-direction: row; flex-wrap: wrap; align-items: center; column-gap: 10px; padding-inline-end: 44px; }
  .signup-plan-card .signup-plan-name { flex: 1 1 0; min-width: 0; }
  .signup-plan-card .signup-plan-price,
  .signup-plan-card .signup-plan-limits { flex-basis: 100%; }
}

.signup-plan-other { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.signup-plan-chip { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 6px 12px; }
.signup-plan-chip .signup-plan-icon { font-size: 18px; }
.signup-plan-chip-text { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 6px; }
.signup-plan-chip .signup-plan-name { font-size: var(--fs-base); }
.signup-plan-chip .signup-plan-price { font-size: var(--fs-small); font-weight: 400; color: var(--text-secondary); }
.signup-plan-description { margin: 14px 0 0; font-size: var(--fs-base); line-height: 1.6; color: var(--text-secondary); }

.signup-customize { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--border); }
.signup-customize-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; }
.signup-customize-hint { font-size: var(--fs-small); color: var(--text-secondary); }
.signup-customize-chevron { transition: transform .15s ease; }
[aria-expanded="true"] > .signup-customize-chevron { transform: rotate(180deg); }
.signup-customize-panel { margin-top: 14px; padding: 18px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-panel); }
.signup-customize-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.signup-customize-head h2 { margin: 0; font-size: 17px; font-weight: 700; color: var(--text-primary); }
.signup-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 16px; }
.signup-tab {
  min-height: 36px; padding: 0 14px; border: 1px solid var(--btn-line); border-radius: var(--radius-md);
  background: var(--bg); color: var(--text-primary); font: inherit; font-size: var(--fs-base); cursor: pointer;
}
.signup-tab[aria-selected="true"] { background: var(--action-bg); border-color: var(--action-bg); color: var(--on-action); font-weight: 700; }
.signup-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.signup-section-label { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 4px 12px; margin-bottom: 10px; }
.signup-section-label h3 { margin: 0; font-size: var(--fs-base); font-weight: 700; color: var(--text-primary); }
.signup-section-label small { font-size: var(--fs-small); color: var(--text-secondary); }

.signup-modules { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.signup-module {
  display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-panel); cursor: pointer;
}
.signup-module input { margin: 0; }
.signup-module:has(input:checked:not(:disabled)) { border-color: var(--primary); background: var(--primary-light); }
.signup-module.is-included { background: var(--bg); cursor: default; }
.signup-module-icon { flex: none; font-size: 18px; }
.signup-module-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.signup-module-name { font-size: var(--fs-base); font-weight: 700; color: var(--text-primary); overflow-wrap: anywhere; }
.signup-module-text small { font-size: var(--fs-small); color: var(--text-secondary); }
.signup-module-lock { flex: none; color: var(--text-secondary); }

.signup-levels { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.signup-level { display: flex; flex-direction: column; gap: 6px; padding: 12px; min-width: 0; }
.signup-level.is-below { border-style: dashed; border-color: var(--border); background: var(--bg); color: var(--text-secondary); cursor: not-allowed; }
.signup-level-head { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 2px 8px; }
.signup-level-name { font-size: var(--fs-base); font-weight: 700; }
.signup-level.is-below .signup-level-name { color: var(--text-secondary); }
.signup-level-limits { display: grid; font-size: var(--fs-small); line-height: 1.5; color: var(--text-secondary); }
.signup-level-note { min-height: 1.4em; font-size: var(--fs-small); color: var(--text-secondary); }

.signup-customize-actions { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.signup-customize-note { font-size: var(--fs-small); color: var(--text-secondary); }

.signup-step-actions { margin-top: 24px; }
.signup-step-panel[data-step-panel="account"] .signup-step-actions { justify-content: space-between; }

.signup-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 14px; }
.signup-field-help { font-size: var(--fs-small); color: var(--text-secondary); }
.signup-field-error { display: flex; align-items: flex-start; gap: 6px; font-size: var(--fs-small); font-weight: 700; color: var(--danger-text); }
.signup-form .form-group input[aria-invalid="true"] { border-color: var(--danger-line); box-shadow: inset 0 0 0 1px var(--danger-line); }
.signup-terms { display: flex; align-items: flex-start; gap: 10px; margin: 2px 0 6px; font-size: var(--fs-base); line-height: 1.6; color: var(--text-primary); }
.signup-terms input { margin-top: 3px; }
.signup-terms input[aria-invalid="true"] { outline: 2px solid var(--danger-line); outline-offset: 2px; }
.signup-form .form-group select[aria-invalid="true"] { border-color: var(--danger-line); box-shadow: inset 0 0 0 1px var(--danger-line); }

/* Who pays, the payer's details and the administrator (2026-10-11): three titled parts of step 2. */
.signup-section-title { margin: 22px 0 4px; font-size: 15px; font-weight: 700; color: var(--text-primary); }
.signup-section-note { margin: 0 0 12px; font-size: var(--fs-small); line-height: 1.5; color: var(--text-secondary); }
.signup-optional { font-weight: 400; color: var(--text-secondary); }
.signup-optional::before { content: "· "; }
.signup-switch-row { margin-bottom: 14px; }
.signup-payer-type { display: flex; gap: 4px; padding: 4px; margin: 4px 0 18px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-panel); min-width: 0; }
.signup-payer-option { position: relative; flex: 1; min-width: 0; }
.signup-payer-option input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
.signup-payer-option span {
  display: flex; align-items: center; justify-content: center; min-height: 40px; padding: 6px 8px; border-radius: var(--radius-sm);
  font-size: var(--fs-small); line-height: 1.3; text-align: center; color: var(--text-secondary); cursor: pointer;
}
.signup-payer-option:has(input:checked) span { background: var(--action-bg); color: var(--on-action); font-weight: 700; }
.signup-payer-option:has(input:focus-visible) span { outline: 2px solid var(--primary); outline-offset: 2px; }

.signup-period { display: flex; gap: 4px; padding: 4px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-panel); }
.signup-period-option { position: relative; flex: 1; min-width: 0; }
.signup-period-option span {
  display: flex; align-items: center; justify-content: center; min-height: 38px; padding: 6px 8px; border-radius: var(--radius-sm);
  font-size: var(--fs-small); line-height: 1.3; text-align: center; color: var(--text-secondary); cursor: pointer;
}
.signup-period-option:has(input:checked) span { background: var(--action-bg); color: var(--on-action); font-weight: 700; }

.signup-summary {
  --summary-bg: var(--primary-text);
  --summary-ink: var(--bg-panel);
  --summary-muted: color-mix(in srgb, var(--summary-ink) 74%, var(--summary-bg));
  --summary-line: color-mix(in srgb, var(--summary-ink) 20%, var(--summary-bg));
  padding: 20px; border-radius: var(--radius-lg); background: var(--summary-bg); color: var(--summary-ink);
  transition: opacity .15s ease;
}
html[data-theme="dark"] .signup-summary,
html[data-theme="terminal"] .signup-summary {
  --summary-bg: color-mix(in srgb, var(--primary) 14%, var(--bg-panel));
  --summary-ink: var(--text-primary);
  border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--bg-panel));
}
.signup-summary[aria-busy="true"] { opacity: .7; }
.signup-summary-label { margin-bottom: 10px; font-size: var(--fs-micro); font-weight: 700; color: var(--summary-muted); }
.signup-summary-empty { margin: 0; font-size: var(--fs-base); color: var(--summary-muted); }
.signup-summary-plan { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 18px; font-weight: 700; }
/* The plan's own colour sits on a disc of the panel's ink, so it reads on the dark ground too. */
.signup-summary-plan .ti { display: inline-grid; place-items: center; flex: none; width: 30px; height: 30px; border-radius: 50%; background: var(--bg-panel); font-size: 18px; }
.signup-summary-rows { margin: 0; }
.signup-summary-row { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--summary-line); font-size: var(--fs-small); }
.signup-summary-row dt { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.signup-summary-row dd { margin: 0; font-weight: 700; white-space: nowrap; }
.signup-summary-total { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; margin-top: 14px; }
.signup-summary-total-label { flex-basis: 100%; font-size: var(--fs-micro); font-weight: 700; color: var(--summary-muted); }
.signup-summary-amount { font-size: 30px; line-height: 1.2; font-weight: 700; }
.signup-summary-per { font-size: var(--fs-small); color: var(--summary-muted); }
.signup-summary-period { margin-top: 2px; font-size: var(--fs-small); color: var(--summary-muted); }
.signup-summary-limits { list-style: none; display: grid; gap: 2px; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px solid var(--summary-line); font-size: var(--fs-small); color: var(--summary-muted); }
.signup-summary-note { margin: 10px 0 0; font-size: var(--fs-small); line-height: 1.5; }
.signup-summary-error { margin: 10px 0 0; }

.signup-mobilebar { display: none; }

.signup-main-result { display: flex; justify-content: center; align-items: flex-start; padding-top: 48px; }
.signup-result {
  width: 100%; max-width: 520px; padding: 32px; text-align: center;
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.signup-result-icon { display: block; margin-bottom: 12px; font-size: 44px; line-height: 1; color: var(--primary); }
.signup-result-icon.is-success { color: var(--success-text); }
.signup-result-icon.is-warning { color: var(--warn-text); }
.signup-result .signup-lead { margin: 8px 0 16px; overflow-wrap: anywhere; }
.signup-result-note { margin: 14px 0 0; font-size: var(--fs-small); color: var(--text-secondary); }
.signup-result-actions { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.signup-result-facts { margin: 16px 0; padding: 4px 14px; text-align: start; border: 1px solid var(--border); border-radius: var(--radius-md); }
.signup-result-facts div { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-base); }
.signup-result-facts div:last-child { border-bottom: 0; }
.signup-result-facts dt { color: var(--text-secondary); }
.signup-result-facts dd { margin: 0; font-weight: 700; text-align: end; overflow-wrap: anywhere; }

@media (max-width: 900px) {
  .signup-columns { grid-template-columns: minmax(0, 1fr); }
  .signup-aside { position: static; }
  .signup-levels { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The summary drops below the choices here, so the total travels with the screen instead. */
  .signup-mobilebar:not([hidden]) {
    position: sticky; bottom: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin: 20px -24px 0; padding: 10px 24px; background: var(--bg-panel); border-top: 1px solid var(--border);
    box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.08);
  }
  .signup-mobilebar-total { font-size: 16px; font-weight: 700; color: var(--text-primary); }
  .signup-step-panel[data-step-panel="plan"] .signup-step-actions { display: none; }
  .signup-main { padding-bottom: 0; }
  .signup-step-panel[data-step-panel="account"] { padding-bottom: 32px; }
  .signup-aside { margin-bottom: 24px; }
}

@media (max-width: 640px) {
  .signup-page { padding: 0 0 24px; }
  .signup-window { border-radius: 0; border-left: 0; border-right: 0; box-shadow: none; }
  .signup-header { padding: 10px 16px; }
  .signup-brand-img { width: 140px; }
  .signup-main { padding: 18px 16px 0; }
  .signup-mobilebar:not([hidden]) { margin-inline: -16px; padding-inline: 16px; }
  .signup-title { font-size: 20px; }
  .signup-plan-grid,
  .signup-modules,
  .signup-form-grid { grid-template-columns: minmax(0, 1fr); }
  .signup-plan-card { flex-direction: row; flex-wrap: wrap; align-items: center; column-gap: 10px; padding: 14px 44px 14px 14px; }
  .signup-plan-card .signup-plan-price { margin-inline-start: auto; font-size: 16px; }
  .signup-plan-card .signup-plan-limits { flex-basis: 100%; }
  .signup-plan-chip { flex: 1 1 100%; }
  .signup-customize-panel { padding: 14px 12px; }
  .signup-step-panel[data-step-panel="account"] .signup-step-actions .btn { flex: 1 1 100%; justify-content: center; }
  .signup-result { padding: 24px 18px; }
  .signup-main-result { padding-top: 24px; padding-bottom: 32px; }
}

/* The registration card on /admin/organizations/{id}: label and value on one line. */
.signup-admin-facts { margin: 0; }
.signup-admin-facts div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-base); }
.signup-admin-facts div:last-child { border-bottom: 0; }
.signup-admin-facts dt { color: var(--text-secondary); }
.signup-admin-facts dd { margin: 0; text-align: end; overflow-wrap: anywhere; }
.signup-plan-price small { font-size: var(--fs-small); font-weight: 400; color: var(--text-secondary); }
/* The module's own line in the registration list: what it is, under its name (v0.238.3). */
.signup-module-about { font-size: var(--fs-small); line-height: 1.45; color: var(--text-secondary); }
.signup-module { align-items: flex-start; }
.signup-module input, .signup-module .signup-module-icon { margin-top: 2px; }

/* Approved platform logo v2: keep brand colors; do not affect organization logos. */
img[src*="/brand-v2-"]{filter:none!important}
html[data-theme="dark"] img[src$="brand-v2-ka.svg"],html[data-theme="terminal"] img[src$="brand-v2-ka.svg"],html[data-theme="pro-slate"] .app-header img[src$="brand-v2-ka.svg"],html[data-theme="pharmacy"] .app-header img[src$="brand-v2-ka.svg"],html[data-theme="enamel"] .app-header img[src$="brand-v2-ka.svg"]{content:url(/assets/img/brand-v2-ka-duotone-lighter.svg)}
html[data-theme="dark"] img[src$="brand-v2-en.svg"],html[data-theme="terminal"] img[src$="brand-v2-en.svg"],html[data-theme="pro-slate"] .app-header img[src$="brand-v2-en.svg"],html[data-theme="pharmacy"] .app-header img[src$="brand-v2-en.svg"],html[data-theme="enamel"] .app-header img[src$="brand-v2-en.svg"]{content:url(/assets/img/brand-v2-en-duotone-lighter.svg)}
/* The phone header's symbol on the dark headers, in the same lighter pair of greys as the wordmark (2026-10-05). */
html[data-theme="dark"] .app-header img.app-header-symbol,html[data-theme="terminal"] .app-header img.app-header-symbol,html[data-theme="pro-slate"] .app-header img.app-header-symbol,html[data-theme="pharmacy"] .app-header img.app-header-symbol,html[data-theme="enamel"] .app-header img.app-header-symbol{content:url(/assets/img/brand-v2-symbol-duotone-lighter.svg)}

/* Login lockup: retain readable descriptor without changing the application header. */
.login-card .login-logo-img[src*="/brand-v2-"]{width:230px;max-width:100%;height:auto}

/* Larger platform lockup, within sidebar-aligned header and phone controls. */
.app-header .app-header-logo img[src*="/brand-v2-"]{width:168px;height:auto;max-width:calc(100% - 64px);flex:0 1 168px}
@media(max-width:900px){.app-header .app-header-logo img[src*="/brand-v2-"]{max-width:168px}}
@media(max-width:600px){.app-header .app-header-logo img[src*="/brand-v2-"]{width:min(150px,calc(100vw - 200px));height:auto;max-width:100%;flex-basis:auto}}
/* „Included in the plan" wears a tick and its own colour, so it reads as a state rather than as one
   more line of the description above it - on a module and on the plan's own level alike (owner, 2026-09-18). */
.signup-included { display: inline-flex; align-items: center; gap: 5px; margin-top: 5px; font-weight: 700; color: var(--text-secondary); }
.signup-included .ti { font-size: 15px; color: var(--success-line); }

/* A capacity limit wears the icon the rest of the application gives it - staff, animals, contacts,
   storage - so a level reads as four facts, not four lines of text (owner, 2026-09-18). */
.signup-level-limits span, .signup-summary-limits li { display: flex; align-items: center; gap: 6px; }
.signup-limit-icon { flex: none; width: 15px; font-size: 14px; text-align: center; }
.signup-plan-limits .signup-limit-icon { margin-inline-end: 3px; }
.signup-level-note .signup-included { margin-top: 0; }
/* The line under a module or a level is bold in both panels - the price, the reason a row is
   locked and the included badge alike (owner, 2026-09-18). */
.signup-module-text small, .signup-level-note { font-weight: 700; }
/* The login door names the product's home and a way to reach us; the card is centred, so the
   line sits under it rather than inside it (owner, 2026-09-18). */
.login-stack { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 400px; }
.login-stack:has(.login-card-demo) { max-width: 540px; }
.login-stack .login-card { max-width: none; }
/* The invitation sits between the form and the card footer, so it carries no rule of its own:
   the footer hairline is the only line in that corner of the card. */
.login-signup { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 20px; font-size: var(--fs-small); color: var(--text-secondary); }
.social-auth-list { display: grid; gap: 8px; margin-bottom: 16px; }
.social-auth-list-register { grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 18px 0 12px; }
.social-auth-button { justify-content: center; min-height: 42px; border: 1px solid var(--border); background: var(--bg-panel); color: var(--text-primary); font-weight: 700; }
.social-auth-button:hover { border-color: var(--primary); background: var(--primary-light); color: var(--primary-text); }
.social-auth-button .ti { font-size: 18px; }
.social-auth-divider { display: flex; align-items: center; gap: 10px; margin: 0 0 16px; color: var(--text-secondary); font-size: var(--fs-small); }
.social-auth-divider::before, .social-auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.social-auth-divider span { white-space: nowrap; }
.social-auth-selected { display: flex; align-items: center; gap: 9px; margin: 16px 0; padding: 11px 13px; border: 1px solid var(--primary); border-radius: var(--radius-md); background: var(--primary-light); color: var(--primary-text); font-size: var(--fs-base); font-weight: 700; }
.social-auth-selected .ti { flex: none; font-size: 19px; }
@media (max-width: 620px) { .social-auth-list-register { grid-template-columns: 1fr; } }
.login-foot {
  display: flex; justify-content: center; align-items: center; gap: 10px;
  margin: 24px 0 0; padding-top: 16px; border-top: 1px solid var(--border);
  font-size: var(--fs-small); color: var(--text-secondary);
}
.login-foot a { color: var(--text-secondary); }
.login-foot a:hover { color: var(--primary-text); }
.login-foot-dot { opacity: .45; }
/* The wordmark keeps its two halves, in the themed red and blue rather than the brand's own
   hex: those two tokens are contrast-checked in all nine themes, the brand pair is not. */
.brandmark { font-weight: 800; letter-spacing: .1px; }
.brandmark .v { color: var(--danger-text); }
.brandmark .m { color: var(--primary-text); }

/* /admin/billing: the add-on modules of an agreement, as a compact grid of ticks. */
.billing-modules { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 6px 14px; margin: 6px 0 8px; }
.billing-modules label { display: flex; align-items: center; gap: 8px; font-size: var(--fs-base); font-weight: 400; }

/* Finance pairs stay side by side on a working screen and become readable cards on a phone. */
.finance-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-bottom: 16px;
}
@media (max-width: 600px) {
  .finance-card-grid { grid-template-columns: 1fr; }
}

/* Upload progress (ajax-form.js): one panel at the foot of the screen for any form that sends a
   file, since several upload forms are hidden behind a menu item and have no place of their own. */
.upload-progress {
  position: fixed;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 1100;
  width: min(380px, calc(100vw - 32px));
  padding: 10px 12px 12px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
  color: var(--text-primary);
  font-size: 13px;
}
.upload-progress[hidden] { display: none; }
.upload-progress-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.upload-progress-label { flex: 1; min-width: 0; }
.upload-progress-figures {
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.upload-progress-close {
  border: 0;
  background: none;
  padding: 0 2px;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
}
.upload-progress-track {
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}
.upload-progress-bar {
  height: 100%;
  width: 0;
  background: var(--primary);
  transition: width 0.2s ease;
}
/* Every byte is out and the server is storing the file: the bar stands full and the loader's tiles
   beside the label keep turning (v1.11.9; it used to pulse). */
.upload-progress.is-error { background: var(--danger-bg); color: var(--danger-text); border-color: var(--danger-line); }
.upload-progress.is-error .upload-progress-close { color: var(--danger-text); }
.upload-progress.is-error .upload-progress-track { display: none; }
.upload-progress.is-error .upload-progress-head { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) {
  .upload-progress-bar { transition: none; }
}

/* Toasts (assets/js/toast.js, partials/toasts.php): what a save or a refusal just said, top right
   under the header (owner's pick, 2026-09-25). A success leaves by itself when its bar runs out;
   hovering or focusing it stops the bar. A warning or an error stays until it is closed.
   Inside an open dialog the stack is the dialog's own child: the dialog is in the top layer and
   everything outside it is inert, so a stack on <body> would sit behind it, unclickable. */
.toast-stack {
  position: fixed;
  top: 68px;
  right: 16px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
}
.toast-stack:empty { display: none; }
.toast {
  --toast-tone: var(--success-text);
  --toast-line: var(--success-line);
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 10px 12px 12px;
  overflow: hidden;
  border: 1px solid var(--toast-line);
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  color: var(--text-primary);
  font-size: 13px;
  line-height: 1.45;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
  pointer-events: auto;
  animation: toast-in .18s ease-out;
}
.toast-warning { --toast-tone: var(--warn-text); --toast-line: color-mix(in srgb, var(--warn-text) 55%, var(--bg-panel)); }
.toast-error { --toast-tone: var(--danger-text); --toast-line: var(--danger-line); }
.toast-icon { flex: none; font-size: 18px; line-height: 1.1; color: var(--toast-tone); }
.toast-text { flex: 1; min-width: 0; padding-top: 1px; overflow-wrap: anywhere; }
.toast-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin: -3px -3px 0 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
}
.toast-close:hover { background: var(--bg); color: var(--text-primary); }
.toast-close .ti { font-size: 16px; }
.toast-timer {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 3px;
  background: var(--toast-tone);
  transform-origin: left center;
  animation: toast-timer 6s linear forwards;
}
.toast:hover .toast-timer,
.toast:focus-within .toast-timer { animation-play-state: paused; }
.toast.is-leaving { opacity: 0; transform: translateX(12px); transition: opacity .18s ease, transform .18s ease; }
@keyframes toast-in { from { opacity: 0; transform: translateX(12px); } }
@keyframes toast-timer { to { transform: scaleX(0); } }
@media (max-width: 600px) {
  .toast-stack { right: 16px; left: 16px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.is-leaving { transition: none; }
}
@media print {
  .toast-stack { display: none; }
}

/* ============================================================
   ADMIN MAIL CLIENT (/admin/mail, v0.249.0)
   Folders beside the list on a wide screen, above it on a phone. The message body is not styled
   here: it lives in its own sandboxed frame with its own page, white like the paper it imitates.
   ============================================================ */
.mail-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.mail-folders {
  display: flex;
  flex-direction: column;
  padding: 6px;
  gap: 2px;
}
.mail-folder {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-size: 13.5px;
}
.mail-folder:hover { background: var(--tbl-hover); }
.mail-folder.is-current { background: var(--primary-light); color: var(--primary-text); font-weight: 700; }
.mail-folder .ti { font-size: 16px; }
.mail-folder-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-folder-count,
.mail-count {
  font-size: 11px;
  font-weight: 700;
  border-radius: 10px;
  padding: 1px 7px;
  background: var(--accent);
  color: #fff;
}
.mail-count { margin-left: 4px; }
.mail-table tr.mail-unread td { font-weight: 700; }
.mail-table .mail-col-person { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-table .mail-col-subject .ti { color: var(--text-secondary); margin-right: 4px; }
.mail-table .mail-col-date { white-space: nowrap; width: 1%; }
.mail-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--primary);
  margin-right: 6px;
  vertical-align: middle;
}
.mail-meta { margin-bottom: 12px; }
.mail-meta-list {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 14px;
  margin: 0;
  font-size: 13.5px;
}
.mail-meta-list dt { color: var(--text-secondary); }
.mail-meta-list dd { margin: 0; overflow-wrap: anywhere; }
.mail-addr { color: var(--text-secondary); font-weight: 400; }
.mail-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}
.mail-images-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 12px;
  margin-bottom: 8px;
  border-radius: var(--radius-sm);
  background: var(--warn-bg, var(--bg));
  color: var(--warn-text, var(--text));
  font-size: 13px;
}
.mail-frame {
  display: block;
  width: 100%;
  min-height: 240px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md, 8px);
  background: #fff;
}
.mail-text { white-space: normal; overflow-wrap: anywhere; font-size: 14px; line-height: 1.55; }
.mail-compose { max-width: 900px; }
.mail-body { font-family: inherit; line-height: 1.5; min-height: 280px; }
.mail-check { display: flex; align-items: center; gap: 8px; margin: 4px 0; font-weight: 400; }
@media (max-width: 900px) {
  .mail-layout { grid-template-columns: minmax(0, 1fr); }
  .mail-folders { flex-direction: row; overflow-x: auto; }
  .mail-folder { white-space: nowrap; }
  .mail-folder-name { overflow: visible; }
  .mail-table .mail-col-person { max-width: 140px; }
}

/* An item's size: the number and its unit read as one field (2026-09-27, item forms). */
.item-size-pair { display: flex; gap: 6px; }
.item-size-pair input { flex: 1; min-width: 0; }
.item-size-pair select { width: 130px !important; flex: none; }

/* The operative report on an operation's page (v0.256.0): label over text, one field per row,
   the text keeping the line breaks it was typed with. */
.op-report {
  display: grid;
  gap: 12px;
  margin: 0;
}
.op-report dt {
  font-size: 12px;
  font-weight: 700;
  color: var(--text-secondary);
  margin-bottom: 3px;
}
.op-report dd {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/* The operation's information card (owner's pick „D", 2026-09-28): the animal, the day as a
   planned → started → ended strip, the team, pills, the diagnosis set off. `si-` = surgery info. */
.si-label {
  font-size: 11.5px;
  color: var(--text-secondary);
  margin-bottom: 2px;
}
.si-value {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}
.si-pet {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 22px;
  padding: 10px 12px;
  margin-bottom: 12px;
  background: var(--bg);
  border-radius: var(--radius-md);
}
.si-pet-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 700;
}
.si-pet-name i {
  font-size: 22px;
  color: var(--primary);
}
.si-strip {
  display: flex;
  margin-bottom: 14px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.si-step {
  flex: 1;
  min-width: 0;
  padding: 10px 12px;
}
.si-step + .si-step {
  border-left: 1px solid var(--rule);
}
.si-step.is-done {
  background: color-mix(in srgb, var(--primary) 7%, transparent);
}
.si-time {
  font-size: 16px;
}
.si-team {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 16px;
  margin-bottom: 14px;
}
.si-team > div {
  display: flex;
  gap: 10px;
  align-items: center;
  min-width: 0;
}
.si-team i {
  flex: none;
  width: 22px;
  font-size: 18px;
  text-align: center;
  color: var(--primary);
}
.si-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}
.si-callout {
  margin-bottom: 12px;
  padding: 10px 12px;
  background: var(--bg);
  border-left: 3px solid var(--primary);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.si-callout-text {
  font-size: 13.5px;
  font-weight: 600;
}
/* The imaging report's parts in one card, each under its own small heading (owner, 2026-10-01):
   seven cards of one paragraph each read as seven records. */
.imaging-part + .imaging-part {
  margin-top: 16px;
}
.imaging-part-title {
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.si-note {
  font-size: 13px;
  color: var(--text-secondary);
  white-space: pre-line;
}
@media (max-width: 600px) {
  .si-team { grid-template-columns: minmax(0, 1fr); }
}

/* A switched-off row of the surgery checklist settings (v0.256.0): dimmed, the switch itself not. */
.checklist-off td:not(:last-child) {
  opacity: 0.5;
}

/* Two groups of view links side by side (the surgery calendar's day/week/month and its
   surgeons/rooms columns, v0.257.0): a hairline between them, or they read as one row. */
.vlinks + .vlinks {
  margin-left: 12px;
  padding-left: 12px;
  border-left: 1px solid var(--rule);
}

/* A module's way to its settings, on the breadcrumb row (owner's pick „D3", v0.258.0): quiet at
   rest, and on hover it takes a light fill and the brand colour so it reads as clickable. */
.module-settings-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 8px;
  border-radius: var(--radius-md);
  font-size: 12.5px;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.module-settings-link i {
  font-size: 16px;
}
.module-settings-link:hover,
.module-settings-link:focus-visible {
  color: var(--primary-text);
  background: color-mix(in srgb, var(--primary) 12%, transparent);
}
.breadcrumb-view-control {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* The stay's billing tab (v0.257.4): charged / paid / owed on top, the statement closed below. */
.billing-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}
.billing-stat {
  padding: 12px 14px;
  background: var(--bg);
  border-radius: var(--radius-md);
}
.billing-stat-label {
  margin-bottom: 4px;
  font-size: 12px;
  color: var(--text-secondary);
}
.billing-stat-num {
  font-size: 20px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.billing-stat.is-owed {
  background: var(--danger-bg);
}
.billing-stat.is-owed .billing-stat-num {
  color: var(--danger-text);
}
.billing-stat.is-credit .billing-stat-num {
  color: var(--primary-text);
}
/* A contact's receipt list, „what for" column (BL-017): one line per record a receipt settled. */
.pay-dest {
  min-width: 220px;
  font-size: 13px;
  line-height: 1.45;
}
.pay-dest-line + .pay-dest-line,
.pay-dest-more {
  margin-top: 2px;
}
.pay-dest-more > summary {
  font-size: 12px;
  color: var(--text-secondary);
  cursor: pointer;
}
.billing-rate {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 10px;
}
.billing-rate strong {
  font-size: 18px;
}
details.billing-ledger > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
details.billing-ledger > summary::-webkit-details-marker {
  display: none;
}
.billing-ledger-count {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
}
.billing-ledger-caret {
  margin-left: auto;
  transition: transform 0.15s ease;
}
details.billing-ledger[open] .billing-ledger-caret {
  transform: rotate(180deg);
}
@media (max-width: 600px) {
  .billing-stats { grid-template-columns: minmax(0, 1fr); }
}

/* A settings page whose cards all share one width (v0.257.7): the ward's general settings, its
   alerts and its rooms were three different widths. The cards inside keep their own spacing. */
.settings-narrow {
  max-width: 820px;
}
form.settings-narrow > .card + .card,
.settings-narrow.card {
  margin-top: 16px;
}

/* --- The plan (v0.263.0): `partials/plan-panel`, `partials/pet-plan` ---------------------------
   One line per thing still to be done: the square that ticks it, the words (a button that opens
   the line's dialog), who wrote it, and when it is due on the right. */
.plan-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.plan-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 0;
}
.plan-row + .plan-row { border-top: 1px solid var(--border); }
.plan-tick { flex: none; margin: 0; display: inline-flex; }
span.plan-tick { width: 28px; justify-content: center; padding-top: 4px; color: var(--text-tertiary); }
.plan-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-top: 3px;
}
.plan-body {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
  overflow-wrap: anywhere;
}
span.plan-body { cursor: default; }
button.plan-body:hover { text-decoration: underline; }
.plan-meta { font-size: var(--fs-micro); color: var(--text-secondary); }
.plan-due { flex: none; white-space: nowrap; margin-top: 3px; }
.plan-row.plan-done .plan-body { text-decoration: line-through; color: var(--text-tertiary); }
.plan-history { margin-top: 8px; }
.plan-history > summary {
  cursor: pointer;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}
/* A row of chips, not the column `.form-group` lays its children out in. */
.form-group.plan-presets { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px; }
@media (max-width: 600px) {
  /* The square in one column, the words and the due pill under each other in the other; as a
     wrapping flex row the words shrank to one letter a line beside a long „next week" pill. */
  .plan-row { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 8px; row-gap: 4px; }
  .plan-due { grid-column: 2; justify-self: start; margin-top: 0; }
}
/* The strip under the patient alerts. Not red: the plan is what is next, not a warning; its colour
   is carried by the due pills alone, and by the left edge when something is overdue or due. */
.plan-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 8px;
  padding: 8px 14px;
  margin-bottom: var(--sp-group);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius-md);
  background: var(--bg-panel);
  font-size: 13px;
}
.plan-strip-overdue { border-left-color: var(--danger-text); }
.plan-strip-due { border-left-color: var(--warn-text); }
.plan-strip > .ti { color: var(--text-secondary); }
.plan-strip-items { display: contents; }
.plan-strip-item { display: inline-flex; align-items: center; gap: 5px; }
.plan-strip-item + .plan-strip-item::before { content: "·"; color: var(--text-tertiary); margin-right: 3px; }
.plan-strip .btn-linky { margin-left: auto; font-size: var(--fs-small); }
.plan-strip-chip { margin-bottom: var(--sp-group); }
.plan-strip-chip > .pill { border: none; cursor: pointer; font: inherit; font-size: var(--fs-small); }
/* The ward card's pill (`partials/plan-due-chip`): on the board it follows the name and the care chips. */
.board-group-head > .pill { margin-left: 8px; vertical-align: middle; }

/* `/settings/subscription` (owner, 2026-10-02): one line on what the clinic pays, then tabs. The
   configuration is read as rows (label | value); „Change" turns the same rows into a compact editor -
   plans and levels as chips, modules as a short list - with a bar that appears once something
   differs from what the clinic has. */
.sub-overview { margin-bottom: 16px; }
.sub-overview-main { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 20px; }
.sub-overview-name { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: 800; color: var(--text-primary); }
.sub-overview-amount { font-size: 18px; font-weight: 800; color: var(--primary); }
.sub-overview-paid { margin-inline-start: auto; font-size: var(--fs-small); color: var(--text-secondary); }
.subscription-change-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 12px; padding: 8px 12px;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg); font-size: var(--fs-small);
}
.subscription-change-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }

.sub-config-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
.sub-config-head .card-title { margin: 0; }
.sub-summary { margin: 0; }
.sub-row { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 6px 20px; padding: 12px 0; border-top: 1px solid var(--border); }
.sub-row-label { margin: 0; font-size: var(--fs-small); font-weight: 700; color: var(--text-secondary); }
.sub-row-label small { display: block; margin-top: 2px; font-weight: 400; }
.sub-row-body { margin: 0; min-width: 0; }
.sub-summary-line { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-size: var(--fs-base); color: var(--text-primary); }
.sub-summary-line + .sub-summary-line { margin-top: 4px; }
.sub-summary-price { white-space: nowrap; font-weight: 700; }
.sub-row-total .sub-summary-price { font-size: 18px; color: var(--primary); }
.sub-row-total .sub-summary-line > span { font-size: var(--fs-small); color: var(--text-secondary); }
.sub-detail { margin: 6px 0 0; font-size: var(--fs-small); line-height: 1.5; color: var(--text-secondary); }
.sub-included .ti { color: var(--success-line); }

.sub-meters { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px 18px; margin-top: 10px; }
.sub-meter-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 8px; margin-bottom: 4px; font-size: var(--fs-small); color: var(--text-secondary); }
.sub-meter-head strong { color: var(--text-primary); white-space: nowrap; }
.sub-meter-track { height: 6px; border-radius: 3px; background: var(--bg); border: 1px solid var(--border); overflow: hidden; }
.sub-meter-track > div { height: 100%; background: var(--primary); }
.sub-meter.is-high .sub-meter-track > div { background: var(--warn-text); }
.sub-meter.is-full .sub-meter-track > div { background: var(--danger-text); }
.sub-meter.is-full .sub-meter-head strong { color: var(--danger-text); }

/* A chip is the control; its radio stays in the tab order, only unseen (as on /register). */
.sub-chips, .sub-modules { border: 0; margin: 0; padding: 0; min-width: 0; }
.sub-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.sub-chip, .sub-module {
  position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 4px 12px;
  border: 1px solid var(--btn-line); border-radius: var(--radius-md); background: var(--bg-panel);
  font-size: var(--fs-base); color: var(--text-primary); cursor: pointer; transition: border-color .15s ease, background-color .15s ease;
}
.sub-chip input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
.sub-chip:hover, .sub-module:not(.is-locked):hover { border-color: var(--primary); }
.sub-chip:has(input:checked) { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); background: var(--primary-light); }
.sub-chip:has(input:focus-visible), .sub-module:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
.sub-chip.is-locked { border-style: dashed; border-color: var(--border); background: var(--bg); color: var(--text-secondary); cursor: not-allowed; }
.sub-chip-name { font-weight: 700; }
.sub-chip-price { font-size: var(--fs-small); color: var(--text-secondary); white-space: nowrap; }
.sub-chip-current { font-size: 12px; color: var(--success-line); }
.sub-modules { display: grid; grid-template-columns: repeat(auto-fill, minmax(275px, 1fr)); gap: 6px; }
.sub-needed .ti { color: var(--text-secondary); }
.sub-module input { margin: 0; }
.sub-module .sub-chip-price { margin-inline-start: auto; }
.sub-module:has(input:checked:not(:disabled)) { border-color: var(--primary); background: var(--primary-light); }
.sub-module.is-locked { background: var(--bg); cursor: default; }
.sub-module-name { min-width: 0; overflow-wrap: normal; line-height: 1.3; }
.sub-period { max-width: 460px; margin: 0; }
.sub-period .signup-period-option span { flex-direction: column; gap: 1px; min-height: 46px; }
.sub-period .signup-period-option small { font-size: var(--fs-micro); font-weight: 400; opacity: .85; }

.sub-form[aria-busy="true"] [data-sub-bar] { opacity: .7; }
/* The bar follows the screen while the choices are scrolled. */
.sub-bar:not([hidden]) {
  position: sticky; bottom: 0; z-index: 20; display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 12px 20px;
  margin-top: 8px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border));
  border-radius: var(--radius-lg); background: var(--bg-panel); box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.10);
}
.sub-bar-main { flex: 1 1 360px; min-width: 0; }
.sub-bar-price { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; font-size: var(--fs-base); color: var(--text-secondary); }
.sub-bar-new { color: var(--text-primary); }
.sub-bar-new strong { font-size: 18px; }
.sub-bar-changes { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; padding: 0; }
.sub-bar-changes li { padding: 2px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); font-size: var(--fs-small); color: var(--text-primary); }
.sub-bar-changes li.is-added { border-color: var(--success-line); }
.sub-bar-changes li.is-removed { border-color: var(--danger-line); color: var(--text-secondary); }
.sub-bar-when { display: flex; align-items: flex-start; gap: 6px; margin: 8px 0 0; font-size: var(--fs-small); color: var(--text-secondary); }
.sub-bar-when .ti, .sub-bar-warning > .ti { margin-top: 2px; }
.sub-bar-warning { display: flex; align-items: flex-start; gap: 6px; margin: 8px 0 0; padding: 8px 10px; border-radius: var(--radius-md); font-size: var(--fs-small); line-height: 1.5; }
.sub-bar-warning.is-warn { background: var(--warn-bg, color-mix(in srgb, var(--warn-text) 10%, var(--bg-panel))); color: var(--text-primary); }
.sub-bar-warning.is-danger { background: color-mix(in srgb, var(--danger-text) 10%, var(--bg-panel)); color: var(--danger-text); font-weight: 700; }
.sub-bar-warning ul { margin: 4px 0; padding-inline-start: 18px; }
.sub-bar-ack { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-weight: 700; cursor: pointer; }
.sub-bar-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.subscription-sms-balance { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 10px; margin-bottom: 6px; }
.subscription-sms-balance strong { font-size: 28px; line-height: 1; }
.subscription-sms-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; }
.subscription-sms-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding: 16px; min-width: 0;
  border: 1px solid var(--btn-line); border-radius: var(--radius-md); background: var(--bg-panel); color: var(--text-primary);
}
.subscription-sms-card form { margin-top: 6px; }
@media (max-width: 640px) {
  .sub-row { grid-template-columns: minmax(0, 1fr); }
  .sub-overview-paid { margin-inline-start: 0; flex-basis: 100%; }
  /* On a phone a bar this tall would cover the choices; it ends the form instead. */
  .sub-bar:not([hidden]) { position: static; padding: 12px; box-shadow: none; }
  .sub-bar-actions { width: 100%; }
  .sub-bar-actions .btn-action { flex: 1 1 auto; justify-content: center; }
}
/* The summary's two module rows (owner, 2026-10-02): the same icon and name in both - the plan's own
   in columns, the add-ons as a list with their prices aligned with the other rows'. `--mod-color` is the module's colour (`Modules::COLORS`). */
.sub-plan-modules, .sub-addons { list-style: none; margin: 0; padding: 0; }
.sub-plan-modules { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 2px 16px; }
.sub-plan-modules li { display: flex; align-items: center; gap: 10px; padding: 5px 0; min-width: 0; font-size: var(--fs-base); color: var(--text-primary); }
@media (max-width: 640px) { .sub-plan-modules { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 10px; } }
.sub-addons li { display: flex; align-items: center; gap: 10px; padding: 5px 0; min-width: 0; }
.sub-addons li + li { border-top: 1px dashed var(--border); }
.sub-addon-icon {
  display: inline-grid; place-items: center; flex: none; width: 26px; height: 26px; border-radius: 50%;
  background: color-mix(in srgb, var(--mod-color) 14%, transparent); color: var(--mod-color); font-size: 15px;
}
.sub-addon-name { flex: 1; min-width: 0; font-size: var(--fs-base); color: var(--text-primary); }
.sub-addon-price { flex: none; font-size: var(--fs-base); font-weight: 700; color: var(--text-primary); white-space: nowrap; }
/* What a module is, opened from its row (owner, 2026-10-02, chosen over an info icon): a click on a
   module fills one panel under its list - the plan's modules, the add-ons and the editor's
   checkboxes alike (`subscription.js`). */
.sub-desc-toggle {
  display: flex; align-items: center; gap: 10px; width: 100%; min-width: 0; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: start;
}
button.sub-desc-toggle { cursor: pointer; }
button.sub-desc-toggle:hover .sub-addon-name { color: var(--primary); }
button.sub-desc-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }
.sub-desc-chevron { flex: none; font-size: 15px; color: var(--text-secondary); transition: transform .15s ease; }
.sub-desc-toggle[aria-expanded="true"] .sub-desc-chevron { transform: rotate(180deg); color: var(--primary); }
.sub-desc-toggle[aria-expanded="true"] .sub-addon-name { color: var(--primary); }
.sub-plan-modules li { padding: 0; }
.sub-plan-modules .sub-desc-toggle { padding: 5px 0; }
.sub-plan-modules .sub-addon-name { flex: 0 1 auto; }
.sub-addons li { flex-direction: column; align-items: stretch; gap: 0; }
.sub-addon-row { padding: 0; }
/* The chevron follows the name, as in the plan's modules; the price keeps to the right edge. */
.sub-addon-row .sub-addon-name { flex: 0 1 auto; }
.sub-addon-row .sub-addon-price { margin-inline-start: auto; }
.sub-desc-panel p { margin: 0; font-size: var(--fs-small); line-height: 1.55; color: var(--text-secondary); }
.sub-desc-panel { margin-top: 8px; padding: 10px 12px; border-radius: var(--radius-md); background: var(--bg); border: 1px solid var(--border); }
.sub-desc-panel strong { display: block; margin-bottom: 2px; font-size: var(--fs-small); color: var(--text-primary); }
.sub-module-more { width: auto; flex: none; padding: 2px; border-radius: var(--radius-sm); }
.sub-module-more:hover .sub-desc-chevron { color: var(--primary); }
/* The „better than the plan's own" mark beside a raised capacity level (owner, 2026-10-02). Its note
   opens upward, from the mark's start, so it neither lengthens the page nor runs off a phone's edge. */
.sub-better {
  position: relative; display: inline-block; margin-inline-start: 6px; border-radius: var(--radius-sm);
  color: var(--success-text); font-size: 17px; line-height: 1; vertical-align: -2px; cursor: help;
}
.sub-better:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.sub-better::after {
  content: attr(data-tip); display: none; position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 30;
  width: max-content; max-width: min(280px, calc(100vw - 32px)); padding: 6px 10px;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-panel);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14); font-size: var(--fs-small); font-weight: 400; line-height: 1.4;
  color: var(--text-primary); white-space: normal;
}
.sub-better:hover::after, .sub-better:focus::after { display: block; }
.sub-meter-icon { margin-inline-end: 2px; font-size: 14px; vertical-align: -2px; }
/* The „current" mark on a plan, level or module chip: a dot, since the icon font has no filled point. */
.sub-chip-current { display: inline-block; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--success-line); }
/* Yearly: what it saves against twelve months (owner, 2026-10-02). */
.sub-period .sub-saving { font-weight: 700; color: var(--success-text); opacity: 1; }
.sub-period .signup-period-option:has(input:checked) .sub-saving { color: inherit; }
/* The billing tab (owner, 2026-10-02): what is due, how to pay it, the account as a statement. */
.sub-due { display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 10px 24px; padding-bottom: 16px; border-bottom: 1px solid var(--border); }
.sub-due-amount { font-size: 28px; font-weight: 800; line-height: 1.2; color: var(--text-primary); }
.sub-due.is-overdue .sub-due-amount { color: var(--danger-text); }
.sub-due-amount.is-clear { display: flex; align-items: center; gap: 8px; font-size: 20px; color: var(--success-text); }
.sub-due-when { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; margin-top: 4px; font-size: var(--fs-small); color: var(--text-secondary); }
.sub-due-paid { font-size: var(--fs-small); color: var(--text-secondary); }
.sub-pay, .sub-statement { padding-top: 16px; }
.sub-pay { padding-bottom: 16px; border-bottom: 1px solid var(--border); }
.sub-pay h3, .sub-statement h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 10px; font-size: var(--fs-title); font-weight: 700; color: var(--text-primary); }
.sub-pay-fields { display: grid; gap: 6px; margin: 0 0 8px; max-width: 640px; }
.sub-pay-fields > div { display: grid; grid-template-columns: 170px minmax(0, 1fr); align-items: center; gap: 4px 14px; }
.sub-pay-fields dt { font-size: var(--fs-small); font-weight: 700; color: var(--text-secondary); }
.sub-pay-fields dd { margin: 0; min-width: 0; color: var(--text-primary); }
.sub-pay-copy { display: flex; gap: 6px; }
.sub-pay-copy input { flex: 1; min-width: 0; font-family: var(--font-mono, ui-monospace, monospace); font-weight: 700; }
.sub-statement tr.is-void td { color: var(--text-secondary); }
.sub-statement tr.is-void td.num { text-decoration: line-through; }
.sub-pay-in { color: var(--success-text); }
@media (max-width: 640px) {
  .sub-pay-fields > div { grid-template-columns: minmax(0, 1fr); }
}
/* On a phone the statement's five columns become one card per row: date and amount, the
   description under them, the balance last (owner, 2026-10-02). */
@media (max-width: 640px) {
  .sub-statement-wrap { border: 0; overflow: visible; }
  .sub-statement-wrap table, .sub-statement-wrap tbody { display: block; }
  .sub-statement-wrap thead { display: none; }
  .sub-statement-wrap tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px;
    padding: 10px 2px; border-bottom: 1px solid var(--border); background: none !important;
  }
  .sub-statement-wrap td, .sub-statement-wrap .tbl td { display: block; padding: 0; border: 0 !important; background: none !important; }
  .sub-statement-wrap td:nth-child(1) { grid-column: 1; grid-row: 1; font-size: var(--fs-small); color: var(--text-secondary); }
  .sub-statement-wrap td:nth-child(2) { grid-column: 1 / -1; grid-row: 2; }
  .sub-statement-wrap td:nth-child(3), .sub-statement-wrap td:nth-child(4) { grid-column: 2; grid-row: 1; font-weight: 700; }
  .sub-statement-wrap td:empty { display: none; }
  .sub-statement-wrap td.sub-statement-balance { grid-column: 1 / -1; grid-row: 3; font-size: var(--fs-small); color: var(--text-secondary); }
  .sub-statement-wrap td.sub-statement-balance::before { content: attr(data-label) ": "; }
}

/* The finance statement's filters (v0.284.1): on a phone the fields fold behind one button that
   counts what is set; the period links stay visible. The checkbox is the state, so the panel
   opens with or without the script (`data-toggle-class` mirrors it as `.is-open`). */
.statement-filters-toggle { display: none; }
@media (max-width: 700px) {
  .statement-filters-toggle { display: inline-flex; align-items: center; gap: 6px; margin-top: 10px; }
  .statement-filters-toggle .pill { padding: 0 7px; }
  .statement-filters:not(.is-open) > #statement-filters-toggle:not(:checked) ~ .statement-filters-body { display: none; }
  #statement-filters-toggle:checked + .statement-filters-toggle .menu-caret { transform: rotate(180deg); }
  #statement-filters-toggle:focus-visible + .statement-filters-toggle { outline: 2px solid var(--primary); outline-offset: 2px; }
}

/* The print-font choice on /settings/profile (v0.285.0): each option's sample in its own face, the
   same stacks print.css uses for `data-print-font`. */
.font-sample { font-size: 14px; color: var(--text-primary); margin-top: 2px; }
.font-sample-dejavu { font-family: 'DejaVu Sans', 'Noto Sans Georgian', Tahoma, Arial, sans-serif; }
.font-sample-noto { font-family: 'Noto Sans Georgian', 'DejaVu Sans', Tahoma, Arial, sans-serif; }

/* ══ Patient context: what the animal had before this episode (v0.288.0) ══════════════════════
   One card under the patient alerts on the stay, visit, surgery and lab pages
   (.docs/patient-context-spec.md). Lists, not tables: it is read at a glance, and every row
   links to the record that holds the detail. */
.pctx { padding-top: 12px; padding-bottom: 12px; }
.pctx-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; flex-wrap: wrap;
  cursor: pointer; list-style: none; font-weight: 700;
}
.pctx-head::-webkit-details-marker { display: none; }
.pctx-title { display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.pctx-title > i { color: var(--primary); }
.pctx-window { font-size: 11.5px; font-weight: 400; color: var(--text-secondary); }
.pctx-all, .pctx-more { font-size: 12px; font-weight: 600; }
.pctx[open] > .pctx-head { margin-bottom: 10px; }
.pctx-protect {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px 16px;
  padding: 10px 12px; margin-bottom: 10px; background: var(--bg); border-radius: var(--radius-md);
}
.pctx-protect-item { display: flex; gap: 10px; align-items: center; min-width: 0; }
.pctx-protect-item > i { flex: none; width: 22px; font-size: 18px; text-align: center; color: var(--primary); }
.pctx-section + .pctx-section, .pctx-protect + .pctx-section { margin-top: 10px; }
.pctx-label { font-size: 11.5px; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; }
.pctx-list { list-style: none; margin: 0; padding: 0; }
.pctx-list > li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px;
  padding: 5px 0; border-top: 1px solid var(--rule); font-size: 13px;
}
.pctx-list > li:first-child { border-top: 0; }
.pctx-when { font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
.pctx-type { font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
.pctx-detail { flex-basis: 100%; font-size: 11.5px; color: var(--text-secondary); }
.pctx-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pctx-chip {
  display: inline-flex; flex-direction: column; gap: 1px; padding: 5px 9px;
  border: 1px solid var(--rule); border-radius: var(--radius-md); font-size: 12.5px;
}
.pctx-flag { display: inline-block; margin-right: 8px; font-weight: 600; }
.pctx-flag-high, .pctx-flag-critical_high, .pctx-flag-low, .pctx-flag-critical_low, .pctx-flag-positive { color: var(--danger-text); }
.pctx-name { min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pctx-col > .pctx-section:first-child { margin-top: 0; }
@media (min-width: 1000px) {
  .pctx-cols.has-two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 28px; }
}
@media (max-width: 999px) {
  .pctx-col + .pctx-col { margin-top: 10px; }
}
.pctx-counts { flex-basis: 100%; font-size: 11.5px; font-weight: 400; color: var(--text-secondary); }
.pctx[open] .pctx-counts { display: none; }
.pctx:not([open]) .pctx-all { display: none; }
@media (max-width: 600px) {
  .pctx-chips { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pctx-chip { min-width: 0; overflow-wrap: anywhere; }
  .pctx-chip .pctx-when { white-space: normal; }
}

/* A tab's empty cards, folded into one line by fold-empty.js (v0.288.1, shared v0.288.2). */
.fold-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.fold-strip-lead { font-size: 12px; color: var(--text-secondary); margin-right: 4px; }
/* An opened card stops below the sticky header, as the medical tab's jumps do. */
.tab-panel[data-fold-empty] .card { scroll-margin-top: 80px; }

/* The glucose curve (views/partials/glucose-curve.php, assets/js/glucose-curve.js). The legend is
   HTML rather than Chart.js's own: it has to explain two point shapes on one line and a band, and
   the canvas legend can only list datasets. Marks take the same tokens the script draws with. */
.gc-head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
.gc-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.gc-seg > button { min-height: 30px; padding: 0 10px; font-size: var(--fs-small); }
.gc-summary { display: flex; flex-wrap: wrap; gap: 6px 22px; margin: 2px 0 8px; font-size: var(--fs-small); }
.gc-summary:empty { display: none; }
.gc-stat { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 0 6px; }
.gc-stat b { font-size: var(--fs-base); color: var(--text-primary); font-variant-numeric: tabular-nums; white-space: nowrap; }
.gc-muted { color: var(--text-secondary); font-weight: 400; }
.gc-legend { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0 0 6px; padding: 0; list-style: none; font-size: var(--fs-small); color: var(--text-secondary); }
.gc-legend li { display: inline-flex; align-items: center; gap: 5px; }
.gc-key { display: inline-block; flex: none; }
.gc-key-line { width: 16px; height: 2px; background: var(--primary); }
.gc-key-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }
.gc-key-diamond { width: 7px; height: 7px; background: var(--primary); transform: rotate(45deg); }
.gc-key-dose { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 10px solid var(--special-text); }
.gc-key-band { width: 16px; height: 10px; background: var(--success-bg); border: 1px solid var(--success-line); border-radius: 2px; }
.gc-key-hypo { width: 16px; height: 0; border-top: 2px dashed var(--danger-text); }
.gc-plot { position: relative; height: 300px; }
.gc-empty { margin: 8px 0 0; font-size: var(--fs-small); color: var(--text-secondary); }
.gc-undated { margin: 8px 0 0; font-size: var(--fs-small); color: var(--text-secondary); }
.gc-table { margin-top: 10px; }
.gc-table > summary { cursor: pointer; font-size: var(--fs-small); color: var(--text-secondary); }
.gc-table > .table-wrap { margin-top: 8px; }
@media (max-width: 600px) {
  .gc-plot { height: 240px; }
  .gc-tools { width: 100%; }
}

/* The lab range rail on screen (views/partials/lab_range_rail.php). Only the positions are
   inline; print draws its own inline, ink-safe copy, email a row of table cells. */
.lab-rail { position: relative; height: 14px; min-width: 70px; }
.lab-rail > span { position: absolute; }
.lab-rail-track { top: 6px; left: 0; right: 0; height: 2px; background: var(--border); border-radius: 1px; }
.lab-rail-band { top: 5px; height: 4px; background: var(--primary); opacity: .35; border-radius: 2px; }
.lab-rail-ghost { top: 4px; width: 6px; height: 6px; margin-left: -3px; border-radius: 50%; background: var(--bg-panel); border: 1.5px solid var(--text-tertiary); }
.lab-rail-mark { top: 2px; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: var(--primary); border: 2px solid var(--bg-panel); box-shadow: 0 0 0 1px var(--primary); }
.lab-rail-mark.is-out { background: var(--danger-text); box-shadow: 0 0 0 1px var(--danger-text); }
/* The lab card's out-of-range rows (views/partials/lab-panel.php), one per abnormal value. */
.tbl td.lab-panel-ab { padding-left: 18px; font-size: 12.5px; }
.lab-panel-ab-value { margin-left: 6px; }
.lab-panel-ab-range { color: var(--text-tertiary); font-size: 11px; margin-left: 6px; }
.tbl td.lab-panel-ab-rail { width: 110px; }
.tbl td.lab-panel-more { font-size: 11.5px; color: var(--text-tertiary); }
/* The treatment form's „agents used" rows. A grid item's minimum is its content's unless it says
   otherwise, and two inputs side by side are ~410px of content: on a phone the single-column form
   grew past the dialog and scrolled sideways (found 2026-10-09). */
.form-grid > .agent-rows-block { min-width: 0; }
/* A legal document's text (/legal/{code}, the acceptance page, /admin/legal versions). Long
   paragraphs at the browser's default spacing were hard to read (owner, 2026-10-11). */
.legal-body { line-height: 1.7; }
.legal-body h2, .legal-body h3 { line-height: 1.35; }
.legal-body li + li { margin-top: 6px; }
.legal-body td, .legal-body th { line-height: 1.5; }
/* MarkdownLite writes a plain <a>, and `a` is unstyled in this app, so a link in a legal text read
   as ordinary words (owner, 2026-10-11). It looks like `.link`. */
.legal-body a { color: var(--primary-text); text-decoration: underline; text-underline-offset: 2px; }
.legal-body a:hover, .legal-body a:focus-visible { text-decoration-thickness: 2px; }
/* The fingerprint and the two links under a legal document (views/legal/show.php). */
.legal-foot { line-height: 1.9; }

/* The dose form's check of one presentation (prophylaxis-form.js, v0.297.0): the dose for the
   weight entered, then what stands against it. Text only, drawn by the script. */
.med-check { border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; font-size: 12px; display: grid; gap: 4px; }
.med-check[hidden] { display: none; }
.med-check-dose { font-weight: 600; font-size: 13px; }
.med-check-danger { color: var(--danger-text); background: var(--danger-bg); padding: 4px 6px; border-radius: 4px; }
.med-check-warning { color: var(--warn-text); background: var(--warn-bg); padding: 4px 6px; border-radius: 4px; }
.med-check-info, .med-check-note { color: var(--text-secondary); }
.med-check-danger, .med-check-warning { white-space: pre-line; }

/* One compact control strip per source-aware dosage table. The product page may render several
   instances, so the layout and the script are scoped to each data-clinical-dose-list wrapper. */
.clinical-dose-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.clinical-dose-filters > * { min-width: 0; }
.clinical-dose-filters > [data-dose-search] { flex: 1 1 190px; width: auto; }
.clinical-dose-results { white-space: nowrap; }
.clinical-dose-species[role="group"] { flex: none; max-width: 100%; }
.clinical-dose-species.filter-seg > button {
  width: 38px;
  min-width: 38px;
  padding-left: 0;
  padding-right: 0;
  justify-content: center;
}
.clinical-dose-species .ti { font-size: 17px; }
.clinical-dose-species.filter-seg > button:disabled {
  color: var(--text-tertiary);
  background: var(--bg);
  cursor: not-allowed;
  opacity: .48;
}
.clinical-dose-more { flex: 0 1 240px; min-width: 0; }
.clinical-dose-more-trigger { width: 100%; max-width: 240px; }
.clinical-dose-more-trigger [data-dose-more-label] {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.clinical-dose-more-trigger.is-active { border-color: var(--action-bg); color: var(--primary-text); }
.clinical-dose-filter-menu {
  width: min(320px, calc(100vw - 16px));
  padding: 12px;
}
.clinical-dose-filter-menu label {
  display: block;
  width: auto;
  padding: 0;
  margin: 0 0 5px;
  color: var(--text-secondary);
  font-size: var(--fs-small);
  white-space: normal;
  cursor: default;
}
.clinical-dose-filter-menu label:hover { background: none; }
.clinical-dose-filter-menu label:focus-within { outline: none; }
.clinical-dose-filter-menu .field-control { width: 100%; }
.clinical-dose-filter-menu .field-control + label { margin-top: 12px; }
.clinical-dose-note { margin-top: 5px; }
.clinical-dose-note > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  list-style: none;
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  background: var(--bg-panel);
  cursor: pointer;
}
.clinical-dose-note > summary::-webkit-details-marker { display: none; }
.clinical-dose-note > summary:hover,
.clinical-dose-note > summary:focus-visible,
.clinical-dose-note[open] > summary {
  color: var(--primary-text);
  border-color: var(--action-bg);
}
.clinical-dose-note > summary .ti { font-size: 16px; }
.clinical-dose-note-body {
  max-height: 180px;
  overflow-y: auto;
  margin-top: 6px;
  padding: 8px 10px;
  border-left: 2px solid var(--rule);
  color: var(--text-secondary);
  font-size: 11px;
  line-height: 1.5;
}
.clinical-dose-note-item + .clinical-dose-note-item { margin-top: 8px; }
.clinical-dose-note-item > strong { display: block; margin-bottom: 2px; color: var(--text-primary); }

/* Ingredient guidance is a text accordion, not the 26px note icon used inside dose tables. */
.clinical-dose-guidance > summary {
  display: flex;
  width: 100%;
  height: auto;
  min-height: 40px;
  justify-content: flex-start;
  gap: 10px;
  padding: 9px 11px;
  text-align: left;
}
.clinical-dose-guidance-title {
  min-width: 0;
  flex: 1 1 auto;
  overflow-wrap: anywhere;
}
.clinical-dose-guidance-caret {
  flex: none;
  margin-left: auto;
  transition: transform .16s ease;
}
.clinical-dose-guidance[open] .clinical-dose-guidance-caret { transform: rotate(180deg); }

/* One calculator surface shared by ingredient and product detail pages. */
.inline-dose-calculator {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.inline-dose-calculator-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 10px;
  font-weight: 700;
}
.inline-dose-calculator > form {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 10px;
}
.inline-dose-calculator input[name="weight_kg"] { width: 110px; }
.inline-dose-calculator-variants { display: contents; }
.inline-dose-calculator-hint {
  margin-top: 8px;
  color: var(--text-secondary);
  font-size: var(--fs-micro);
}
.inline-dose-calculator-output { margin-top: 14px; }
.inline-dose-calculator-unavailable {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--text-secondary) 5%, var(--bg-panel));
  color: var(--text-secondary);
  font-size: var(--fs-small);
  line-height: 1.45;
}
.inline-dose-calculator-unavailable .ti { flex: none; margin-top: 2px; }
.inline-dose-calculator + [data-clinical-dose-list] {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* Medication catalogue editor: formulation metadata and product-dose rules have enough fields
   to need explicit tracks. Flex wrapping made the route legend collide with the first row, while
   the six-column dose table compressed notes into a vertical strip on phones. */
.medication-formulation-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.medication-routes-fieldset { grid-column: 1 / -1; min-width: 0; }
.product-dose-editor-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
.product-dose-field-species,
.product-dose-field-method { grid-column: span 2; }
.product-dose-editor-grid select,
.product-dose-editor-grid input { width: 100%; min-width: 0; }

@media (max-width: 680px) {
  .medication-formulation-grid { grid-template-columns: minmax(0, 1fr); }
  .medication-routes-fieldset { grid-column: auto; }
  .product-dose-editor-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .product-dose-field-species,
  .product-dose-field-method,
  .product-dose-field-frequency { grid-column: 1 / -1; }

  .product-dose-editor-table,
  .product-dose-editor-table tbody,
  .product-dose-editor-table tr,
  .product-dose-editor-table td { display: block; width: 100%; }
  .product-dose-editor-table thead { display: none; }
  .product-dose-editor-table tbody { padding: 0; }
  .product-dose-editor-table tbody tr {
    padding: 9px 10px;
    border-bottom: 1px solid var(--border);
  }
  .product-dose-editor-table tbody tr:last-child { border-bottom: 0; }
  .product-dose-editor-table tbody td {
    display: grid;
    grid-template-columns: minmax(88px, 34%) minmax(0, 1fr);
    gap: 8px;
    padding: 4px 0;
    border: 0;
    text-align: left;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  .product-dose-editor-table tbody td::before {
    content: attr(data-label);
    color: var(--text-secondary);
    font-size: 11px;
    font-weight: 600;
  }
  .product-dose-editor-table .row-actions { justify-content: flex-start; }
}
.clinical-dose-table [data-dose-filter-empty] td { position: static; }

@media (max-width: 700px) {
  .clinical-dose-filters {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 36px;
  }
  .clinical-dose-more { width: 100%; max-width: none; }
  .clinical-dose-more-trigger { max-width: none; }
  .clinical-dose-filters > [data-dose-search] { grid-column: 1 / -1; width: 100%; }
  .clinical-dose-results { grid-column: 1 / 3; }
}

/* Inline (i) detail beside a field value: a <details> whose body floats to the right of the icon, so
   the next line's (i) stays clickable; info-pop.js closes it on Esc, outside click or leaving (v0.302.3). */
.info-pop { display: inline-block; position: relative; vertical-align: middle; margin-left: 4px; }
.info-pop > summary {
    list-style: none; cursor: pointer; color: var(--text-secondary); font-weight: normal;
    /* the icon keeps one size wherever it sits (inside 11 px source lines it shrank to 11 px), and the
       hit area is a comfortable square around it */
    display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%;
}
.info-pop > summary .ti { font-size: 17px; line-height: 1; }
.info-pop > summary:hover, .info-pop[open] > summary { color: var(--primary); background: color-mix(in srgb, var(--primary) 14%, transparent); }
.info-pop > summary::-webkit-details-marker { display: none; }
.info-pop[open] > summary { color: var(--primary); }
.info-pop-body {
    position: absolute; z-index: 30; top: -8px; left: calc(100% + 8px);
    width: min(340px, calc(100vw - 48px)); display: grid; gap: 6px; padding: 10px 12px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-md);
    box-shadow: 0 6px 18px rgba(0, 0, 0, .14); font-size: var(--fs-small); font-weight: normal;
    color: var(--text-primary); overflow-wrap: anywhere; text-align: left; white-space: normal;
    max-height: min(60vh, 420px); overflow-y: auto; line-height: 1.5;
}
@media (max-width: 600px) {
    .info-pop-body { position: fixed; top: auto; bottom: 16px; left: 16px; right: 16px; width: auto; transform: none; }
}

/* Product card status block (v0.302.3): registration per market and legal control, one line each,
   label column + answers that wrap as whole items (pill and its (i) never split). */
.fact-status {
    display: grid; grid-template-columns: max-content 1fr; gap: 8px 16px; align-items: center;
    margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); font-size: 13px;
}
.fact-status-items { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; font-weight: 600; }
.fact-status-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
@media (max-width: 600px) { .fact-status { grid-template-columns: 1fr; gap: 2px 0; } .fact-status-items { margin-bottom: 8px; } }

/* Product card: a composition of more than two parts as a full-width list in columns (v0.302.3). */
.composition-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 0 24px; font-weight: 600; margin-top: 2px; }
.composition-item { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 3px 0; border-bottom: 1px dotted var(--border); }
.composition-dose { white-space: nowrap; color: var(--text-secondary); }

/* „Photo or file" (views/partials/file-pick.php, v1.10.11): the chosen files under the button. */
/* The hidden input that is posted sits under the button, where a „choose a file" bubble points (v1.10.12). */
.file-pick {
  position: relative;
}

/* Uploads that send on pick (a study's „+ File", a thumbnail's „Replace", v1.10.13): a phone
   gets „photo / camera" and „file" items, a mouse the one picker it always had. Only ever
   hides, so each element keeps its own display where it shows. */
@media not all and (pointer: coarse) {
  .pick-coarse {
    display: none !important;
  }
}

@media (pointer: coarse) {
  .pick-fine {
    display: none !important;
  }
}

.file-pick-list {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
}

.file-pick-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: var(--fs-base);
}

.file-pick-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* =========================================================
   LOADER (v1.11.9, owner's pick 2026-10-20: the „changed" tiles)
   ---------------------------------------------------------
   Four rounded tiles in a 2x2 grid. They rest small and soft, and light one at a time,
   clockwise from the top left: each pops to full size in the brand blue, the last one
   (bottom left) in the brand red. One cycle is 1.2 s, a tile every 0.3 s.

   Markup: views/partials/loader.php, or vetmanLoader.html() / create() / button() in
   assets/js/loader.js. The wrapper is the `role="status"`; the tiles are aria-hidden.

   It fades in only 300 ms after it is put on the page (`vm-loader-in`, a zero-length
   animation held back by its delay), so a wait that ends sooner never flashes it and no
   script has to run a timer for that. The brand colours are fixed, not the theme's
   --primary: the tiles are the mark, and they read the same in every theme; only the
   resting tile follows the ground, so it is a soft step off a dark panel too.
   ========================================================= */
:root {
  --loader-soft: #C7D7EC;
  --loader-on: #4872AB;
  --loader-last: #B5493F;
}
html[data-theme="dark"] { --loader-soft: #34465F; }
html[data-theme="terminal"] { --loader-soft: #3B3426; }

.vm-loader {
  --loader-tile: 24px;
  --loader-gap: 6px;
  --loader-radius: 7px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  vertical-align: middle;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  line-height: 1.3;
  animation: vm-loader-in 0s linear 0.3s both;
}
.vm-loader-sm {
  --loader-tile: 7px;
  --loader-gap: 2px;
  --loader-radius: 2px;
  gap: 6px;
}
.vm-loader-tiles {
  display: inline-grid;
  grid-template-columns: repeat(2, var(--loader-tile));
  grid-auto-rows: var(--loader-tile);
  gap: var(--loader-gap);
  flex: none;
}
.vm-loader-tiles > span {
  --loader-lit: var(--loader-on);
  border-radius: var(--loader-radius);
  background: var(--loader-soft);
  transform: scale(0.78);
  animation: vm-loader-tile 1.2s ease-in-out infinite;
}
/* The grid fills top left, top right, bottom left, bottom right; clockwise is 1, 2, 4, 3. */
.vm-loader-tiles > span:nth-child(2) { animation-delay: 0.3s; }
.vm-loader-tiles > span:nth-child(4) { animation-delay: 0.6s; }
.vm-loader-tiles > span:nth-child(3) { animation-delay: 0.9s; --loader-lit: var(--loader-last); }
@keyframes vm-loader-tile {
  0%, 46%, 100% { transform: scale(0.78); background: var(--loader-soft); }
  12%, 28% { transform: scale(1); background: var(--loader-lit); }
}
@keyframes vm-loader-in {
  from { opacity: 0; visibility: hidden; }
  to { opacity: 1; visibility: visible; }
}
/* A block of its own, centred in the space it waits for (a panel, a list). */
.vm-loader-block {
  display: flex;
  justify-content: center;
  padding: var(--sp-group) 0;
}
/* Over a box whose content is still coming (vetmanLoader.over): centred, letting clicks through. */
.vm-loader-host { position: relative; }
.vm-loader-over {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
}
/* The upload panel's own: no tiles once it is reporting a failure. */
.upload-progress.is-error .upload-progress-loader { display: none; }
/* In a button: the button's own size and ink for the words, the tiles in place of its icon.
   On the one filled appearance the blue tile would vanish into the fill, so there the tiles
   are drawn in the button's ink instead. */
.btn .vm-loader { color: inherit; font-size: inherit; }
.btn-action .vm-loader {
  --loader-soft: color-mix(in srgb, var(--on-action) 38%, transparent);
  --loader-on: var(--on-action);
  --loader-last: var(--on-action);
}
@media (prefers-reduced-motion: reduce) {
  /* Standing still: the mark itself, three blue tiles and the red one. */
  .vm-loader-tiles > span { animation: none; transform: none; background: var(--loader-on); }
  .vm-loader-tiles > span:nth-child(3) { background: var(--loader-last); }
}
