/* =====================================================================
 *  app.css — components
 *  Depends on theme.css tokens. RTL is the native direction here, so
 *  logical properties (inline-start/end) are used throughout rather than
 *  left/right, and nothing needs mirroring.
 * ===================================================================== */

/* ---------------------------------------------------------------- base */
body {
    font-family: var(--font-ui);
    background: var(--surface-0);
    color: var(--on-surface);
    min-height: 100dvh;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--dur-normal) var(--ease-standard),
                color var(--dur-normal) var(--ease-standard);
}

/* Respect the notch / home indicator when installed as a PWA. */
.safe-area {
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
}

:focus-visible {
    outline: 3px solid var(--md-primary);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

/* --------------------------------------------------------- glass card */
.glass-card {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--glass-shadow);
}

/* backdrop-filter is unsupported on older Android WebView — fall back to
   an opaque surface rather than an unreadable transparent panel. */
@supports not (backdrop-filter: blur(1px)) {
    .glass-card {
        background: var(--surface-1);
    }
}

/* ------------------------------------------------------------- auth UI */
.auth-body {
    display: grid;
    place-items: center;
    min-height: 100dvh;
    padding: 1rem;
    overflow-x: hidden;
}

.auth-aurora {
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
}

.auth-aurora .orb {
    position: absolute;
    border-radius: 50%;
    filter: blur(80px);
    opacity: 0.45;
    animation: float 20s var(--ease-standard) infinite alternate;
}

.orb-1 { width: 380px; height: 380px; background: var(--md-primary);   top: -10%; inset-inline-start: -8%; }
.orb-2 { width: 320px; height: 320px; background: var(--md-tertiary);  bottom: -12%; inset-inline-end: -6%; animation-delay: -7s; }
.orb-3 { width: 260px; height: 260px; background: var(--md-secondary); top: 40%; inset-inline-end: 30%; animation-delay: -14s; }

@keyframes float {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to   { transform: translate3d(30px, -40px, 0) scale(1.15); }
}

.auth-wrap { width: 100%; max-width: 440px; }

.auth-card {
    padding: 2rem 1.75rem;
    position: relative;
    animation: card-in var(--dur-slow) var(--ease-emphasized) both;
}

@keyframes card-in {
    from { opacity: 0; transform: translateY(16px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

.auth-head { text-align: center; margin-bottom: 1.5rem; }

.auth-badge {
    width: 64px;
    height: 64px;
    margin: 0 auto 0.875rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-md);
    background: linear-gradient(135deg, var(--md-primary), var(--md-tertiary));
    color: #fff;
    font-size: 1.75rem;
    box-shadow: var(--elev-2);
}

/* The offline screen's badge: amber-to-red instead of the brand gradient,
   because it signals a fault rather than the app's identity. Carried in a
   style="" attribute until v1.11, which our own CSP strips — so the badge
   silently fell back to the brand colours and the one visual cue that this
   screen means "something is wrong" was lost. */
.auth-badge.is-offline {
    background: linear-gradient(135deg, #d97706, #dc2626);
}

.auth-title { font-weight: 700; margin-bottom: 0.25rem; }
.auth-sub   { color: var(--on-surface-muted); font-size: 0.9375rem; margin: 0; }
.auth-foot  { margin-top: 1.5rem; text-align: center; border-top: 1px solid var(--outline); padding-top: 1rem; }

/* Theme switch, pinned to the card's leading corner (flips under RTL). */
.theme-toggle {
    position: absolute;
    top: 1rem;
    inset-inline-start: 1rem;
    width: 40px;
    height: 40px;
    border: 1px solid var(--outline);
    border-radius: var(--radius-full);
    background: var(--surface-2);
    color: var(--on-surface);
    display: grid;
    place-items: center;
    cursor: pointer;
    transition: transform var(--dur-fast) var(--ease-spring),
                background var(--dur-fast) var(--ease-standard);
}

.theme-toggle:hover  { background: var(--surface-3); }
.theme-toggle:active { transform: scale(0.9) rotate(-15deg); }

/* Pins the theme button to the auth card's leading corner (the RIGHT
   corner under RTL). Without this the button sits in normal flow and
   pushes the whole card header down. */
.theme-toggle-pos {
    position: absolute;
    top: 1rem;
    inset-inline-start: 1rem;
}

/* Password reveal, positioned on the trailing edge. */
.password-field { position: relative; }

.pw-toggle {
    position: absolute;
    top: 50%;
    inset-inline-end: 0.75rem;
    transform: translateY(-50%);
    border: 0;
    background: transparent;
    color: var(--on-surface-muted);
    font-size: 1.1rem;
    cursor: pointer;
    padding: 0.25rem;
    z-index: 5;
}

.pw-toggle:hover { color: var(--md-primary); }

.pw-strength {
    height: 6px;
    background: var(--surface-3);
    border-radius: var(--radius-full);
    overflow: hidden;
}

.pw-strength-bar {
    height: 100%;
    width: 0;
    border-radius: var(--radius-full);
    background: var(--md-danger);
    transition: width var(--dur-normal) var(--ease-standard),
                background var(--dur-normal) var(--ease-standard);
}

.pw-rules {
    list-style: none;
    padding-inline-start: 0;
    margin-bottom: 0;
}

.pw-rules li {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.pw-rules .rule-mark {
    display: inline-block;
    width: 1rem;
    font-weight: 700;
}

.pw-rules li.rule-ok { color: var(--md-success); }

/* The reveal button must clear the 44px target — it was ~26px. */
.pw-toggle {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
}

/* ---------------------------------------------------- micro-interaction */
.btn-press {
    position: relative;
    overflow: hidden;
    transition: transform var(--dur-fast) var(--ease-spring),
                box-shadow var(--dur-fast) var(--ease-standard);
}

.btn-press:active { transform: scale(0.97); }

/* M3 ripple. Triggered by app.js adding .rippling with --x/--y set. */
.btn-press::after {
    content: '';
    position: absolute;
    top: var(--y, 50%);
    inset-inline-start: var(--x, 50%);
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.4);
    transform: translate(50%, -50%);
    opacity: 0;
    pointer-events: none;
}

.btn-press.rippling::after {
    animation: ripple 600ms var(--ease-standard);
}

@keyframes ripple {
    0%   { width: 0; height: 0; opacity: 0.5; }
    100% { width: 320px; height: 320px; opacity: 0; }
}

/* ------------------------------------------------------------ app bar */
.app-bar {
    position: sticky;
    top: 0;
    z-index: 1030;
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border-bottom: 1px solid var(--outline);
    padding: 0.75rem 1rem;
    padding-top: calc(0.75rem + env(safe-area-inset-top));
}

.app-bar-title { font-weight: 700; font-size: 1.0625rem; margin: 0; line-height: 1.2; }
.app-bar-sub   { font-size: 0.75rem; color: var(--on-surface-muted); margin: 0; }

.avatar {
    width: 40px;
    height: 40px;
    border-radius: var(--radius-full);
    background: linear-gradient(135deg, var(--md-primary), var(--md-tertiary));
    color: #fff;
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: 0.875rem;
    flex-shrink: 0;
}

/* ------------------------------------------------------- progress card */
.progress-card { padding: 1.25rem; margin-bottom: 1rem; }

.progress-ring { --pct: 0; width: 92px; height: 92px; position: relative; flex-shrink: 0; }

.progress-ring::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: conic-gradient(var(--md-primary) calc(var(--pct) * 1%), var(--surface-3) 0);
    transition: --pct var(--dur-slow) var(--ease-emphasized);
}

.progress-ring::after {
    content: '';
    position: absolute;
    inset: 9px;
    border-radius: 50%;
    background: var(--surface-1);
}

.progress-ring.met::before { background: conic-gradient(var(--md-success) calc(var(--pct) * 1%), var(--surface-3) 0); }

.progress-ring-label {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    z-index: 1;
    font-weight: 700;
    font-size: 1.125rem;
}

/* Animating a conic-gradient stop needs the property registered. */
@property --pct {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
}

/* ------------------------------------------------------------ calendar */
.calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 4px;
}

.calendar-head {
    text-align: center;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--on-surface-muted);
    padding: 0.5rem 0;
}

.calendar-day {
    aspect-ratio: 1;
    /* Containing block for the "בוצעה" check badge in the corner. */
    position: relative;
    /*
       A grid item defaults to min-width:auto, i.e. it refuses to be
       narrower than its own min-content. That was harmless while the cell
       held only a number and some dots — but the holiday label (v1.59) is
       white-space:nowrap, so its full width became the cell's minimum and
       every column grew to fit the longest label in the month. Measured:
       a 343px grid computed 56px columns, 416px in total, overflowing the
       page sideways on a phone.

       min-width:0 lets the track win instead, which is also what makes the
       label's own overflow:hidden/ellipsis able to fire at all.
    */
    min-width: 0;
    /* WCAG 2.2 target size is 24px, but these get tapped one-handed while
       walking. 44px is the platform guideline and the one that matters here. */
    min-height: 44px;
    border-radius: var(--radius-sm);
    background: var(--surface-1);
    border: 1px solid var(--outline);
    padding: 0.25rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    cursor: pointer;
    transition: transform var(--dur-fast) var(--ease-spring),
                box-shadow var(--dur-fast) var(--ease-standard),
                border-color var(--dur-fast) var(--ease-standard);
}

.calendar-day:hover  { transform: translateY(-2px); box-shadow: var(--elev-2); }
.calendar-day:active { transform: scale(0.96); }
.calendar-day.is-empty { background: transparent; border-color: transparent; cursor: default; }
.calendar-day.is-empty:hover { transform: none; box-shadow: none; }

.calendar-day.today {
    border-color: var(--md-primary);
    border-width: 2px;
}

/* Without this the day filter applied silently and the calendar looked
   frozen — the user had no way to tell a day was selected, or how to
   clear it. See also the filter chip rendered above the shift list. */
.calendar-day.selected {
    background: var(--md-primary);
    border-color: var(--md-primary);
    color: var(--md-on-primary);
    font-weight: 700;
}

.calendar-day.selected .day-dot {
    box-shadow: 0 0 0 1.5px var(--md-on-primary);
}

/* Dismissible "filtered to <date>" chip. */
.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.75rem;
    margin-bottom: 0.75rem;
    border: 1px solid var(--md-primary);
    border-radius: var(--radius-full);
    background: color-mix(in srgb, var(--md-primary) 14%, transparent);
    color: var(--on-surface);
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
}

.filter-chip:hover { background: color-mix(in srgb, var(--md-primary) 24%, transparent); }

.calendar-day.has-mine { background: color-mix(in srgb, var(--state-mine) 12%, var(--surface-1)); }

.day-number { font-size: 0.8125rem; font-weight: 600; }

.day-dots { display: flex; gap: 2px; flex-wrap: wrap; justify-content: center; }

.day-dot {
    /* inline-block, not inline: a bare <span> ignores width/height, which
       silently made the calendar legend render as invisible dots. */
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--state-open);
    vertical-align: middle;
}

.day-dot.full      { background: var(--state-full); }
.day-dot.needs_one { background: var(--state-needs-one); }
.day-dot.empty     { background: var(--state-empty); }
.day-dot.mine      { background: var(--state-mine); box-shadow: 0 0 0 2px color-mix(in srgb, var(--state-mine) 30%, transparent); }

/* Squad-wide shift (no team). Team dots get their colour set inline from
   the DB through the CSSOM (see calendar.js), so they carry no class of
   their own; this is the one that needs a static colour — and it must be
   a VISIBLE one (--team-all), not a neutral grey: grey read as "nothing
   here" and a whole-squad duty day carried no indication at all. */
.day-dot.all-teams { background: var(--team-all); }

/* -------------------------------------------- performed + display filter */
/* A day whose shifts have all ENDED carries a check badge — "בוצעו". The
   badge sits in the corner so the team tint underneath stays readable. */
.day-done {
    position: absolute;
    top: 2px;
    inset-inline-start: 3px;
    font-size: 0.7rem;
    line-height: 1;
    color: var(--md-success);
    pointer-events: none;
}
.calendar-day.selected .day-done { color: var(--md-on-primary); }

/* Days the active display filter excludes are DIMMED, not hidden — the
   month picture must stay whole; the filter only says where to look first.
   The selected day never dims, or tapping a muted day would look ignored. */
.calendar-day.is-muted:not(.selected) { opacity: 0.55; }

/* ------------------------------------------------------------- holidays */
/* A STRIPE along the top edge, not another dot. The dots at the bottom of
   the cell mean staffing and the tint behind it means team; a fourth dot
   in a fourth colour would be read as a fifth staffing state, which is
   exactly the misreading this app cannot afford. A stripe is a different
   shape in a different place, so it says "different kind of fact".

   It is also direction-agnostic — the marker spans the cell whichever way
   the language runs, which a corner wedge built from border tricks does not.

   DRAWN AS A TOP BORDER ON A FULL-SIZE BOX, not as a 3px bar.

   The first attempt was a bar: inset-inline:0, height:3px, with the cell's
   12px corner radius asked for on its own corners. It overhung the rounded
   corners visibly, and the reason is arithmetic — CSS clamps a radius to
   the box, so on a 3px-tall bar a 12px radius becomes a 3px arc. A 3px arc
   and a 12px arc are different curves, and the bar's straight ends stuck
   out over the cell's inward curve.

   A separate rectangle cannot follow the parent's arc. This element is
   therefore the SAME rounded rectangle as the cell — inset:0 plus
   border-radius:inherit — and paints only its top edge. The curve is
   identical by construction rather than by matching, and it stays correct
   on its own if --radius-sm ever changes.

   Only border-top is declared: the initial border-style is none, so the
   other three sides compute to zero width without being zeroed. */
.calendar-day.has-holiday::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    border-top: 3px solid var(--holiday-festive);
    pointer-events: none;
}

.calendar-day.holiday-national::after { border-top-color: var(--holiday-national); }
.calendar-day.holiday-mourning::after { border-top-color: var(--holiday-mourning); }

/* An EVE is the same colour, half-strength: the evening before Yom Kippur
   matters operationally, but it is not the day itself and must not look
   like it. */
.calendar-day.holiday-eve::after {
    border-top-color: color-mix(in srgb, var(--holiday-festive) 45%, transparent);
}
.calendar-day.holiday-eve.holiday-national::after {
    border-top-color: color-mix(in srgb, var(--holiday-national) 45%, transparent);
}
.calendar-day.holiday-eve.holiday-mourning::after {
    border-top-color: color-mix(in srgb, var(--holiday-mourning) 45%, transparent);
}

/* The "performed" check sits at top:2px and would clip the stripe. */
.calendar-day.has-holiday .day-done { top: 5px; }

/* The selected day inverts to the primary fill, where a gold or navy
   stripe goes muddy. White reads on it and keeps the marker present. */
.calendar-day.selected.has-holiday::after { border-top-color: var(--md-on-primary); }

/* The abbreviation printed inside the cell.
   The full name is in the aria-label, the title, and the day list — this
   line is a reminder for someone scanning the month, not the record. */
.day-holiday {
    font-size: 0.5rem;
    line-height: 1.1;
    font-weight: 700;
    /* Reclaim the cell's side padding. The label is a full-width strip,
       not inline prose, so the padding buys nothing here and costs 8px —
       which is the difference between "ערב העצמאות" fitting and being
       clipped on a 375px phone. Measured: 36px of room becomes 44px. */
    width: calc(100% + 0.5rem);
    margin-inline: -0.25rem;
    text-align: center;
    /* A safety net, not the plan: the abbreviations were measured against
       36px of text room on a 375px phone and fit. This catches the cases
       measurement cannot reach — a 320px screen, or a language whose
       abbreviation turns out longer than the Hebrew one. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--holiday-festive);
}
.calendar-day.holiday-national .day-holiday { color: var(--holiday-national); }
.calendar-day.holiday-mourning .day-holiday { color: var(--holiday-mourning); }
.calendar-day.selected .day-holiday { color: var(--md-on-primary); }

/* An eve is stated in words ("ערב ר״ה"), so it does not also need to be
   dimmed — that would make the one label in the cell the hardest to read. */
.calendar-day.holiday-eve .day-holiday { font-weight: 600; opacity: 0.85; }

/*
   THE LABEL IS DROPPED AT THE LARGE TEXT SIZES, and that is deliberate.
   --font-scale multiplies the ROOT font size, but a calendar cell is 1fr
   of the container and does not grow with it; at 1.3 the day number, the
   label and the dots stop fitting in a square that stayed the same size.
   Given the choice between clipping the day number and dropping a
   secondary label, the label goes — the stripe still marks the day and
   the name is one tap away in the list. Accessibility outranks density.
*/
:root[data-font-scale="large"]  .day-holiday,
:root[data-font-scale="xlarge"] .day-holiday { display: none; }

/* Holiday line above the shift list for the chosen day. */
.holiday-note {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    margin-bottom: 0.75rem;
    border-radius: var(--radius-sm);
    border-inline-start: 3px solid var(--holiday-festive);
    background: color-mix(in srgb, var(--holiday-festive) 10%, transparent);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--on-surface);
}
.holiday-note.holiday-national {
    border-inline-start-color: var(--holiday-national);
    background: color-mix(in srgb, var(--holiday-national) 10%, transparent);
}
.holiday-note.holiday-mourning {
    border-inline-start-color: var(--holiday-mourning);
    background: color-mix(in srgb, var(--holiday-mourning) 10%, transparent);
}

/* "בוצעה" chip on a performed shift's card. */
.chip.done {
    background: color-mix(in srgb, var(--state-full) 24%, transparent);
    color: var(--chip-full-fg);
}

/* Status + team display filters above the shift list. */
.board-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

/* ------------------------------------------------ assignment panel */
/* Scrollable so a 30-name squad does not push the modal buttons off a
   phone screen; the modal itself is scrollable too, but bounding the list
   keeps the mode/lock controls reachable without hunting for them. */
.assign-list {
    max-height: 15rem;
    overflow-y: auto;
    border: 1px solid var(--outline);
    border-radius: var(--radius-md, 10px);
    padding: .25rem;
}

.assign-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .45rem .6rem;
    border-radius: 8px;
    cursor: pointer;
    /* 44px min target: these rows are tapped on a phone, often in a hurry. */
    min-height: 44px;
}
.assign-row:hover { background: var(--surface-2); }
.assign-row + .assign-row { border-top: 1px solid var(--outline); }

/* Why the ranking placed this person here — "behind on hours", "not
   assigned for nine days". A commander who cannot see the reasoning stops
   trusting the order the first time it looks odd. */
.assign-why {
    display: block;
    font-size: .72rem;
    line-height: 1.35;
    color: var(--on-surface-muted);
}

/* Declared unavailable, or already on an overlapping shift.
   FADED, NOT DISABLED. The squad's rule is that a commander may assign
   anyway — people come back early, and a checkbox that refuses would be
   worked around by deleting the absence, which is worse. */
.assign-row.is-blocked { opacity: .62; }
.assign-row.is-blocked .assign-why { color: var(--state-empty); }

/* Team-colour swatch in the calendar legend — a small tile that mirrors the
   tint of a whole day cell. Colour set via CSSOM (see calendar.js). */
.legend-swatch {
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 4px;
    vertical-align: middle;
    background: var(--surface-3);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12);
}
.legend-swatch.all-teams { background: var(--team-all); }

/* Team colour swatch on the team-management cards. Colour set via CSSOM. */
.team-swatch {
    display: inline-block;
    width: 18px;
    height: 18px;
    border-radius: 5px;
    margin-top: 2px;
    flex-shrink: 0;
    background: var(--surface-3);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15);
}

/* --------------------------------------------------------- shift card */
.shift-card {
    background: var(--surface-1);
    border: 1px solid var(--outline);
    border-radius: var(--radius-md);
    border-inline-start: 4px solid var(--state-open);
    padding: 1rem;
    margin-bottom: 0.75rem;
    box-shadow: var(--elev-1);
    transition: transform var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard);
    animation: slide-in var(--dur-normal) var(--ease-emphasized) both;
}

.shift-card:hover { transform: translateY(-2px); box-shadow: var(--elev-2); }

.shift-card[data-state="full"]      { border-inline-start-color: var(--state-full); }
.shift-card[data-state="needs_one"] { border-inline-start-color: var(--state-needs-one); }
.shift-card[data-state="empty"]     { border-inline-start-color: var(--state-empty); }
.shift-card.is-mine                 { border-inline-start-color: var(--state-mine); background: color-mix(in srgb, var(--state-mine) 6%, var(--surface-1)); }

@keyframes slide-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

.shift-title { font-weight: 600; font-size: 0.9375rem; margin: 0; }

.shift-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    font-size: 0.8125rem;
    color: var(--on-surface-muted);
    margin-top: 0.375rem;
}

/* The slot count is the most decision-relevant number on the card, so it
   does not get the faint muted token at the smallest size. */
.slot-count {
    font-size: 0.875rem;
    color: var(--on-surface);
    font-weight: 600;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.25rem 0.625rem;
    border-radius: var(--radius-full);
    font-size: 0.75rem;
    font-weight: 600;
    background: var(--surface-2);
    color: var(--on-surface-muted);
}

/* 32% tint + a dedicated foreground token: at 16% with the raw state hue
   every one of these measured between 2.6:1 and 4.5:1, i.e. all ten
   combinations failed. These are the app's primary status indicator and
   get read outdoors at night, so they must clear 4.5:1. */
.chip.full      { background: color-mix(in srgb, var(--state-full) 32%, transparent);      color: var(--chip-full-fg); }
.chip.needs_one { background: color-mix(in srgb, var(--state-needs-one) 32%, transparent); color: var(--chip-needs-one-fg); }
.chip.open      { background: color-mix(in srgb, var(--state-open) 32%, transparent);      color: var(--chip-open-fg); }
.chip.mine      { background: color-mix(in srgb, var(--state-mine) 32%, transparent);      color: var(--chip-mine-fg); }
.chip.empty     { background: color-mix(in srgb, var(--state-empty) 32%, transparent);     color: var(--chip-empty-fg); }

/* A shift whose date has passed, in the management list. It is still fully
   editable and cancellable — the muting says "history", not "disabled" — so
   the card keeps normal contrast on its text and only loses saturation. */
/* Unread count on a bottom-nav item. Absolutely positioned against the
   link, which .bottom-nav a already establishes as a containing block —
   inline would push the icon off centre and make the tap target jump as
   the number appears and disappears. */
.bottom-nav a { position: relative; }

.nav-badge {
    position: absolute;
    top: .15rem;
    inset-inline-end: 22%;
    min-width: 1.05rem;
    padding: 0 .25rem;
    border-radius: 999px;
    background: var(--state-empty);
    color: #fff;
    font-size: .62rem;
    font-weight: 700;
    line-height: 1.05rem;
    text-align: center;
}

/* The same count in the sidebar, where there is room for it in the flow. */
.sidebar-badge-count {
    margin-inline-start: auto;
    min-width: 1.2rem;
    padding: 0 .3rem;
    border-radius: 999px;
    background: var(--state-empty);
    color: #fff;
    font-size: .68rem;
    font-weight: 700;
    text-align: center;
}

/* Unread marker on a board notice. A dot rather than bold text or a
   different background: the card has to look the same before and after
   reading, or clearing the dot visibly redraws the thing being read. */
.ann-dot {
    display: inline-block;
    width: .55rem;
    height: .55rem;
    border-radius: 50%;
    background: var(--state-mine);
    margin-inline-end: .35rem;
    vertical-align: middle;
}

/* Notice text keeps the author's line breaks. */
.ann-body {
    white-space: pre-wrap;
    word-break: break-word;
}

/* =====================================================================
   Emergency call-out banner.
   ---------------------------------------------------------------------
   Deliberately the loudest thing in the app, and the only element allowed
   to be. It carries a request for people to leave what they are doing and
   come now; if it looked like the rest of the interface it would be read
   like the rest of the interface.

   Not animated. A pulsing or flashing banner is the first thing people
   learn to tune out, and for anyone with a vestibular disorder it is worse
   than useless — the contrast does the work instead.
   ===================================================================== */
.sos-call {
    background: linear-gradient(135deg, #b91c1c, #dc2626);
    color: #fff;
    border-radius: 14px;
    padding: 1rem;
    margin-bottom: .75rem;
    box-shadow: 0 6px 24px rgba(220, 38, 38, .35);
}

/* The affirmative answer is a light button on red: the highest contrast
   pairing available here, on the action that matters most. */
.sos-call .btn-light { color: #b91c1c; }

.sos-answered {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-top: .6rem;
    font-weight: 600;
}

/* The commander's half — separated by a rule rather than a colour change,
   so the card still reads as one alert. */
.sos-panel {
    border-top: 1px solid rgba(255, 255, 255, .35);
    padding-top: .6rem;
}
.sos-panel a { color: #fff; }

/* =====================================================================
   Dialog entry animation — CSS ANIMATION, not Bootstrap's `fade` class.
   ---------------------------------------------------------------------
   Every modal in this app dropped `fade`, on purpose, and gets its motion
   from here instead.

   Bootstrap's Modal.hide() begins with:

       if (!this._isShown || this._isTransitioning) { return }

   `_isTransitioning` is raised while the OPEN transition is in flight and
   lowered when `transitionend` arrives — or, if it never arrives, by a
   setTimeout fallback. A BACKGROUNDED TAB THROTTLES THAT TIMEOUT. So a
   volunteer who taps "צור משמרת" and switches apps while the request is in
   flight comes back to a shift that was created, a toast that said so, and
   a dialog over a dark backdrop that will not close: the hide() fired
   while the flag was still up and was discarded in silence. Measured
   directly — `hide called _isShown=true _isTransitioning=true`, and the
   flag read false a moment later, far too late.

   Without `fade`, Bootstrap treats the dialog as un-animated, never raises
   the flag, and hide() is synchronous and unconditional. A CSS animation
   is not a transition and takes no part in any of that machinery, so the
   motion survives and the failure mode does not.

   Keep new modals free of `fade`. They animate from here for nothing.
   ===================================================================== */
@keyframes modal-rise {
    from { opacity: 0; transform: translateY(-12px) scale(.985); }
    to   { opacity: 1; transform: none; }
}
@keyframes backdrop-in {
    from { opacity: 0; }
    to   { opacity: .5; }
}

.modal.show .modal-dialog   { animation: modal-rise .22s cubic-bezier(.2, .7, .3, 1) both; }
.modal-backdrop.show        { animation: backdrop-in .15s ease-out both; }

/* Motion is decoration here; the dialog must still appear instantly for
   anyone who has asked the OS for less of it. */
@media (prefers-reduced-motion: reduce) {
    .modal.show .modal-dialog,
    .modal-backdrop.show { animation: none; }
}

/* Bulk-cancel bar in shift management. Sticky, not fixed: the list can run
   to dozens of cards and the action has to stay reachable without scrolling
   back up, but it must not float over unrelated pages. `sticky` also keeps
   it inside its own scroll container, so it never covers the bottom nav.
   z-index is above the cards and far below the modal layer (1050+). */
.bulk-bar {
    position: sticky;
    top: .5rem;
    z-index: 5;
}

.chip.past      { background: color-mix(in srgb, currentColor 12%, transparent); color: var(--on-surface-muted); }
.shift-card.is-past { opacity: .72; }
.shift-card.is-past:hover,
.shift-card.is-past:focus-within { opacity: 1; }

/* The "starts within 24h and nobody is on it" warning in the sector view.
   It carried its colour in a style="" attribute, which our own CSP drops —
   the warning rendered in ordinary body text, i.e. it did not read as a
   warning at all. Same token, applied through a class so it survives. */
.text-urgent { color: var(--chip-empty-fg); }

/* ------------------------------------------------------ skeleton loader */
.skeleton {
    background: linear-gradient(90deg,
        var(--surface-2) 25%,
        var(--surface-3) 50%,
        var(--surface-2) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.4s infinite;
    border-radius: var(--radius-sm);
}

/* RTL: the shimmer sweeps right-to-left to match the reading direction.
   (Previously animated -200% -> 200%, i.e. left-to-right, contradicting
   the comment that sat above it.) */
@keyframes shimmer {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

.skeleton-card { height: 92px; margin-bottom: 0.75rem; }
.skeleton-line { height: 12px; margin-bottom: 0.5rem; }
.skeleton-line.short { width: 45%; }

/* ------------------------------------------------------------- toasts */
.toast-stack {
    position: fixed;
    /* Clear of the bottom nav — toasts were landing on top of it, so an
       error about a full shift covered the navigation. */
    bottom: calc(5rem + env(safe-area-inset-bottom));
    inset-inline: 1rem;
    margin-inline: auto;
    z-index: 1090;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    max-width: 420px;
    pointer-events: none;
}

@media (min-width: 992px) {
    .toast-stack { bottom: calc(1.5rem + env(safe-area-inset-bottom)); }
}

.toast-item {
    pointer-events: auto;
    background: var(--surface-1);
    border: 1px solid var(--outline);
    border-inline-start: 4px solid var(--md-info);
    border-radius: var(--radius-sm);
    box-shadow: var(--elev-3);
    padding: 0.75rem 1rem;
    font-size: 0.875rem;
    display: flex;
    align-items: center;
    gap: 0.625rem;
    animation: toast-in var(--dur-normal) var(--ease-spring) both;
}

.toast-item.success { border-inline-start-color: var(--md-success); }
.toast-item.error   { border-inline-start-color: var(--md-danger); }
.toast-item.warning { border-inline-start-color: var(--md-warning); }
.toast-item.leaving { animation: toast-out var(--dur-fast) var(--ease-standard) forwards; }

@keyframes toast-in  { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(10px) scale(0.97); } }

/* ------------------------------------------------------ offline banner */
.offline-banner {
    /* sticky, not fixed: fixed positioning covered the app bar, hiding the
       user's own name and both top-bar buttons. */
    position: sticky;
    top: 0;
    inset-inline: 0;
    z-index: 1100;
    /* #fff on #d97706 measured 3.19:1 — this is the one line that must be
       readable in daylight glare. Dark text on a light amber gets 8.6:1. */
    background: #fed7aa;
    color: #7c2d12;
    text-align: center;
    padding: 0.5rem;
    font-size: 0.8125rem;
    font-weight: 600;
    padding-top: calc(0.5rem + env(safe-area-inset-top));
    animation: drop-in var(--dur-normal) var(--ease-emphasized) both;
}

@keyframes drop-in { from { transform: translateY(-100%); } to { transform: none; } }

.stale-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.75rem;
    color: var(--md-warning);
    font-weight: 600;
}

/* --------------------------------------------------------- bottom nav */
.bottom-nav {
    position: fixed;
    bottom: 0;
    inset-inline: 0;
    z-index: 1040;
    display: grid;
    grid-auto-flow: column;
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border-top: 1px solid var(--outline);
    padding-bottom: env(safe-area-inset-bottom);
}

.bottom-nav a {
    padding: 0.625rem 0.25rem;
    text-align: center;
    text-decoration: none;
    color: var(--on-surface-muted);
    font-size: 0.6875rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    transition: color var(--dur-fast) var(--ease-standard);
}

.bottom-nav a i      { font-size: 1.25rem; }
.bottom-nav a.active { color: var(--md-primary); }

@media (min-width: 992px) {
    .bottom-nav { display: none; }
}

.has-bottom-nav { padding-bottom: 5rem; }

@media (min-width: 992px) {
    .has-bottom-nav { padding-bottom: 2rem; }
}

/* ------------------------------------------------------- install prompt */
.install-prompt {
    position: fixed;
    bottom: calc(5rem + env(safe-area-inset-bottom));
    inset-inline: 1rem;
    z-index: 1050;
    padding: 1rem;
    display: none;
    align-items: center;
    gap: 0.75rem;
    animation: toast-in var(--dur-normal) var(--ease-spring) both;
}

.install-prompt.show { display: flex; }

/* ------------------------------------------------------------ modal */
.modal-content {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg);
}

.modal.fade .modal-dialog {
    transition: transform var(--dur-normal) var(--ease-emphasized);
    transform: translateY(24px) scale(0.97);
}

.modal.show .modal-dialog { transform: none; }

/* --------------------------------------------------------- empty state */
.empty-state {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--on-surface-muted);
}

.empty-state i { font-size: 3rem; opacity: 0.35; display: block; margin-bottom: 0.75rem; }

/* =====================================================================
 *  Utility classes.
 *  These exist because CSP `style-src 'self'` blocks style="" ATTRIBUTES
 *  outright — a nonce does not cover them. Every inline style in this app
 *  was silently dead (the page max-width never applied at all). Anything
 *  static lives here; anything dynamic is set through the CSSOM from JS.
 * ================================================================== */
.page-container { max-width: 960px; }

.skeleton-w-35 { width: 35%; }
.skeleton-w-45 { width: 45%; }
.skeleton-w-55 { width: 55%; }
.skeleton-w-60 { width: 60%; }
.skeleton-last { margin-bottom: 0; }
.skeleton-square { aspect-ratio: 1; }

.empty-state-compact { padding: 1.5rem 1rem; }

/* =====================================================================
 *  Sidebar. Off-canvas drawer on phones, docked from lg up.
 * ================================================================== */
@media (min-width: 992px) {
    .app-shell {
        display: grid;
        grid-template-columns: 260px minmax(0, 1fr);
        align-items: start;
    }

    .app-sidebar {
        position: sticky;
        top: 68px;                    /* below the sticky app bar */
        height: calc(100dvh - 68px);
        overflow-y: auto;
        border-inline-start: 1px solid var(--outline);
        background: var(--surface-1);
    }
}

.app-sidebar-body { padding: 1rem 0.75rem; }

/* Bootstrap turns `.offcanvas-lg .offcanvas-body` into display:flex at the
   lg breakpoint, and the default flex-direction is ROW — which laid the
   nav sections out side by side and squeezed every label into a 2-character
   vertical column. Force normal block flow back. */
/* Selector deliberately outranks Bootstrap's `.offcanvas-lg .offcanvas-body`
   (two classes) — a single-class rule loses on specificity even though this
   file loads later. */
@media (min-width: 992px) {
    .app-sidebar.offcanvas-lg .offcanvas-body {
        display: block;
        flex-grow: 1;
        padding: 1rem 0.75rem;
        overflow-y: auto;
    }
}

.sidebar-identity {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.5rem 1rem;
    border-bottom: 1px solid var(--outline);
    margin-bottom: 0.75rem;
}

.sidebar-badge {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin: 0 0.25rem 0.75rem;
    padding: 0.375rem 0.625rem;
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--md-tertiary) 18%, transparent);
    color: var(--chip-mine-fg);
    font-size: 0.75rem;
    font-weight: 600;
}

.sidebar-section {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--on-surface-muted);
    margin: 1rem 0.75rem 0.375rem;
}

.sidebar-list {
    list-style: none;
    padding: 0;
    margin: 0 0 0.25rem;
}

.sidebar-link {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    /* 44px minimum row height — these get tapped one-handed. */
    min-height: 44px;
    padding: 0.5rem 0.75rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--on-surface);
    text-decoration: none;
    font-size: 0.9375rem;
    font-weight: 500;
    text-align: start;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard);
}

.sidebar-link i { font-size: 1.125rem; flex-shrink: 0; }

.sidebar-link:hover { background: var(--surface-2); color: var(--on-surface); }

.sidebar-link.active {
    background: color-mix(in srgb, var(--md-primary) 18%, transparent);
    /* Not --md-primary: on its own 18% wash that measured 4.01:1 in the
       light theme and 4.42:1 in the dark one. See theme.css. */
    color: var(--nav-active-fg);
    font-weight: 700;
}

/* The active marker is a bar on the inline-START edge, which is the RIGHT
   edge in RTL — mirrors correctly without any override. */
.sidebar-link.active {
    box-shadow: inset 3px 0 0 0 var(--md-primary);
}

.sidebar-link-danger { color: var(--nav-danger-fg); }
.sidebar-link-danger:hover {
    background: color-mix(in srgb, var(--md-danger) 14%, transparent);
    color: var(--nav-danger-fg);
}

/* ------------------------------------------------------- skip link */
/* Keyboard users must be able to jump past the app bar. Visible only
   when focused, but never display:none — that removes it from tab order. */
.skip-link {
    position: absolute;
    top: -100px;
    inset-inline-start: 1rem;
    z-index: 2000;
    padding: 0.625rem 1rem;
    background: var(--md-primary);
    color: var(--md-on-primary);
    border-radius: var(--radius-sm);
    font-weight: 600;
    text-decoration: none;
    transition: top var(--dur-fast) var(--ease-standard);
}

.skip-link:focus {
    top: calc(0.5rem + env(safe-area-inset-top));
}

/* --------------------------------------------------------- icon button */
/* 44x44 minimum: the WCAG 2.2 target size, and the difference between
   tapping this and missing it while walking a patrol at night. */
.icon-btn {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    border: 1px solid var(--outline);
    border-radius: var(--radius-full);
    background: var(--surface-2);
    color: var(--on-surface);
    display: grid;
    place-items: center;
    cursor: pointer;
    transition: transform var(--dur-fast) var(--ease-spring),
                background var(--dur-fast) var(--ease-standard);
}

.icon-btn:hover  { background: var(--surface-3); }

/*
   THE APP BAR RAN OUT OF ROOM WHEN THE BELL ARRIVED (v1.61.1).

   Six 44px buttons plus their gaps and the container padding come to
   336px, which on a 375px phone leaves 39px for the page title — so
   "לוח בקרה" rendered as "לוח...". The title is the thing telling the
   volunteer where they are; it does not lose to a settings control.

   The two that stand down are the two that are SETTINGS rather than
   actions, and both have a home on the settings screen: text size was
   already there, and language was given one in the same change — hiding a
   control whose only instance is here would have taken the single in-app
   way to leave Hebrew from precisely the readers who need it.

   What stays is the menu, the bell, search and night mode. Night mode
   looks like a setting and is not: this squad patrols at night, and a
   control you need in the dark belongs one tap away.

   480px rather than a Bootstrap breakpoint because the number comes from
   the arithmetic above, not from a grid tier. */
@media (max-width: 480px) {
    .app-bar-optional { display: none; }
}

/* ------------------------------------------------- notification centre */
/* The anchor exists only to give the panel something to hang from; the
   bell keeps the app bar's own flex sizing. */
.notif-anchor { position: relative; flex-shrink: 0; }

.notif-center {
    position: fixed;
    /*
       POSITION IS SET BY JAVASCRIPT, not by an inset here.

       Two attempts pinned it to an edge and both were wrong: which side of
       the app bar the bell sits on decides whether "grow toward inline-end"
       goes into the screen or out of it, and that is a property of the
       layout, not of the direction. Guessing it produced a panel a third
       off the right edge, then one clipped off the left.

       So notif_center.js measures the bell and clamps the panel into the
       viewport on every open. That is correct in both directions, at any
       width, wherever the bell ends up — and it is what a dropdown has to
       do anyway once the anchor can be anywhere.
    */
    /* PHYSICAL left/top, and only these — they are what place() writes.
       An inset-inline-* here resolves to `right` in Hebrew, and a box with
       left, right AND width set is over-constrained: CSS then discards the
       `left` the script had just computed, so the clamp ran correctly and
       was thrown away. One axis, one owner — and the owner is the script.

       NO HORIZONTAL INSET IS DECLARED HERE AT ALL. app.css is written in
       logical properties so the layout mirrors on the dir attribute alone
       (tests/phase_d.php enforces it), and a physical `left` in this file
       would be the one exception that breaks the rule for everyone. The
       script may write one because the value it computes is a measurement
       against the viewport, not a layout choice — and it reads `direction`
       itself to decide which edge to align. Until it runs the panel is
       display:none, so there is nothing to position. */
    top: 0;
    width: min(21rem, calc(100vw - 1.5rem));
    z-index: 1060;
    display: none;
    flex-direction: column;
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-md, 16px);
    box-shadow: var(--elev-3);
    overflow: hidden;
    animation: toast-in var(--dur-fast) var(--ease-spring) both;
}
.notif-center.show { display: flex; }

/* No media query for the narrow case: the runtime clamp above already
   keeps the panel inside the viewport at every width, and a second rule
   that moved it would be one more place for the two to disagree. */

.notif-center-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.7rem 0.9rem;
    border-bottom: 1px solid var(--outline);
    font-size: 0.85rem;
}

/* Capped rather than grown: the panel answers "what is new", and a list
   that scrolls for a screen and a half is the inbox page's job. */
.notif-center-body { max-height: min(60vh, 22rem); overflow-y: auto; }

.notif-center-item {
    display: flex;
    gap: 0.55rem;
    width: 100%;
    padding: 0.65rem 0.9rem;
    border: 0;
    border-bottom: 1px solid var(--outline);
    background: none;
    color: inherit;
    text-align: start;
    cursor: pointer;
}
.notif-center-item:last-child { border-bottom: 0; }
.notif-center-item:hover      { background: color-mix(in srgb, var(--md-primary) 10%, transparent); }
.notif-center-item i          { color: var(--md-primary); flex-shrink: 0; }
.notif-center-item .t         { font-weight: 600; font-size: 0.82rem; }
.notif-center-item .m,
.notif-center-item .w         { font-size: 0.75rem; color: var(--on-surface-muted); }

.notif-center-empty {
    padding: 1.4rem 0.9rem;
    text-align: center;
    font-size: 0.82rem;
    color: var(--on-surface-muted);
}

.notif-center-foot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.6rem;
    border-top: 1px solid var(--outline);
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--md-primary);
    text-decoration: none;
}
.notif-center-foot:hover { background: color-mix(in srgb, var(--md-primary) 12%, transparent); }
.icon-btn:active { transform: scale(0.9); }

/* -------------------------------------------------------------- FAB */
.fab {
    position: fixed;
    bottom: calc(5.5rem + env(safe-area-inset-bottom));
    inset-inline-start: 1rem;
    z-index: 1045;
    width: 56px;
    height: 56px;
    border: 0;
    border-radius: var(--radius-md);
    background: var(--md-primary);
    color: var(--md-on-primary);
    font-size: 1.5rem;
    display: grid;
    place-items: center;
    box-shadow: var(--elev-3);
    cursor: pointer;
}

/* --------------------------------------------------------- stat tile */
.stat-tile {
    padding: 0.875rem 0.5rem;
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    height: 100%;
}

.stat-value { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
.stat-label { font-size: 0.6875rem; color: var(--on-surface-muted); }

/* ----------------------------------------------------- temp password */
.temp-password {
    font-family: 'Courier New', monospace;
    font-size: 1.375rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-align: center;
    padding: 0.875rem;
    background: var(--surface-2);
    border: 1px dashed var(--outline);
    border-radius: var(--radius-sm);
    user-select: all;
}

/* ------------------------------------------------------- user row */
.user-row {
    background: var(--surface-1);
    border: 1px solid var(--outline);
    border-radius: var(--radius-md);
    padding: 0.875rem;
    margin-bottom: 0.625rem;
    box-shadow: var(--elev-1);
}

.user-row.is-suspended { opacity: 0.62; border-style: dashed; }

/* --------------------------------------------------- progress bar row */
.mini-bar {
    height: 8px;
    background: var(--surface-3);
    border-radius: var(--radius-full);
    overflow: hidden;
    margin-top: 0.375rem;
}

.mini-bar-fill {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--md-primary);
    transition: width var(--dur-slow) var(--ease-emphasized);
}

.mini-bar-fill.met { background: var(--md-success); }

/* ----------------------------------------------- forms on our surfaces */
.form-control {
    background: var(--surface-2);
    border-color: var(--outline);
    color: var(--on-surface);
    border-radius: var(--radius-sm);
    transition: border-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard);
}

.form-control:focus {
    background: var(--surface-2);
    color: var(--on-surface);
    border-color: var(--md-primary);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--md-primary) 18%, transparent);
}

.form-floating > label { color: var(--on-surface-muted); }

.btn-primary {
    --bs-btn-bg: var(--md-primary);
    --bs-btn-border-color: var(--md-primary);
    --bs-btn-hover-bg: var(--md-primary-hover);
    --bs-btn-hover-border-color: var(--md-primary-hover);
    --bs-btn-color: var(--md-on-primary);
    --bs-btn-hover-color: var(--md-on-primary);
    border-radius: var(--radius-sm);
    font-weight: 600;
}

/* ---------------------------------------------------------------------
   Attribution line — login footer, sidebar foot, About page.
   Quiet by default: it should be findable, never competing with the
   content. Kept as one rule so the three placements cannot drift.
   --------------------------------------------------------------------- */
.app-credit {
    margin-top: 1rem;
    font-size: .75rem;
    line-height: 1.5;
    text-align: center;
    color: var(--on-surface-muted);
    opacity: .85;
}

.app-credit-lg {
    margin-top: 0;
    font-size: .875rem;
    opacity: 1;
}

.about-mark {
    display: block;
    margin-bottom: .75rem;
    font-size: 2.5rem;
    color: var(--md-primary);
}

/* The sidebar credit sits below the last group, pushed to the bottom when
   the menu is shorter than the viewport. */
.app-sidebar-body .app-credit {
    margin-top: auto;
    padding-top: 1rem;
    border-top: 1px solid var(--glass-border);
}

/* ---------------------------------------------------------------------
   Credential fields — ID numbers and passwords.

   These are Latin strings mixed with digits and punctuation, typed into
   an RTL page. Left to inherit the page direction, the bidi algorithm
   reorders them on screen: "DimonaAdmin#2026" displays as
   "Dimona!Admin2026"-style garbage, and the user retypes a password that
   was correct all along. The stored value was never wrong — only the
   rendering was.

   dir="ltr" on the input fixes the order; text-align:right keeps the
   field looking like the rest of the RTL form.
   --------------------------------------------------------------------- */
[dir="rtl"] .form-control[dir="ltr"] {
    text-align: right;
}

/* The eye toggle (.pw-toggle) sits at the PAGE's inline-end — left in RTL,
   right in LTR. The input itself is forced dir="ltr", so its own logical
   sides are fixed: inline-start = left, inline-end = right. Scoping by the
   page direction keeps the padding on whichever physical side the toggle
   actually occupies. */
[dir="rtl"] .password-field .form-control[dir="ltr"] {
    padding-inline-start: 2.75rem;   /* left — where the toggle is in RTL */
}
[dir="ltr"] .password-field .form-control[dir="ltr"] {
    padding-inline-end: 2.75rem;     /* right — where the toggle is in LTR */
}

/* Divider between the password form and the biometric alternative. */
.auth-divider {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin: 1.25rem 0 1rem;
    color: var(--on-surface-muted);
    font-size: .8125rem;
}

.auth-divider::before,
.auth-divider::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--glass-border);
}

/* =====================================================================
   Reports
   ===================================================================== */

.report-table {
    width: 100%;
    border-collapse: collapse;
    font-size: .9375rem;
}

.report-table th,
.report-table td {
    padding: .625rem .75rem;
    text-align: start;
    border-bottom: 1px solid var(--glass-border);
    vertical-align: middle;
}

.report-table thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--surface-1);
    font-size: .8125rem;
    font-weight: 600;
    color: var(--on-surface-muted);
    white-space: nowrap;
}

.report-table tbody tr:last-child td { border-bottom: 0; }

.report-table .text-center { text-align: center; }

.table-scroll {
    overflow-x: auto;
    border-radius: var(--radius-lg, 1rem);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
}

/* A row that can be opened must look like it. */
.report-table tr.drillable { cursor: pointer; }

.report-table tr.drillable:hover,
.report-table tr.drillable:focus-visible {
    background: var(--md-primary-container);
    outline: none;
}

.report-table tr.drillable:focus-visible {
    box-shadow: inset 0 0 0 2px var(--md-primary);
}

/* Breadcrumb showing how deep the drill-down went. */
.drill-path {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem;
    margin-bottom: 1rem;
}

.drill-crumb {
    border: 1px solid var(--glass-border);
    background: var(--glass-bg);
    color: inherit;
    border-radius: 999px;
    padding: .25rem .75rem;
    font-size: .8125rem;
    cursor: pointer;
}

.drill-crumb:hover { background: var(--md-primary-container); }

.drill-path .drill-crumb:not(:last-child)::after {
    content: '‹';
    margin-inline-start: .5rem;
    opacity: .5;
}

/* Shown only on paper. */
.print-only { display: none; }

/* =====================================================================
   Print / PDF
   =====================================================================
   "Export to PDF" is the browser's own print-to-PDF. That is deliberate:
   a PHP PDF library would have to embed and shape a Hebrew font itself,
   and RTL shaping is exactly where those libraries fail. Printing reuses
   the fonts already on the page, so the Hebrew is correct by construction
   and the output matches what the user just looked at.
   ===================================================================== */
@media print {
    @page {
        size: A4;
        margin: 14mm 12mm;
    }

    /* Ink is expensive and the glass effect is meaningless on paper. */
    :root, [data-bs-theme] {
        --glass-bg: #fff;
        --glass-border: #d0d5dd;
        --on-surface-muted: #475467;
    }

    html, body {
        background: #fff !important;
        color: #000 !important;
        font-size: 11pt;
    }

    /* Chrome omits backgrounds by default; the progress bars carry meaning. */
    * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

    .no-print,
    .app-sidebar,
    .app-bar,
    .bottom-nav,
    .offline-banner,
    .toast-stack,
    .install-prompt,
    #drillPath { display: none !important; }

    .print-only { display: block; }

    main, .app-main, .container, .container-fluid {
        max-width: none !important;
        margin: 0 !important;
        padding: 0 !important;
    }

    .glass-card {
        background: #fff !important;
        border: 1px solid #d0d5dd !important;
        box-shadow: none !important;
        border-radius: 0 !important;
        page-break-inside: avoid;
    }

    .report-letterhead {
        border-bottom: 2px solid #000;
        margin-bottom: 8mm;
        padding-bottom: 3mm;
    }

    /* Was .report-letterhead h1 — the element became a <p> so the page has
       one h1. Scoped to .report-letterhead so it outranks the sibling
       '.report-letterhead p' rule below, which is more specific than a bare
       class and would otherwise render the masthead at 9pt grey. */
    .report-letterhead .letterhead-title {
        font-size: 16pt;
        margin: 0;
        font-weight: 700;
    }

    .report-letterhead p {
        margin: 1mm 0 0;
        font-size: 9pt;
        color: #475467;
    }

    .table-scroll {
        overflow: visible !important;
        border: 0 !important;
        background: none !important;
    }

    .report-table { font-size: 9.5pt; }

    .report-table th,
    .report-table td {
        border-bottom: 1px solid #d0d5dd;
        padding: 1.5mm 2mm;
    }

    /* Repeat the header on every page of a long roster. */
    .report-table thead { display: table-header-group; }
    .report-table tfoot { display: table-footer-group; }
    .report-table tr    { page-break-inside: avoid; }

    .stat-tile {
        border: 1px solid #d0d5dd;
        padding: 2mm;
    }

    .stat-value { font-size: 14pt; }
    .stat-label { font-size: 8pt; }

    .mini-bar {
        border: 1px solid #98a2b3;
        background: #fff !important;
        height: 6px;
    }

    .mini-bar-fill { background: #667085 !important; }
    .mini-bar-fill.met { background: #12b76a !important; }

    .report-footer {
        margin-top: 6mm;
        padding-top: 2mm;
        border-top: 1px solid #d0d5dd;
        font-size: 8pt;
        color: #475467;
        text-align: center;
    }

    a[href]::after { content: ''; }   /* no URL noise on paper */
}

/* =====================================================================
   Leaderboard
   ===================================================================== */

.rank-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    font-weight: 700;
    font-size: .875rem;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    color: var(--on-surface-muted);
}

/* Only the top three are marked. Below that a plain number reads better
   than a medal nobody is proud of. */
.rank-badge.place-1 { background: #fef0c7; border-color: #f79009; color: #b54708; }
.rank-badge.place-2 { background: #eaecf0; border-color: #98a2b3; color: #475467; }
.rank-badge.place-3 { background: #fde8d7; border-color: #e0863a; color: #93370d; }

[data-bs-theme="dark"] .rank-badge.place-1 { background: #4e3a09; border-color: #f79009; color: #fedf89; }
[data-bs-theme="dark"] .rank-badge.place-2 { background: #333741; border-color: #98a2b3; color: #eaecf0; }
[data-bs-theme="dark"] .rank-badge.place-3 { background: #4a2a12; border-color: #e0863a; color: #fcd9b6; }

/* Your own row, so it is findable in a long list without scanning. */
.user-row.is-me {
    border-color: var(--md-primary);
    background: var(--md-primary-container);
}

/* =====================================================================
   Certificate of appreciation
   =====================================================================
   Sized as a landscape A4 sheet on screen too, so what the coordinator
   previews is what comes out of the printer.
   ===================================================================== */

.certificate {
    /* The approved ceremonial design (v1.39.3). Screen shows the exact
       A4-landscape composition; print locks it to the physical sheet. */
    --c-navy: #142b52; --c-gold: #b8912f; --c-gold-soft: #d9bc6b;
    --c-paper: #fdfbf5; --c-ink: #1b2436; --c-muted: #5b6478;
    position: relative;
    max-width: 1100px;
    margin: 0 auto;
    aspect-ratio: 297 / 210;
    background: radial-gradient(115% 90% at 50% 0%, #ffffff 0%, var(--c-paper) 55%, #f4eeda 100%);
    color: var(--c-ink);
    overflow: hidden;
    container-type: inline-size;
    font-family: "David Libre", David, "Frank Ruehl", FrankRuehl, "Times New Roman", serif;
    display: flex; flex-direction: column; align-items: center;
    padding: 3% 5% 2.2%;
    box-shadow: 0 10px 40px rgba(0,0,0,.18);
    text-align: center;
}
/* A printed artefact keeps its cream-and-navy look in dark mode: a dark
   certificate is not a thing anyone hangs on a wall. */
[data-bs-theme="dark"] .certificate { background: radial-gradient(115% 90% at 50% 0%, #ffffff 0%, var(--c-paper) 55%, #f4eeda 100%); color: var(--c-ink); }

.certificate.grade-appreciation { --c-gold: #5a6f96; --c-gold-soft: #8ea3c8; }

.c2-frame-outer { position: absolute; inset: 1.6%; border: 3px solid var(--c-navy); border-radius: 4px; pointer-events: none; }
.c2-frame-inner { position: absolute; inset: 2.6%; border: 1.5px solid var(--c-gold); border-radius: 3px; pointer-events: none; }
.c2-corner { position: absolute; width: 4.4cqi; height: 4.4cqi; border: 3px solid var(--c-gold); pointer-events: none; }
.c2-tl { top: 1.6%; right: 1.05%; border-left: 0; border-bottom: 0; }
.c2-tr { top: 1.6%; left: 1.05%; border-right: 0; border-bottom: 0; }
.c2-bl { bottom: 2.2%; right: 1.05%; border-left: 0; border-top: 0; }
.c2-br { bottom: 2.2%; left: 1.05%; border-right: 0; border-top: 0; }

.c2-watermark {
    position: absolute; top: 50%; left: 50%; width: 46%;
    transform: translate(-50%, -44%);
    opacity: .05; pointer-events: none; filter: grayscale(35%);
}

.c2-head { display: flex; align-items: center; gap: 3.5%; width: 92%; justify-content: space-between; }
.c2-logo-side { height: 7cqi; width: auto; }
.c2-logo-main { height: 9.8cqi; width: auto; display: block; margin: 0 auto 6px; filter: drop-shadow(0 2px 6px rgba(0,0,0,.25)); }
.c2-head-mid { text-align: center; flex: 1; }
.c2-org { margin: 0; font-size: 1.45cqi; letter-spacing: .14em; color: var(--c-muted); }
.c2-org.c2-strong { font-size: 1.73cqi; font-weight: 700; color: var(--c-navy); letter-spacing: .1em; }

.c2-rule { display: flex; align-items: center; gap: 10px; width: 64%; margin: 1.4% 0 .8%; color: var(--c-gold); }
.c2-rule::before, .c2-rule::after { content: ""; flex: 1; height: 0; border-top: 1.5px solid var(--c-gold); }
.c2-rule span { font-size: 1.36cqi; }

.c2-title { margin: 0; font-size: 4.5cqi; font-weight: 800; color: var(--c-navy); letter-spacing: .02em; text-shadow: 0 1px 0 #fff; }
.c2-period {
    margin: 1.4% 0 0; font-size: 1.82cqi; color: var(--c-navy);
    border: 1.5px solid var(--c-gold); border-radius: 999px;
    padding: 4px 26px; background: rgba(216,188,107,.12);
}
.c2-lead { margin: 1.3% 0 .2%; font-size: 1.64cqi; color: var(--c-muted); }
.c2-name { margin: 0; font-size: 3.7cqi; font-weight: 700; color: var(--c-ink); border-bottom: 2.5px solid var(--c-gold); padding: 0 40px 5px; }
.c2-team { margin: .6% 0 0; font-size: 1.45cqi; color: var(--c-muted); letter-spacing: .06em; }

.c2-body { margin: 1.4% 0 0; max-width: 78%; font-size: 1.7cqi; line-height: 1.75; }
.c2-body b { color: var(--c-navy); }
.c2-motto { margin: 1.2% 0 0; font-size: 1.64cqi; color: var(--c-gold); font-style: italic; }

.c2-foot { margin-top: auto; width: 88%; display: flex; align-items: flex-end; justify-content: space-between; gap: 4%; }
.c2-sig { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 1; }
.c2-sig-line { width: 88%; border-bottom: 2px solid var(--c-ink); height: 3.1cqi; }
.c2-sig-role { font-size: 1.45cqi; font-weight: 700; color: var(--c-ink); }
.c2-sig-sub { font-size: 1.18cqi; color: var(--c-muted); }
.c2-seal { text-align: center; flex: 0 0 auto; }
.c2-seal-ring {
    width: 7.9cqi; height: 7.9cqi; margin: 0 auto 5px; border-radius: 50%;
    border: 2.5px double var(--c-gold); color: var(--c-gold);
    display: flex; align-items: center; justify-content: center;
    font-size: 1.27cqi; font-weight: 700; line-height: 1.25; letter-spacing: .04em;
    background: radial-gradient(circle at 32% 28%, #fffdf2, #f4e9c8);
    box-shadow: inset 0 0 0 4px var(--c-paper);
    transform: rotate(-6deg);
    padding: 8px;
}
.c2-issue { margin: 2px 0 0; font-size: 1.27cqi; color: var(--c-ink); }
.c2-meta { margin: 0; font-size: 1.1cqi; color: var(--c-muted); }
.c2-meta bdi { font-family: ui-monospace, Consolas, monospace; }

/* Certificate PRINT rules live in certificate.css, loaded ONLY by
   certificate.php via $pageStyles — a bare @page there cannot leak,
   and the named-page approach cost a guaranteed blank sheet. */

/* =====================================================================
   Notifications
   ===================================================================== */

.notif-icon {
    font-size: 1.125rem;
    color: var(--on-surface-muted);
    margin-top: .125rem;
}

/* Unread is marked with a bar rather than a colour wash, so it stays
   legible in both themes and for anyone who cannot rely on hue. */
.notif.is-unread {
    border-inline-start: 3px solid var(--md-primary);
    cursor: pointer;
}

.notif.is-unread .notif-icon { color: var(--md-primary); }
.notif.is-unread strong::after {
    content: '';
    display: inline-block;
    width: .5rem;
    height: .5rem;
    margin-inline-start: .375rem;
    border-radius: 50%;
    background: var(--md-primary);
    vertical-align: middle;
}

.sidebar-badge-count {
    display: inline-block;
    min-width: 1.25rem;
    padding: 0 .375rem;
    margin-inline-start: auto;
    border-radius: 999px;
    background: var(--md-primary);
    color: #fff;
    font-size: .6875rem;
    font-weight: 700;
    text-align: center;
    line-height: 1.25rem;
}

/* =====================================================================
   Institutional crests
   =====================================================================
   THE DARK-MODE PROBLEM: the police emblem is solid navy on transparency
   and the station crest relies on a white outline. Dropped straight onto
   a dark surface both nearly vanish. Rather than tint official emblems —
   which one does not do to a police crest — each sits on a light plate
   that appears only in dark mode. The mark stays exactly as issued.
   ===================================================================== */

.crest {
    /* Never distort an emblem. Fixed box, contained aspect. */
    object-fit: contain;
    display: block;
    flex-shrink: 0;
}

.crest-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    margin-top: 1rem;
}

.crest-row .crest {
    opacity: .75;
    transition: opacity .2s ease;
}

.crest-row:hover .crest { opacity: 1; }

[data-bs-theme="dark"] .crest-row .crest,
[data-bs-theme="dark"] .app-bar-crest {
    background: rgba(255, 255, 255, .92);
    border-radius: .375rem;
    padding: .1875rem;
}

/* About page: crest above its caption. */
.crest-row-labelled { gap: 1.75rem; flex-wrap: wrap; }

.crest-row-labelled figure {
    margin: 0;
    text-align: center;
}

.crest-row-labelled figcaption {
    margin-top: .375rem;
    font-size: .6875rem;
    color: var(--on-surface-muted);
}

.crest-row-labelled .crest { margin-inline: auto; }

/* ---- login ---- */
.auth-crest {
    display: block;
    margin: 0 auto .75rem;
    object-fit: contain;
    /* The patch is dark and detailed; a soft glow lifts it off the card
       without a hard border that would fight the glassmorphism. */
    filter: drop-shadow(0 4px 12px rgba(0, 0, 0, .28));
}

/* ---- app bar ---- */
.app-bar-crest {
    object-fit: contain;
    align-self: center;
}

/* ---- about ---- */
.about-crest {
    display: block;
    margin: 0 auto .75rem;
    object-fit: contain;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, .22));
}

/* ---- certificate ---- */
.cert-crests {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1.5rem;
    margin-bottom: .5rem;
}

.cert-crest-main { object-fit: contain; }

/* The force and the station flank the squad's own patch, smaller: the
   certificate is awarded by the squad, under their authority. */
.cert-crest-side { object-fit: contain; opacity: .9; }

/* ---- printed letterhead ---- */
.report-letterhead .letterhead-crest { object-fit: contain; }
.letterhead-text { flex-grow: 1; }

@media print {
    .report-letterhead {
        display: flex;
        align-items: center;
        gap: 6mm;
        text-align: center;
    }

    /* Emblems must actually appear on paper — Chrome drops backgrounds
       and images by default when printing. */
    .crest, .letterhead-crest, .cert-crest-main, .cert-crest-side {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    /* No dark-mode plate on paper: white paper is already the light
       background these emblems were designed for. */
    .crest, .app-bar-crest { background: none !important; padding: 0 !important; }

    .cert-crests { gap: 10mm; }
}

/* =====================================================================
   Fixes — v1.10
   ===================================================================== */

/*
 * .min-width-0 was used in ten places and defined in none — it is not a
 * Bootstrap 5 utility either. Without it a flex child keeps min-width:auto
 * and refuses to shrink below its longest line, so EVERY text-truncate in
 * the app was dead: a long name in the app bar pushed the theme button off
 * a 360px screen.
 */
.min-width-0 { min-width: 0; }

/*
 * The bottom nav grows by the safe-area inset on an iPhone, but the body
 * reserved a flat 5rem — so the last ~12px of every page, which is where
 * the last row's action button lives, sat underneath it. Android is
 * unaffected because the inset is 0 there, which is why it survived testing.
 */
.has-bottom-nav { padding-bottom: calc(5rem + env(safe-area-inset-bottom)); }

@media (min-width: 992px) {
    .has-bottom-nav { padding-bottom: 2rem; }
}

/* Sticky report headers referenced --md-surface, which is not defined
   anywhere; the background fell back to transparent and rows scrolled
   through the header. */
.report-table thead th { background: var(--surface-1); }

/*
 * A table with width:100% can never exceed its scroll container, so
 * .table-scroll never scrolled and six columns squashed to ~50px each on a
 * phone. Give it a real minimum and let the container do its job — except
 * on paper, where the sheet is wide enough and must not overflow.
 */
/* min-width already beats the inherited width:100% inside an overflow-x
   container, so adding width:auto bought nothing and cost a lot: it let the
   table shrink to its content, and a 900px card rendered a 640px table with
   a ragged right edge on tablet and desktop. */
.report-table { min-width: 40rem; }

@media print {
    .report-table { min-width: 0; width: 100%; }
}

/*
 * Safari zooms the page whenever a focused control is under 16px, and
 * Bootstrap's -sm variants are 14px. The report filters are entirely -sm,
 * so every filter tap zoomed the page in and left it there. Chrome on
 * Android does not do this, which is why it went unnoticed.
 */
@media (pointer: coarse) {
    .form-control-sm,
    .form-select-sm,
    /* Bootstrap sizes a button group through the DESCENDANT selector
       .btn-group-sm > .btn — the .btn-sm class is never present on those
       elements, so every rule keyed on .btn-sm missed the entire reports
       period picker. Measured before this line: 14px text, 31px tall. */
    .btn-group-sm > .btn { font-size: 1rem; }

    /* 44px is the documented minimum touch target, and it is honoured on
       the icon buttons and the calendar — but the booking, swap and admin
       action buttons are .btn-sm at ~31px, wrapped and adjacent, including
       the delete button. */
    .btn-sm,
    .btn-group-sm > .btn {
        min-height: 44px;
        /* .btn is inline-block, so min-height grows the box downward and
           strands the label against the top padding. Centre it on both axes
           instead of leaving a 44px target with a label near its edge. */
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* min-height only fixes one axis. An icon-only button is still ~32px
       wide, which is a 32px target no matter how tall the box is. */
    .btn-sm:has(> i:only-child) { min-width: 44px; }
}

/* The docked sidebar started at a hard-coded 68px while the app bar grows
   by the top inset — on a notched iPad the sidebar slid under the bar. */
@media (min-width: 992px) {
    .app-sidebar {
        top: calc(68px + env(safe-area-inset-top));
        height: calc(100dvh - 68px - env(safe-area-inset-top));
    }
}

/* Five tabs with grid-auto-flow:column and no explicit track sizing get
   unequal widths, so the tap zones did not match the labels. */
.bottom-nav { grid-auto-columns: 1fr; }

/* U+2039 is a mirroring character: inside an RTL page it rendered as ›,
   pointing the opposite way to the drill-down direction it marks. */
.drill-path .drill-crumb:not(:last-child)::after { content: '·'; }

/* Toast close button — see app.js for why a toast without one could be
   pinned open by a stray tap. */
.toast-item { position: relative; padding-inline-end: 2.5rem; }

.toast-close {
    position: absolute;
    inset-inline-end: .375rem;
    top: 50%;
    transform: translateY(-50%);
    width: 2rem;
    height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    opacity: .7;
    cursor: pointer;
    font-size: .75rem;
}

.toast-close:hover { opacity: 1; background: rgba(0, 0, 0, .12); }

/* The toast stack and the install card both sat on top of the swaps FAB,
   which is the only way to offer a shift on that screen.
   Scoped to the phone breakpoint on purpose: above it there is no bottom
   nav and no FAB, so nothing to clear. Unscoped, this left desktop toasts
   hovering 144px above the bottom edge with empty space beneath them. */
@media (max-width: 991.98px) {
    .toast-stack    { bottom: calc(9rem + env(safe-area-inset-bottom)); }
    .install-prompt { bottom: calc(9rem + env(safe-area-inset-bottom)); }
}

/* =====================================================================
   Statistics screen — hand-drawn charts.

   All colour comes from the existing state tokens, so a chart and the
   badge it corresponds to on the shift board are literally the same hue.
   Any new palette here would mean the calendar calling a shift "one
   short" in amber while the heat map called it something else.
   ===================================================================== */

/* The heat map is 13 weeks wide and does not shrink below legibility, so
   it scrolls inside its own box rather than forcing the page sideways. */
.heatmap-scroll {
    overflow-x: auto;
    padding-bottom: .25rem;
}

.hm            { fill: var(--surface-2); }
.hm-none       { fill: var(--surface-2); }
.hm-full       { fill: var(--state-full); }
.hm-thin       { fill: var(--state-needs-one); }
.hm-short      { fill: var(--state-open); }
.hm-empty      { fill: var(--state-empty); }

/* Today gets an outline, not a fill: its staffing state is the thing
   being read, and recolouring it would destroy the information. */
.hm-today      { stroke: var(--on-surface); stroke-width: 1.5; }

.hm-axis {
    fill: var(--on-surface-muted);
    font-size: 9px;
}

/* Legend swatches reuse the same classes, as spans rather than SVG. */
.hm-key {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
}
.hm-key i {
    width: 11px;
    height: 11px;
    border-radius: 3px;
    display: inline-block;
}
.hm-key i.hm-none  { background: var(--surface-2); }
.hm-key i.hm-full  { background: var(--state-full); }
.hm-key i.hm-thin  { background: var(--state-needs-one); }
.hm-key i.hm-short { background: var(--state-open); }
.hm-key i.hm-empty { background: var(--state-empty); }

.chart {
    width: 100%;
    height: auto;
}
.ch-grid { stroke: var(--outline); stroke-width: 1; }
.ch-bar  { fill: var(--md-primary); }
.ch-axis {
    fill: var(--on-surface-muted);
    font-size: 10px;
}

/* Per-team load. Plain HTML: the labels are Hebrew names of arbitrary
   length and the browser lays them out better than measured SVG text. */
.tbar + .tbar { margin-top: .7rem; }
.tbar-head {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    flex-wrap: wrap;
    font-size: .85rem;
    margin-bottom: .25rem;
}
.tbar-track {
    background: var(--surface-2);
    border-radius: var(--radius-full);
    height: 10px;
    overflow: hidden;
}
.tbar-fill {
    background: var(--md-primary);
    height: 100%;
    border-radius: var(--radius-full);
    /* Width is set through the CSSOM — the CSP forbids style="" here. */
    width: 0;
    transition: width var(--dur-normal) var(--ease-standard);
}

.att-bar {
    display: flex;
    height: 14px;
    border-radius: var(--radius-full);
    overflow: hidden;
    background: var(--surface-2);
}
.att-seg { height: 100%; width: 0; }

.att-list {
    list-style: none;
    padding: 0;
    margin: .75rem 0 0;
    font-size: .85rem;
}
.att-list li {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .15rem 0;
}
.att-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    display: inline-block;
    flex-shrink: 0;
}

.att-met,    .att-dot.att-met    { background: var(--state-full); }
.att-close,  .att-dot.att-close  { background: var(--state-needs-one); }
.att-behind, .att-dot.att-behind { background: var(--state-empty); }
.att-none,   .att-dot.att-none   { background: var(--surface-3); }

/* Charts animate their width on load. Anyone who has asked the OS not to
   animate things has asked for a reason, and a growing bar is exactly the
   kind of motion that request is about. */
@media (prefers-reduced-motion: reduce) {
    .tbar-fill { transition: none; }
}

/* =====================================================================
   Annual recap.
   ===================================================================== */

.recap-figures {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
}
.fig { display: flex; flex-direction: column; }
.fig-n {
    font-size: 1.9rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--md-primary);
}
.fig-l { font-size: .8rem; color: var(--on-surface-muted); }

/* Twelve fixed columns whatever the data. A month with nothing in it is
   information, and dropping it would rewrite the year as unbroken. */
.rc-months {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: .3rem;
    align-items: end;
    height: 150px;
}
.rc-mon {
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 100%;
    justify-content: flex-end;
}
.rc-mon-track {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: flex-end;
}
.rc-mon-fill {
    width: 100%;
    background: var(--md-primary);
    border-radius: 3px 3px 0 0;
    /* Height comes from the CSSOM — the CSP forbids style="" here. */
    height: 0;
    min-height: 2px;
    transition: height var(--dur-normal) var(--ease-standard);
}
.rc-mon-label {
    font-size: .65rem;
    color: var(--on-surface-muted);
    margin-top: .25rem;
}

.rc-with { list-style: none; padding: 0; margin: 0; }
.rc-with li {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    padding: .4rem 0;
}
.rc-with li + li { border-top: 1px solid var(--outline); }

.ms-grid {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}
.ms-card {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .45rem .7rem;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    font-size: .85rem;
}
.ms-card i { color: var(--md-primary); }
.ms-date { color: var(--on-surface-muted); font-size: .72rem; }

/* Earned in an earlier year. Kept visible — somebody whose big year was
   two years ago should still see it — but clearly not this year's news. */
.ms-past { opacity: .6; }

@media (prefers-reduced-motion: reduce) {
    .rc-mon-fill { transition: none; }
}

/* =====================================================================
   Language switcher — app bar.

   Sits beside the theme toggle because the two are the same kind of
   control: they change how the interface looks, not what it does. It
   reuses .icon-btn wholesale so it is the same circle, the same border,
   the same press animation as everything else up there.
   ===================================================================== */

.lang-menu {
    min-width: 11rem;
    padding: .35rem;
    border: 1px solid var(--outline);
    border-radius: var(--radius-md);
    background: var(--surface-1);
    box-shadow: var(--elev-3);
}

.lang-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .5rem .7rem;
    border-radius: var(--radius-xs);
    color: var(--on-surface);
    /* 44px tap target, same floor as every other row in this app. */
    min-height: 44px;
}

.lang-item:hover,
.lang-item:focus-visible {
    background: var(--surface-2);
    color: var(--on-surface);
}

/* Each name is written in its own language and therefore its own
   direction — the dir attribute on the anchor handles the text, and this
   keeps the tick on the same side for every row regardless. */
.lang-native { font-weight: 600; }

.lang-item.active,
.dropdown-item.lang-item.active {
    background: color-mix(in srgb, var(--md-primary) 16%, transparent);
    color: var(--nav-active-fg);
}

.lang-check { font-size: 1rem; }

/* The menu animates open. Anyone who asked the OS to stop animations
   asked for a reason. */
@media (prefers-reduced-motion: no-preference) {
    .dropdown-menu.lang-menu.show {
        animation: langIn var(--dur-fast) var(--ease-emphasized);
    }
}

@keyframes langIn {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Login screen controls — language and theme, together in the corner.
   Replaces .theme-toggle-pos, which positioned a single button and had no
   room for a second. */
.auth-controls {
    position: absolute;
    inset-block-start: 1rem;
    inset-inline-end: 1rem;
    display: flex;
    gap: .5rem;
    z-index: 2;
}

/* =====================================================================
 *  v1.4 — PHASE D: text size, the guided tour, and the install poster.
 *
 *  APPENDED, and appended is where it stays. Another agent appends here
 *  too; nothing above this banner has been reformatted or reordered.
 * ===================================================================== */

/* ---------------------------------------------------------------------
 *  1. USER-CONTROLLED TEXT SIZE
 *
 *  ONE RULE. --font-scale is defined in theme.css and this is the only
 *  place that reads it.
 *
 *  A PERCENTAGE, not a px value, and that is the load-bearing detail: 100%
 *  is whatever the browser's own default font size resolves to, so someone
 *  who has already raised it in Chrome or in iOS keeps that and our steps
 *  MULTIPLY it. Written as calc(16px * var(--font-scale)) this control
 *  would have silently overridden — and for many people shrunk — a setting
 *  they had already made at the OS level, which is the opposite of what an
 *  accessibility control is for.
 * ------------------------------------------------------------------- */
html {
    font-size: calc(100% * var(--font-scale));
}

/*
 * The three places that had to be checked, and what each needed.
 *
 * CALENDAR GRID. The cells are aspect-ratio 1 with a hard min-height of
 * 44px — the WCAG tap target. On a 320px phone the columns are narrower
 * than that, so the floor is what decides the cell's height, and stated in
 * px it did not move with the text: measured at the largest step, the day
 * number and the staffing dots ended 1.7px BELOW the cell's padding box,
 * so the dots sat on the border. Restated as max(44px, 2.75rem) the floor
 * is byte-identical at the default size (2.75rem = 44px) and grows with
 * the text — the same measurement then has 11.4px of headroom, and the
 * month simply gets taller. The seven columns and the square aspect are
 * untouched.
 */
.calendar-day { min-height: max(44px, 2.75rem); }

/*
 * BOTTOM NAVIGATION. Six slots at most (reports and admin are role-gated),
 * grid-auto-columns: 1fr — which is minmax(AUTO, 1fr), and an auto minimum
 * is min-content, so a label that no longer fits widens its column and
 * pushes the bar wider than the screen. The bar is fixed and full-bleed,
 * so that shows up as the whole PAGE scrolling sideways.
 *
 * The Hebrew labels survive on their own; the Russian ones do not.
 * Measured at 360px on the largest step with the shipped ru catalogue —
 * "Объявления", "Управление" — the old rule overflowed the bar by 35px.
 * minmax(0, 1fr) holds the columns to their share, and the two lines below
 * are what lets a word that still does not fit wrap instead of overflow.
 */
.bottom-nav { grid-auto-columns: minmax(0, 1fr); }

.bottom-nav a span {
    line-height: 1.15;
    overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------
 *  2. THE FONT-SIZE MENU (app bar) AND THE SETTINGS SCREEN CONTROL
 * ------------------------------------------------------------------- */

/* Mirrors .lang-menu next door: same tick, same active wash, so the two
   adjacent menus do not look like they came from different apps. */
.fontsize-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.fontsize-item.active,
.dropdown-item.fontsize-item.active {
    background: color-mix(in srgb, var(--md-primary) 16%, transparent);
    color: var(--nav-active-fg);
}

.fontsize-check { font-size: 1rem; }

/* The sample shows the step at ITS OWN size — em, so it is relative to the
   menu row rather than to the root, and the preview therefore does not
   itself change when the setting does. Marked aria-hidden in the markup:
   it is a picture of a size, and one Hebrew letter read aloud three times
   is noise. */
.fontsize-sample-normal { font-size: 1em; }
.fontsize-sample-large  { font-size: 1.15em; }
.fontsize-sample-xlarge { font-size: 1.3em; }

/* ---------------------------------------------------------------------
 *  3. THE GUIDED TOUR
 *
 *  NO SCRIM, DELIBERATELY. The obvious design dims the page behind the
 *  panel — and the obvious design cannot work here: .glass-card carries
 *  backdrop-filter, which creates both a stacking context and a containing
 *  block for fixed descendants (see the long note in dashboard.php about
 *  the assignment panel). Raising one card above a scrim while the rest of
 *  the page stays under it is exactly the arrangement that trapped the
 *  modal's z-index and made the dialog unclickable. A tour that breaks the
 *  screen it is explaining is worse than no tour, so the page stays lit and
 *  the step is marked by an outline on the thing being described.
 * ------------------------------------------------------------------- */
.tour-panel {
    position: fixed;
    inset-inline: 1rem;
    /* Clear of the bottom bar (5rem) plus the home indicator. */
    bottom: calc(5.5rem + env(safe-area-inset-bottom));
    z-index: 1050;
    max-width: 34rem;
    margin-inline: auto;
    padding: 1rem;
}

@media (min-width: 992px) {
    /* No bottom bar from lg up, so the panel sits lower. */
    .tour-panel { bottom: 1.5rem; }
}

.tour-step-count {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--on-surface-muted);
    margin: 0 0 0.25rem;
}

.tour-body {
    color: var(--on-surface-muted);
    margin: 0 0 0.75rem;
}

.tour-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.tour-spacer { flex-grow: 1; }

/* The highlight. An OUTLINE and not a border: an outline is drawn outside
   the box and takes no space, so nothing on the page shifts when a step
   arrives — a layout that jumps as the tour advances is the fastest way to
   lose the reader's place. */
.tour-target {
    outline: 3px dashed var(--md-primary);
    outline-offset: 4px;
    border-radius: var(--radius-sm);
    /* The app bar is sticky; without this the scrolled-to element lands
       underneath it and the tour points at something invisible. */
    scroll-margin-block-start: 6rem;
    scroll-margin-block-end: 8rem;
}

/* Anyone who asked the OS to stop animations asked for a reason — so the
   pulse exists only for everyone else. The outline above is the part that
   actually conveys the step, and it is static. */
@media (prefers-reduced-motion: no-preference) {
    .tour-target { animation: tourPulse 1.6s var(--ease-standard) infinite; }

    .tour-panel { animation: tourIn var(--dur-normal) var(--ease-emphasized); }
}

@keyframes tourPulse {
    0%, 100% { outline-color: var(--md-primary); }
    50%      { outline-color: color-mix(in srgb, var(--md-primary) 35%, transparent); }
}

@keyframes tourIn {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* ---------------------------------------------------------------------
 *  4. THE INSTALL POSTER (public/install_poster.php)
 *
 *  Sized in mm throughout. The QR is the one element on the sheet with a
 *  hard requirement — it has to be readable by a phone held at arm's
 *  length across a room — and a rem there would have made the printed code
 *  depend on the reader's own font-size setting, which is not a property a
 *  printed sheet should have.
 * ------------------------------------------------------------------- */
.poster-sheet {
    max-width: 190mm;
    margin-inline: auto;
    padding: 10mm;
    text-align: center;
    background: #fff;
    color: #101828;
    border-radius: var(--radius-lg);
}

.poster-crest { width: 26mm; height: auto; }

.poster-title {
    font-size: 9mm;
    font-weight: 700;
    line-height: 1.15;
    margin: 6mm 0 2mm;
}

.poster-sub {
    font-size: 5mm;
    color: #475467;
    margin: 0 0 6mm;
}

/* 62mm across — comfortably over the 5cm floor, and the quiet zone is part
   of the SVG rather than padding here, so a print driver that clips the
   margin cannot eat it. */
.poster-qr {
    width: 62mm;
    height: 62mm;
    display: block;
    margin-inline: auto;
}

.poster-steps {
    list-style: none;
    padding: 0;
    margin: 6mm auto 0;
    max-width: 130mm;
    text-align: start;
    font-size: 4.4mm;
    line-height: 1.6;
    color: #101828;
}

.poster-steps li {
    display: flex;
    gap: 3mm;
    align-items: baseline;
    margin-bottom: 2mm;
}

.poster-step-num {
    flex-shrink: 0;
    width: 7mm;
    height: 7mm;
    border-radius: 50%;
    background: #101828;
    color: #fff;
    font-size: 3.6mm;
    font-weight: 700;
    display: inline-grid;
    place-items: center;
}

.poster-url {
    margin: 5mm 0 0;
    font-size: 3.8mm;
    color: #475467;
    word-break: break-all;
}

.poster-foot {
    margin: 6mm 0 0;
    font-size: 3.4mm;
    color: #667085;
}

@media print {
    /* The sheet IS the page. No card, no shadow, no rounded corner — and
       high contrast, because a QR printed in the theme's near-black on the
       theme's off-white is a QR a cheap laser printer renders as grey. */
    .poster-sheet {
        max-width: none;
        margin: 0;
        padding: 0;
        border-radius: 0;
        box-shadow: none;
        background: #fff;
        color: #000;
        page-break-inside: avoid;
        break-inside: avoid;
    }

    .poster-sub,
    .poster-url,
    .poster-foot { color: #333; }
}

/* =====================================================================
 *  v1.4 PHASE A — SHIFT COMMUNICATION
 *
 *  Appended block, deliberately at the end of the file: everything here
 *  is additive and nothing above it changes. Styles the strip that
 *  shift_notes.js draws under every shift card —
 *
 *    .shift-comms              the host, empty until the batched fetch
 *    .comms-handover-alert     "there is a handover waiting for you"
 *    .comms-notices            who declared late / not coming
 *    .comms-actions            the volunteer's own one-tap buttons
 *    .comms-thread-wrap        the collapsed note count + the thread
 *
 *  RTL: logical properties throughout (inset-inline-*, margin-inline-*,
 *  padding-inline-*, border-inline-start). No left/right anywhere in this
 *  block — the app runs right-to-left by default and is read left-to-right
 *  in Russian and English, and a physical property is correct in exactly
 *  one of those.
 *
 *  CSP: no style="" attributes are used by the module, so every visual
 *  state below is reachable from a class.
 * ===================================================================== */

/* The host stays out of the way entirely when there is nothing to show.
   :empty is what makes a quiet month cost no vertical space at all. */
.shift-comms:empty { display: none; }

.shift-comms {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-block-start: 0.75rem;
    padding-block-start: 0.75rem;
    border-block-start: 1px solid var(--outline);
}

/* ------------------------------------------- the incoming handover */
/* A banner, not a chip. This is the one thing on the card that somebody
   has to read before they leave the house, so it gets the width, the
   colour and the first position. */
.comms-handover-alert {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid color-mix(in srgb, var(--md-primary) 45%, transparent);
    border-radius: var(--radius-sm, 8px);
    background: color-mix(in srgb, var(--md-primary) 14%, transparent);
    color: var(--on-surface);
    font-size: 0.8125rem;
    font-weight: 600;
    text-align: start;
}

.comms-handover-alert:hover {
    background: color-mix(in srgb, var(--md-primary) 22%, transparent);
}

.comms-handover-alert .comms-caret { margin-inline-start: auto; }

/* ------------------------------------------------ roster markers */
.comms-notices {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

/* One line per person. The name and the state read first; the free text
   the volunteer typed follows, muted, because it is context rather than
   the fact the commander is scanning for. */
.comms-notice {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.375rem;
    font-size: 0.8125rem;
    padding: 0.25rem 0.5rem;
    border-radius: var(--radius-sm, 8px);
    border-inline-start: 3px solid transparent;
}

/* Late and not-coming are different problems and must not look alike at a
   glance: one needs the shift covering for half an hour, the other needs a
   replacement found. Same hues the app already uses for warning and
   danger, at the 32% tint the chips settled on for outdoor legibility. */
.comms-notice.is-late {
    background: color-mix(in srgb, var(--state-needs-one) 18%, transparent);
    border-inline-start-color: var(--state-needs-one);
}

.comms-notice.is-not_coming {
    background: color-mix(in srgb, var(--md-danger) 18%, transparent);
    border-inline-start-color: var(--md-danger);
}

.comms-notice-who   { font-weight: 600; }
.comms-notice-state { font-weight: 600; }
.comms-notice-text  { color: var(--on-surface-muted); }

/* ------------------------------------------------ own-action row */
.comms-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* ------------------------------------------------------- the thread */
.comms-thread-wrap {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* Collapsed by default and honest about how much is inside. A disclosure
   with no count is a door with nothing written on it, and nobody opens
   thirty of those to find the two that have anything behind them. */
.comms-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.25rem 0;
    border: 0;
    background: none;
    color: var(--on-surface-muted);
    font-size: 0.8125rem;
    font-weight: 600;
    text-align: start;
}

.comms-toggle:hover { color: var(--on-surface); }

.comms-toggle .comms-caret {
    margin-inline-start: auto;
    transition: transform var(--dur-fast) var(--ease-standard);
}

.comms-toggle.is-open .comms-caret { transform: rotate(180deg); }

.comms-has-handover { color: var(--md-primary); }

.comms-thread {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    padding: 0.625rem;
    border-radius: var(--radius-sm, 8px);
    background: var(--surface-2);
}

.comms-block {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm, 8px);
    background: var(--surface-1);
    border-inline-start: 3px solid var(--md-primary);
}

.comms-h {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--on-surface);
}

.comms-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.comms-item {
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm, 8px);
    background: var(--surface-1);
}

/* Free text a person typed. Validator::cleanText already collapses runs of
   whitespace, so pre-line is belt-and-braces rather than load-bearing;
   overflow-wrap is the one that matters — it stops a pasted URL widening
   the whole card and forcing the month view to scroll sideways. */
.comms-body {
    margin: 0;
    font-size: 0.875rem;
    color: var(--on-surface);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.comms-byline {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin: 0.25rem 0 0;
    font-size: 0.75rem;
    color: var(--on-surface-muted);
}

.comms-note-muted { margin: 0; font-size: 0.75rem; color: var(--on-surface-muted); }

.comms-empty {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--on-surface-muted);
}

/* The delete control sits at the far end of the byline — the end, not the
   right, so it stays away from the text in both directions. */
.comms-del {
    margin-inline-start: auto;
    border: 0;
    background: none;
    padding: 0.125rem 0.25rem;
    color: var(--on-surface-muted);
    border-radius: var(--radius-sm, 8px);
}

.comms-del:hover { color: var(--md-danger); background: var(--surface-2); }

.comms-compose {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.comms-compose .btn { align-self: flex-end; }

.comms-input { font-size: 0.875rem; }

/* The whole strip animates open. Anyone who asked the OS to stop
   animations asked for a reason — matching the rule the language menu
   already follows. */
@media (prefers-reduced-motion: no-preference) {
    .comms-thread:not([hidden]) {
        animation: commsIn var(--dur-fast) var(--ease-emphasized);
    }
}

@keyframes commsIn {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}

/* =====================================================================
 *  v1.62 — the permissions module (admin/permissions.php)
 *
 *  A matrix is read down a column as much as across a row, so the row
 *  header stays put while the roles scroll (sticky inline-start), and a
 *  domain band separates groups without a heavier border. Logical
 *  properties only — tests/phase_d.php enforces it.
 * ===================================================================== */
.perm-matrix { overflow-x: auto; }
.perm-matrix table { min-inline-size: 42rem; }
.perm-matrix thead th { position: sticky; inset-block-start: 0; background: var(--surface-1); z-index: 1; }
.perm-matrix th[scope="row"] {
    position: sticky;
    inset-inline-start: 0;
    background: var(--surface-1);
    font-weight: 500;
    min-inline-size: 15rem;
    z-index: 1;
}
.perm-matrix .perm-domain th {
    background: var(--surface-2);
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--on-surface-muted);
    position: static;
}
.perm-matrix code {
    display: block;
    font-size: 0.68rem;
    color: var(--on-surface-muted);
    direction: ltr;
    text-align: start;
}
.perm-matrix td { text-align: center; vertical-align: middle; }
.perm-matrix .form-check-input { inline-size: 1.2rem; block-size: 1.2rem; margin: 0; cursor: pointer; }
.perm-matrix .form-check-input:disabled { cursor: not-allowed; }
.perm-lock { color: var(--on-surface-muted); font-size: 0.95rem; }

.perm-badge {
    display: inline-block;
    font-size: 0.66rem;
    font-weight: 700;
    padding: 0.1rem 0.4rem;
    border-radius: var(--radius-full);
    margin-inline-start: 0.35rem;
    vertical-align: middle;
}
.perm-badge-high     { background: color-mix(in srgb, var(--md-warning) 22%, transparent); color: var(--md-warning); }
.perm-badge-critical { background: color-mix(in srgb, var(--md-danger) 18%, transparent);  color: var(--md-danger); }
.perm-badge-floor    { background: var(--surface-3); color: var(--on-surface-muted); }
.perm-badge-raw      { background: color-mix(in srgb, var(--md-info) 18%, transparent);    color: var(--md-info); }

.perm-yes { color: var(--md-success); font-weight: 700; }
.perm-no  { color: var(--on-surface-muted); }
.perm-chain { font-size: 0.78rem; color: var(--on-surface-muted); line-height: 1.5; }
.perm-chain .hit { color: var(--on-surface); }

.perm-status {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
    align-items: center;
    font-size: 0.85rem;
    padding: 0.625rem 0.875rem;
    border-radius: var(--radius-md);
    background: var(--surface-2);
    margin-block-end: 1rem;
}
.perm-status .dot {
    display: inline-block;
    inline-size: 0.55rem;
    block-size: 0.55rem;
    border-radius: var(--radius-full);
    margin-inline-end: 0.35rem;
    background: var(--md-success);
}
.perm-status .dot.off { background: var(--md-warning); }

/* An override older than the review window: "is this still needed?" */
.perm-stale { border-inline-start: 3px solid var(--md-warning); }
.perm-revoked { opacity: 0.55; }

.perm-degraded { background: color-mix(in srgb, var(--md-danger) 18%, var(--surface-1)); }
