/* Rabexa panel components.
 *
 * Tokens live in theme.css. Nothing here may contain a literal colour — a test
 * enforces it, so that changing the palette stays a one-file job.
 *
 * Every rule uses CSS logical properties (`padding-inline`,
 * `border-inline-start`, `text-align: start`), so Hebrew and Arabic flip from
 * the single `dir` on <html> with no `rtl:` variants and no direction hardcoded
 * anywhere to be forgotten later.
 */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* The `hidden` attribute wins over any component's `display`. Without this a
 * rule like `.bulk-bar { display: flex }` beats the browser's default
 * `[hidden] { display: none }` on specificity, so an element panel.js toggles
 * with `el.hidden` (the bulk bar, the draft banner, the immersive labels) would
 * never actually hide. */
[hidden] {
  display: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  /* A single wide element used to widen the whole document. Anything that is
   * genuinely wide — a table, the calendar — scrolls inside its own box. */
  overflow-x: hidden;
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  margin: 0;
  line-height: var(--leading-tight);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

h1 {
  font-size: var(--text-lg);
}
h2 {
  font-size: var(--text-md);
}
h3 {
  font-size: var(--text-base);
}

p {
  margin: 0 0 var(--space-3);
}

a {
  color: var(--accent-text);
}

/* One focus treatment everywhere, and never `outline: none`: a dispatcher
 * tabbing through a roster has to be able to see where they are. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
  }
}

/* Visible to a screen reader, not to the eye. An icon-only control still has
 * to say what it does. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- Icons ----------------------------------------------------------------- */

.icon {
  width: 1.125em;
  height: 1.125em;
  flex: none;
  stroke: currentColor;
  fill: none;
  /* The heavier variant (was 1.75): the icons read as thin and weak at small
   * sizes even where the colour contrast is fine (Phase D). One width for the
   * whole set. */
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.icon-sm {
  width: 1em;
  height: 1em;
}

/* --- Shell ----------------------------------------------------------------- */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding: var(--space-4);
  background: var(--surface);
  border-inline-end: 1px solid var(--border);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-inline: var(--space-2);
  color: var(--text);
  text-decoration: none;
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--text-on-accent);
  font-weight: var(--weight-bold);
  font-size: var(--text-sm);
  flex: none;
}

.brand-name {
  font-weight: var(--weight-semibold);
  font-size: var(--text-md);
  letter-spacing: -0.01em;
  line-height: 1.1;
}

.brand-name small {
  display: block;
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.nav {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.nav a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: var(--weight-medium);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.nav a:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.nav a[aria-current="page"] {
  background: var(--surface-active);
  color: var(--accent-text);
  font-weight: var(--weight-semibold);
}

/* Screens listed under a section, indented past its icon. Always shown: a
 * group that opens on click is a hub with extra steps. */
.nav-group {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-inline-start: calc(1.125em + var(--space-3) + var(--space-3));
  padding-inline-start: var(--space-3);
  border-inline-start: 1px solid var(--border);
  margin-block: 2px var(--space-2);
}

.nav-group a {
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
}

.sidebar-foot {
  margin-block-start: auto;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.whoami {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

.avatar {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: none;
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.whoami-name {
  min-width: 0;
  line-height: 1.2;
}

.whoami-name strong {
  display: block;
  font-weight: var(--weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.whoami-name span {
  font-size: var(--text-xs);
  color: var(--text-faint);
}

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

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--topbar-height);
  padding: var(--space-3) var(--space-6);
  background: var(--surface);
  border-block-end: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
}

.topbar .spacer {
  flex: 1;
}

.content {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* --- Segmented controls ---------------------------------------------------- */

.segmented {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
}

.segmented button,
.segmented a {
  border: 0;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.segmented button:hover,
.segmented a:hover {
  color: var(--text);
}

.segmented [aria-current="true"],
.segmented [aria-current="page"] {
  background: var(--surface);
  color: var(--text);
  font-weight: var(--weight-semibold);
  box-shadow: var(--shadow-sm);
}

/* The worker profile's tab bar: a segmented control on its own line, with room
   before the section it switches. Scrolls rather than wraps on a narrow phone,
   so the pills stay one row. */
.segmented.tabs {
  display: flex;
  max-width: max-content;
  margin-bottom: var(--space-4);
  overflow-x: auto;
}

/* A light divider between two forms in one card — the profile above, the
   custom fields below. */
hr.rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin: var(--space-5) 0 var(--space-3);
}

/* Today, when the calendar is already showing today. Still reachable —
 * pressing it is harmless — but it should not look like the one to press.
 *
 * Targeted by its own class rather than by where it sits: an earlier version
 * matched every current item in the schedule's toolbar, which quietly took
 * the highlight off Week and Month as well.
 */
.jump-today[aria-current="true"],
.jump-today[aria-current="true"]:hover {
  color: var(--text-faint);
  font-weight: var(--weight-medium);
  background: none;
  box-shadow: none;
  cursor: default;
}

/* --- Cards ----------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.card > header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--border);
}

.card > header h2 {
  margin-inline-end: auto;
}

.card-body {
  padding: var(--space-5);
}

.grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}

/* --- Hub cards (Companies & workers) --------------------------------------- */

.stat-link {
  display: block;
  padding: var(--space-5);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

.stat-link:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
}

.stat-link .icon {
  color: var(--text-faint);
  width: 1.5rem;
  height: 1.5rem;
  margin-block-end: var(--space-2);
}

.stat-link h2 {
  margin-block-end: var(--space-1);
}

/* --- Stats ----------------------------------------------------------------- */

.stat {
  display: block;
  color: inherit;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

/* A hairline rather than a thick bar: the number is the message, the colour
 * only tells you which number you are looking at. */
.stat::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--border-strong);
}

.stat.ok::before {
  background: var(--ok);
}
.stat.warn::before {
  background: var(--warn);
}
.stat.bad::before {
  background: var(--bad);
}

.stat-value {
  font-size: var(--text-3xl);
  font-weight: var(--weight-semibold);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.stat-label {
  margin-block-start: var(--space-1);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

a.stat {
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

a.stat:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
}

/* --- Badges ---------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0.125rem var(--space-2);
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  /* A status word must not break in half; anything longer than the space
   * available is elided rather than widening whatever contains it. */
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.badge::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: var(--radius-full);
  background: currentColor;
  flex: none;
}

.badge.plain::before {
  display: none;
}

.badge.ok {
  background: var(--ok-soft);
  border-color: var(--ok-border);
  color: var(--ok);
}

.badge.warn {
  background: var(--warn-soft);
  border-color: var(--warn-border);
  color: var(--warn);
}

.badge.bad {
  background: var(--bad-soft);
  border-color: var(--bad-border);
  color: var(--bad);
}

.badge.accent {
  background: var(--accent-soft);
  border-color: var(--accent-soft-border);
  color: var(--accent-text);
}

/* --- Buttons --------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.button:hover {
  background: var(--surface-hover);
  border-color: var(--text-faint);
}

.button:active {
  background: var(--surface-sunken);
}

.button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-on-accent);
}

.button.primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.button.danger {
  color: var(--bad);
  border-color: var(--bad-border);
}

.button.danger:hover {
  background: var(--bad-soft);
}

.button.ghost {
  border-color: transparent;
  background: none;
  color: var(--text-muted);
}

.button.ghost:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.button.icon-only {
  padding: var(--space-2);
}

.linkish {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--accent-text);
  cursor: pointer;
}

.linkish:hover {
  text-decoration: underline;
}

.linkish.danger {
  color: var(--bad);
}

/* --- Tables ---------------------------------------------------------------- */

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

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  text-align: start;
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border);
  white-space: nowrap;
}

thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  /* Column headers label the data — functional small text, so muted (4.5:1
   * light / 7:1 dark), not faint (2.46:1 light, which failed AA). Phase D. */
  color: var(--text-muted);
}

tbody tr:last-child td {
  border-block-end: 0;
}

tbody tr:hover td {
  background: var(--surface-hover);
}

/* The row's own buttons: end-aligned, never wrapping, taking only the width
 * they need so the columns that carry the data keep theirs. */
td.actions,
th.actions {
  text-align: end;
  width: 1%;
}

td.actions form {
  display: inline;
}

td.actions .linkish + form,
td.actions form + form,
td.actions a + form {
  margin-inline-start: var(--space-3);
}

/* A hidden record stays listed so it can be restored, and reads as hidden. */
tbody tr.is-hidden td {
  opacity: 0.55;
}

td.empty-row {
  padding: var(--space-8);
  text-align: center;
  color: var(--text-muted);
}

td a {
  font-weight: var(--weight-medium);
}

tfoot th {
  position: static;
  border-block-start: 1px solid var(--border-strong);
  font-size: var(--text-sm);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}

/* Figures stay LTR inside an RTL sentence, and line up column-wise. */
.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  direction: ltr;
  unicode-bidi: isolate;
}

td.num,
th.num {
  text-align: end;
}

.muted {
  color: var(--text-muted);
}

.small {
  font-size: var(--text-xs);
}

/* --- Empty states ---------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-10) var(--space-5);
  text-align: center;
  color: var(--text-muted);
}

.empty .icon {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--text-faint);
  stroke-width: 1.25;
}

/* --- Notices --------------------------------------------------------------- */

.notice {
  max-width: 26rem;
  margin: 12vh auto;
  padding: var(--space-8);
  text-align: center;
}

.notice h1 {
  font-size: var(--text-xl);
  margin-block-end: var(--space-3);
}

.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  font-size: var(--text-sm);
  margin-block-end: var(--space-3);
}

.alert.bad {
  background: var(--bad-soft);
  border-color: var(--bad-border);
  color: var(--bad);
}

.alert.ok {
  background: var(--ok-soft);
  border-color: var(--ok-border);
  color: var(--ok);
}

.alert.warn {
  background: var(--warn-soft);
  border-color: var(--warn-border);
  color: var(--warn);
}

.alert ul {
  margin: var(--space-1) 0 0;
  padding-inline-start: var(--space-5);
}

/* --- Forms ----------------------------------------------------------------- */

.form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  text-align: start;
  margin-block: var(--space-6);
}

/* --- Data-entry forms ------------------------------------------------------
 * Two columns for short fields, the full width for anything that holds a
 * paragraph or a block of JSON. Labels above, help below, the error right
 * under the control it belongs to — and one footer with Save beside Cancel,
 * so every screen ends the same way.
 */

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-5);
  padding: var(--space-5);
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.field:has(textarea),
.field.wide {
  grid-column: 1 / -1;
}

.field > span {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
}

.field .required {
  color: var(--bad);
}

.field input,
.field select,
.field textarea {
  width: 100%;
}

.field input[type="checkbox"] {
  width: auto;
}

.field.checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  padding-block-start: var(--space-5);
}

.field.checkbox > span {
  color: var(--text);
  font-size: var(--text-sm);
}

.field-help {
  font-size: var(--text-xs);
  color: var(--text-faint);
}

.field-error {
  font-size: var(--text-xs);
  color: var(--bad);
  font-weight: var(--weight-medium);
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea {
  border-color: var(--bad);
}

/* Validate on blur, not on keystroke: a control is styled invalid only after
 * it has been visited and left (panel.js adds `is-touched`), so a required box
 * the reader has not reached yet is not already flagged red. The message stays
 * server-side (an untranslated browser string would break the three-language
 * rule); this is the border cue only. */
.field.is-touched input:invalid,
.field.is-touched select:invalid,
.field.is-touched textarea:invalid {
  border-color: var(--bad);
}

/* The error summary at the top of a form that failed with several fields. */
.form-error-summary .summary-title {
  margin: 0 0 var(--space-1);
  font-weight: var(--weight-semibold);
}

.form-error-summary ul {
  margin: 0;
  padding-inline-start: 1.1em;
}

.form-error-summary a {
  color: inherit;
  font-weight: var(--weight-medium);
}

/* A select never truncates its chosen option without a way to read it: it shows
 * the full text on hover (panel.js sets the title), and clips with an ellipsis
 * rather than cutting a letter mid-word. */
.field select {
  text-overflow: ellipsis;
}

/* The draft-restored banner: the message takes the row, the discard button sits
 * at the end. */
.draft-banner {
  align-items: center;
}

.draft-banner .draft-banner-text {
  flex: 1;
}

.form-footer {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-block-start: 1px solid var(--border);
  background: var(--surface-sunken);
}

/* A small form in a row: the "add a posting" line under a worker. */
.form-inline {
  display: flex;
  gap: var(--space-3);
  align-items: end;
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
}

.form-inline .field {
  min-width: 9rem;
}

/* A record's page: the main form beside its attachments. */
.data-layout {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(18rem, 2fr);
  gap: var(--space-4);
  align-items: start;
}

@media (max-width: 60rem) {
  .form-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /* A 44px touch target on the phone (round 2, Phase E). Checkboxes keep their
   * own size — the whole label row is the target for those. */
  .field input:not([type="checkbox"]),
  .field select,
  .field textarea {
    min-height: 2.75rem;
  }

  .data-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  .nav-group {
    flex-direction: row;
    flex-wrap: wrap;
    margin-inline-start: 0;
    border-inline-start: 0;
    padding-inline-start: 0;
  }
}

label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
}

input,
select,
textarea {
  font: inherit;
  font-size: var(--text-base);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft);
}

textarea.mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

input::placeholder {
  color: var(--text-faint);
}

.toolbar {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

/* Previous · the date range · Next. The range sits between the two buttons
 * that move it, and is centred on a fixed width so it does not shuffle the
 * buttons sideways as the month names change length. */
.range-nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.range-label {
  min-width: 12rem;
  text-align: center;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
}

/* One row. Each control names itself in its first option, so a caption above
 * every one was a second copy of the same word and half the height of the
 * card. */
.filters {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
}

.filters select,
.filters input[type="search"] {
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-2);
  max-width: 12rem;
}

/* A diff reads as one line per field: what it was, what it became. Wrapped
 * rather than truncated — a hidden old value is the one somebody needs. */
.diff-line {
  display: flex;
  gap: var(--space-1);
  align-items: baseline;
  flex-wrap: wrap;
  font-size: var(--text-sm);
}

.diff-field {
  color: var(--text-muted);
  min-width: 7rem;
}

.diff-old {
  text-decoration: line-through;
  color: var(--text-faint);
}

.diff-new {
  font-weight: 500;
}

.pager {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  justify-content: center;
  padding-block: var(--space-3);
}

/* --- Ledger ---------------------------------------------------------------- */

/* The one line of figures that has to be right: shifts, hours, money. Summed on
 * the server, not in the browser — a total counted in JavaScript is how the
 * system this replaces printed NaN. */
.ledger-totals {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: baseline;
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.strong {
  font-weight: var(--weight-semibold);
  color: var(--text);
}

/* --- Schedule -------------------------------------------------------------- */

.toolbar {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

.schedule-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  gap: var(--space-4);
  align-items: start;
}

.calendar-card {
  padding: 0;
  overflow: hidden;
}

.calendar {
  table-layout: fixed;
  min-width: 46rem;
  border-collapse: separate;
  border-spacing: 0;
}

.calendar th {
  position: static;
  text-align: center;
  padding-block: var(--space-2);
  background: var(--surface-sunken);
  border-block-end: 1px solid var(--border);
  color: var(--text-muted);
}

.calendar td {
  vertical-align: top;
  height: 7.5rem;
  padding: var(--space-1);
  border-inline-end: 1px solid var(--border);
  border-block-end: 1px solid var(--border);
  white-space: normal;
}

.calendar td:last-child {
  border-inline-end: 0;
}

.calendar tr:last-child td {
  border-block-end: 0;
}

/* Days from the neighbouring month. Shown, because a shift on them is real,
 * but not competing with the month being read. */
.calendar td.other-month {
  background: var(--surface-sunken);
}

.calendar td.other-month .day-number {
  color: var(--text-faint);
}

.calendar td.is-today {
  background: var(--accent-soft);
}

.day-number {
  display: block;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: end;
  padding: 2px var(--space-1);
  border-radius: var(--radius-sm);
  text-decoration: none;
}

.day-number:hover {
  background: var(--surface);
  color: var(--accent-text);
}

.is-today .day-number {
  font-weight: var(--weight-bold);
  color: var(--accent-text);
}

/* --- A shift chip ---------------------------------------------------------
 * "06:00 · site · 2/3". The pair matters: a shift can be staffed and still
 * short of what the pattern asked for, which a bare count hides.
 */

.shift {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  width: 100%;
  margin-block-end: var(--space-1);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--ok);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: var(--text-xs);
  text-align: start;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease);
}

.shift:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-sm);
}

/* The staffing state colours the whole chip, not only its edge: once the chip
 * is tall enough to hold names, a 3px stripe was too little to read at a
 * glance across a busy month. Green full, amber short or over, red empty. */
.shift.cover-full {
  border-color: var(--ok-border);
  border-inline-start-color: var(--ok);
  background: var(--ok-soft);
}

.shift.cover-short,
.shift.cover-over {
  border-color: var(--warn-border);
  border-inline-start-color: var(--warn);
  background: var(--warn-soft);
}

/* A shift with nobody on it is the number that costs a client; it should be
 * the first thing the eye finds in a busy month. */
.shift.cover-empty {
  border-color: var(--bad-border);
  border-inline-start-color: var(--bad);
  background: var(--bad-soft);
}

/* A day that did not happen. Muted, not red: it is on the record on purpose,
 * not a gap someone forgot to fill. The reason rides in a flag where the
 * cover count would be. */
.shift.cover-none {
  border-inline-start-color: var(--border-strong);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.shift-flag {
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* The one the panel beside the calendar is showing. */
.shift.is-open,
.day-shift.is-open {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent);
}

/* The site sits on its own line under the time, so a long name reads in full
 * and wraps rather than being clipped beside the hours. */
.shift-where {
  font-weight: var(--weight-medium);
  overflow-wrap: anywhere;
}

.shift-count {
  color: var(--text-muted);
  font-size: var(--text-2xs);
}

.shift.cover-empty .shift-count {
  color: var(--bad);
  font-weight: var(--weight-semibold);
}

/* --- The shift's time, site and cover, on one line inside the chip -------- */
.shift-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1);
}

/* Every rostered name under that line, wrapping to as many rows as it needs.
 * Both grids grow to fit — a cell is as tall as its fullest shift. */
.shift-roster {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 4px;
}

.shift-worker {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px var(--space-1);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.shift-roster-empty {
  color: var(--bad);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

/* --- What the colours mean --------------------------------------------------
 *
 * A grid that encodes cover in a colour and never says so makes a new
 * dispatcher guess, and guessing wrong about "nobody on it" costs a client a
 * morning. Inside the calendar card, under the grid it explains.
 *
 * The swatch repeats the chip's own construction — a hairline box with a
 * thick inline-start edge — rather than a plain filled square, so the key and
 * the thing it describes are recognisably the same object.
 */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-block-start: 1px solid var(--border);
  list-style: none;
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.legend li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.legend-key {
  inline-size: 1.5rem;
  block-size: 0.9rem;
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* Each state names its own colour rather than one of the three inheriting the
 * base. A legend whose first entry is "whatever the default was" drifts the
 * day somebody changes the default for another reason. */
.legend-key.cover-full {
  border-inline-start-color: var(--ok);
}

.legend-key.cover-short {
  border-inline-start-color: var(--warn);
}

.legend-key.cover-empty {
  border-inline-start-color: var(--bad);
  background: var(--bad-soft);
}

/* --- One day ---------------------------------------------------------------
 * One row per shift, and one line per row.
 *
 * Time, site and cover are short and fixed, so they line up down the page. The
 * names are the only variable-length thing on the row, so they come last: when
 * they run out of room they truncate, rather than pushing every column after
 * them out of line. Putting them in the middle is what made a shift with six
 * people three lines tall and left nothing below it aligned.
 */

.day-card {
  padding: 0;
}

/* The day takes the whole width. A 21rem panel beside six rows is a column of
 * empty space, and the panel's own empty state made the page taller than the
 * day it was showing. */
.day-layout {
  grid-template-columns: minmax(0, 1fr);
}

.day-table {
  table-layout: fixed;
  width: 100%;
  min-width: 34rem;
}

/* --- Week list ------------------------------------------------------------
 * The week read down the page instead of across the grid: a section per day,
 * each day's shifts grouped by site. Same rows and colours as the day view. */
.layout-toggle {
  margin: var(--space-3) var(--space-3) 0;
}

.week-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-3);
}

.week-day-head {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--border);
}

.week-day.is-today .week-day-head {
  color: var(--accent-text);
  font-weight: var(--weight-semibold);
}

.week-day-empty {
  margin: 0;
}

.day-table thead th:nth-child(1),
.day-table tbody td:nth-child(1) {
  width: 9rem;
}

.day-table thead th:nth-child(2),
.day-table tbody td:nth-child(2) {
  width: 22%;
}

.day-table thead th:nth-child(3),
.day-table tbody td:nth-child(3) {
  width: 5rem;
}

/* Every cell stays on its line. The names carry the full list in a title, so
 * a truncated row is still answerable without opening the shift. */
.day-table td {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.day-table tbody tr {
  cursor: pointer;
  border-inline-start: 3px solid var(--ok);
}

.day-table tbody tr:hover td {
  background: var(--surface-hover);
}

.day-table tbody tr.cover-short,
.day-table tbody tr.cover-over {
  border-inline-start-color: var(--warn);
}

.day-table tbody tr.cover-empty {
  border-inline-start-color: var(--bad);
}

.day-table tbody tr.is-open td {
  background: var(--accent-soft);
}

.day-time {
  font-weight: var(--weight-semibold);
}

.day-site-cell {
  font-weight: var(--weight-medium);
}

.day-people {
  color: var(--text-muted);
}

/* A shift with nobody on it says so in the same cell, in the same grey, in the
 * same weight as a list of names. It is the same kind of row and should read
 * as one; the red edge and the red count already say it is short.
 *
 * An earlier version set this text red and semibold, which made an empty shift
 * look like a different sort of thing rather than the same row with different
 * words in it. */

.panel-head {
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--border);
}

.panel-section {
  padding: var(--space-4) var(--space-5) var(--space-2);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-faint);
  font-weight: var(--weight-semibold);
}

.roster {
  list-style: none;
  margin: 0;
  padding: 0 var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
}

.roster li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding-block: var(--space-2);
  border-block-end: 1px solid var(--border);
}

.roster li:last-child {
  border-block-end: 0;
}

.roster li > span:first-child {
  flex: 1;
  min-width: 6rem;
  font-weight: var(--weight-medium);
  overflow-wrap: anywhere;
}

/* Why a worker should not take this shift. Sentences, so they wrap and take
 * the full width of the row rather than sitting beside the name. */
.reasons {
  flex-basis: 100%;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.reasons li {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
  color: var(--warn);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
}

.roster form {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

.roster select {
  font-size: var(--text-xs);
  padding: 2px var(--space-1);
}

/* --- Attendance ------------------------------------------------------------
 * Every site starts folded. Somebody covering four sites is watching one of
 * them this hour, and a board that opens everything makes them scroll past
 * three to reach it.
 *
 * These rules were lost in an earlier rewrite of the schedule section, which
 * took the whole block between two markers. The board rendered unstyled for a
 * commit — see the test that counts them.
 */

.site {
  overflow: hidden;
}

.site > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

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

.site > summary:hover {
  background: var(--surface-hover);
}

.site[open] > summary {
  border-block-end: 1px solid var(--border);
  background: var(--surface-sunken);
}

.site .spacer {
  flex: 1;
}

.site-name {
  font-weight: var(--weight-semibold);
}

/* Points along the reading direction when closed, down when open — so it
 * flips with the page in Hebrew and Arabic instead of pointing backwards. */
.site .chevron {
  color: var(--text-faint);
  transition: transform var(--fast) var(--ease);
}

[dir="rtl"] .site .chevron {
  transform: rotate(180deg);
}

.site[open] > summary .chevron,
[dir="rtl"] .site[open] > summary .chevron {
  transform: rotate(90deg);
}

/* What a folded-up site says on its own. Closed is the default, so for most
 * sites most of the time this line is the whole board. */
.tally {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.tally-part {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.tally-part .dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--text-faint);
}

.tally-part.ok {
  color: var(--ok);
}

.tally-part.ok .dot {
  background: var(--ok);
}

.tally-part.warn {
  color: var(--warn);
}

.tally-part.warn .dot {
  background: var(--warn);
}

.tally-part.bad {
  color: var(--bad);
}

.tally-part.bad .dot {
  background: var(--bad);
}

/* One table per site, with the shift time as a row inside it. Separate tables
 * size their own columns, so "Clocked in" landed in a different place under
 * every shift; fixed widths keep every cell in the same column down the whole
 * board. */
/* Proportions rather than fixed rems, so the table fills whatever width the
 * page has instead of leaving a stripe of empty card beside it. The minimum
 * is what the override form needs before the row starts scrolling. */
.board-table {
  table-layout: fixed;
  width: 100%;
  min-width: 48rem;
}

.board-table thead th:nth-child(1),
.board-table tbody td:nth-child(1) {
  width: 22%;
}

.board-table thead th:nth-child(2),
.board-table tbody td:nth-child(2) {
  width: 13%;
}

.board-table thead th:nth-child(3),
.board-table thead th:nth-child(4),
.board-table tbody td:nth-child(3),
.board-table tbody td:nth-child(4) {
  width: 13%;
}

.board-table td {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The time a group of rows belongs to. Air above it, so one shift is visibly
 * separate from the next without breaking the table apart. */
.shift-heading th {
  position: static;
  padding: var(--space-3) var(--space-3) var(--space-1);
  background: var(--surface);
  border-block-end: 1px solid var(--border-strong);
  color: var(--text);
  font-size: var(--text-xs);
  text-transform: none;
  letter-spacing: 0;
}

tbody + tbody .shift-heading th {
  padding-block-start: var(--space-5);
}

/* The board is read at a glance, all morning. Roomy rows mean scrolling to
 * see who is on site, which is the one thing it is for. */
table.dense th,
table.dense td {
  padding: var(--space-1) var(--space-3);
}

table.dense tbody td {
  height: 2rem;
}

.empty.compact {
  padding: var(--space-4);
}

@media (max-width: 45rem) {
  .site > summary .muted {
    display: none;
  }
}


.override {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

.override input {
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-2);
  min-width: 7rem;
}

.override .button {
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
}

.pulse {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--ok);
  animation: pulse 2.4s var(--ease) infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.25;
  }
}

/* htmx sets this while a request is in flight. */
.htmx-indicator {
  opacity: 0;
  transition: opacity var(--normal) var(--ease);
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}

/* --- Inbox ----------------------------------------------------------------
 * A list beside the request being read. An inbox is worked through: the point
 * is to see what is waiting and move down it, not to scroll one item at a
 * time through full-width cards.
 */

.inbox-layout {
  display: grid;
  grid-template-columns: 22rem minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}

.inbox {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 70vh;
  overflow-y: auto;
}

.inbox a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border);
  color: inherit;
  text-decoration: none;
}

.inbox a:hover {
  background: var(--surface-hover);
}

.inbox a[aria-current="true"] {
  background: var(--surface-active);
  box-shadow: inset 3px 0 0 var(--accent);
}

[dir="rtl"] .inbox a[aria-current="true"] {
  box-shadow: inset -3px 0 0 var(--accent);
}

.inbox-name {
  flex: 1;
  min-width: 7rem;
  font-weight: var(--weight-medium);
}

.inbox-dates {
  width: 100%;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text);
}

.checkbox input {
  width: auto;
  margin: 0;
}

@media (max-width: 70rem) {
  .inbox-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  .inbox {
    max-height: 22rem;
  }
}

/* --- Requests -------------------------------------------------------------- */

.request header h2 {
  font-size: var(--text-base);
}

.decide {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

.decide input {
  flex: 1;
  min-width: 10rem;
}

.clashes {
  border: 1px solid var(--warn-border);
  background: var(--warn-soft);
  border-radius: var(--radius);
  padding-block-end: var(--space-1);
  margin-block-start: var(--space-4);
}

.clashes .panel-section {
  padding-block-start: var(--space-3);
  color: var(--warn);
}

.clashes .roster {
  padding-inline: var(--space-5);
}

/* --- Small screens ---------------------------------------------------------
 * A dispatcher covering sites uses a phone. The sidebar becomes a scrolling
 * row of sections above the content rather than a hidden drawer, because a
 * drawer hides exactly what the panel is for.
 */
@media (max-width: 70rem) {
  .schedule-layout {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The hamburger and the scrim exist only on mobile; the desktop grid keeps the
 * sidebar in place. */
.nav-toggle,
.nav-scrim {
  display: none;
}

@media (max-width: 60rem) {
  .shell {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The sidebar is an off-canvas drawer: full height, its own scroll, every
   * section AND the language / theme / sign-out foot reachable. Closed it sits
   * just off the inline-start edge; `.is-nav-open` on <html> slides it in.
   * RTL slides from the opposite edge — transform is physical, so the direction
   * is flipped explicitly rather than left to a logical property. */
  .sidebar {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 50;
    width: min(20rem, 82vw);
    height: 100vh;
    transform: translateX(-100%);
    transition: transform var(--normal) var(--ease);
    box-shadow: var(--shadow-lg);
  }

  [dir="rtl"] .sidebar {
    transform: translateX(100%);
  }

  html.is-nav-open .sidebar {
    transform: translateX(0);
  }

  .nav-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 40;
    background: var(--scrim);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--normal) var(--ease), visibility var(--normal) var(--ease);
  }

  html.is-nav-open .nav-scrim {
    opacity: 1;
    visibility: visible;
  }

  html.is-nav-open {
    overflow: hidden;
  }

  .nav-toggle {
    display: inline-flex;
  }

  .content,
  .topbar {
    padding: var(--space-4);
  }
}

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

/* --- Full width -------------------------------------------------------------
 *
 * Set on <html> by panel.js. Only the sidebar goes: the top bar carries the
 * date nav and the view switcher, which is what a dispatcher reaches for next
 * while reading the grid.
 *
 * Below 60rem the sidebar is already a horizontal strip rather than a column,
 * so there is nothing here to reclaim and the rule would only take the
 * navigation away on the screen that can least spare it.
 */

@media (min-width: 60.0625rem) {
  .is-immersive .shell {
    grid-template-columns: minmax(0, 1fr);
  }

  .is-immersive .sidebar {
    display: none;
  }
}

@media print {
  .sidebar,
  .topbar,
  .toolbar {
    display: none;
  }

  .card {
    border: 0;
    box-shadow: none;
  }
}

/* --- Shared table components ------------------------------------------------
 * Built once here, included per page: a toolbar (count + search), a richer
 * empty state, comfortable tap targets, and a busy state for a submitting
 * button. Tokens only, like the rest of this file.
 */

/* One row above a table: the count on one side, a search on the other. Wraps
 * to two rows on a narrow screen rather than overflowing. */
.table-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-block-end: var(--space-4);
}

.table-toolbar .row-count {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.table-toolbar .toolbar-search {
  /* Grows to fill the row, shrinks to nothing on a narrow one — so the Search
   * button beside it is never pushed off the edge and clipped. */
  flex: 1 1 12rem;
  min-width: 0;
}

.toolbar-search-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* When the row runs out of width the button drops below the input instead of
   * being cut off. */
  flex-wrap: wrap;
}

/* Bulk row selection. The form the checkboxes submit to lives outside the table
 * (so the per-row action forms are never nested inside it) and carries only a
 * CSRF token, so it takes no space. */
.bulk-form {
  display: none;
}

/* The selection bar: appears under the toolbar once a row is checked. */
.bulk-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-block-end: var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--accent-soft);
  border: 1px solid var(--accent-soft-border);
  border-radius: var(--radius-md);
}

.bulk-bar .bulk-count {
  color: var(--text);
  font-size: var(--text-sm);
}

.bulk-bar .bulk-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The checkbox column: narrow, box centred in it. */
th.check,
td.check {
  width: 1%;
  white-space: nowrap;
  text-align: center;
}

th.check input,
td.check input {
  cursor: pointer;
}

/* Sortable column headers: the whole label is the link, and the direction is
 * drawn from `aria-sort` so there is no icon to keep in sync. */
th.sortable a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: inherit;
  text-decoration: none;
}

th.sortable a:hover {
  color: var(--text);
}

th.sortable a::after {
  content: "↕";
  opacity: 0.4;
  font-size: 0.85em;
}

th.sortable[aria-sort="ascending"] a::after {
  content: "↑";
  opacity: 1;
}

th.sortable[aria-sort="descending"] a::after {
  content: "↓";
  opacity: 1;
}

/* An empty table says why it is empty and offers the way out of that, instead
 * of a blank cell. Centred inside the spanning row. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  color: var(--text-muted);
}

.empty-state .empty-icon {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--text-muted);
}

.empty-state p {
  margin: 0;
}

/* Comfortable tap targets (WCAG 2.5.5). Standalone buttons reach 44px; the
 * console stays dense, so in-table text actions take the smaller AA minimum
 * (2.5.8, 24px) rather than bloating every row to 44. */
.button {
  min-height: 2.75rem;
}

.button.icon-only {
  min-width: 2.75rem;
}

.linkish {
  display: inline-flex;
  align-items: center;
  min-height: 1.5rem;
}

/* Row actions read as buttons, not bare text, and give a real 44px tap target
 * (B6). Hide / Revoke / Remove / End were red text links — destructive with no
 * affordance and far below a thumb's width. Here every action in the last
 * column is a bordered control; the destructive ones carry the danger colour
 * and a danger border so they are unmistakable. */
td.actions .linkish,
th.actions .linkish {
  min-height: 2.75rem;
  padding-inline: var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-weight: var(--weight-medium);
  text-decoration: none;
}

td.actions .linkish:hover {
  background: var(--surface-hover);
  border-color: var(--text-faint);
  text-decoration: none;
}

td.actions .linkish.danger {
  color: var(--bad);
  border-color: var(--bad-border);
}

td.actions .linkish.danger:hover {
  background: var(--bad-soft);
  border-color: var(--bad);
}

/* A submitting button, disabled while the request is in flight so it cannot be
 * pressed twice. Driven by panel.js on submit. */
[aria-busy="true"] {
  opacity: 0.6;
  pointer-events: none;
}

/* Flash messages as toasts, stacked in the corner, above everything. Fixed, so
 * they float over the page; panel.js dismisses them. */
.toasts {
  position: fixed;
  inset-block-end: var(--space-4);
  inset-inline-end: var(--space-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: min(24rem, calc(100vw - 2rem));
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
  transition: opacity var(--normal) var(--ease);
}

.toast.ok {
  border-inline-start-color: var(--ok);
}

.toast.warn {
  border-inline-start-color: var(--warn);
}

.toast.bad {
  border-inline-start-color: var(--bad);
}

.toast-text {
  flex: 1;
}

.toast-undo {
  display: inline;
}

.toast-close {
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: var(--space-1);
  display: inline-flex;
}

.toast-close:hover {
  color: var(--text);
}

/* A disabled action reads as unavailable, not merely faint. */
.button:disabled,
.button[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Requests decide row: the note, then the actions with Reject pushed to the far
 * end so the destructive button never sits flush against Approve (B4). */
.decide-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
}

.decide-actions .reject {
  margin-inline-start: auto;
}

/* "Unsaved changes" sits before its Save, pushing the button to the end so the
 * warning is read before the click (B2). */
.dirty-hint {
  margin-inline-end: auto;
  color: var(--warn);
  font-weight: var(--weight-medium);
}

/* Password field with an inline Show/Hide toggle (B7). The input reserves room
 * on the end so its text never runs under the button; logical properties keep
 * it correct in RTL. */
.password-field {
  position: relative;
  display: block;
}

.password-field input {
  width: 100%;
  padding-inline-end: 4rem;
}

.pw-toggle {
  position: absolute;
  inset-inline-end: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  min-height: 2rem;
  padding-inline: var(--space-2);
  border: 0;
  background: none;
  color: var(--accent-text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
}

.pw-toggle:hover {
  text-decoration: underline;
}

/* --- Public legal pages (privacy policy) ----------------------------------- */
/* A plain readable column on a card, no panel chrome. */
.legal {
  max-width: 44rem;
  margin-inline: auto;
  padding: var(--space-6);
}

.legal-body {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}

.legal-body h1 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-2);
}

.legal-body h2 {
  font-size: var(--text-md);
  margin: var(--space-5) 0 var(--space-2);
}

.legal-body p,
.legal-body li {
  color: var(--text);
  line-height: 1.6;
}

.legal-updated {
  color: var(--text-muted);
  font-size: var(--text-sm);
}
