﻿/*
 * AlignerManager unified - application styles.
 *
 * Sits on top of Bootstrap 5.3 and only adds what Bootstrap does not provide.
 * Everything theme-dependent uses Bootstrap's own CSS variables or a
 * [data-bs-theme="dark"] override, so light and dark stay in step.
 *
 * Typography: Noto Sans for the interface, Roboto Serif Bold for headings.
 * Both are self-hosted in /assets/webfonts - the CSP forbids CDN fonts, and
 * self-hosting also means the app renders correctly offline.
 */

/* -- fonts ---------------------------------------------------------- */

/* Noto Sans, the interface face. Replaced Prompt on 2026-08-05.
 *
 * WHY. Prompt is a geometric display face (Cadson Demak, designed Thai-first)
 * and this app gave it the one job it is worst at: headings already belong to
 * Roboto Serif and case ids to IBM Plex Mono, so Prompt only ever rendered
 * table cells, labels and body text - dense small-size material, where a
 * condensed face with closed apertures is weakest. Noto Sans is humanist,
 * wider, with open counters, and its founding purpose is complete and
 * correctly positioned diacritics. The data here is Slovak and Hungarian
 * names: ľ ĺ ď ť ň ô ŕ ž š č, ő ű.
 *
 * ⚠ ONE VARIABLE FILE PER SUBSET, hence font-weight: 100 900.
 *
 * Asking Google for wght@400;600;700 returns SIX files that are byte-identical
 * in pairs - Noto Sans is a variable font and the discrete-weight stylesheet
 * points three font-faces at the same file. That would have shipped 611 KB and
 * three cache entries for 204 KB of content. The variable stylesheet gives one
 * file per subset and the browser interpolates every weight from it, including
 * 500 and 600 - which Prompt never had at all, so Bootstrap's fw-semibold was
 * being faked by the browser or rounded up to 700.
 *
 * ⚠ latin-ext is NOT optional, for the same reason as Roboto Serif below.
 * The unicode-range values are Google's own, so only the subset a page needs
 * is fetched - and in this app latin-ext is fetched on nearly every page,
 * because the patient names need it.
 *
 * ⚠ The prompt-v4-* files are deliberately LEFT IN PLACE. Reverting is this
 * block and one line of --bs-body-font-family; deleting them first would make
 * a revert a download.
 */

@font-face {
  font-family: 'Noto Sans';
  src: url('/assets/webfonts/noto-sans-latin-ext.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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';
  src: url('/assets/webfonts/noto-sans-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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;
}

/* Roboto Serif Bold, in two subsets.
 *
 * latin-ext is NOT optional here: Slovak needs č š ž ť ľ ý ô ä ň ď and
 * Hungarian needs ő ű, none of which are in the basic latin subset. Without it
 * headings would silently drop to the serif fallback the moment an accented
 * character appeared - or render tofu.
 *
 * The unicode-range values are Google's own, so the browser downloads only the
 * subset a page actually needs. Both files are served from this domain; nothing
 * is fetched from Google at runtime. */

@font-face {
  font-family: 'Roboto Serif';
  src: url('/assets/webfonts/roboto-serif-700-latin-ext.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  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: 'Roboto Serif';
  src: url('/assets/webfonts/roboto-serif-700-latin.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  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;
}

/* IBM Plex Mono, for case and account codes in tables.
 *
 * Chosen over JetBrains Mono, which was tried first and is too narrow to read
 * comfortably at a glance - these are codes people check character by
 * character, so the letterforms need room.
 *
 * Same two subsets and the same unicode-range values as Roboto Serif above,
 * and the same reason for taking them by NAME: Google's stylesheet lists
 * cyrillic-ext first, so "the first woff2 URL" is a valid font file with no
 * Latin glyphs in it. Case ids are ASCII, so in practice only the latin file
 * is ever fetched - latin-ext is insurance. */

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/assets/webfonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  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: 'IBM Plex Mono';
  src: url('/assets/webfonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  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;
}

:root {
  --am-brand: #2563eb;
  --am-brand-2: #7c3aed;
  --am-radius: .75rem;

  /* Swapped from 'Prompt' on 2026-08-05 - see the @font-face note above.
     This is the ONLY line outside that block that named the interface face. */
  --bs-body-font-family: 'Noto Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --am-display: 'Roboto Serif', Georgia, 'Times New Roman', serif;
  --am-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

body {
  font-family: var(--bs-body-font-family);
  -webkit-font-smoothing: antialiased;
}

/* Headings and anything that should feel like a title. */
h1, h2, h3, .display-font, .navbar-brand, .stat-value {
  font-family: var(--am-display);
  font-weight: 700;
  letter-spacing: -.01em;
}

/* -- page background ------------------------------------------------ */

body {
  background-image: url('/assets/img/bg-light.svg');
  background-size: cover;
  background-position: center top;
  background-attachment: fixed;
  background-repeat: no-repeat;
  min-height: 100vh;
}

[data-bs-theme="dark"] body {
  background-image: url('/assets/img/bg-dark.svg');
}

/* -- brand ---------------------------------------------------------- */

.brand-mark {
  width: 2rem;
  height: 2rem;
  flex: none;
  display: block;
}

.brand-mark-lg { width: 3.5rem; height: 3.5rem; }

.navbar-brand { font-size: 1.05rem; }

/* -- surfaces ------------------------------------------------------
   Cards sit on a patterned background, so they get a slight translucency
   and a blur - it keeps the curves visible without hurting legibility. */

/* Each translucent surface declares a SOLID colour first. A browser without
   color-mix() ignores the second declaration and keeps the solid one - without
   the fallback those panels would render fully transparent, which looks broken
   rather than merely plain. */

.card {
  border-radius: var(--am-radius);
  background-color: var(--bs-body-bg);
  background-color: color-mix(in srgb, var(--bs-body-bg) 92%, transparent);
  backdrop-filter: blur(6px);
}

.navbar {
  background-color: var(--bs-body-bg) !important;
  background-color: color-mix(in srgb, var(--bs-body-bg) 88%, transparent) !important;
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--bs-border-color);
}

/* -- navbar links --------------------------------------------------- */

.navbar .nav-link {
  font-size: .925rem;
  padding-inline: .85rem;
  border-radius: .5rem;
  color: var(--bs-secondary-color);
}

.navbar .nav-link:hover { background: var(--bs-tertiary-bg); }

.navbar .nav-link.active {
  color: var(--am-brand);
  background: rgba(37, 99, 235, .10);
  background: color-mix(in srgb, var(--am-brand) 10%, transparent);
  font-weight: 600;
}

[data-bs-theme="dark"] .navbar .nav-link.active { color: #93b4fd; }

.theme-toggle {
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  border-radius: .5rem;
  border: 1px solid var(--bs-border-color);
  background: transparent;
  color: var(--bs-secondary-color);
}

.theme-toggle:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }

/* Role chips in the navbar. Bootstrap's text-bg-light is a light-only utility
   and would stay pale in dark mode, so these follow the theme variables. */
.role-badge {
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  border: 1px solid var(--bs-border-color);
  font-weight: 500;
}

/* The card header sits directly above a table; keep it visually part of the
   same block rather than a brighter strip.
   ⚠ Vertically NARROW on purpose (Daniel, 2026-08-06: "more vertically
   narrower heads on cards, less top bottom padding/margin around the card
   title") - the title is a label, not a banner. */
.card-header {
  background: var(--bs-tertiary-bg);
  border-bottom-color: var(--bs-border-color);
  padding-top: .35rem;
  padding-bottom: .35rem;
  /* A label, not a headline (Daniel, 2026-08-06): uppercase, normal weight. */
  text-transform: uppercase;
  font-weight: 400;
}

.card-header h1, .card-header h2, .card-header h3,
.card-header .h5, .card-header .h6,
.card-header .section-title,
.card-header .fw-semibold,
.card-header a {
  margin-bottom: 0;
  font-weight: 400 !important;
}

/* The ⋯ menu opens INSIDE the header, so without this it inherited the
   header's uppercase (Daniel, 2026-08-06: never uppercase dropdown items). */
.card-header .dropdown-menu { text-transform: none; }

/* Icons in dropdown items vary in width, so the labels after them zig-zag.
   A fixed slot - fa-fw's own measure - lines every label up (Daniel,
   2026-08-06). App-wide: the navbar menus benefit the same way. */
.dropdown-item i[class*="fa-"] {
  display: inline-block;
  width: 1.25em;
  text-align: center;
}

[data-bs-theme="dark"] .card-header {
  background: rgba(0, 0, 0, .22);
}

/* -- login ---------------------------------------------------------- */

.login-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 1.5rem;
}

.login-card {
  width: 100%;
  max-width: 24rem;
  background: var(--bs-body-bg);
  background: color-mix(in srgb, var(--bs-body-bg) 94%, transparent);
  backdrop-filter: blur(10px);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--am-radius);
  padding: 2.25rem 1.85rem;
  box-shadow: 0 18px 48px rgba(16, 24, 40, .12);
}

[data-bs-theme="dark"] .login-card { box-shadow: 0 18px 48px rgba(0, 0, 0, .45); }

/* The language picker and the theme toggle, together in the corner of the
   sign-in page. One row so they read as one set of controls about how the
   page is shown, rather than two unrelated buttons. */
.login-chrome {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* -- stat cards ----------------------------------------------------- */

.stat-icon {
  display: inline-grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: .625rem;
  font-size: 1.05rem;
  flex: none;
}

.stat-value { font-size: 1.6rem; line-height: 1.1; }
.stat-label { font-size: .8rem; color: var(--bs-secondary-color); }

.stat-primary .stat-icon { background: rgba(37, 99, 235, .12); color: #3b82f6; }
.stat-info    .stat-icon { background: rgba(13, 148, 186, .12); color: #22b8cf; }
.stat-success .stat-icon { background: rgba(22, 163, 74, .12);  color: #34c77b; }
.stat-warning .stat-icon { background: rgba(217, 119, 6, .14);  color: #f0a02c; }

/* -- provider switcher ---------------------------------------------- */

.provider-switch { display: inline-flex; gap: .35rem; flex-wrap: wrap; }

.provider-switch .btn {
  border-radius: 999px;
  font-size: .85rem;
  padding: .3rem .9rem;
  border: 1px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
  background: var(--bs-body-bg);
  background: color-mix(in srgb, var(--bs-body-bg) 70%, transparent);
}

.provider-switch .btn:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }

.provider-switch .btn.active {
  background: var(--am-brand);
  border-color: var(--am-brand);
  color: #fff;
  font-weight: 600;
}

/* -- saved queues ----------------------------------------------------
   Underline tabs rather than pills: the provider switcher directly below
   already uses pills, and two rows of pills read as one undifferentiated
   control strip. The count is the point of these, so it is bold. */

.view-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1.4rem;
  border-bottom: 1px solid var(--bs-border-color);
}

.view-tab {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .1rem .6rem;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--bs-secondary-color);
  text-decoration: none;
  font-size: .9rem;
  white-space: nowrap;
}

.view-tab:hover { color: var(--bs-body-color); }

.view-tab.active {
  color: var(--am-brand);
  border-bottom-color: var(--am-brand);
  font-weight: 600;
}

[data-bs-theme="dark"] .view-tab.active { color: #93b4fd; border-bottom-color: #93b4fd; }

.view-count {
  font-size: .72rem;
  font-weight: 700;
  padding: .1rem .4rem;
  border-radius: 999px;
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

.view-tab.active .view-count {
  background: rgba(37, 99, 235, .14);
  background: color-mix(in srgb, var(--am-brand) 14%, transparent);
  color: var(--am-brand);
}

[data-bs-theme="dark"] .view-tab.active .view-count { color: #93b4fd; }

/* -- tables ---------------------------------------------------------
   Header recessed, rows raised above it.

   These set Bootstrap's own --bs-table-bg rather than putting background
   on th/td directly. Bootstrap applies that variable through
   ".table > :not(caption) > * > *", which is more specific than a plain
   ".table-cases thead th" and would otherwise win. Setting the variable on
   thead/tbody lets the cells inherit it and avoids a specificity fight. */

.table-cases { --bs-table-bg: transparent; }

.table-cases > thead { --bs-table-bg: var(--bs-tertiary-bg); }

.table-cases thead th {
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  font-weight: 600;
  border-bottom-width: 1px;
  white-space: nowrap;
}

/* Dark theme: header noticeably darker than the card, body rows a little
   lighter, so the head reads as a recessed strip and the data sits on top. */
[data-bs-theme="dark"] .table-cases > thead {
  --bs-table-bg: rgba(0, 0, 0, .32);
}

[data-bs-theme="dark"] .table-cases > tbody {
  --bs-table-bg: rgba(255, 255, 255, .035);
}

[data-bs-theme="dark"] .table-cases thead th {
  color: #aab4c4;
  border-bottom-color: rgba(255, 255, 255, .10);
}

[data-bs-theme="dark"] .table-cases tbody td {
  border-bottom-color: rgba(255, 255, 255, .06);
}

/* ⚠ .875rem, not .9rem, since the interface face became Noto Sans
   (2026-08-05). Noto Sans is roughly 5-8% wider per character than Prompt, so
   at the old size the columns that hide below xxl would start hiding a
   breakpoint sooner and the table would wrap on laptops it used to fit. This
   is the compensation, and it is the only size that moved. */
.table-cases td { font-size: .875rem; }

/* The appliance-type badge has a column to itself so a run of them lines up
   down the page instead of starting wherever each case id happens to end.
   Wide enough for the longest badge (QUICK) and no wider - most cases are
   plain aligners and leave it empty. */
.table-cases .col-type {
  width: 1%;
  padding-left: 0;
  white-space: nowrap;
}

/* ---- the patient's plan page -------------------------------------------
   The only page a member of the public sees. Deliberately plain: no navbar,
   no theme toggle, nothing to click but the visualisation. It has to read on
   a phone in a waiting room, so everything is one column and large. */
.plan-body { background: var(--bs-tertiary-bg); }

.plan-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem 1rem;
}

.plan-card {
  width: 100%;
  max-width: 34rem;
  background: var(--bs-body-bg);
  border-radius: 1rem;
  box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .08);
  padding: 2rem;
}

.plan-card h1 { font-size: 1.35rem; margin-bottom: 1.25rem; }

.plan-langs { display: flex; justify-content: flex-end; margin-bottom: .5rem; }

/* Label over value rather than beside it: Hungarian and German labels are
   long, and a two-column grid wraps them badly at phone widths. */
.plan-facts { margin: 0 0 1rem; }
.plan-facts dt {
  font-weight: 400;
  font-size: .85rem;
  color: var(--bs-secondary-color);
  margin-top: .6rem;
}
.plan-facts dd { font-weight: 600; margin: 0; }

.plan-visual { margin-top: 1.5rem; }
.plan-visual-label { font-size: .85rem; color: var(--bs-secondary-color); }

.plan-button {
  display: inline-block;
  margin-top: .5rem;
  padding: .6rem 1.25rem;
  border-radius: .5rem;
  background: var(--bs-primary);
  color: #fff;
  text-decoration: none;
  font-weight: 600;
}

.plan-pending { color: var(--bs-secondary-color); margin: .5rem 0 0; }
.plan-gone p { color: var(--bs-secondary-color); }

/* The per-row patient search, same reasoning as .col-type: its own column so
   the magnifiers line up down the page instead of trailing each name at a
   different width. Narrow, and the padding is taken off the left so the icon
   sits against the name it belongs to. */
.table-cases .col-find {
  width: 1%;
  padding-left: 0;
  white-space: nowrap;
}

/* Quiet until the row is under the pointer - the same rule as the case page's
   facts list, where a coloured icon on every line would compete with the
   values. On a touch screen there is no hover, so it never drops below .55. */
.table-cases .col-find .fact-search { margin-left: 0; }
.table-cases tr:hover .col-find .fact-search { opacity: 1; color: var(--bs-link-color); }
.table-cases tbody tr:last-child td { border-bottom: 0; }

/* The header is inside a rounded card, so its top corners need clipping or
   the square header corners poke through the card radius. */
.card > .table-responsive:first-child { border-radius: var(--am-radius) var(--am-radius) 0 0; }
.card > .table-responsive:only-child  { border-radius: var(--am-radius); }

/* -- table rows ------------------------------------------------------
   PLAIN. Rows alternate between two neutral tints and carry no colour of
   their own - colour belongs on the pills, where it is read deliberately
   rather than glanced past.

   They were tinted by status, then by role, and the effect was that a
   table's appearance depended on which mixture of statuses it happened to
   hold: shipments, whose rows are nearly all one status, looked calm while
   a case list put eleven different colours on screen at once. Same rules,
   different data, and nothing on screen to explain the difference. One
   neutral stripe everywhere removes the question.

   Set through --bs-table-bg rather than background-color: Bootstrap paints
   cells with "inset 0 0 0 9999px" from that variable, so a plain background
   on the row would be covered over. Using the variable also leaves
   .table-hover's own state colour free to win on hover.                    */

/* The two shades are deliberately CLOSE: the second is only a little darker
   than the first. A stripe is there to stop the eye sliding between rows, not
   to be looked at - once the difference is obvious it starts reading as
   meaning something. */
.table-cases tbody tr[data-shade="a"] { --bs-table-bg: rgba(100, 116, 139, .03); }
.table-cases tbody tr[data-shade="b"] { --bs-table-bg: rgba(100, 116, 139, .055); }

[data-bs-theme="dark"] .table-cases tbody tr[data-shade="a"] { --bs-table-bg: rgba(0, 0, 0, .09); }
[data-bs-theme="dark"] .table-cases tbody tr[data-shade="b"] { --bs-table-bg: rgba(0, 0, 0, .16); }

/* ⚠ TRIED AND REVERTED 2026-08-05. Tinting each row with its own status hue
   was asked for, built, and taken straight back out: "revert alpha, too much
   colors". The rows keep the neutral stripe above.

   Recorded rather than deleted silently, because the idea is a reasonable one
   to have again. What was wrong with it is not the alpha - it was tried low -
   but that the table already carries colour in the status pill and the type
   badge, and a third carrier turns a list into a chart. The row is the thing
   you scan; it should stay quiet.

   data-tone is no longer emitted on the row either - see cases.php. */

/* ---- The dropdown caret is a Font Awesome glyph -------------------------
   Daniel, 2026-08-05: use the FA caret in menu dropdowns instead of the
   browser-drawn one.

   ⚠ HE ASKED FOR fa-caret-large-down, WHICH IS NOT IN THIS BUILD. The loaded
   Font Awesome 6 Pro has caret-down/up/left/right plus circle and square
   variants and nothing named "caret-large" - so this is fa-caret-down (U+F0D7)
   at a slightly larger size, which is what that name was reaching for.

   ⚠ border: 0 IS NOT OPTIONAL. Bootstrap draws its triangle with borders on
   this same ::after; without the reset you get the glyph AND the triangle.

   Only the three navbar toggles carry a caret at all - the card ⋯ menus are
   their own affordance and deliberately have none. */
.dropdown-toggle::after,
.dropup .dropdown-toggle::after,
.dropend .dropdown-toggle::after,
.dropstart .dropdown-toggle::before {
  content: "\f0d7";                        /* fa-caret-down */
  font-family: "Font Awesome 6 Pro";
  font-weight: 900;                        /* the solid face */
  font-style: normal;
  border: 0;                               /* ⚠ kills Bootstrap's triangle */
  width: auto;
  height: auto;
  vertical-align: baseline;
  margin-left: .4em;
  font-size: 1.05em;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Each direction gets its own glyph rather than a rotated one - FA has all
   four, and a transform would blur at these sizes. */
.dropup .dropdown-toggle::after    { content: "\f0d8"; }   /* caret-up */
.dropend .dropdown-toggle::after   { content: "\f0da"; }   /* caret-right */
.dropstart .dropdown-toggle::before {
  content: "\f0d9";                                        /* caret-left */
  margin-left: 0;
  margin-right: .4em;
}

/* ---- An upload, in the system log ---------------------------------------
   Daniel, 2026-08-05: "apply also green alpha to rows that show UPLD as
   result."

   ⚠ Two attributes, so this beats the neutral data-shade stripe above on
   specificity without !important - the same arrangement the reverted status
   tint used, and the reason that one was structurally sound even though the
   idea was not.

   This one survives where the cases-table tint did not, and the difference is
   that UPLD is RARE and means one thing: a file has arrived and a case became
   workable. Colouring every result code would turn the log back into a chart.
   Do not extend it. */
/* ⚠ "upload", not "upld" since 2026-08-07: the old name came from a result
   code the retired apps wrote and this one never has, so the rule had been
   matching no rows at all. See log.php. */
.table-cases tbody tr[data-result="upload"][data-shade="a"] { --bs-table-bg: rgba(22, 163, 74, .07); }
.table-cases tbody tr[data-result="upload"][data-shade="b"] { --bs-table-bg: rgba(22, 163, 74, .11); }

[data-bs-theme="dark"] .table-cases tbody tr[data-result="upload"][data-shade="a"] { --bs-table-bg: rgba(22, 163, 74, .15); }
[data-bs-theme="dark"] .table-cases tbody tr[data-result="upload"][data-shade="b"] { --bs-table-bg: rgba(22, 163, 74, .21); }

/* The blue sign-in tint was REMOVED on 2026-08-07 (Daniel). Two tinted
   results was one too many - the same "too much colors" that reverted the
   cases-table tint. Green, for a new upload, is the only one. */

/* ---- PDF / HTML chip on the clinic's document shelf ---------------------
   The live page uses a red PDF badge and grey HTML ones. Kept, because it
   tells the reader whether a click opens a page or downloads a file - which is
   the one thing they want to know before clicking. */
.doc-kind {
  display: inline-block;
  margin-left: .4rem;
  padding: 0 .35rem;
  border-radius: .25rem;
  font-size: .64rem;
  font-weight: 700;
  letter-spacing: .04em;
  vertical-align: middle;
}

.doc-kind-pdf  { background: rgba(220, 38, 38, .14); color: #c62b2b; }
.doc-kind-html { background: rgba(100, 116, 139, .18); color: #55606f; }

[data-bs-theme="dark"] .doc-kind-pdf  { color: #f07272; }
[data-bs-theme="dark"] .doc-kind-html { color: #a5aebc; }

/* ---- A legal document, rendered inside the app -------------------------
   These texts were written for a full-width page on submit.*; here they sit in
   a card, so they need a measure. Nothing else is restyled - the headings and
   paragraphs are the document's own. */
.legal-doc { max-width: 62rem; }

.legal-doc h3 { font-size: 1.25rem; margin-bottom: 1rem; }
.legal-doc h4 { font-size: 1rem; margin-top: 1.5rem; margin-bottom: .5rem; font-weight: 600; }
.legal-doc p,
.legal-doc li { font-size: .92rem; line-height: 1.6; }
.legal-doc ul,
.legal-doc ol { padding-left: 1.25rem; }

/* Printing one is a real thing to want - a clinic keeps these on file. */
@media print {
  .legal-doc { max-width: none; }
}

/* ---- The price list on the dashboard card -------------------------------
   Three columns in a third of a row, so everything is a size smaller than the
   full table and the labels are allowed to wrap while the numbers are not. */
.price-card { font-size: 1rem; } /* matched to the vigilance card link (Daniel, 2026-08-06) */

.price-card thead th {
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--bs-secondary-color);
  border-bottom: 1px solid var(--bs-border-color);
  padding: .3rem .5rem;
  background: transparent;
}

.price-card td { padding: .35rem .5rem; vertical-align: top; }

/* The label may wrap; the prices never should, or a figure breaks across two
   lines and stops reading as one number. */
.price-card td:first-child { line-height: 1.35; }

/* .cat-pill was a small neutral label carrying the price list's currency on
   the dashboard card. Removed 2026-08-05 with the badge itself - it named
   something every figure in the column already ends with. Nothing else used
   it, so the rule went too rather than waiting to be found by somebody
   wondering what it was for. */

/* ---- The country beside an address --------------------------------------
   Flag plus the two letters. The letters are the answer and the flag is
   decoration - several flags are indistinguishable at this size, and an emoji
   is not text to a screen reader. */
.geo-country {
  display: inline-block;
  margin-left: .35rem;
  font-size: .78rem;
  white-space: nowrap;
  letter-spacing: .02em;
}

/* ---- Pill tooltips ------------------------------------------------------
   Daniel, 2026-08-05: "can we use pill like tooltips instead of title
   elements. That's for the whole app."

   Bootstrap's own tooltip, restyled as a pill: fully rounded, one line, no
   arrow. The arrow is removed rather than restyled - a pill with a spike is
   neither shape - which also means the tooltip can sit closer to what it
   describes.

   ⚠ Colours are fixed rather than themed. A tooltip floats above whatever is
   underneath it, including a table row that already carries a tint, so it
   needs its own contrast rather than inheriting the page's. */
.tooltip.show { opacity: 1; }

.tooltip .tooltip-arrow { display: none; }

.tooltip .tooltip-inner {
  max-width: 22rem;
  padding: .3rem .7rem;
  border-radius: 999px;
  background: #1f2632;
  color: #f2f5f9;
  font-size: .78rem;
  line-height: 1.45;
  box-shadow: 0 .35rem .9rem rgba(0, 0, 0, .22);
}

[data-bs-theme="dark"] .tooltip .tooltip-inner {
  background: #e8ecf2;
  color: #1b2027;
  box-shadow: 0 .35rem .9rem rgba(0, 0, 0, .5);
}

/* Long text stops being a pill and becomes a rounded box - a 300px-wide
   capsule reads badly. */
.tooltip .tooltip-inner:not(:empty) { white-space: normal; }

/* ---- A disclosure that folds a group of fields away --------------------
   Used for the eight treatment preferences on the submission form. <details>
   rather than JavaScript, so the fields stay in the form and still post. */
.fold > summary {
  cursor: pointer;
  padding: .35rem 0;
  font-size: .88rem;
  font-weight: 500;
  list-style: none;
}

.fold > summary::-webkit-details-marker { display: none; }

.fold > summary::before {
  content: "\203A";           /* › - rotates to point down when open */
  display: inline-block;
  width: 1rem;
  font-size: 1.1rem;
  line-height: 1;
  transition: transform .12s ease;
}

.fold[open] > summary::before { transform: rotate(90deg); }

/* ---- "no scans yet" -----------------------------------------------------
   The one badge in the case table that is a TASK rather than a description, so
   it gets a colour under the scarce-colour rule where the type and category
   badges do not.

   Reuses the attention amber the status pills already use for waiting, question
   and modify - the same meaning, so the same hue, rather than introducing a
   ninth colour for a ninth thing. Only ever drawn on a case awaiting a plan;
   see Cases::FILES_EXPECTED. */
.pill-nofiles {
  display: inline-block;
  margin-left: .3rem;
  padding: .05rem .4rem;
  border-radius: .35rem;
  font-size: .7rem;
  font-weight: 600;
  white-space: nowrap;
  vertical-align: middle;
  background: rgba(217, 119, 6, .14);
  color: #b06a06;
}

.pill-nofiles i { margin-right: .25rem; }

[data-bs-theme="dark"] .pill-nofiles { color: #e6a63a; }

/* Text somebody typed into a textarea, shown with their line breaks intact.
   An adverse-event description arrives as paragraphs and reads as one wall of
   words without this. A class rather than a style attribute: the CSP here is
   default-src 'self' with no style-src, so an inline style is covered by it. */
.prewrap { white-space: pre-wrap; }

/* ---- What a field used to say -----------------------------------------
   Each entry is one overwritten value. The OLD text is the content; who and
   when are the label above it, kept small because you scan for the text. */
.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .7rem;
  /* Both append-only cards (this and Activity) are capped and scroll inside
     themselves (Daniel, 2026-08-07) - a case edited for months would
     otherwise stretch its card far past the deck around it. The newest
     entries are rendered first, so the cap hides only the old. */
  max-height: 22rem;
  overflow-y: auto;
}

.history-list > li {
  padding-left: .6rem;
  border-left: 2px solid var(--bs-border-color);
}

.history-head {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem .5rem;
  align-items: baseline;
  margin-bottom: .15rem;
}

.history-field { font-size: .8rem; font-weight: 600; }

.history-who {
  font-size: .74rem;
  color: var(--bs-secondary-color);
}

.history-was {
  font-size: .85rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;      /* a pasted URL must not widen the card */
}

.history-empty {
  color: var(--bs-secondary-color);
  font-style: italic;
}

/* ---- Something somebody said, shown back to them ----------------------- */
.quote-block {
  padding: .5rem .75rem;
  border-left: 3px solid var(--bs-border-color);
  border-radius: .25rem;
  background: var(--bs-tertiary-bg);
  font-size: .88rem;
}

/* ---- The three steps of submitting a case ------------------------------
   submit.*'s progress bar, kept because that subdomain's instruction is to
   stay FAMILIAR. Rendered as an ordered list rather than a Bootstrap progress
   bar: it is a list of steps, and a screen reader should read it as one.

   The numbers are in the text, not generated by CSS counters - they come from
   the translations, where they can be reworded per language. */
.submit-steps ol {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem .5rem;
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: .84rem;
  color: var(--bs-secondary-color);
}

.submit-steps li {
  padding: .2rem .6rem;
  border: 1px solid var(--bs-border-color);
  border-radius: 999px;
  white-space: nowrap;
}

.submit-steps li.is-current {
  border-color: transparent;
  background: rgba(37, 99, 235, .12);
  color: #2c66c9;
  font-weight: 500;
}

/* A step already behind you. Quieter than the current one but not as faint as
   one still ahead - the point of the bar is knowing where you are in it. */
.submit-steps li.is-done {
  border-color: transparent;
  background: rgba(22, 163, 74, .10);
  color: #16833f;
}

.submit-steps li.is-done::before {
  content: "\2713";               /* ✓ */
  margin-right: .3rem;
  font-weight: 600;
}

[data-bs-theme="dark"] .submit-steps li.is-current { color: #6ea3ff; }
[data-bs-theme="dark"] .submit-steps li.is-done    { color: #4dc98a; }

/* ---- Files a clinic has sent -------------------------------------------
   A plain list, not a table: there are three or four of them and the only
   things worth knowing are the name and the size. */
.file-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .3rem;
  font-size: .88rem;
}

.file-list > li { overflow-wrap: anywhere; }

/* ---- "We have your case" ----------------------------------------------- */
.submit-done-mark {
  font-size: 2.6rem;
  line-height: 1;
  color: #16a34a;
}

[data-bs-theme="dark"] .submit-done-mark { color: #4dc98a; }

.submit-done-id {
  display: inline-block;
  padding: .35rem 1rem;
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  background: var(--bs-tertiary-bg);
  font-size: 1.5rem;
  letter-spacing: .06em;
}

/* ---- The treatment category -------------------------------------------
   Deliberately COLOURLESS. The row already carries the status hue and the type
   badge carries its own; a third colour competing in the same row would make
   the table harder to read, not easier. This is a label, not a state. */
.category-pill {
  display: inline-block;
  padding: .1rem .45rem;
  border: 1px solid var(--bs-border-color);
  border-radius: .35rem;
  font-size: .74rem;
  font-weight: 500;
  white-space: nowrap;
  color: var(--bs-secondary-color);
}

/* Table body text.
   Bootstrap's table colour is the EMPHASIS colour, not the body colour: pure
   black in light mode and nearly white (#dee2e6) in dark. Across fifty rows
   that glares, and it showed most in the case ID and patient columns because
   every other column is already muted by .text-body-secondary. */
.table-cases > tbody { --bs-table-color: var(--bs-body-color); }
[data-bs-theme="dark"] .table-cases > tbody { --bs-table-color: #aeb6c2; }

/* A menu item that is its own form - blocking, resetting a password and
   deleting an account, on the client page. The form must not add a line box
   of its own or the items sit apart from the ones that are plain links. */
.dropdown-menu form { margin: 0; }

/* A card open for editing. Marked so it reads as a different mode without
   moving anything - the whole point is that the card stays where it was. */
.section-item .card.is-editing {
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

/* -- files ------------------------------------------------------------
   Names only. The size, the date and what you can do with a file are behind
   a click, so the card stops competing with the case for attention. */
.file-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .15rem;
}

.file-chip {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .3rem .45rem;
  border: 0;
  border-radius: var(--bs-border-radius);
  background: none;
  color: inherit;
  text-align: left;
  font-size: .875rem;
}

.file-chip:hover,
.file-chip:focus-visible {
  background: var(--bs-tertiary-bg);
  color: var(--bs-link-color);
}

.file-chip > i {
  flex: none;
  color: var(--bs-secondary-color);
}

.file-chip:hover > i { color: inherit; }

/* The name is the only thing shown, so it must never push the card wider. */
.file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One panel to drop onto or click, instead of an input and a button. */
.file-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .2rem;
  width: 100%;
  padding: 1rem .75rem;
  border: 1px dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  color: var(--bs-secondary-color);
  text-align: center;
  cursor: pointer;
}

.file-drop:hover,
.file-drop.is-over {
  border-color: var(--bs-primary);
  color: var(--bs-link-color);
  background: var(--bs-tertiary-bg);
}

.file-drop > i { font-size: 1.25rem; }
.file-drop-label { font-weight: 600; }
.file-drop-hint { font-size: .75rem; }

/* ---- the planner's reference shelf ----------------------------------
   Manuals as a list, patches as a grid. A manual is read once and its title
   is a sentence; a patch is fetched and its name is two words, so nine of
   them read faster in columns than down a page. */
.doc-list { list-style: none; margin: 0; padding: 0; }

.doc-list a {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .45rem .3rem;
  border-radius: .35rem;
  color: inherit;
  text-decoration: none;
}

.doc-list a:hover { background: var(--bs-tertiary-bg); color: var(--bs-link-color); }
.doc-name { flex: 1 1 auto; min-width: 0; font-size: 1rem; }
.doc-size { font-size: .75rem; color: var(--bs-secondary-color); white-space: nowrap; }

.patch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: .5rem;
}

.patch {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .6rem;
  border: 1px solid var(--bs-border-color);
  border-radius: .45rem;
  color: inherit;
  text-decoration: none;
  font-size: .8125rem;
}

.patch:hover { border-color: var(--bs-primary); color: var(--bs-link-color); }
.patch-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.patch-type {
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: .05em;
  color: var(--bs-secondary-color);
}

/* The magnifier beside the patient's name, which finds everything else they
   have. Quiet until wanted: it sits inside a facts list, and a coloured icon
   on every name would pull the eye away from the values themselves. */
.fact-search {
  margin-left: .4rem;
  color: var(--bs-secondary-color);
  opacity: .55;
  text-decoration: none;
}

dd:hover > .fact-search,
.fact-search:focus-visible { opacity: 1; color: var(--bs-link-color); }

/* The case id in the page heading, which copies itself when clicked.
   It has to READ as the heading it is part of - a button that looks like a
   button in an <h1> turns the title into a toolbar - so everything Bootstrap
   gives a button is taken back off, and only the cursor and a hover tint say
   it can be clicked. */
.copy-id {
  border: 0;
  background: transparent;
  padding: 0 .15rem;
  margin: 0 -.15rem;
  color: inherit;
  font: inherit;
  border-radius: .3rem;
  cursor: pointer;
}

.copy-id:hover { background: rgba(127, 127, 127, .16); }
.copy-id:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }

/* ---- the command palette --------------------------------------------
 *
 * A dialog over the page, near the top rather than centred: it fills
 * downwards as results arrive, and a box that grows from the middle of the
 * screen moves its own first row out from under the eye while somebody is
 * still reading it.
 */
.palette {
  position: fixed;
  inset: 0;
  z-index: 1080;              /* over the sticky navbar (1030) */
  background: rgba(0, 0, 0, .45);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 4rem 1rem 1rem;
}

[data-bs-theme="dark"] .palette { background: rgba(0, 0, 0, .62); }

/* The page must not scroll behind an open palette. */
body.palette-open { overflow: hidden; }

.palette-box {
  width: 100%;
  max-width: 42rem;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: .75rem;
  box-shadow: 0 24px 64px rgba(0, 0, 0, .35);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 6rem);
  overflow: hidden;
}

.palette-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .85rem 1rem;
  border-bottom: 1px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
}

/* Deliberately not .form-control: this is the whole dialog's input, so a
   border and a focus ring inside a bordered box would be two frames. */
.palette-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--bs-body-color);
  font-size: 1.05rem;
  outline: none;
}

.palette-list {
  overflow-y: auto;
  padding: .35rem 0;
}

.palette-group {
  padding: .5rem 1rem .25rem;
  font-size: .7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--bs-secondary-color);
}

.palette-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .45rem 1rem;
  border: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  font-size: .875rem;
}

/* One highlight, driven by the keyboard AND by the mouse - see palette.js.
   :hover is not used, or the two would disagree about what Enter opens. */
.palette-row.is-active { background: var(--bs-tertiary-bg); }

.palette-icon { width: 1rem; text-align: center; color: var(--bs-secondary-color); }

/* The patient's name column. Fixed width so ids and pills line up down the
   list even though names vary wildly in length. */
.palette-who {
  flex: 0 0 11rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

/* The second and later cases of the same patient: the column is still there,
   holding the alignment, but the name is not repeated. */
.palette-cont { border-left: 2px solid var(--bs-border-color); margin-left: .35rem; }

.palette-id  { font-weight: 600; white-space: nowrap; }
.palette-dim { color: var(--bs-secondary-color); font-weight: 400; white-space: nowrap; }
.palette-right { margin-left: auto; }

.palette-none,
.palette-foot {
  padding: .75rem 1rem;
  color: var(--bs-secondary-color);
  font-size: .8rem;
}

.palette-foot { border-top: 1px solid var(--bs-border-color); }

/* The navbar trigger. The shortcut is ON the control, because a keyboard
   shortcut nobody can see is a keyboard shortcut nobody uses. */
.nav-search kbd {
  font-size: .7rem;
  padding: .1rem .35rem;
  border: 1px solid var(--bs-border-color);
  border-radius: .25rem;
  background: transparent;
  color: inherit;
  opacity: .75;
}

/* The IPR paste box.
 *
 * Daniel, 2026-08-05: "just enough to see that it's there, but not to disturb
 * the view." It holds a wall of pasted markup that nobody reads on this page -
 * at full contrast it is the loudest thing in the deck and it means nothing.
 *
 * So: LIGHT grey on a light page, DARK grey on a dark one - dimmed towards the
 * background in both directions, not one colour that happens to suit one
 * theme. The label above it stays at full contrast, because that is the part
 * you are meant to read.
 */
.ipr-paste {
  color: #b3bac2;
  font-size: .78rem;
  line-height: 1.35;
}

[data-bs-theme="dark"] .ipr-paste { color: #4d555f; }

/* Typing into it is a deliberate act, so it comes back to normal while the
   cursor is in it - otherwise you cannot see what you are pasting over. */
.ipr-paste:focus { color: var(--bs-body-color); }

/* The search field, which contains its own controls.
 *
 * ⚠ THERE IS NO SEARCH BUTTON ANYWHERE (Daniel, 2026-08-05). The magnifier
 * IS the submit, and the × IS the reset. They have to read as parts of the
 * field rather than as buttons stuck to it, so they take the input's own
 * background and only colour up on hover.
 */
.search-field .search-go,
.search-field .search-clear {
  background: var(--bs-body-bg);
  color: var(--bs-secondary-color);
  border-color: var(--bs-border-color);
  text-decoration: none;
}

.search-field .search-go:hover,
.search-field .search-go:focus-visible,
.search-field .search-clear:hover,
.search-field .search-clear:focus-visible { color: var(--bs-link-color); }

/* The input sits between them, so it must not draw a border against either. */
.search-field .form-control { border-left: 0; border-right: 0; }

.search-field .form-control:focus {
  box-shadow: none;
  border-color: var(--bs-border-color);
}

/* Focus belongs to the whole field, not to one third of it. */
.search-field:focus-within {
  border-radius: var(--bs-border-radius);
  box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .18);
}

/* WebKit draws its own × inside type=search. Ours is a link that resets the
   whole query, which is not the same thing - two would be confusing. */
.search-field input[type="search"]::-webkit-search-cancel-button { display: none; }

/* The hierarchy tree.
   Nested lists with a rule down the left, so the shape reads without any
   JavaScript and prints correctly. */
.tree,
.tree ul {
  list-style: none;
  margin: 0;
  padding-left: 1.1rem;
}

.tree-level + .tree-level {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--bs-border-color);
}

.tree-level-label {
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  margin-bottom: .5rem;
}

.tree-node {
  position: relative;
  padding: .2rem 0 .2rem .85rem;
  border-left: 1px solid var(--bs-border-color);
}

/* The elbow into each node. */
.tree-node::before {
  content: "";
  position: absolute;
  left: 0;
  top: .95rem;
  width: .6rem;
  border-top: 1px solid var(--bs-border-color);
}

/* The last child stops the rule at its own elbow, so the line does not run on
   past the end of a branch. */
.tree-node:last-child {
  border-left-color: transparent;
}

.tree-node:last-child::after {
  content: "";
  position: absolute;
  left: -1px;
  top: 0;
  height: .95rem;
  border-left: 1px solid var(--bs-border-color);
}

.tree-provider > div:first-child,
.tree-overseer {
  font-weight: 600;
}

.tree-role-label {
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  margin-right: .4rem;
}

/* A clinic's readable name, beside its code. Same treatment as .case-who. */
.tree-name {
  color: var(--bs-secondary-color);
  margin-left: .45rem;
  font-weight: 400;
}

/* Nineteen clinics under one provider: let them run in columns on a wide
   screen rather than as one long ladder. */
@media (min-width: 992px) {
  .tree-clinics {
    columns: 2;
    column-gap: 2rem;
  }

  .tree-clinics > li {
    break-inside: avoid;
  }
}

@media (min-width: 1400px) {
  .tree-clinics {
    columns: 3;
  }
}

/* Sortable column headings. The heading stays a heading - same weight and
   colour as before - and the arrow carries the state. */
.sort-link {
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.sort-link:hover {
  color: var(--bs-link-color);
}

/* The arrow is quiet until the column is the one in use, so a row of headings
   does not read as seven active controls. */
.sort-icon {
  opacity: .25;
  font-size: .85em;
}

.sort-link:hover .sort-icon,
.sort-active .sort-icon {
  opacity: 1;
}

.sort-active {
  color: var(--bs-link-color);
}

/* Where deleting a case lives: below the deck, outside it, and visibly not
   one of the cards. color-mix has a solid fallback declared first, as
   everywhere else in this file. */
.danger-zone {
  border: 1px solid var(--bs-border-color);
  border-left: 3px solid var(--bs-danger);
  border-radius: var(--bs-border-radius);
  padding: .9rem 1rem;
  background: var(--bs-tertiary-bg);
  background: color-mix(in srgb, var(--bs-danger) 4%, var(--bs-tertiary-bg));
}

/* A one-time password, shown once and never stored in readable form. Big,
   monospaced and selectable, because it gets read down a telephone. */
.new-password {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: .12em;
  user-select: all;
  word-break: break-all;
}

/* -- language switch ------------------------------------------------- */

.lang-switch {
  display: inline-flex;
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  overflow: hidden;
}

.lang-option {
  padding: .25rem .5rem;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .03em;
  text-decoration: none;
  color: var(--bs-secondary-color);
  line-height: 1.4;
}

.lang-option:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }

.lang-option.active { background: var(--am-brand); color: #fff; }

/* -- IPR table -------------------------------------------------------
   Follows the app's own theme rather than copying the live apps' colours.
   The markup is pasted out of OnyxCeph and sanitized by Ipr::sanitize();
   rows with no values are dropped in PHP by Ipr::dropEmptyRows(), not
   hidden here, so the filtering also holds when the page is printed.

   A pasted table may carry its own cell colours - OnyxCeph paints some of
   them - and those are kept, because on an IPR table colour is meaning.
   Nothing here overrides them.                                            */

.ipr-table { overflow-x: auto; }

.ipr-table table {
  width: 100%;
  border-collapse: collapse;
  font-size: .85rem;
}

.ipr-table th,
.ipr-table td {
  border: 1px solid var(--bs-border-color);
  padding: .3rem .45rem;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
}

/* First column is the row's name, so it reads better left aligned. */
.ipr-table td:first-child,
.ipr-table th:first-child { text-align: left; }

.ipr-table th {
  background: var(--bs-tertiary-bg);
  font-weight: 600;
}

/* -- IPR diagram -----------------------------------------------------
   The renderer (app/Lib/ipr_diagram.php) writes colours as SVG
   PRESENTATION ATTRIBUTES - fill="#333", not style="fill:#333" - and any
   CSS declaration beats a presentation attribute. So the diagram can
   follow the theme without touching a proven clinical renderer.

   The selectors below match the exact values that file emits, which is
   also what documents them:

     rect[fill="white"]   the plate the diagram draws itself on
     text[fill="#333"]    tooth numbers and IPR amounts
     text[fill="#666"]    the "before step N" labels
     line[stroke="#e3e3e3"]  the midline and arch guides

   Deliberately NOT touched: the step badges (their text colour comes from
   a palette and is chosen to sit on their own pastel fill), the tooth
   circles (faded 65.7% toward white, legible either way), the orange IPR
   arrows, and the red crosses on excluded teeth. Those carry meaning and
   re-colouring them would be re-drawing the diagram, not theming it.     */

.ipr-diagram {
  max-width: 100%;
  overflow-x: auto;
  border-radius: .4rem;
  padding: .5rem;
}

.ipr-diagram svg { width: 100%; height: auto; display: block; }

[data-bs-theme="dark"] .ipr-diagram svg rect[fill="white"] { fill: #12161a; }

/* The tooth circles are faded 65.7% TOWARD WHITE by the renderer, which is
   right on a white page and glares on a dark one - they came out looking
   like white discs. Darkened here with a filter rather than by changing the
   fade, because the fade is a decision inside a proven clinical renderer and
   this is only how it is displayed. saturate() puts back the colour that
   dimming takes out, so a molar still reads as a molar. */
[data-bs-theme="dark"] .ipr-diagram svg circle { filter: brightness(.42) saturate(1.9); }

/* The tooth NUMBER sits inside its circle, so it follows the circle, not the
   page: with the circles darkened it needs to be light. Selected by its own
   font size, which is how the renderer distinguishes it from the millimetre
   amounts - both are emitted as fill="#333". */
[data-bs-theme="dark"] .ipr-diagram svg text[font-size="14"] { fill: #e9edf2; }

/* These two sit on the page background: the IPR amounts (16) and the
   "before step N" labels (15). */
[data-bs-theme="dark"] .ipr-diagram svg text[font-size="16"],
[data-bs-theme="dark"] .ipr-diagram svg text[font-size="15"] { fill: #dee2e6; }

[data-bs-theme="dark"] .ipr-diagram svg line[stroke="#e3e3e3"] { stroke: #39414a; }

/* -- printing --------------------------------------------------------
   The IPR table is taken to the chair on paper, so what prints is the
   table and nothing else: no navbar, no buttons, no background. */

/* -- CRM statuses ----------------------------------------------------
   Dragged by the grip only, never by the row: a whole row that drags picks
   itself up when somebody is trying to select the status name to copy it. */

.crm-grip {
  border: 0;
  background: none;
  padding: .1rem .35rem;
  color: var(--bs-secondary-color);
  cursor: grab;
  line-height: 1;
}

.crm-grip:hover { color: var(--bs-body-color); }
.crm-grip:active { cursor: grabbing; }

.table-cases tbody tr.is-dragging { opacity: .45; }

/* The row the dragged one would land against. An outline rather than a
   background, so it does not fight the alternating row shades. */
.table-cases tbody tr.is-over td {
  box-shadow: inset 0 -2px 0 0 var(--am-brand);
}

/* -- server error log ------------------------------------------------
   Severity as a small solid tag. The colours are the log's own, not the
   app's palette: red means error wherever you have seen a log before, and
   this is the one screen where matching that convention beats matching the
   rest of the interface. */

.log-level {
  display: inline-block;
  min-width: 4.2rem;
  padding: .15rem .45rem;
  border-radius: .25rem;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-align: center;
  text-transform: uppercase;
  text-decoration: none;
  color: #fff;
  background: #6b7280;
}

a.log-level { opacity: .55; }
a.log-level:hover,
a.log-level.is-on { opacity: 1; color: #fff; }

.lvl-emerg, .lvl-alert, .lvl-crit { background: #7f1d1d; }
.lvl-error  { background: #dc2626; }
.lvl-warn   { background: #d97706; }
.lvl-notice { background: #2563eb; }
.lvl-info, .lvl-debug { background: #6b7280; }

/* A stack trace is written with its own line breaks and its own alignment,
   and loses all meaning reflowed as a paragraph. */
.serverlog-body {
  font-family: var(--am-mono, monospace);
  font-size: .8125rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.serverlog-chip {
  display: inline-block;
  margin: .15rem .3rem 0 0;
  padding: .05rem .4rem;
  border-radius: .25rem;
  background: rgba(100, 116, 139, .14);
  color: var(--bs-secondary-color);
  font-family: var(--am-mono, monospace);
  font-size: .6875rem;
}

.serverlog-chip b { color: var(--bs-body-color); font-weight: 600; }

/* -- assistant -------------------------------------------------------
   Newest exchange at the top, so the answer to what was just asked is
   where the eye already is rather than below a screen of history. */

.assistant-history {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  max-height: 60vh;
  overflow-y: auto;
}

.assistant-row { display: flex; }

.assistant-bubble {
  max-width: 85%;
  padding: .55rem .85rem;
  border-radius: var(--am-radius);
  /* An answer arrives with its own line breaks and its own spacing, and it
     is inserted as text - so the whitespace has to be honoured here or a
     list comes out as one paragraph. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.assistant-you { justify-content: flex-end; }

.assistant-you .assistant-bubble {
  background: var(--bs-primary-bg-subtle);
  color: var(--bs-emphasis-color);
}

.assistant-claude .assistant-bubble,
.assistant-waiting .assistant-bubble {
  background: rgba(100, 116, 139, .1);
}

[data-bs-theme="dark"] .assistant-claude .assistant-bubble,
[data-bs-theme="dark"] .assistant-waiting .assistant-bubble {
  background: rgba(0, 0, 0, .22);
}

.assistant-waiting .assistant-bubble {
  color: var(--bs-secondary-color);
  font-style: italic;
}

/* -- declaration of conformity --------------------------------------
   A regulatory document that goes into a patient file. Plain, black on
   white, and laid out the same on screen as on paper - somebody comparing
   a printed sheet against the screen should not have to wonder whether
   they are looking at the same thing. */

/* ⚠ A SHEET OF PAPER ON SCREEN TOO (Daniel, 2026-08-05), not only under the
   printer. The document is black on white when it comes out, so showing it as
   pale text on a dark page meant checking it against the print preview to know
   what you were actually signing. It is white in BOTH themes, deliberately -
   this is the one place in the app that does not follow the theme, because it
   is not interface, it is a document. */
/* ⚠ THE PROPORTIONS OF A4, ON SCREEN TOO. 46rem was a measure chosen for
   reading and it made the sheet look square - which is not what comes out of
   the printer, so the preview was misleading about the thing it previews.
   An A4 page is 210mm wide; at 96dpi that is close enough to 52rem, and
   min-height keeps the shape recognisable even for a short document. */
.declaration {
  max-width: 52rem;
  min-height: 60rem;
  margin: 0 auto;
  line-height: 1.5;
  background: #fff;
  color: #000;
  padding: 2.5rem 2.75rem;
  border-radius: .25rem;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .18);
}

[data-bs-theme="dark"] .declaration { box-shadow: 0 10px 34px rgba(0, 0, 0, .55); }

/* ⚠ EVERY WORD BLACK (Daniel, 2026-08-05). Not "mostly black with the small
   print in grey" - the law reference, the place and the date are part of the
   document, and a regulatory sheet that renders half its text in a lighter
   colour reads as though some of it is less binding. The * is what stops a
   themed variable leaking in through any element added here later. */
.declaration,
.declaration * { color: #000; }

.declaration h2 { border-bottom-color: #ccc; }

@media (max-width: 575.98px) {
  .declaration { padding: 1.5rem 1.25rem; }
}

.declaration-head { margin-bottom: 1.5rem; }

.declaration-head h1 {
  font-size: 1.35rem;
  font-weight: 600;
  margin-bottom: .35rem;
}

/* ⚠ NO COLOUR ON ANY OF THESE. They used themed variables, and because they
   sit LATER in the file than the "every word black" rule above - at the same
   specificity - they won and the small print stayed grey. Size and weight do
   the work of separating them instead, which is what a printed document uses
   anyway. */
.declaration-law {
  font-size: .8125rem;
  font-weight: 400;
  margin-bottom: 0;
}

.declaration h2 {
  font-size: 1rem;
  font-weight: 600;
  margin: 1.5rem 0 .5rem;
  padding-bottom: .3rem;
  border-bottom: 1px solid #ccc;
}

/* Two columns of facts, as on the live page. One column below sm, where two
   would wrap every line. */
.declaration-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  margin-bottom: 1rem;
}

@media (min-width: 576px) {
  .declaration-grid { grid-template-columns: 1fr 1fr; gap: 2rem; }
}

.declaration-note {
  font-size: .875rem;
}

.declaration-sign {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: 1rem;
  margin-top: 2.5rem;
}

/* .declaration-place had a themed colour and no longer needs any rule at all:
   the place and the date are signed matter, not a caption. See
   .declaration-law for why nothing in this document is grey. */

/* ⚠ A4 PORTRAIT, WITH REAL MARGINS (Daniel, 2026-08-05: "consent should be a4
   portrait. This is square. Smaller font. It needs to fit single page. Add some
   margins also. Same for MDR declaration in other environments").

   @page is the only way to tell a browser the paper size and margin - a CSS
   width cannot, because the sheet is not an element. Without it the browser
   uses its own default margin, which differs between Chrome and Firefox, and
   the "square" look was the on-screen 46rem measure surviving into the print
   layout.

   ⚠ 15mm all round rather than the browser's usual ~10: these are documents
   that get filed, hole-punched and photocopied, and a signature line 10mm from
   the edge is the one that gets clipped. */
/* ⚠ THE DEFAULT PAPER FOR EVERY PRINTED PAGE IN THE APP (Daniel,
   2026-08-06: "Only IPR should be printed on A4 landscape, all the rest
   print on A4 portrait"). 12mm: not the wide browser default, but still
   clear of every ordinary printer's unprintable edge (~5mm). Pages that
   need different paper load their own stylesheet after this one - ipr.css
   (landscape), landscape.css (Italdent), declaration.css (margin 0). */
@page {
  size: A4 portrait;
  margin: 12mm;
}

@media print {
  .navbar,
  .d-print-none,
  .section-grip,
  .section-menu,
  .section-toggle { display: none !important; }

  /* ---- fitting one sheet -------------------------------------------------
     The consent runs to about a page and a quarter at screen sizes, and a
     second sheet carrying nothing but the signature line is worse than a
     slightly smaller face. These sizes were chosen against the longest of the
     documents - the Slovak consent with a full plan description. */
  .declaration {
    font-size: 9.5pt;
    line-height: 1.35;
  }

  .declaration h2 {
    font-size: 13pt;
    margin: 0 0 .35rem;
    padding-bottom: .25rem;
  }

  .declaration h3 {
    font-size: 10.5pt;
    margin: .7rem 0 .2rem;
  }

  .declaration p,
  .declaration li { margin: 0 0 .35rem; }

  .declaration ul {
    margin: .2rem 0 .5rem;
    padding-left: 1.1rem;
  }

  /* The law reference under the title is small print in the literal sense. */
  .declaration-law { font-size: 8.5pt; margin-bottom: .6rem; }

  /* Room to actually sign, without pushing onto a second sheet. */
  .declaration-sign { margin-top: 1.2rem; }

  /* ⚠ The card wrapper around a legal document must not print its own frame -
     the page is the frame now. */
  .card,
  .card-body {
    border: 0 !important;
    box-shadow: none !important;
    padding: 0 !important;
    background: transparent !important;
  }

  /* Paper is white whatever the screen was. Without this a dark-mode user
     prints white text onto white paper - the sheet comes out blank and the
     ink is gone before anybody notices. */
  .declaration,
  .declaration * {
    color: #000 !important;
    background: transparent !important;
  }

  /* The on-screen sheet's own padding, radius and shadow go: the paper IS the
     page now, and a drop shadow prints as a grey smear down the margin.
     ⚠ !important IS LOAD-BEARING HERE. The screen shadow is set twice, and
     the second one is `[data-bs-theme="dark"] .declaration` - a higher
     specificity than this rule, and it sits earlier in the file. Without
     !important a dark-mode user's print preview kept the shadow, which is
     exactly the reader who is least likely to expect it. */
  .declaration {
    max-width: none;
    min-height: 0;          /* ⚠ or a short document forces a second sheet */
    padding: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    border: 0 !important;
  }

  .declaration h2 { border-bottom-color: #000 !important; }

  /* Never split the signature away from the statement it signs. */
  .declaration-sign { page-break-inside: avoid; }
  .declaration h2   { page-break-after: avoid; }

  body {
    background: #fff !important;
    color: #000;
  }

  .card {
    border: 0 !important;
    box-shadow: none !important;
    background: transparent !important;
    backdrop-filter: none !important;
  }

  /* ⚠ NO @page RULE HERE ANY MORE. Landscape used to be the app-wide
     default for the IPR table's sake, which put every OTHER print preview
     on sideways paper (Daniel, 2026-08-06). The default is now the A4
     portrait rule above; the IPR page alone loads ipr.css for landscape. */

  /* THE TABLE MUST FIT THE PAGE, and three things were stopping it:
     - overflow-x:auto scrolls on screen but CLIPS on paper, so the last
       columns were simply cut off the right-hand edge
     - white-space:nowrap kept every cell at its natural width
     - the default auto layout sizes columns to content, not to the page
     Fixed layout at 100% divides the width evenly instead, and the first
     column is given room because it holds "Before Step 1" rather than a
     millimetre. */
  .ipr-table { overflow: visible !important; }

  .ipr-table table {
    table-layout: fixed;
    width: 100%;
    font-size: 8px;
    font-weight: 700;
  }

  .ipr-table th,
  .ipr-table td {
    border-color: #999;
    padding: 1px;
    white-space: normal;
    overflow: hidden;
    word-break: break-word;
  }

  .ipr-table th:first-child,
  .ipr-table td:first-child { width: 12%; }

  .ipr-table,
  .ipr-diagram { background: #fff !important; padding: 0 !important; }

  /* Paper is white whatever the screen was, so every dark-mode override is
     undone here rather than printed. */
  .ipr-diagram svg rect[fill="white"] { fill: #fff !important; }
  .ipr-diagram svg circle { filter: none !important; }
  .ipr-diagram svg text[font-size="14"],
  .ipr-diagram svg text[fill="#333"] { fill: #333 !important; }
  .ipr-diagram svg text[font-size="15"],
  .ipr-diagram svg text[fill="#666"] { fill: #666 !important; }
  .ipr-diagram svg text[font-size="16"] { fill: #333 !important; }
  .ipr-diagram svg line[stroke="#e3e3e3"] { stroke: #e3e3e3 !important; }

  /* Never break a row of teeth across two pages. */
  .ipr-table tr { page-break-inside: avoid; }

  main { padding: 0 !important; }
}

/* -- provider pills -------------------------------------------------
   Was Bootstrap's .badge, which is square-cornered, heavier and a size
   apart from the status pill beside it. Two differently shaped tags in one
   row read as two different kinds of thing, so the provider now takes the
   same pill as the status and differs only in colour.                   */

/* Swapped 2026-08-07 (Daniel): Nevident is the purple, JustSmile the blue. */
.provider-pill.provider-nevident  { background: rgba(168, 85, 247, .14); color: #b168f7; }
.provider-pill.provider-justsmile { background: rgba(37, 99, 235, .12);  color: #3b82f6; }
/* Non-clinic rows on /accounts: what the account IS, not a provider. */
.provider-pill.provider-planner   { background: rgba(20, 184, 166, .14); color: #14b8a6; }
.provider-pill.provider-staff     { background: var(--bs-tertiary-bg);   color: var(--bs-secondary-color); }
/* Yellow on Daniel's word, 2026-08-07 - also ends the near-collision with the Planner teal. */
.provider-pill.provider-smileco   { background: rgba(234, 179, 8, .16);  color: #c99a06; }

/* -- status pills ---------------------------------------------------
   Grouped by meaning rather than one colour per status: waiting on
   someone, moving, ready, being made, or stopped. An unknown status falls
   back to the neutral base, so adding one never breaks the layout.      */

/* One shape, listed once: a provider pill and a status pill that drifted
   apart by a tenth of a rem would look like a mistake nobody could name. */
.status-pill,
.provider-pill {
  display: inline-block;
  padding: .2rem .55rem;
  border-radius: 999px;
  font-size: .75rem;
  font-weight: 600;
  white-space: nowrap;
  background: var(--bs-secondary-bg);
  color: var(--bs-secondary-color);
}

/* Pills that sit BESIDE the status rather than instead of it - what has
   happened to an approved case since. Outlined rather than filled, so they
   read as annotations on the status and never compete with it. */
.status-pill + .status-pill { margin-left: .25rem; }

.pill-invoiced {
  background: transparent;
  border: 1px solid rgba(22, 163, 74, .55);
  color: #16833f;
}

.pill-kline {
  background: transparent;
  border: 1px solid rgba(124, 58, 237, .55);
  color: #6b35cf;
}

[data-bs-theme="dark"] .pill-invoiced { border-color: rgba(47, 179, 108, .7); color: #4dc98a; }
[data-bs-theme="dark"] .pill-kline    { border-color: rgba(157, 107, 240, .7); color: #b189f5; }

/* One rule per TONE, not per status - which statuses share a tone is decided
   in PHP (Cases::TONES) and used for the row tint as well, so a pill and its
   row can never disagree. A status with no tone keeps the neutral base above,
   which is what archived gets. */
.tone-attention  { background: rgba(217, 119, 6, .14); color: #b06a06; }
.tone-incoming   { background: rgba(37, 99, 235, .12); color: #2c66c9; }
.tone-planned    { background: rgba(13, 148, 186, .14); color: #0f7c9b; }
.tone-approved   { background: rgba(22, 163, 74, .14); color: #16833f; }
.tone-production { background: rgba(124, 58, 237, .14); color: #6b35cf; }
.tone-sent       { background: rgba(100, 116, 139, .16); color: #55606f; }
.tone-stopped    { background: rgba(220, 38, 38, .12); color: #c62b2b; }

/* Dark mode wants the lighter end of each hue for the text, or the pill reads
   as a smudge rather than a word. */
[data-bs-theme="dark"] .tone-attention  { color: #e6a63a; }
[data-bs-theme="dark"] .tone-incoming   { color: #6ea3ff; }
[data-bs-theme="dark"] .tone-planned    { color: #45bcd8; }
[data-bs-theme="dark"] .tone-approved   { color: #4dc98a; }
[data-bs-theme="dark"] .tone-production { color: #b189f5; }
[data-bs-theme="dark"] .tone-sent       { color: #a5aebc; }
[data-bs-theme="dark"] .tone-stopped    { color: #f07272; }

/* CRM status. Deliberately flat and neutral next to the coloured status pill:
   two colour-coded columns side by side compete, and the production status is
   the one that has to be readable at a glance. */
.crm-pill {
  display: inline-block;
  padding: .15rem .5rem;
  border-radius: .4rem;
  font-size: .75rem;
  white-space: nowrap;
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  border: 1px solid var(--bs-border-color);
}

/* -- clickable rows -------------------------------------------------
   The whole row navigates (app.js), but the case ID is a real link, so
   keyboard users get a focus ring in the normal place and nothing depends
   on JavaScript being available. */

.row-link { cursor: pointer; }

/* Case and account codes: monospaced, so a column of them lines up character
   by character and a transposed pair is visible rather than merely wrong. */
.case-link {
  font-family: var(--am-mono);
  font-size: .86rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--bs-border-color);
}

.case-link:hover, .case-link:focus-visible { color: var(--am-brand); }

[data-bs-theme="dark"] .case-link:hover,
[data-bs-theme="dark"] .case-link:focus-visible { color: #93b4fd; }

/* -- case detail ----------------------------------------------------- */

/* The heading pills run a size larger than the ones in a table. The provider
   is listed here too: it stands directly beside the status, and one of the
   two coming up short would be the mismatch this shape exists to avoid. */
.case-head .status-pill,
.case-head .provider-pill { font-size: .8rem; padding: .25rem .7rem; }

/* The patient's name beside the case ID: present but subordinate, so the ID
   still reads as the heading and the name as who it belongs to. */
.case-who {
  font-family: var(--bs-body-font-family);
  font-weight: 400;
  font-size: .9rem;
  color: var(--bs-secondary-color);
  margin-left: .6rem;
  letter-spacing: 0;
}

/* Type of appliance, beside the case ID. Deliberately quieter than the
   status pill: it says what the case IS, not what is happening to it. */
.case-type {
  display: inline-block;
  padding: .2rem .5rem;
  border-radius: .4rem;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .05em;
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  border: 1px solid var(--bs-border-color);
}

/* ⚠ ONE COLOUR PER TYPE, AND THEY COME FROM plan.*'s ROW LABELS - blue for a
   refinement, amber for a retainer, red for an alternative plan, cyan for
   whitening, green for a QUICK. Somebody who has read those queues for six
   years should not have to learn a second scheme. rowElements() in
   plan.alignermanager/incs/_functions.php is the original.

   `add`, `ocl` and `da2` had no rule at all and fell back to grey, which made
   three different appliances look like the same unnamed thing. */
.case-type-ref { background: rgba(37, 99, 235, .12);  color: #4287f5; border-color: transparent; }
.case-type-alt { background: rgba(220, 38, 38, .12);  color: #e05656; border-color: transparent; }
.case-type-ret { background: rgba(217, 119, 6, .14);  color: #d98a12; border-color: transparent; }
.case-type-whi { background: rgba(13, 148, 186, .14); color: #21a5c4; border-color: transparent; }
.case-type-qck { background: rgba(22, 163, 74, .14);  color: #3aab63; border-color: transparent; }
.case-type-add { background: rgba(124, 58, 237, .13); color: #9a72e8; border-color: transparent; }
.case-type-ocl { background: rgba(13, 148, 136, .14); color: #22a99b; border-color: transparent; }
.case-type-da2 { background: rgba(190, 24, 93, .12);  color: #e0568f; border-color: transparent; }

/* ---- THE BAND -------------------------------------------------------
 *
 * plan.* colours its rows so a planner can tell a refinement from a retainer
 * without reading - Daniel asked for the same on the case page. A pill beside
 * the id is a thing you read; this is a thing you notice.
 *
 * A rule under the heading rather than a block behind it: the case head
 * already carries the id, the patient, the type, the status and the provider,
 * and a tinted panel behind all of that would make every one of them harder
 * to read to say one thing that the pill already says in words.
 *
 * ⚠ NO BAND ON A PLAIN ALIGNER, deliberately. Two thirds of all cases are
 * aligners, so a colour there would be a marking that means "ordinary" - and a
 * signal shown on the majority is not a signal. The band appears exactly when
 * the case is NOT the usual thing.
 */
.case-head { position: relative; }

/* ⚠ A WASH BEHIND THE WHOLE HEADING, NOT A RULE UNDER IT.
 *
 * The first attempt was a 3px line below the heading. Daniel: "that coloured
 * band does not work here to identify refinement, retainer" - and he was
 * right. A hairline at the bottom edge of a block is read as a divider, which
 * is furniture; the eye skips it. A tinted panel is read as a property OF the
 * thing it is behind, which is what the type is.
 *
 * ⚠ A CUSTOM PROPERTY, NOT currentColor. The obvious version sets `color` on
 * .case-head and tints from it - but the id, the type pill, the status pill
 * and the provider pill are all children, and half of them inherit their
 * colour. --band touches nothing but the panel.
 */
.case-head[class*="case-band-"] {
  background: color-mix(in srgb, var(--band) 10%, transparent);
  border-left: 5px solid var(--band);
  border-radius: .1rem .5rem .5rem .1rem;
  padding: .75rem 1rem;
  margin-left: -.25rem;
}

/* Dark mode needs more of the colour to read as anything at all - 10% of an
   amber over a near-black page is invisible. */
[data-bs-theme="dark"] .case-head[class*="case-band-"] {
  background: color-mix(in srgb, var(--band) 16%, transparent);
}

/* color-mix is not universal. Where it is missing the border alone still
   carries the type, which is the half that does the work. */
@supports not (background: color-mix(in srgb, red 10%, transparent)) {
  .case-head[class*="case-band-"] { background: transparent; }
}

.case-band-ref { --band: #4287f5; }
.case-band-alt { --band: #e05656; }
.case-band-ret { --band: #d98a12; }
.case-band-whi { --band: #21a5c4; }
.case-band-qck { --band: #3aab63; }
.case-band-add { --band: #9a72e8; }
.case-band-ocl { --band: #22a99b; }
.case-band-da2 { --band: #e0568f; }

/* Label/value pairs. Two columns on anything but a phone, where a cramped
   value column turns every patient name into three lines. */
.facts {
  display: grid;
  grid-template-columns: 1fr;
  gap: .1rem .75rem;
  margin: 0;
  font-size: .9rem;
}

@media (min-width: 480px) {
  .facts { grid-template-columns: minmax(8rem, 12rem) 1fr; }
}

.facts dt {
  color: var(--bs-secondary-color);
  font-weight: 500;
  padding: .3rem 0;
}

.facts dd {
  margin: 0;
  padding: .3rem 0;
  color: var(--bs-body-color);
}

@media (min-width: 480px) {
  .facts dt, .facts dd { border-top: 1px solid var(--bs-border-color); }
  .facts dt:first-of-type, .facts dt:first-of-type + dd { border-top: 0; }
}

.facts-tight { font-size: .85rem; margin-bottom: 1.25rem; }

/* Free-text blocks. A quiet left rule rather than a box, so several in a row
   read as one document instead of a stack of panels. */
.note {
  border-left: 3px solid var(--bs-border-color);
  padding: .1rem 0 .1rem .85rem;
  margin-top: 1rem;
}

.note-label {
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  font-weight: 600;
  margin-bottom: .15rem;
}

.note p { font-size: .9rem; }

/* Internal notes are marked, because the difference between a comment the
   client can see and one they cannot must never rest on remembering. */
.note-internal { border-left-color: #d98a12; }
.note-internal .note-label { color: #d98a12; }

/* -- section colours -------------------------------------------------
   One colour per part of a case, used on BOTH the read view and the
   editor, so the colour means the same thing wherever you meet it.

   The tint is deliberately weak and the accent bar does the work: five
   fully coloured headers on one screen is a paint chart, not an
   interface. Colours are the palette already used by the status pills.

   Written as rgba rather than color-mix so they need no fallback, and
   they sit on top of the theme's own header background in both modes. */

.card.section-patient      > .card-header { --section: #4287f5; }
.card.section-treatment    > .card-header { --section: #21a5c4; }
.card.section-instructions > .card-header { --section: #9d6bf0; }
/* The Handling card is gone; Files takes its amber so the deck keeps the same
   spread of colour. The .section-handling rules stay because the CLIENT pages
   still use that name for their own card. */
.card.section-handling     > .card-header,
.card.section-files        > .card-header { --section: #d98a12; }
.card.section-production   > .card-header { --section: #2fb36c; }
/* CRM - JustSmile and Smile&Co only, so it appears on about five of six
   cases and needs a colour of its own rather than borrowing one. */
.card.section-crm          > .card-header { --section: #d9418c; }
.card.section-conversation > .card-header { --section: #26b79b; }
.card.section-activity     > .card-header { --section: #8b95a5; }
/* Client sections. Contact reuses the patient blue, because both answer
   "who is this"; billing and tax share the money-coloured end. */
.card.section-contact      > .card-header { --section: #4287f5; }
.card.section-billing      > .card-header { --section: #2fb36c; }
.card.section-tax          > .card-header { --section: #d98a12; }
.card.section-delivery     > .card-header { --section: #21a5c4; }
.card.section-account      > .card-header { --section: #9d6bf0; }
.card.section-cases        > .card-header { --section: #8b95a5; }
.card.section-ipr          > .card-header { --section: #21a5c4; }

.card[class*="section-"] > .card-header {
  border-left: 3px solid var(--section);
  border-top-left-radius: var(--am-radius);
  color: var(--section);
  /* 400, like every card header (Daniel, 2026-08-06) - this used to say 600,
     which only showed on headers whose title is bare text (Delayed cases):
     the ones wrapped in .section-title were already overridden to normal. */
  font-weight: 400;
}

/* A whisper of the accent behind the header, enough to tell two adjacent
   cards apart at a glance without shouting. */
/* The tint is the same colour as the accent at low alpha. color-mix would say
   that once instead of twice, but it is below the browser floor this app
   supports, so the values are spelled out. Keep each pair in step. */
.card.section-patient      > .card-header,
.card.section-contact      > .card-header { background: rgba(66, 135, 245, .10); }
.card.section-treatment    > .card-header,
.card.section-ipr          > .card-header,
.card.section-delivery     > .card-header { background: rgba(33, 165, 196, .10); }
.card.section-instructions > .card-header,
.card.section-account      > .card-header { background: rgba(157, 107, 240, .10); }
.card.section-handling     > .card-header,
.card.section-files        > .card-header,
.card.section-tax          > .card-header { background: rgba(217, 138, 18, .10); }
.card.section-production   > .card-header,
.card.section-billing      > .card-header { background: rgba(47, 179, 108, .10); }
.card.section-crm          > .card-header { background: rgba(217, 65, 140, .10); }
.card.section-conversation > .card-header { background: rgba(38, 183, 155, .10); }
.card.section-activity     > .card-header,
.card.section-cases        > .card-header { background: rgba(139, 149, 165, .12); }

/* Dark mode needs a touch more, or the tint disappears into the card. */
[data-bs-theme="dark"] .card.section-patient      > .card-header,
[data-bs-theme="dark"] .card.section-contact      > .card-header { background: rgba(66, 135, 245, .16); }
[data-bs-theme="dark"] .card.section-treatment    > .card-header,
[data-bs-theme="dark"] .card.section-ipr          > .card-header,
[data-bs-theme="dark"] .card.section-delivery     > .card-header { background: rgba(33, 165, 196, .16); }
[data-bs-theme="dark"] .card.section-instructions > .card-header,
[data-bs-theme="dark"] .card.section-account      > .card-header { background: rgba(157, 107, 240, .16); }
[data-bs-theme="dark"] .card.section-handling     > .card-header,
[data-bs-theme="dark"] .card.section-files        > .card-header,
[data-bs-theme="dark"] .card.section-tax          > .card-header { background: rgba(217, 138, 18, .16); }
[data-bs-theme="dark"] .card.section-crm          > .card-header { background: rgba(217, 65, 140, .16); }
[data-bs-theme="dark"] .card.section-production   > .card-header,
[data-bs-theme="dark"] .card.section-billing      > .card-header { background: rgba(47, 179, 108, .16); }
[data-bs-theme="dark"] .card.section-conversation > .card-header { background: rgba(38, 183, 155, .16); }
[data-bs-theme="dark"] .card.section-activity     > .card-header,
[data-bs-theme="dark"] .card.section-cases        > .card-header { background: rgba(139, 149, 165, .18); }

/* -- section headers: grip, title, fold ------------------------------ */

.section-head {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.section-title { flex: 1 1 auto; min-width: 0; }

/* The grip, and the ⋯ that opens a card's menu.
 *
 * ⚠ THESE TWO ARE THE ONLY CONTROLS IN A CARD HEADER (Daniel, 2026-08-05).
 * There used to be a chevron and up to two labelled buttons as well, which
 * made a toolbar out of every heading. The chevron went with collapsing; the
 * buttons went into the menu. Do not add a third.
 *
 * .section-toggle is also the plus in the notes panel header, which is a link
 * rather than a button - hence the text-decoration reset.
 */
.section-grip,
.section-toggle,
.section-action {
  flex: none;
  border: 0;
  background: transparent;
  color: inherit;
  opacity: .55;
  padding: .15rem .25rem;
  border-radius: .3rem;
  line-height: 1;
  text-decoration: none;
}

.section-grip:hover,
.section-toggle:hover,
.section-action:hover,
.section-action[aria-expanded="true"] { opacity: 1; background: rgba(127, 127, 127, .14); }

.section-grip:focus-visible,
.section-toggle:focus-visible,
.section-action:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; opacity: 1; }

/* grab, not pointer: this thing moves rather than activates. touch-action
   none is what stops a finger drag scrolling the page instead. */
.section-grip { cursor: grab; touch-action: none; }
.section-item.is-dragging .section-grip { cursor: grabbing; }

/* No dropdown caret - the ⋯ already says "there is more here", and a caret
   beside it is a second glyph saying the same thing. */
.section-menu { flex: none; line-height: 1; }
.section-action::after { display: none !important; }

/* Confirmation after "Copy for OnyxCeph", on the item itself. It stays open
   just long enough to be read - see case-edit.js. */
.dropdown-item.is-copied { color: #2fb36c; }

/* The OnyxCeph Handover button in the case heading row. The look settled
   2026-08-06 after FOUR rounds (purple -> gradient pill -> plain purple ->
   gradient again), and the FINAL WORD is: an ordinary Bootstrap button in
   ONYXCEPH'S OWN BLUE, with OnyxCeph's gauge icon - it is dressed as the
   program it talks to. The blue is sampled from OnyxCeph's title bar; used
   nowhere else in the app, which is the point: the one action that leaves
   the app entirely. Same solid blue in both themes, hover/pressed a step
   darker - the standard Bootstrap variant treatment. */
.btn-onyx { --bs-btn-bg: #3f51a0; --bs-btn-border-color: #3f51a0; --bs-btn-color: #fff;
            --bs-btn-hover-bg: #344385; --bs-btn-hover-border-color: #344385; --bs-btn-hover-color: #fff;
            --bs-btn-active-bg: #2b376d; --bs-btn-active-border-color: #2b376d; --bs-btn-active-color: #fff; }
/* The handover failed - plain danger red for the few seconds the message
   shows. See case-edit.js. */
.btn-onyx.is-onyx-fail { --bs-btn-bg: var(--bs-danger); --bs-btn-border-color: var(--bs-danger); }

.section-item.is-dragging { opacity: .55; }
.section-item.is-dragging .card { outline: 2px dashed currentColor; }

/* While sorting, nothing else should react to the pointer passing over it. */
.row.is-sorting .section-item { transition: transform .12s ease; }
.row.is-sorting .card-body { pointer-events: none; }

/* "Saved." beside the section heading it belongs to. */
.saved-flag {
  font-size: .75rem;
  font-weight: 600;
  color: #2fb36c;
  white-space: nowrap;
}

/* -- edit form -------------------------------------------------------
   Two columns of short fields on anything wider than a phone; long ones
   (the URL, the textareas) get a card body of their own and run full
   width, because a wrapped visualization link is unreadable. */

/* auto-fit rather than a media query, because the card itself may be a
   third of the page, a half, or the whole of it depending on the screen -
   and since sections can be dragged around, no breakpoint knows which.
   The card decides how many columns fit; 13rem is the narrowest a labelled
   control stays readable at. */
.edit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: .9rem 1.1rem;
}

.edit-field + .edit-field { margin-top: .9rem; }

/* Inside the grid the gap already does the spacing. */
.edit-grid .edit-field + .edit-field { margin-top: 0; }

.edit-field .form-label {
  font-size: .8rem;
  font-weight: 500;
  color: var(--bs-secondary-color);
  margin-bottom: .25rem;
}

/* Internal notes are marked in the editor exactly as they are in the read
   view, so the field a client must never see is never in doubt. */
.note-internal-wrap {
  border-left: 3px solid #d98a12;
  padding-left: .85rem;
  margin-top: 1rem;
}

.note-internal-wrap .form-label { color: #d98a12; }

/* Each section saves itself, so the button belongs to the card rather than
   to the page. Kept at the foot of the card it saves, never floating. */
.card-footer {
  background: transparent;
  border-top: 1px solid var(--bs-border-color);
  padding: .7rem 1rem;
}

/* -- dashboard notes -------------------------------------------------
   Each note is its own form. The actions sit under the textarea and stay
   quiet until the note is hovered or focused, so a column of five notes
   reads as notes rather than as a wall of buttons. */

.notes { display: flex; flex-direction: column; }

.note-form + .note-form { margin-top: .85rem; padding-top: .85rem; border-top: 1px solid var(--bs-border-color); }

.note-form textarea { font-size: .875rem; resize: vertical; }

.note-actions {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin-top: .35rem;
  opacity: .35;
  transition: opacity .15s ease;
}

.note-form:hover .note-actions,
.note-form:focus-within .note-actions,
.note-form.is-editing .note-actions { opacity: 1; }

/* Touch has no hover, so never hide the only way to save. */
@media (hover: none) {
  .note-actions { opacity: 1; }
}

.note-when {
  flex: 1 1 auto;
  font-size: .72rem;
  color: var(--bs-secondary-color);
}

/* A saved note reads as text; it only becomes a field once Edit is clicked. */
.note-text {
  font-size: .875rem;
  line-height: 1.5;
  white-space: pre-wrap;   /* keep the line breaks that were typed */
  overflow-wrap: anywhere; /* a pasted address must not widen the panel */
}

/* Addresses inside a note, shown as the host alone. Always visible - unlike
   the edit and delete buttons, this is something you came to the panel to
   click, so it must not wait for a hover. */
.note-link {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  max-width: 100%;
  vertical-align: baseline;
  padding: .05rem .5rem;
  border-radius: 999px;
  font-size: .75rem;
  text-decoration: none;
  border: 1px solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
  color: var(--am-brand);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.note-link i { font-size: .65rem; opacity: .7; flex: none; }

.note-link:hover,
.note-link:focus-visible {
  border-color: var(--am-brand);
  color: var(--am-brand);
}

[data-bs-theme="dark"] .note-link { color: #93b4fd; }
[data-bs-theme="dark"] .note-link:hover,
[data-bs-theme="dark"] .note-link:focus-visible { border-color: #93b4fd; }

/* -- conversation ---------------------------------------------------- */

.chat { display: flex; flex-direction: column; gap: .85rem; }

.chat-msg { max-width: 85%; }

.chat-staff { align-self: flex-end; text-align: right; }

.chat-meta {
  font-size: .72rem;
  color: var(--bs-secondary-color);
  margin-bottom: .2rem;
}

.chat-bubble {
  display: inline-block;
  text-align: left;
  padding: .5rem .8rem;
  border-radius: .7rem;
  font-size: .88rem;
  background: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
}

.chat-staff .chat-bubble {
  background: rgba(37, 99, 235, .12);
  color: var(--bs-body-color);
}

/* Removing a message. Deliberately faint and only on hover: deleting somebody
   else's words should take aim, not be one slip away at all times. It stays
   visible on keyboard focus, or it would be unreachable without a mouse. */
.chat-del {
  border: 0;
  background: transparent;
  color: inherit;
  padding: 0 .2rem;
  line-height: 1;
  opacity: 0;
  transition: opacity .12s ease;
}

.chat-msg:hover .chat-del,
.chat-del:focus-visible { opacity: .7; }
.chat-del:hover { opacity: 1; color: var(--bs-danger); }

/* Writing one. Full width under the thread rather than inside it - it is not
   a message, it is where messages come from. */
.chat-compose {
  align-self: stretch;
  margin-top: .35rem;
  padding-top: .85rem;
  border-top: 1px solid var(--bs-border-color);
}

/* -- cost ------------------------------------------------------------ */

.cost {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
  margin-top: 1rem;
  padding-top: .85rem;
  border-top: 1px solid var(--bs-border-color);
}

.cost-label { font-size: .8rem; color: var(--bs-secondary-color); }

.cost-value {
  font-family: var(--am-display);
  font-weight: 700;
  font-size: 1.25rem;
}

/* -- activity -------------------------------------------------------- */

/* Capped and scrolls inside itself, same reasoning as .history-list. */
.activity { display: flex; flex-direction: column; gap: .7rem; max-height: 22rem; overflow-y: auto; }

.activity-row { border-left: 2px solid var(--bs-border-color); padding-left: .7rem; }

.activity-msg  { font-size: .85rem; }
.activity-meta { font-size: .72rem; color: var(--bs-secondary-color); }

/* The public information pages (/about, /manual) - a readable prose column. */
.page-doc        { max-width: 860px; }
.page-doc-narrow { max-width: 720px; }

/* The manual's floating Contents button, opening the offcanvas TOC. */
.toc-fab { position: fixed; bottom: 1.25rem; left: 1.25rem; z-index: 1040; }

/* The manual opts out of Bootstrap's smooth scrolling - a TOC jump should
   land, not glide four screens. */
.no-smooth { scroll-behavior: auto !important; }

/* -- collapsible info cards (Daniel, 2026-08-06) ----------------------
   Price list, Information for providers and Vigilance fold behind their
   headers and load collapsed. The header is an <a> so Bootstrap collapse
   works with no script of ours; the chevron turns when open. */
.card-collapse-toggle { text-decoration: none; color: inherit; }

.card-collapse-chevron {
  margin-left: auto;
  color: var(--bs-secondary-color);
  transition: transform .2s ease;
}

.card-collapse-toggle:not(.collapsed) .card-collapse-chevron { transform: rotate(180deg); }

/* Body text in those cards reads in one voice - the vigilance hint's
   secondary tone (Daniel, 2026-08-06). */
.price-card tbody td,
.doc-list a { color: var(--bs-secondary-color); }

/* -- success alerts float (Daniel, 2026-08-06) ------------------------
   Pulled to the right over the content, half the screen wide, so a
   confirmation never pushes the page down. Success only: warnings and
   errors stay inline, where the thing they complain about is. */
.alert.alert-success {
  position: fixed;
  top: 4.5rem;              /* clear of the sticky navbar */
  right: 1rem;
  width: 50vw;
  z-index: 1080;
  box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .18);
}

@media (max-width: 767.98px) {
  .alert.alert-success { width: calc(100vw - 2rem); left: 1rem; right: 1rem; }
}

/* A collapsed card ends at its header, and a header only rounds its TOP
   corners - the tint painted square over the card's rounded bottom. While
   the toggle is collapsed the header takes the card's own radius below. */
.card > .card-collapse-toggle.collapsed {
  border-bottom-left-radius: var(--bs-card-inner-border-radius);
  border-bottom-right-radius: var(--bs-card-inner-border-radius);
  border-bottom: 0;
}

/* Stat tiles as links (staff): same card, plus a hand and a lift. */
a.stat-link { text-decoration: none; color: inherit; transition: transform .1s ease, box-shadow .1s ease; }
a.stat-link:hover { transform: translateY(-2px); box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .12) !important; }

/* The planner admin's own share under a stat tile's label. */
.stat-sub { font-size: .78rem; color: var(--bs-secondary-color); margin-top: .1rem; }

/* The copy flag's warning face: the click did nothing, and says so. */
.saved-flag-warn { color: #d98a12; }

/* -- crowded navbar: ellipsis, never a second row (Daniel, 2026-08-06) --
   In the expanded range the item row must not wrap. Flex children refuse
   to shrink below their content without min-width: 0, so with it each
   item gives ground and its label truncates with an ellipsis; the icons
   stay. Below lg the burger menu is vertical and has no crowding. */
@media (min-width: 992px) {
  .navbar-collapse { min-width: 0; }
  .navbar-nav { flex-wrap: nowrap; min-width: 0; }
  .navbar-nav .nav-item { min-width: 0; flex: 0 1 auto; }

  .navbar-nav .nav-link {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* Steps in the staff listing: halves aligned around a fixed middot, so the
   separator sits at the same x down the whole column. */
.steps-cell { display: inline-flex; align-items: baseline; }
.steps-cell > span:first-child { min-width: 2ch; text-align: right; }
.steps-cell > span:last-child  { min-width: 2ch; text-align: left; }
.steps-sep { padding: 0 .35ch; color: var(--bs-secondary-color); }

/* Yes/no icon pills: green check, red cross, each on its alpha tint. */
.yn-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: .4rem;
  font-size: .8rem;
}

.yn-yes { color: #198754; background: rgba(25, 135, 84, .14); }
.yn-no  { color: #dc3545; background: rgba(220, 53, 69, .14); }

/* Click-to-copy table cells (the K Line payload columns): plain text with a
   copy cursor, truncated rather than widening the table. */
.copy-cell {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--bs-secondary-color);
  cursor: copy;
  text-align: left;
  display: inline-block;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.copy-cell:hover { color: var(--bs-link-color); }

/* The K Line columns as copy pills: the icon says "click puts this in the
   clipboard" - the payload itself stays in the hidden span and the title. */
.copy-pill-btn {
  display: inline-flex;
  align-items: center;
  border: 0;
  border-radius: .4rem;
  padding: .1rem .5rem;
  font-size: .78rem;
  /* Close to the surrounding text, not a call to action (Daniel,
     2026-08-06) - the tint alone marks it as clickable. */
  color: var(--bs-secondary-color);
  background: var(--bs-tertiary-bg);
  cursor: copy;
  white-space: nowrap;
}

.copy-pill-btn:hover { color: var(--bs-body-color); }

/* The in-place answer: green Copied, amber Nothing to copy. */
.copy-pill-btn.copy-done { color: #198754; background: rgba(25, 135, 84, .14); }
.copy-pill-btn.copy-warn { color: #d98a12; background: rgba(217, 138, 18, .14); }

/* The OnyxCeph quick-open, beside the project code. Same furniture as the
   copy pills - close to the surrounding text rather than a call to action -
   but it OPENS something rather than copying it, so the cursor says so. */
.onyx-open-btn {
  display: inline-flex;
  align-items: center;
  border: 0;
  border-radius: .4rem;
  padding: .1rem .5rem;
  font-size: .78rem;
  color: var(--bs-secondary-color);
  background: var(--bs-tertiary-bg);
  cursor: pointer;
  white-space: nowrap;
}

.onyx-open-btn:hover { color: var(--bs-body-color); }

/* ⚠ Amber, never red, for every failure here. OnyxCeph not running is a
   state to fix rather than an error, and the app cannot tell success from
   failure anyway - see case-edit.js. */
.onyx-open-btn.onyx-done { color: #198754; background: rgba(25, 135, 84, .14); }
.onyx-open-btn.onyx-warn { color: #d98a12; background: rgba(217, 138, 18, .14); }
