/*
 * DentCare – Component-level Styles
 */

/* 1. Cards and Panels */
.card {
    background-color: var(--bg-card);
    border-radius: var(--radius-m);
    border: 1px solid var(--border-color);
    padding: var(--space-m);
    box-shadow: var(--shadow-soft);
    margin-bottom: var(--space-m);
    transition: var(--transition-clean);
}

.card:hover {
    box-shadow: var(--shadow-medium);
}

.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid var(--border-color-light);
    padding-bottom: var(--space-s);
    margin-bottom: var(--space-s);
}

/* 2. Custom Tables */
.table-container {
    overflow-x: auto;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-m);
    background-color: var(--bg-card);
    box-shadow: var(--shadow-soft);
    margin-bottom: var(--space-m);
}

/* Read by screen readers, invisible on screen. */
.visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Shown only while the browser reports no connection — js/app.js. */
.offline-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 4000;
    padding: 8px 16px;
    background-color: var(--color-warning-light);
    border-bottom: 1px solid color-mix(in srgb, var(--color-warning) 40%, transparent);
    color: var(--text-primary);
    font-size: 14px;
    font-weight: 600;
    text-align: center;
}

/* A list that failed to load — see renderTableLoadError() in js/app.js. */
.table-load-error {
    text-align: center;
    color: var(--color-danger);
    font-weight: 600;
}

.table-load-error .btn-sm {
    margin-left: 8px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TABLES — read like a statement, not a spreadsheet screenshot
   ═══════════════════════════════════════════════════════════════════════════

   These are the screens the clinic works from all day: appointments, bills,
   stock. Everything below is about making a long list scannable by somebody
   who is mid-conversation with a patient at the desk.

   border-collapse is SEPARATE, not collapse, and that is load-bearing rather
   than a preference. A collapsed border belongs to the table, not the cell, so
   a position:sticky heading row scrolls away from its own underline and the
   frozen column loses its edge — in Chrome the border simply vanishes under
   the scrolled content. With border-spacing:0 the layout is identical to
   collapse, and each cell keeps the border it draws.
   ------------------------------------------------------------------------ */

table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    text-align: left;
    font-size: 14px;
}

/* Row height is one variable so the density control has a single thing to
   change, and every table changes together. */
:root { --row-pad: 16px; }

th {
    background-color: #f8fafc;
    color: var(--text-primary);
    font-weight: 700;
    padding: var(--row-pad);
    border-bottom: 1px solid var(--border-color);
    white-space: nowrap;
}

td {
    padding: var(--row-pad);
    border-bottom: 1px solid var(--border-color-light);
    color: var(--text-secondary);
}

tbody tr:last-child td { border-bottom: none; }

tbody tr:hover td,
tbody tr:focus-within td {
    background-color: #f3f7f9;
    box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-accent) 16%, transparent),
                inset 0 -1px 0 color-mix(in srgb, var(--color-accent) 16%, transparent);
}

/* A row needs one stable visual anchor, not a transform on every cell. The
   inset rule and shallow elevation make the active line easy to follow while
   sticky columns and native scrolling keep their geometry unchanged. */
.table-container tbody tr:hover td:first-child,
.table-container tbody tr:focus-within td:first-child {
    box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-accent) 16%, transparent),
                inset 0 -1px 0 color-mix(in srgb, var(--color-accent) 16%, transparent),
                5px 0 12px rgba(15, 43, 38, 0.08);
}

/* ── Numbers line up like a bank statement ──────────────────────────────────
   Right-aligned so the units, tens and hundreds columns agree down the page,
   and tabular figures so a 1 occupies the same width as a 9. Without both,
   comparing two amounts means reading them rather than seeing them. Promoted
   from .bill-table, where this already existed and was doing the same job. */
.num,
td.num,
th.num {
    text-align: right;
    white-space: nowrap;           /* an amount must never wrap mid-figure */
    font-variant-numeric: tabular-nums;
}

/* ── An empty cell says so ──────────────────────────────────────────────────
   A blank cell is ambiguous: nothing recorded, zero, or still loading all look
   identical. A dash says "we looked, there is nothing here". Done in CSS so it
   applies to every table without editing a dozen row builders, each of which
   would be a chance to miss one. */
td:empty::after {
    content: "—";
    color: var(--text-muted);
}

/* ── Long text truncates; numbers never do ─────────────────────────────────
   Opt-in, because truncation has to be a decision per column. A clipped name
   is recoverable — the full text is in the title tooltip — but a clipped
   amount is a different number, which is why .num above is nowrap and is
   never given this class. */
.truncate {
    /* inline-block, not the default inline. max-width, overflow and
       text-overflow have no effect on a non-replaced inline element, so as a
       plain <span> this class silently did nothing and a long name ran the
       full width of its column. It looked like a styling preference that had
       been ignored; it was a property that does not apply. */
    display: inline-block;
    max-width: 22ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;        /* keeps the baseline with its neighbours */
}

/* ── The heading stays put ──────────────────────────────────────────────────
   Only useful once the container can actually scroll vertically, hence the
   max-height. It is generous on purpose: a short table never reaches it and
   behaves exactly as before, so this costs nothing on the screens that do not
   need it. */
.table-container { max-height: 72vh; }

thead th {
    position: sticky;
    top: 0;
    z-index: 2;
}

/* ── The first column stays put while the rest scrolls sideways ─────────────
   Opt-in with data-freeze on the container, because it is only worth it where
   the first column is the row's identity — a patient's name, an invoice. On a
   narrow phone these tables scroll horizontally, and without this the reader
   scrolls the amount into view and loses the name it belongs to.

   The background is not optional: a sticky cell is transparent by default and
   the scrolled content would run underneath it. */
/* The frozen column is capped, and that cap is the whole point on a phone.
   Left to size itself, one long patient name made this column as wide as the
   viewport — so the name stayed pinned and there was nothing left to scroll
   INTO view beside it. A frozen column that fills the screen is just a narrow
   table. 44% keeps the identity readable and leaves the majority for the data
   the reader is scrolling to reach. */
.table-container[data-freeze] tbody td:first-child,
.table-container[data-freeze] thead th:first-child {
    max-width: 44vw;
    overflow: hidden;
    text-overflow: ellipsis;
}

.table-container[data-freeze] tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background-color: var(--bg-card);
    border-right: 1px solid var(--border-color-light);
}

.table-container[data-freeze] thead th:first-child {
    position: sticky;
    left: 0;
    z-index: 3;                    /* above both the frozen column and the head */
    background-color: #f8fafc;
    border-right: 1px solid var(--border-color);
}

/* The hover tint has to be repainted on the frozen cell too, or the row
   highlights everywhere except the column the eye is actually following. */
.table-container[data-freeze] tbody tr:hover td:first-child,
.table-container[data-freeze] tbody tr:focus-within td:first-child {
    background-color: #f3f7f9;
}

/* An empty-state row spans every column, so freezing its first cell would pin
   the message to the left edge and slide the rest of the sentence away. */
.table-container[data-freeze] tbody td[colspan] {
    position: static;
    background-color: transparent;
    border-right: 0;
}

/* ── Row actions ───────────────────────────────────────────────────────────
   Revealed on hover, and ONLY where hovering is possible. A touch screen has
   no hover state, so hiding these unconditionally would leave the front desk
   on a tablet unable to approve anything — the buttons would simply never
   appear. Keyboard focus reveals them too, or they would be unreachable by
   anyone tabbing through. */
@media (hover: hover) and (pointer: fine) {
    .table-actions {
        opacity: 0;
        transition: opacity .12s ease;
    }

    tbody tr:hover .table-actions,
    tbody tr:focus-within .table-actions {
        opacity: 1;
    }
}

/* Somebody who asked for less motion still gets the reveal, just without the
   fade — the information is not the animation. */
@media (prefers-reduced-motion: reduce) {
    .table-actions { transition: none; }
}

/* 3. Status Badges */
.badge {
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border-radius: 9999px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.badge-pending   { background-color: var(--color-warning-light); color: var(--color-warning); }
.badge-approved  { background-color: var(--color-accent-light); color: var(--color-accent); }
.badge-completed { background-color: var(--color-success-light); color: var(--color-success); }
.badge-rejected  { background-color: var(--color-danger-light); color: var(--color-danger); }
.badge-cancelled { background-color: #f1f5f9; color: #64748b; }

/* Added 2026-08-29 with the no-show / expiry rules.
   The status string is lower-cased into the class name, so "No-Show" becomes
   .badge-no-show — both spellings are defined because the sweep writes
   "Expired" and a person clicking the button writes "No-Show".
   Amber, not red: a missed appointment is a fact to act on, not a failure. */
.badge-expired   { background-color: var(--color-warning-light); color: var(--color-warning); }
.badge-no-show   { background-color: var(--color-warning-light); color: var(--color-warning); }

/* Queue states, added 2026-08-29 with the walk-in flow. The status string is
   lower-cased and spaces become hyphens, so "Awaiting Consultation" needs
   .badge-awaiting-consultation. */
.badge-awaiting-consultation { background-color: var(--color-accent-light); color: var(--color-accent); }
.badge-in-consultation       { background-color: var(--color-teal-light); color: var(--color-teal-hover); }

.badge-paid      { background-color: var(--color-success-light); color: var(--color-success); }
.badge-unpaid    { background-color: var(--color-danger-light); color: var(--color-danger); }

/* 4. Interactive SVG Dental Charting Styles */
.dental-chart-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-m);
    margin: var(--space-m) 0;
}

.jaw-arch {
    display: flex;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
    background-color: #ffffff;
    padding: var(--space-s);
    border-radius: var(--radius-m);
    border: 1px solid var(--border-color);
    width: 100%;
}

.tooth-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    cursor: pointer;
    transition: var(--transition-clean);
    user-select: none;
    position: relative;
}

.tooth-item:hover {
    transform: scale(1.08);
}

.tooth-svg-wrapper {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-s);
    border: 1px solid var(--border-color);
    background-color: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: var(--transition-clean);
}

.tooth-item:hover .tooth-svg-wrapper {
    border-color: var(--color-accent);
    box-shadow: 0 0 8px rgba(2, 132, 199, 0.2);
}

/* Tooth status colors mapped on the SVG fill class */
.tooth-svg {
    width: 36px;
    height: 36px;
}

.tooth-svg path {
    fill: #e2e8f0; /* Default healthy/untreated shade */
    stroke: var(--text-primary);
    stroke-width: 1.5;
    transition: fill var(--transition-clean);
}

/* Color codes */
.tooth-healthy .tooth-svg path { fill: #f8fafc !important; }
.tooth-decayed .tooth-svg path { fill: #fca5a5 !important; } /* Red */
.tooth-missing .tooth-svg path { fill: #cbd5e1 !important; fill-opacity: 0.2 !important; stroke-dasharray: 2,2 !important; } /* Dotted gray */
.tooth-filled .tooth-svg path { fill: #6ee7b7 !important; } /* Mint Green */
.tooth-crowned .tooth-svg path { fill: #fde047 !important; } /* Golden yellow */
.tooth-bridge .tooth-svg path { fill: #c084fc !important; } /* Purple */

.tooth-number-label {
    font-size: 11px;
    font-weight: 700;
    color: var(--text-primary);
    margin-top: 4px;
}

.tooth-legend {
    display: flex;
    gap: var(--space-s);
    flex-wrap: wrap;
    margin-top: var(--space-s);
    justify-content: center;
}

.legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    font-weight: 600;
}

.legend-dot {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    border: 1px solid var(--text-primary);
}

/* 5. Modals and Overlays */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(15, 43, 38, 0.4);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2000;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-clean);
}

.modal-overlay.active {
    opacity: 1;
    pointer-events: auto;
}

.modal-card {
    background-color: var(--bg-card);
    border-radius: var(--radius-l);
    width: 90%;
    max-width: 550px;
    box-shadow: var(--shadow-large);
    border: 1px solid var(--border-color);
    overflow: hidden;
    transform: translateY(20px);
    transition: transform var(--transition-clean);
}

.modal-overlay.active .modal-card {
    transform: translateY(0);
}

/* The header used to paint white text on var(--bg-sidebar). That token is
   white, so every modal title in the app was invisible. It now reads as a
   plain titled bar. */
.modal-header {
    background-color: var(--bg-subtle);
    border-bottom: 1px solid var(--border-color);
    padding: var(--space-s) var(--space-m);
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-s);
}

.modal-header h3 {
    color: var(--text-primary);
    font-size: 17px;
    margin: 0;
}

/* 44px tap target (2026-09-13): the glyph alone was about 20px, hard to hit
   on a phone. The negative margin keeps the header the height it was. */
.modal-close-btn {
    background: none;
    color: var(--text-muted);
    border: none;
    padding: 0;
    min-width: 44px;
    min-height: 44px;
    margin: -10px -12px -10px 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    cursor: pointer;
    line-height: 1;
}

/* Was #ffffff — on the light modal header the × vanished under the pointer. */
.modal-close-btn:hover {
    color: var(--text-primary);
    transform: none;
}

.modal-body {
    padding: var(--space-m);
    max-height: 75vh;
    overflow-y: auto;
}

.modal-footer {
    padding: var(--space-s) var(--space-m);
    border-top: 1px solid var(--border-color-light);
    display: flex;
    justify-content: flex-end;
    gap: var(--space-s);
}

/* Opt-in right edge for rows with a real Actions column. Keep it bounded so
   the pinned patient/item identity and the middle can coexist on a phone. */
.table-container[data-pin-actions] tbody td:last-child,
.table-container[data-pin-actions] thead th:last-child {
    box-sizing: border-box;
    width: 132px;
    min-width: 132px;
    max-width: 132px;
    white-space: normal;
    border-left: 1px solid var(--border-color-light);
}

/* Both edges leave actual space for the scrollable middle. The old 22vw cap
   made a phone's identity cell about 70px wide and overflow-wrap:anywhere then
   split ordinary names letter by letter. This bounded width keeps the row's
   context recognizable and only wraps at real word boundaries. */
.table-container[data-freeze][data-pin-actions] tbody td:first-child,
.table-container[data-freeze][data-pin-actions] thead th:first-child {
    width: clamp(120px, 34vw, 220px);
    min-width: clamp(120px, 34vw, 220px);
    max-width: clamp(120px, 34vw, 220px);
    white-space: normal;
    overflow-wrap: normal;
    word-break: normal;
    hyphens: none;
    text-overflow: clip;
}

.table-container[data-pin-actions] tbody td:last-child {
    position: sticky;
    right: 0;
    z-index: 1;
    background-color: var(--bg-card);
}

.table-container[data-pin-actions] thead th:last-child {
    position: sticky;
    right: 0;
    z-index: 3;
    background-color: #f8fafc;
}

.table-container[data-pin-actions] tbody tr:hover td:last-child,
.table-container[data-pin-actions] tbody tr:focus-within td:last-child {
    background-color: #f3f7f9;
}

.table-container[data-pin-actions] tbody td[colspan] {
    position: static;
    max-width: none;
    background-color: transparent;
    border-left: 0;
}

/* Browser zoom turns a 375px phone into roughly 188 CSS pixels. At that
   effective width the normal action column can collide with the frozen row
   identity. Keep both anchors distinct and stack their controls; the middle
   remains horizontally scrollable and every control keeps its touch height. */
@media (max-width: 240px) {
    .table-container[data-freeze][data-pin-actions] tbody td:first-child,
    .table-container[data-freeze][data-pin-actions] thead th:first-child {
        width: 60px;
        min-width: 60px;
        max-width: 60px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .table-container[data-pin-actions] tbody td:last-child,
    .table-container[data-pin-actions] thead th:last-child {
        width: 86px;
        min-width: 86px;
        max-width: 86px;
        padding-left: 6px;
        padding-right: 6px;
    }

    .table-container[data-pin-actions] tbody td:last-child button,
    .table-container[data-pin-actions] tbody td:last-child .btn {
        display: block;
        width: 100%;
        min-height: 44px;
        margin: 4px 0;
        padding: 6px 3px;
        white-space: normal;
    }
}

.modal-overlay[hidden] { display: none; }

.approval-review-card { max-width: 560px; }
.approval-review-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; margin: 18px 0 0; }
.approval-review-details > div { min-width: 0; padding: 12px 14px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--bg-subtle); }
.approval-review-details > div:first-child, .approval-review-details > div:nth-child(4), .approval-review-details > div:last-child { grid-column: 1 / -1; }
.approval-review-details > div[hidden] { display: none; }
.approval-review-details dt { margin-bottom: 5px; color: var(--text-secondary); font-size: 12px; font-weight: 600; }
.approval-review-details dd { margin: 0; color: var(--text-primary); font-size: 14px; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap; }
#modal-approval-review button[hidden] { display: none; }
#modal-approval-review .modal-footer[hidden] { display: none; }
#modal-approval-review .modal-close-btn {
    margin: 0;
    border: 1px solid color-mix(in srgb, var(--color-danger) 28%, transparent);
    border-radius: 50%;
    background: var(--color-danger-light);
    color: var(--color-danger);
    transition: background-color 150ms ease, border-color 150ms ease;
}
#modal-approval-review .modal-close-btn:hover {
    background: color-mix(in srgb, var(--color-danger-light) 74%, var(--color-danger));
    border-color: var(--color-danger);
    color: #8e241b;
}
#modal-approval-review .modal-close-btn:focus-visible {
    outline: 3px solid var(--color-danger);
    outline-offset: 2px;
}
.appt-detail-row { cursor: pointer; }
.appt-detail-row:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.appt-detail-row:hover td { background-color: #f3f7f9; }
@media (max-width: 480px) { .approval-review-details { grid-template-columns: minmax(0, 1fr); } }

/* Compact appointment actions share one rhythm. Visible icons never replace
   accessible names, and every control keeps a full 44px touch target. */
.appt-action-set {
    display: inline-flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 6px;
    white-space: nowrap;
}

/* Three or four actions (the dentist's Approved row) form a 2-by-2 block. */
.appt-action-set--grid {
    display: inline-grid;
    grid-template-columns: repeat(2, max-content);
}

.appt-action-set--grid > .appt-action { width: 100%; }

.cell-nowrap { white-space: nowrap; }

.appt-action-note {
    display: block;
    margin-bottom: 4px;
    font-size: 11px;
    color: var(--text-muted);
    white-space: normal;
}

/* Appointment tables size their pinned Actions column to the controls it
   holds, so the pair sits side by side instead of wrapping into a stack. */
@media (min-width: 241px) {
    .appt-table-container[data-pin-actions] tbody td:last-child,
    .appt-table-container[data-pin-actions] thead th:last-child {
        width: 1%;
        min-width: 0;
        max-width: none;
        white-space: nowrap;
        padding-inline: 12px;
    }

    .appt-table-container[data-pin-actions] tbody td[colspan] { white-space: normal; }
}

/* On a laptop the seven appointment columns overflowed their card by about
   140px, so the pinned Actions sat over Status. Tighter side padding and a
   slightly narrower name column let the whole row fit at common widths;
   row height (the density setting) is untouched. */
@media (min-width: 901px) {
    .appt-table-container th,
    .appt-table-container td { padding-inline: 12px; }

    /* Two-word headings may take two lines; the data under them decides.
       A phone number or "34 yrs / F" is one token and never splits. */
    .appt-table-container thead th:not(:last-child) { white-space: normal; }

    .appt-table-container[data-freeze][data-pin-actions] tbody td:first-child,
    .appt-table-container[data-freeze][data-pin-actions] thead th:first-child {
        width: 180px;
        min-width: 180px;
        max-width: 180px;
    }
}

.appt-icon-action {
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    display: inline-grid;
    place-items: center;
    padding: 0;
    border-radius: var(--radius-s);
    line-height: 1;
    box-shadow: 0 1px 2px rgba(15, 43, 38, 0.08);
}

.appt-icon-action svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; }
.appt-icon-action:hover { box-shadow: 0 3px 8px rgba(15, 43, 38, 0.13); }
.appt-icon-action--approve { background: var(--color-success-light); color: var(--color-success); border: 1px solid color-mix(in srgb, var(--color-success) 34%, transparent); }
.appt-icon-action--approve:hover { background: color-mix(in srgb, var(--color-success-light) 78%, var(--color-success)); }
.appt-icon-action--reject { background: var(--color-danger-light); color: var(--color-danger); border: 1px solid color-mix(in srgb, var(--color-danger) 34%, transparent); }
.appt-icon-action--reject:hover { background: color-mix(in srgb, var(--color-danger-light) 80%, var(--color-danger)); }

button.appt-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 44px;
    height: 44px;
    padding: 0 12px;
    border: 1px solid transparent;
    border-radius: var(--radius-s);
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    box-shadow: 0 1px 2px rgba(15, 43, 38, 0.08);
}

button.appt-action:hover { box-shadow: 0 3px 8px rgba(15, 43, 38, 0.13); }
.appt-action .icon { width: 16px; height: 16px; flex-shrink: 0; }

button.appt-action--accent { background-color: var(--color-accent-light); color: var(--color-accent); border-color: color-mix(in srgb, var(--color-accent) 32%, var(--border-color)); }
button.appt-action--accent:hover { background-color: color-mix(in srgb, var(--color-accent-light) 80%, var(--color-accent)); border-color: var(--color-accent); color: var(--color-accent-hover); }
button.appt-action--success { background-color: var(--color-success-light); color: var(--color-success); border-color: color-mix(in srgb, var(--color-success) 34%, var(--border-color)); }
button.appt-action--success:hover { background-color: color-mix(in srgb, var(--color-success-light) 78%, var(--color-success)); border-color: var(--color-success); }
button.appt-action--danger { background-color: var(--color-danger-light); color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 34%, var(--border-color)); }
button.appt-action--danger:hover { background-color: color-mix(in srgb, var(--color-danger-light) 80%, var(--color-danger)); border-color: var(--color-danger); }
button.appt-action--neutral { background-color: var(--bg-subtle); color: var(--text-primary); border-color: var(--border-color); }
button.appt-action--neutral:hover { background-color: var(--bg-canvas); border-color: var(--text-muted); }

/* At about 200% zoom on a phone (188 CSS px) the column is too narrow for
   two squares side by side; stack them, each keeping its full touch size. */
@media (max-width: 240px) {
    .appt-action-set,
    .appt-action-set--grid { display: flex; flex-direction: column; align-items: stretch; width: 100%; }
}

/* Compact, labelled record actions. These share the appointment button's
   touch size and accent language without forcing several controls into one
   narrow pinned cell. The inventory's Stock and Delete columns stay separate. */
button.record-action {
    min-height: 44px;
    padding: 8px 11px;
    white-space: nowrap;
    font-size: 13px;
    line-height: 1.2;
    box-shadow: 0 1px 2px rgba(15, 43, 38, 0.08);
}

.record-action .icon { width: 16px; height: 16px; }
button.record-action:hover { box-shadow: 0 3px 8px rgba(15, 43, 38, 0.13); }
button.record-action--accent { background: var(--color-accent-light); color: var(--color-accent); border-color: color-mix(in srgb, var(--color-accent) 32%, var(--border-color)); }
button.record-action--accent:hover { background: color-mix(in srgb, var(--color-accent-light) 80%, var(--color-accent)); color: var(--color-accent-hover); border-color: var(--color-accent); }
button.record-action--neutral { background: var(--bg-subtle); color: var(--text-primary); border-color: var(--border-color); }
button.record-action--neutral:hover { background: var(--bg-canvas); border-color: var(--text-muted); }
button.record-action--danger { background: var(--color-danger-light); color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 34%, var(--border-color)); }
button.record-action--danger:hover { background: color-mix(in srgb, var(--color-danger-light) 80%, var(--color-danger)); border-color: var(--color-danger); }

/* Manage Schedule only: two distinct, labelled actions must stay together in
   its pinned cell. The calendar data and every other table keep their styles. */
.schedule-action-set { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
button.schedule-action {
    width: 44px;
    min-width: 44px;
    height: 44px;
    min-height: 44px;
    padding: 0;
    border-radius: var(--radius-s);
    box-shadow: 0 1px 2px rgba(15, 43, 38, 0.08);
}
.schedule-action .icon { width: 18px; height: 18px; }
button.schedule-action--edit { background: var(--color-accent-light); color: var(--color-accent); border-color: color-mix(in srgb, var(--color-accent) 32%, var(--border-color)); }
button.schedule-action--edit:hover { background: color-mix(in srgb, var(--color-accent-light) 80%, var(--color-accent)); border-color: var(--color-accent); }
button.schedule-action--delete { background: var(--color-danger-light); color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 34%, var(--border-color)); }
button.schedule-action--delete:hover { background: color-mix(in srgb, var(--color-danger-light) 80%, var(--color-danger)); border-color: var(--color-danger); }
.schedule-table-container[data-pin-actions] tbody td:last-child,
.schedule-table-container[data-pin-actions] thead th:last-child { padding-inline: 12px; }
@media (max-width: 240px) {
    .schedule-action-set { width: 100%; flex-direction: column; gap: 4px; }
}

.inventory-table {
    min-width: 0;
    table-layout: fixed;
}

.inventory-table th,
.inventory-table td { padding: 10px 6px; }

.inventory-table-container[data-freeze] tbody td:first-child,
.inventory-table-container[data-freeze] thead th:first-child {
    width: 15%;
    min-width: 0;
    max-width: none;
    white-space: normal;
    overflow-wrap: anywhere;
    word-break: normal;
}
.inventory-table th:nth-child(2), .inventory-table td:nth-child(2) { width: 12%; }
.inventory-table th:nth-child(3), .inventory-table td:nth-child(3) { width: 10%; }
.inventory-table th:nth-child(4), .inventory-table td:nth-child(4) { width: 9%; }
.inventory-table th:nth-child(5), .inventory-table td:nth-child(5) { width: 16%; }
.inventory-table th:nth-child(6), .inventory-table td:nth-child(6) { width: 27%; }
.inventory-table th:nth-child(7), .inventory-table td:nth-child(7) { width: 11%; }

.inventory-table .badge { padding-inline: 8px; letter-spacing: 0.03em; }
.inventory-table button.record-action { padding-inline: 8px; }

.inventory-stock-actions { min-width: 0; }
.inventory-action-group { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.inventory-actions { min-width: 0; white-space: nowrap; }

/* The table's actual available width decides when its seven columns become
   cards. This also covers a desktop with the sidebar open. */
.inventory-table-container { container: inventory-list / inline-size; }
@media (min-width: 601px) {
@container inventory-list (max-width: 850px) {
    .inventory-table,
    .inventory-table tbody {
        display: block;
        width: 100%;
    }

    .inventory-table thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    .inventory-table tbody tr {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 6px 12px;
        margin: 0 0 8px;
        padding: 14px 16px;
        border: 1px solid var(--border-color);
        border-radius: 12px;
        background: #ffffff;
    }

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

    .inventory-table tbody td:nth-child(n) {
        display: block;
        width: auto;
        min-width: 0;
        padding: 4px 0;
        border: 0;
        overflow-wrap: anywhere;
    }

    .inventory-table-container[data-freeze] tbody td:first-child {
        grid-column: 1 / 3;
        grid-row: 1;
        width: auto;
        min-width: 0;
        max-width: none;
        position: static;
        background: transparent;
        border-right: 0;
    }

    .inventory-table tbody td:nth-child(2) {
        grid-column: 3;
        grid-row: 1;
        justify-self: end;
        align-self: start;
    }

    .inventory-table tbody td:nth-child(3),
    .inventory-table tbody td:nth-child(4),
    .inventory-table tbody td:nth-child(5) { grid-row: 2; }

    .inventory-table tbody td:nth-child(3)::before,
    .inventory-table tbody td:nth-child(4)::before,
    .inventory-table tbody td:nth-child(5)::before {
        display: block;
        color: var(--text-muted);
        font-size: 11px;
        font-weight: 600;
    }

    .inventory-table tbody td:nth-child(3)::before { content: "In stock"; }
    .inventory-table tbody td:nth-child(4)::before { content: "Min level"; }
    .inventory-table tbody td:nth-child(5)::before { content: "Status"; }

    .inventory-table tbody td:nth-child(6) {
        grid-column: 1 / 3;
        grid-row: 3;
        padding-top: 10px;
        border-top: 1px solid var(--border-color-light);
    }

    .inventory-table tbody td:nth-child(7) {
        grid-column: 3;
        grid-row: 3;
        justify-self: end;
        padding-top: 10px;
        border-top: 1px solid var(--border-color-light);
    }
}
}
.billing-state { min-width: 164px; white-space: nowrap; }
.billing-balance { display: block; margin-top: 5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.billing-actions { text-align: center; }

/* At extreme browser zoom the inventory viewport can be narrower than its
   first cell plus one action. Let the first column scroll away there so the
   stock and Delete controls can actually be reached; the header still sticks
   vertically. Normal phone/tablet widths keep the item name pinned. */
@media (max-width: 240px) {
    .inventory-table-container[data-freeze] tbody td:first-child { position: static; }
    .inventory-table-container[data-freeze] thead th:first-child { position: sticky; top: 0; left: auto; z-index: 2; }
}

.staff-intake-card {
    box-sizing: border-box;
    width: 100%;
    padding: clamp(28px, 4.5vw, 60px);
    background: #fff;
}
.staff-intake-intro { margin: 0 0 28px; }
.staff-intake-card .auth-form { width: 100%; }
.staff-intake-footer { align-items: stretch; }
.staff-intake-footer > .btn-secondary { flex: 0 0 auto !important; }
.staff-intake-actions { display: flex; flex-wrap: wrap; gap: 12px; flex: 2; min-width: 0; }
.staff-intake-actions button { flex: 1 1 230px; min-height: 48px; padding: 12px 16px; }
.staff-saved-patient { padding: 24px; border: 1px solid var(--border-color); border-radius: var(--radius-s); background: #fff; }
.staff-saved-patient p { margin: 12px 0 20px; }
.staff-saved-patient button[hidden] { display: none; }
@media (max-width: 700px) {
    .staff-intake-footer { flex-direction: column; }
    .staff-intake-footer > .btn-secondary { flex-basis: auto !important; }
    .staff-intake-actions { flex-direction: column; }
    .staff-intake-actions button { flex-basis: auto; width: 100%; }
    .staff-intake-card .steps { gap: 6px; }
    .staff-intake-card .step { min-width: 0; font-size: 12px; }
}
/* 6. Toast Notifications */
.toast-container {
    position: fixed;
    bottom: var(--space-m);
    right: var(--space-m);
    z-index: 3000;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.toast {
    background-color: var(--bg-card);
    /* A thin border tinted to the message type. Not a thick bar down the left
       (removed 2026-09-13, the templated "side-tab" look): the coloured label
       below already says what kind of message this is. */
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    padding: 14px 18px;
    border-radius: var(--radius-s);
    box-shadow: var(--shadow-medium);
    font-size: 14px;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-s);
    min-width: 280px;
    animation: toastSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes toastSlideIn {
    from {
        opacity: 0;
        transform: translateY(20px) scale(0.95);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.toast-success { border-color: color-mix(in srgb, var(--color-success) 40%, var(--border-color)); }
.toast-warning { border-color: color-mix(in srgb, var(--color-warning) 40%, var(--border-color)); }
.toast-error   { border-color: color-mix(in srgb, var(--color-danger) 40%, var(--border-color)); }

/* The small word above the message ("Done", "Problem"…) that replaced the
   emoji the toast used to lead with. */
.toast-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.toast-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.toast-success .toast-label { color: var(--color-success); }
.toast-warning .toast-label { color: var(--color-warning); }
.toast-error   .toast-label { color: var(--color-danger); }

.toast-message {
    font-weight: 500;
    line-height: 1.5;
}

.toast-close {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 20px;
    line-height: 1;
    padding: 0 0 0 4px;
}
.toast-close:hover {
    color: var(--text-primary);
    transform: none;
}


/* Patient/procedure summary strip at the top of a clinical modal */
.summary-strip {
    margin-bottom: var(--space-s);
    padding: 12px 14px;
    border: 1px solid color-mix(in srgb, var(--color-teal) 30%, transparent);
    background-color: var(--color-teal-light);
    border-radius: var(--radius-s);
    color: var(--text-primary);
    font-size: 14px;
    line-height: 1.6;
}


/* ---------------------------------------------------------------------------
   SVG ICONS
   Used on every screen — the landing page, the booking flow and all three
   dashboards — so these live here rather than in one page's stylesheet.
   The sprite itself is templates/partials/icon-sprite.php; draw one with
   icon('name') in PHP, or <svg class="icon"><use href="#ic-name"></use></svg>.
   Icons take their colour from the surrounding text.
   ------------------------------------------------------------------------ */

.icon-sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

.icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.icon--lg { width: 24px; height: 24px; }

/* ---------------------------------------------------------------------------
   SHARED BUTTON MODIFIERS
   ---------------------------------------------------------------------------
   base.css styles the bare <button> element, which is what gives every control
   in the app its fill, padding and radius. These two modifiers change that
   base and are used well outside the landing page -- the booking screen and
   the walk-in modal both rely on them.

   They lived in css/landing.css as global rules until that file was scoped to
   `.landing` to stop it restyling the dashboards. Scoping was right, but it
   took these with it and left an outline button rendering as a solid one. They
   are not landing components; they belong here, where every page can see them.

   Deliberately expressed in the DASHBOARD tokens, not the landing palette: a
   modal button should look like the app it opens over, and the landing page
   overrides both of these anyway at higher specificity.
--------------------------------------------------------------------------- */
.btn--outline {
    background-color: transparent;
    border: 1px solid var(--border-color);
    color: var(--text-primary);
}

.btn--outline:hover {
    border-color: var(--color-accent);
    color: var(--color-accent);
    background-color: transparent;
}

.btn--block {
    display: flex;
    width: 100%;
    justify-content: center;
}


/* A button that reads as a link. For an action offered inside a sentence,
   where a real button would break the line and overstate the choice. */
.btn-link {
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-accent);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

.btn-link:hover { color: var(--color-accent-hover); }


/* ── Row density ────────────────────────────────────────────────────────────
   Three heights, set once on <body> so every table in the app agrees. A staff
   member clearing a morning's bookings wants as many rows on screen as will
   fit; the same person checking one patient's history wants room to read. That
   is a preference about the person, not about the table, which is why it is
   one setting rather than a control per screen.

   Driven through --row-pad so the padding stays the single source of the row
   height, and nothing has to hard-code three sets of numbers.
   ------------------------------------------------------------------------ */

body[data-density="compact"]     { --row-pad: 6px  14px; }
body[data-density="default"]     { --row-pad: 10px 16px; }
body[data-density="comfortable"] { --row-pad: 14px 18px; }

.density {
    display: inline-flex;
    padding: 3px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-s);
    background-color: var(--bg-subtle);
}

.density__btn {
    padding: 5px 11px;
    border: 0;
    border-radius: calc(var(--radius-s) - 1px);
    background: transparent;
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.3;
    cursor: pointer;
    white-space: nowrap;
}

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

.density__btn[aria-pressed="true"] {
    background-color: var(--bg-card);
    color: var(--text-primary);
    font-weight: 600;
    box-shadow: var(--shadow-small);
}

/* Approve Bookings filter pills: each shows its live count and which one is
   selected (aria-pressed), so "Pending 3" answers the question before a click. */
.appt-filter-pills { display: flex; gap: 6px; flex-wrap: wrap; }
button.appt-filter {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 40px;
    padding: 6px 12px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background-color: var(--bg-subtle);
    color: var(--text-primary);
    font-size: 13px;
    font-weight: 600;
}
button.appt-filter--warning { background-color: var(--color-warning-light); color: var(--color-warning); }
button.appt-filter--accent { background-color: var(--color-accent-light); color: var(--color-accent); }
button.appt-filter--success { background-color: var(--color-success-light); color: var(--color-success); }
button.appt-filter[aria-pressed="true"] { border-color: currentColor; box-shadow: inset 0 0 0 1px currentColor; }
.appt-filter .filter-count {
    min-width: 22px;
    padding: 0 6px;
    border-radius: 999px;
    background-color: #ffffff;
    font-size: 12px;
    line-height: 20px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* Inventory expiry line under the stock status (js/inventory.js). Colour
   follows the state; the words carry it too, never colour alone. */
button.inventory-expiry {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 44px;
    margin-top: 2px;
    padding: 0 6px 0 0;
    border: none;
    background: none;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
    text-underline-offset: 3px;
}
button.inventory-expiry:hover { color: var(--color-accent); background: none; }
.inventory-expiry .icon { width: 14px; height: 14px; }
button.inventory-expiry--ok { color: var(--text-secondary); }
button.inventory-expiry--soon { color: var(--color-warning); }
button.inventory-expiry--expired { color: var(--color-danger); }

/* In-app dialog (appDialog() in js/app.js). Replaces every browser
   alert/confirm/prompt, so it sits above other modals, the notification
   panel and the mobile menu. */
.modal-overlay.app-dialog { z-index: 6000; }
.app-dialog__card { max-width: 460px; }
.app-dialog__card--danger .modal-header { border-top: 3px solid var(--color-danger); }
.app-dialog__body label { display: block; margin-top: 12px; }
.app-dialog__body textarea { resize: vertical; }
.app-dialog__message {
    margin: 0;
    color: var(--text-primary);
    font-size: 15px;
    line-height: 1.55;
    white-space: pre-line;          /* keeps the paragraph breaks in the question */
    overflow-wrap: anywhere;
}
.app-dialog__error { margin: 10px 0 0; color: var(--color-danger); font-size: 13px; font-weight: 600; }
.app-dialog__error[hidden] { display: none; }
.app-dialog .modal-footer button { min-height: 44px; min-width: 96px; }
@media (max-width: 480px) {
    .app-dialog .modal-footer { flex-direction: column-reverse; }
    .app-dialog .modal-footer button { width: 100%; }
}

/* Typed confirmation inside the in-app dialog (e.g. RESTORE). A highlighted
   panel so the one thing to do is obvious: the exact word is shown, letters
   appear in capitals, and a tick appears once it matches. The confirm button
   stays disabled until then. */
.app-dialog__confirm-box {
    margin-top: 16px;
    padding: 14px;
    border: 1px solid color-mix(in srgb, var(--color-danger) 30%, var(--border-color));
    border-radius: 10px;
    background-color: var(--color-danger-light);
    transition: border-color var(--transition-clean), background-color var(--transition-clean);
}
.app-dialog__confirm-box.is-match {
    border-color: color-mix(in srgb, var(--color-success) 45%, var(--border-color));
    background-color: var(--color-success-light);
}
.app-dialog__confirm-lead { margin: 0 0 8px; color: var(--text-primary); font-size: 14px; }
.app-dialog__confirm-lead code {
    padding: 2px 8px;
    border-radius: 6px;
    background-color: #ffffff;
    border: 1px solid var(--border-color);
    color: var(--color-danger);
    font-weight: 700;
    letter-spacing: 0.08em;
}
.app-dialog__confirm-box label { margin-top: 0; font-size: 12px; color: var(--text-muted); }
.app-dialog__confirm-row { position: relative; }
.app-dialog__body .app-dialog__confirm-input {
    width: 100%;
    min-height: 48px;
    margin: 4px 0 0;
    padding-right: 44px;
    background-color: #ffffff;
    font-family: 'Courier New', monospace;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.app-dialog__confirm-tick {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-40%) scale(0.6);
    color: var(--color-success);
    font-size: 22px;
    font-weight: 700;
    opacity: 0;
    transition: opacity var(--transition-clean), transform var(--transition-clean);
}
.is-match .app-dialog__confirm-tick { opacity: 1; transform: translateY(-40%) scale(1); }
.app-dialog .modal-footer button:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
@media (prefers-reduced-motion: reduce) {
    .app-dialog__confirm-box, .app-dialog__confirm-tick { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PHONES: every data table is a stack of record cards (2026-09-25)
   ═══════════════════════════════════════════════════════════════════════════
   Owner-approved phone layout rules, rule 8. A seven-column table on a 390px
   screen can only be read by scrolling sideways, however it is tuned; the
   plain tables of 2026-09-24 and the peek strip after them were both ways of
   living with that. Now each row is one card:
     - the title column (th[data-card="title"], else the first) on top;
     - the status column (th[data-card="badge"]) at the top right;
     - every other column as a label / value line, labelled from its header
       (data-label, written by stampTableCards() in js/app.js);
     - the buttons (th[data-card="actions"]) along the bottom, equal widths.
   Nothing is sticky and nothing scrolls sideways, so the 2026-09-24 fix for
   blank tiles on Android (no composited cells) still holds. Tablets and
   desktops keep their tables and pinned columns exactly as before.

   Every selector here ends in :not(#_). It matches every element and adds
   ID weight, which lifts the block above the pinned-column rules (up to four
   classes deep, at all widths) without an !important on each property. */
@media (max-width: 600px) {
    .table-container:not(#_) {
        max-height: none !important;   /* even over an inline style: a card list never scrolls inside itself */
        overflow: visible;
        margin: 0;
        border: 0;
        border-radius: 0;
        background: transparent;
        box-shadow: none;
    }

    .table-container table:not(#_),
    .table-container tbody:not(#_) {
        display: block;
        width: 100%;
        min-width: 0 !important;       /* even over an inline min-width */
    }

    /* The column names live on in each cell's label; the header row itself
       is hidden from sight, not from screen readers. */
    .table-container thead:not(#_) {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    /* Not sticky either: a hidden sticky header is still a separately
       composited layer, the cost the 2026-09-24 phone fix removed. */
    .table-container thead th:not(#_) {
        position: static;
    }

    .table-container tbody tr:not(#_) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: 12px;
        margin: 0 0 8px;
        padding: 14px 16px;
        background: #ffffff;
        border: 1px solid var(--border-color);
        border-radius: 12px;
    }

    .table-container tbody tr:last-child:not(#_) { margin-bottom: 0; }

    /* Keyboard focus on any button in a card marks the whole card. */
    .table-container tbody tr:focus-within:not(#_) {
        border-color: color-mix(in srgb, var(--color-accent) 55%, var(--border-color));
        box-shadow: 0 0 0 3px var(--color-accent-light);
    }

    /* A hairline under the title line, placed in grid row 2 so the detail
       lines flow in from row 3. */
    .table-container tbody tr:not(#_)::after {
        content: "";
        grid-column: 1 / -1;
        grid-row: 2;
        height: 1px;
        margin: 10px 0 6px;
        background: var(--border-color-light);
    }

    /* A label / value line. The label floats into the cell's left padding,
       so the value flows as ordinary text however many pieces it is made of
       ("1250 boxes", a row of time-slot pills), and flow-root makes the line
       as tall as a label that wraps to two lines. */
    .table-container tbody td:not(#_) {
        position: relative;          /* keeps icon buttons' hidden labels inside */
        display: flow-root;
        grid-column: 1 / -1;
        width: auto;
        min-width: 0;
        max-width: none;
        padding: 4px 0 4px calc(6.75rem + 12px);
        border: 0;
        background: transparent;
        box-shadow: none;
        color: var(--text-primary);
        font-size: 14px;
        line-height: 1.45;
        text-align: left;
        white-space: normal;
        overflow-wrap: anywhere;
    }

    .table-container tbody td:not(#_)::before {
        content: attr(data-label);
        float: left;
        width: 6.75rem;
        margin-left: calc(-6.75rem - 12px);
        color: var(--text-muted);
        font-size: 12.5px;
        font-weight: 600;
        line-height: 1.6;
        overflow-wrap: normal;
    }

    .table-container tbody td[data-card="title"]:not(#_) {
        display: block;
        grid-column: 1;
        grid-row: 1;
        align-self: center;
        padding: 0;
        font-size: 15px;
        font-weight: 700;
        line-height: 1.35;
        overflow-wrap: break-word;
    }

    .table-container tbody td[data-card="badge"]:not(#_) {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 4px;
        grid-column: 2;
        grid-row: 1;
        max-width: 11rem;
        padding: 0;
        text-align: right;
        font-size: 12.5px;
    }

    /* A status cell with more than its badge (a reschedule note, "₱500 left"):
       the badge goes to the corner and the rest becomes its own full-width
       line, instead of squeezing a sentence into the corner beside the name. */
    .table-container tbody td[data-card="badge"]:has(> .badge):not(#_) {
        display: contents;
    }

    .table-container tbody td[data-card="badge"]:has(> .badge) > .badge:not(#_) {
        grid-column: 2;
        grid-row: 1;
        align-self: center;
        justify-self: end;
    }

    .table-container tbody td[data-card="badge"]:has(> .badge) > :not(.badge):not(#_) {
        grid-column: 1 / -1;
        margin: 4px 0 0;
        font-size: 13px;
        text-align: left;
    }

    /* A bill's balance becomes its own labelled line, like the columns. */
    .table-container tbody td[data-card="badge"] > .billing-balance:not(#_) {
        display: flow-root;
        margin: 0;
        padding: 4px 0 4px calc(6.75rem + 12px);
        font-size: 14px;
    }

    .table-container tbody td[data-card="badge"] > .billing-balance:not(#_)::before {
        content: "Balance";
        float: left;
        width: 6.75rem;
        margin-left: calc(-6.75rem - 12px);
        color: var(--text-muted);
        font-size: 12.5px;
        font-weight: 600;
        line-height: 1.6;
    }

    .table-container tbody td[data-card="actions"]:not(#_) {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 8px;
        padding: 12px 0 0;
        border-top: 1px solid var(--border-color-light);
    }

    /* Inventory has two button columns (Stock, then Delete): one button area. */
    .table-container tbody td[data-card="actions"] + td[data-card="actions"]:not(#_) {
        margin-top: 0;
        padding-top: 8px;
        border-top: 0;
    }

    .table-container tbody td[data-card="title"]:not(#_)::before,
    .table-container tbody td[data-card="badge"]:not(#_)::before,
    .table-container tbody td[data-card="actions"]:not(#_)::before { content: none; }

    /* The button groups spread across the card, two to a line: a pair side
       by side, four as a 2-by-2 block, an odd one out taking the full line. */
    .table-container td[data-card="actions"] .appt-action-set:not(#_),
    .table-container td[data-card="actions"] .inventory-action-group:not(#_),
    .table-container td[data-card="actions"] .schedule-action-set:not(#_) {
        display: flex;
        flex: 1 1 100%;
        flex-direction: row;         /* the 188px zoom rules stack them in a column */
        flex-wrap: wrap;
        align-items: stretch;
        gap: 8px;
        width: 100%;
        white-space: normal;
    }

    .table-container td[data-card="actions"] button:not(#_) {
        display: inline-flex;
        flex: 1 1 calc(50% - 4px);
        align-items: center;
        justify-content: center;
        gap: 6px;
        width: auto;
        min-width: 0;
        min-height: 44px;
        margin: 0;
        padding: 0 12px;
    }

    /* A card has room for the words, so icon-only buttons say what they do.
       (Their accessible names are unchanged.) */
    .table-container td[data-card="actions"] .appt-icon-action .visually-hidden:not(#_) {
        position: static !important;   /* .visually-hidden pins this with !important */
        width: auto;
        height: auto;
        margin: 0;
        overflow: visible;
        clip: auto;
        font-size: 13px;
        font-weight: 600;
    }
    .table-container td[data-card="actions"] .schedule-action--edit:not(#_)::after { content: "Edit"; font-size: 13px; font-weight: 600; }
    .table-container td[data-card="actions"] .schedule-action--delete:not(#_)::after { content: "Delete"; font-size: 13px; font-weight: 600; }

    /* "No appointments found." and the like: one quiet line, no card chrome. */
    .table-container tbody tr[data-card-empty]:not(#_) {
        display: block;
        padding: 20px 16px;
        color: var(--text-muted);
        text-align: center;
    }
    .table-container tbody tr[data-card-empty]:not(#_)::after { content: none; }
    .table-container tbody tr[data-card-empty] td:not(#_) {
        display: block;
        padding: 0;
        color: inherit;
        text-align: center;
    }
    .table-container tbody tr[data-card-empty] td:not(#_)::before { content: none; }
}

/* About 200% zoom on a phone (188 CSS px): each label sits above its value,
   which would otherwise be left about 10px beside it, and one button per
   line, full width, so none is squeezed below its own words. */
@media (max-width: 240px) {
    .table-container tbody td:not(#_),
    .table-container tbody td[data-card="badge"] > .billing-balance:not(#_) { padding-left: 0; }
    .table-container tbody td:not(#_)::before,
    .table-container tbody td[data-card="badge"] > .billing-balance:not(#_)::before {
        float: none;
        display: block;
        width: auto;
        margin-left: 0;
    }
    .table-container td[data-card="actions"] button:not(#_) { flex-basis: 100%; }
}

/* Blur behind a dialog is a full-screen filter re-run on every frame; phones
   and tablets get the same dim without it. */
@media (max-width: 900px) {
    .modal-overlay { -webkit-backdrop-filter: none; backdrop-filter: none; background-color: rgba(15, 43, 38, 0.5); }
}
