/*
 * Mobile-first. The base rules below target a phone in portrait; the
 * `min-width` blocks near the bottom progressively widen the layout for
 * tablets, laptops and large displays. Nothing is hidden on small screens -
 * the schedule timeline is the primary view at every size.
 */

:root {
  color-scheme: light;
  --bg: #f7f7f4;
  --surface: #ffffff;
  --surface-muted: #efefe8;
  --text: #222522;
  --muted: #687067;
  --line: #d8dcd2;
  --accent: #0f766e;
  --accent-strong: #115e59;
  --accent-soft: #e7f4f1;
  --danger: #b42318;
  --focus: #1d4ed8;
  --radius: 8px;
  /* Minimum comfortable touch target. */
  --tap: 2.75rem;
  /* The two gaps the whole app is laid out with - screens, forms, lists,
     toolbars and dialogs alike: one between elements stacked above each
     other, one between elements side by side. Elements set no margins of
     their own; the containers space their children. */
  --gap-v: 0.85rem;
  --gap-h: 0.5rem;
  /* Height of one hour of schedule. The rem half of the max() is what keeps
     the grid usable when someone raises their browser's default font size:
     rows then grow with the text instead of clipping it. app.js measures the
     rendered row rather than reading this value, so the two can never drift
     apart. */
  --hour-height: max(72px, 4.5rem);
  /* Each kind of window has a light tint of its own, and each section in it
     a colour for its stripe and heading. Light enough that text, fields and
     buttons keep their contrast on top; the headings' darker shades pass
     4.5:1 on white. */
  --tone-appointment: #e9f5f1;
  --tone-client: #eaf1fb;
  --tone-course: #edf6e9;
  --tone-notice: #fdf4e2;
  --tone-staff: #f2eefa;
  --tone-place: #f5f0e8;
  --section-appointments: #0f766e;
  --section-notes: #b7791f;
  --section-notes-text: #7c4a03;
  --section-notes-card: #fffaeb;
  --section-notes-line: #f1dfae;
  --section-courses: #3f7d36;
  --section-phones: #64748b;
  --section-history: #6d4fb3;
  --time-col-width: 3.5rem;
  --resource-col-min: 10.5rem;
}

* {
  box-sizing: border-box;
}

/* `hidden` means gone, whatever display a class gives the element: a hidden
   row left displayed takes no height but still gets a gap either side. */
[hidden] {
  display: none !important;
}

html {
  /* Stops iOS from re-scaling text in landscape. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: Arial, Helvetica, sans-serif;
}

button,
input,
select,
textarea {
  font: inherit;
}

/* 1rem == 16px keeps iOS Safari from zooming in when a field is focused. */
input,
select,
textarea {
  font-size: 1rem;
}

button {
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  padding: 0.5rem 0.75rem;
  /* Removes the 300ms tap delay without disabling pinch-zoom. */
  touch-action: manipulation;
}

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

button.danger {
  color: #fff;
  border-color: var(--danger);
  background: var(--danger);
}

button.active {
  border-color: var(--accent);
  color: var(--accent-strong);
  background: var(--accent-soft);
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- shell -- */

/*
 * The shell is a full-height column: header, then a workspace that takes the
 * remaining space. The schedule's scroll container sizes itself from that
 * leftover space, which is what keeps the grid usable in phone landscape
 * where there are only a few hundred pixels of height to spend.
 */
.app-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  height: 100dvh;
}

.loading-panel {
  max-width: 32rem;
  margin-inline: auto;
  padding: 4rem 1.25rem;
  text-align: center;
}

.login-screen {
  display: grid;
  min-height: 100dvh;
  place-items: center;
  padding: 1rem;
}

.login-panel,
.panel,
.modal {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.login-panel {
  width: min(100%, 28rem);
  padding: 1.25rem;
}

.topline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-v) var(--gap-h);
  flex-wrap: wrap;
}

/* minmax(0, …) throughout this header: a default `auto` grid track sizes to
   its widest item, so at a raised browser font size the title row would make
   the track wider than the screen and drag the whole header off it. The rows
   wrap instead. */
.app-header {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap-v) var(--gap-h);
  padding: 0.5rem 0.75rem;
  background: var(--surface);
  border-block-end: 1px solid var(--line);
}

/* One row on every phone: on a narrow one the title gives way (it ends in
   an ellipsis) rather than the buttons dropping to a row of their own, which
   would take a whole row of height away from the schedule. */
.app-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: nowrap;
  min-width: 0;
  gap: var(--gap-h);
}

.header-actions {
  flex: 0 0 auto;
}

.app-brand {
  min-width: 0;
  font-size: 1rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.header-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  min-width: 0;
  gap: var(--gap-v) var(--gap-h);
}

.header-actions select {
  min-height: var(--tap);
}

/* The person signed in: an icon-sized button on a phone, their name from
   a tablet up (see the 48rem block). */
.account-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-width: var(--tap);
  padding-inline: 0.5rem;
  white-space: nowrap;
}

.account-button__name {
  display: none;
  color: var(--muted);
  font-size: 0.875rem;
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Who is signed in, above the form to change their password. */
.account-facts {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.3rem var(--gap-h);
  margin: 0;
}

.account-facts dt {
  color: var(--muted);
}

.account-facts dd {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.account-done {
  margin: 0;
  color: var(--accent-strong);
  font-weight: 600;
}

/* Nav is a single scrollable chip row on phones instead of a tall wrapped
   block that would eat the schedule's vertical space. */
.app-nav {
  display: flex;
  gap: var(--gap-v) var(--gap-h);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-block-end: 0.1rem;
}

.app-nav::-webkit-scrollbar {
  display: none;
}

.app-nav button {
  flex: 0 0 auto;
  padding-inline: 0.7rem;
}

.workspace {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: auto;
  padding: 0.6rem;
}

.panel {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
  padding: var(--gap-v);
}

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--gap-v) var(--gap-h);
}

.toolbar h1,
.panel > h1 {
  font-size: 1.15rem;
  margin: 0;
}

/* Every form is a stack spaced by the vertical gap, like the dialogs; a
   button on its own keeps its natural width instead of stretching. */
form {
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
}

form > button {
  align-self: flex-start;
}

/* --------------------------------------------------------------- forms --- */

/* minmax(0, 1fr) rather than the default `auto`: the availability check
   appends a reason to option labels ("Maya Cohen - The worker is already
   booked"), and an auto-sized track would grow to that widest option and push
   the whole dialog sideways on a phone. */
.field {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.3rem;
}

.field input,
.field select,
.field textarea,
select {
  min-height: var(--tap);
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding-inline: 0.6rem;
  background: #fff;
  max-width: 100%;
}

/* Only fields stretch: a bare select elsewhere (the header language picker,
   the availability worker picker) should stay its natural size. */
.field input,
.field select,
.field textarea {
  width: 100%;
}

/* One name per language. The fieldset keeps the three inputs reading as a
   single field: the legend carries the label that used to sit on the one
   input, and each row is labelled by the language it holds. */
.name-group {
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: var(--gap-v);
  margin: 0;
  min-inline-size: 0;
}

.name-group legend {
  padding-inline: 0.35rem;
  font-weight: 600;
}

.name-group .field > .field-label {
  color: var(--muted);
  font-size: 0.85rem;
}

/* The explanation attached to a field. A title attribute would cover the mouse
   and leave a phone with nothing, so this is a real button with a bubble
   positioned against the field it belongs to. */
.field,
.checkbox,
.name-group {
  position: relative;
}

.field-label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.field-help {
  position: relative;
  flex: none;
  min-height: 1.5rem;
  width: 1.5rem;
  padding: 0;
  border-radius: 50%;
  border-color: var(--line);
  background: var(--surface-muted);
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1;
}

/* Widens the tap target past the circle, without letting it reach far enough
   to swallow taps meant for the input below. */
.field-help::after {
  content: "";
  position: absolute;
  inset: -0.35rem -0.5rem;
}

.field-help:hover,
.field-help[aria-expanded="true"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}

.field-help__bubble {
  position: absolute;
  z-index: 5;
  /* The bubble overlays the field below it, so let taps fall through: hitting
     the input beneath both dismisses the bubble and focuses the input, which
     is what someone reaching past it meant to do. */
  pointer-events: none;
  inset-inline-start: 0;
  inset-block-start: 100%;
  width: min(26rem, 100%);
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 6px 16px rgb(0 0 0 / 0.14);
  font-size: 0.85rem;
  font-weight: 400;
  line-height: 1.4;
  text-align: start;
  white-space: normal;
}

/* The legend sits on the border, so its bubble would otherwise start halfway
   up the first input. */
.name-group > .field-help__bubble {
  inset-block-start: 2.1rem;
  inset-inline-start: 0.75rem;
  width: min(26rem, calc(100% - 1.5rem));
}

.checkbox {
  display: flex;
  gap: var(--gap-h);
  align-items: center;
  min-height: var(--tap);
}

.checkbox input {
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
}

.computed-end-time {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: 0.6rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-muted);
  color: var(--muted);
}

.toast {
  color: var(--danger);
  min-height: 1.25rem;
  margin: 0.25rem 0;
}

.field-warning {
  display: block;
  min-height: 1.1rem;
  margin-block-start: -0.6rem;
  margin-block-end: 0.6rem;
  color: var(--danger);
  font-size: 0.85rem;
}

/* With nothing to say it takes no room, rather than leaving a gap between
   the fields around it. It stays displayed, so it is still a live region
   when a warning arrives. */
.field-warning:empty {
  min-height: 0;
  margin-block: 0;
}

/* ------------------------------------------------------------ schedule --- */

.schedule-view {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--gap-v);
}

.schedule-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-v) var(--gap-h);
}

.date-nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-v) var(--gap-h);
  flex: 1 1 auto;
  min-width: 0;
}

.date-nav input[type="date"] {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding-inline: 0.5rem;
  background: #fff;
}

.date-nav button {
  flex: 0 0 auto;
  min-width: var(--tap);
  padding-inline: 0.6rem;
}

/* Two mutually exclusive choices rendered as a joined pair of buttons.
   `flex: 0 1 auto` with shrinkable buttons so that at a large browser font
   size the pair narrows and ellipsises rather than spilling out of the
   toolbar - it stays one recognisable control either way. */
.segmented {
  display: flex;
  flex: 0 1 auto;
  min-width: 0;
}

.segmented button {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border-radius: 0;
  margin-inline-start: -1px;
  /* Snug on a phone, so Day|Week and Rooms|Workers share a row even in
     Russian, the longest of the three languages; the tablet block widens
     the padding again. */
  padding-inline: 0.55rem;
  font-size: 0.9375rem;
}

.segmented button:first-child {
  border-start-start-radius: 6px;
  border-end-start-radius: 6px;
  margin-inline-start: 0;
}

.segmented button:last-child {
  border-start-end-radius: 6px;
  border-end-end-radius: 6px;
}

.segmented button.active {
  position: relative;
  z-index: 1;
}

.schedule-new {
  flex: 0 0 auto;
  margin-inline-start: auto;
  min-width: var(--tap);
  padding-inline: 0.75rem;
  font-size: 1.25rem;
  line-height: 1;
}

.schedule-new__full {
  display: none;
}

/* Vertical space on a phone belongs to the grid, not to instructions. */
.schedule-hint {
  display: none;
  margin: 0;
  color: var(--muted);
  font-size: 0.8rem;
}

.timeline-scroll {
  flex: 1 1 auto;
  min-height: 11rem;
  overflow: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

.timeline {
  display: grid;
  grid-template-columns: var(--time-col-width) repeat(var(--resource-count, 1), minmax(var(--resource-col-min), 1fr));
  grid-template-rows: auto 1fr;
  min-width: min-content;
}

/* A week: one column per day, every appointment of the day inside it, the
   ones that overlap laid side by side (see assignLanes in app.js). Who is
   in is a gutter of thin bars at the column's inline start, one per worker
   in their colour, which the legend above the grid explains. */
.timeline--week {
  --resource-col-min: 9rem;
}

.week-view {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--gap-v);
}

/* One line of chips: side by side at the horizontal gap, and when it wraps,
   the wrapped part stays close, as the lines of one element. */
.worker-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem var(--gap-h);
  font-size: 0.8rem;
}

.worker-legend__chip::before {
  content: '';
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  margin-inline-end: 0.3rem;
  vertical-align: -0.1rem;
  border-radius: 0.2rem;
  background: color-mix(in srgb, var(--chip-color) 30%, white);
  box-shadow: inset 0 0 0 2px var(--chip-color);
}

.timeline-head--day {
  justify-content: center;
  font-size: 0.85rem;
}

.timeline-head--day.is-today,
.timeline-head--resource.is-today {
  background: var(--accent-soft);
}

/* Cards that overlap share the column's width out by lane. */
.timeline--week .appointment-card {
  inset-inline-start: calc(100% * var(--lane-start, 0) + 2px);
  width: calc(100% / var(--lanes, 1) - 4px);
}

/* A worker's column in the day view reads as "off" until their hours shade
   it in, and an absence hatches over that. The bands sit under the cards
   (2) and the now line (3), and take no pointer events, so a drop still
   lands on the column beneath. */
.timeline-body--off {
  background: var(--surface-muted);
}

.availability-band,
.absence-band {
  position: absolute;
  inset-inline: 0;
  z-index: 1;
  pointer-events: none;
}

.availability-band {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px rgb(15 118 110 / 0.15);
}

.absence-band {
  background: repeating-linear-gradient(135deg, rgb(220 38 38 / 0.14) 0 6px, transparent 6px 12px);
}

/* The week's lanes: the same bands, the column split between the workers
   and each lane tinted in its worker's colour. */
.availability-lane {
  inset-inline: auto;
  inset-inline-start: calc(100% / var(--lane-count) * var(--lane-index) + 1px);
  width: calc(100% / var(--lane-count) - 2px);
  box-shadow: none;
  border-radius: 3px;
}

.availability-lane.availability-band {
  background: color-mix(in srgb, var(--lane-color) 13%, white);
}

.availability-lane.absence-band {
  background: repeating-linear-gradient(135deg, color-mix(in srgb, var(--lane-color) 35%, white) 0 4px, transparent 4px 8px);
}

/* Header row: sticks to the top of the scroll container. */
.timeline-head {
  position: sticky;
  top: 0;
  z-index: 4;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  padding: 0.4rem 0.5rem;
  background: var(--surface-muted);
  border-block-end: 1px solid var(--line);
  border-inline-end: 1px solid var(--line);
  font-weight: 700;
  font-size: 0.875rem;
}

.timeline-body {
  position: relative;
  border-inline-end: 1px solid var(--line);
}

/* The stripe is the same idiom as an appointment card, so a column and the
   cards inside it read as the same kind of marking. The time column has no
   resource and so no stripe. */
.timeline-head--resource {
  border-inline-start: 5px solid var(--resource-accent, var(--line));
}

/* Time gutter: sticks to the inline start so the hours stay readable while
   the resource columns scroll sideways. Its z-index sits above the cards
   (2) and the header row (4) so nothing slides over it. */
.time-head {
  inset-inline-start: 0;
  z-index: 6;
}

.time-body {
  position: sticky;
  inset-inline-start: 0;
  z-index: 5;
  background: var(--surface);
}

.time-slot,
.grid-slot {
  height: var(--hour-height);
  border-block-end: 1px solid var(--line);
}

.time-slot {
  padding: 0.2rem 0.4rem;
  color: var(--muted);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

.now-indicator {
  position: absolute;
  inset-inline: 0;
  height: 2px;
  background: var(--danger);
  pointer-events: none;
  z-index: 3;
}

.now-indicator::before {
  content: "";
  position: absolute;
  inset-inline-start: -4px;
  top: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--danger);
}

/* ------------------------------------------------------ appointment card - */

/* A card is coloured three ways: its workers fill it, its room is the
   footer strip, and the service keeps the stripe down the side. */
.appointment-card {
  position: absolute;
  inset-inline: 0.25rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  align-content: space-between;
  overflow: hidden;
  background: var(--card-fill, #f3fbf9);
  border: 1px solid var(--line);
  border-inline-start: 5px solid var(--card-accent, var(--accent));
  border-radius: 6px;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.07);
  cursor: pointer;
  /* No double-tap-to-zoom (that gesture is the open action here), but panning
     stays available so the grid can still be scrolled from on top of a card.
     The drag itself cancels touchmove once a press-and-hold has been
     recognised. */
  touch-action: manipulation;
  /* A double click would otherwise select the card's text. */
  user-select: none;
}

.appointment-card:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
}

.appointment-card__body {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: grid;
  /* min-content rows, or the rows compress to share the card's fixed height
     and every line renders as a sliver of itself. app.js decides how many
     lines actually fit and hides the rest. */
  grid-auto-rows: min-content;
  gap: 0.1rem;
  align-content: start;
  padding: 0.3rem 0.4rem;
  text-align: start;
  background: none;
  border: 0;
  border-radius: 0;
  font-size: 0.8125rem;
  /* Tight enough that a one-hour card still fits all three lines at the
     desktop row height of 64px, rather than dropping the service name. */
  line-height: 1.2;
  overflow: hidden;
}

.appointment-card__body strong,
.appointment-card__body span,
.appointment-card__body small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.appointment-card__body span,
.appointment-card__body small {
  color: var(--muted);
}

/* The room, as the card's footer: its colour, and its name in it. Takes
   its own row under the body and the tick. */
.appointment-card__room {
  flex: 1 0 100%;
  align-self: end;
  padding: 0.05rem 0.4rem;
  background: color-mix(in srgb, var(--room-color, var(--line)) 32%, white);
  border-block-start: 2px solid var(--room-color, var(--line));
  color: #1f2937;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.appointment-card--unassigned .appointment-card__room {
  color: #92400e;
}

/* Checked marker: a single click or tap ticks a card off. It sits over the
   top corner rather than in the flow: as a flex item it wrapped onto a line
   of its own in a narrow card and pushed the room footer out of the bottom. */
.appointment-card__tick {
  display: none;
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  padding: 0 0.3rem;
  border-end-start-radius: 6px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 700;
  line-height: 1.2;
}

.appointment-card.is-checked {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.appointment-card.is-checked .appointment-card__tick {
  display: block;
}

/* Short cards drop detail lines rather than clipping them. Which class is
   applied is decided from the measured line height, so raising the browser
   font size moves the thresholds with it. */
.appointment-card.is-two-line .appointment-card__body span,
.appointment-card.is-one-line .appointment-card__body span,
.appointment-card.is-one-line .appointment-card__body small {
  display: none;
}

.appointment-card.is-dragging {
  opacity: 0.3;
}

/* A keyboard move is pending on this card: arrows adjust, Enter commits. */
.appointment-card.is-moving {
  outline: 2px dashed var(--accent);
  outline-offset: 1px;
}

.appointment-card.status-cancelled {
  opacity: 0.55;
  background: var(--surface-muted);
}

/* An appointment still missing its worker or its room: striped amber, so it
   stands out from every finished booking around it, with the ⚠ and the
   reason in the text for anyone who does not see the colour. */
.appointment-card--unassigned {
  border-color: #d97706;
}

.appointment-card--unassigned .appointment-card__body small {
  color: #92400e;
  font-weight: 700;
}

/* The column for appointments with no room / no worker yet. */
.timeline-head--unassigned {
  font-style: italic;
  --resource-accent: #d97706;
}

.timeline-body--unassigned {
  background: repeating-linear-gradient(135deg, transparent 0 12px, rgb(217 119 6 / 0.06) 12px 24px);
}

.drag-ghost {
  position: fixed;
  /* Physical top/left, because the follow-the-pointer transform below is in
     physical pixels - a logical inset would flip it in RTL. */
  top: 0;
  left: 0;
  z-index: 1000;
  pointer-events: none;
  padding: 0.35rem 0.45rem;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-inline-start: 5px solid var(--card-accent, var(--accent));
  border-radius: 6px;
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.22);
  font-size: 0.8125rem;
  line-height: 1.25;
  overflow: hidden;
}

.drop-preview {
  position: absolute;
  inset-inline: 0.25rem;
  z-index: 3;
  display: grid;
  place-items: center;
  padding: 0.2rem;
  border: 2px dashed var(--accent);
  border-radius: 6px;
  background: rgb(15 118 110 / 0.12);
  color: var(--accent-strong);
  font-size: 0.75rem;
  font-weight: 700;
  text-align: center;
  pointer-events: none;
}

.drop-preview.is-invalid {
  border-color: var(--danger);
  background: rgb(180 35 24 / 0.12);
  color: var(--danger);
}

body.is-dragging-appointment {
  /* Keeps a mouse drag from selecting card text as it sweeps the grid. */
  user-select: none;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------- agenda/list -- */

.agenda__day {
  margin: 0;
  font-size: 0.95rem;
}

.agenda {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  display: grid;
  align-content: start;
  gap: var(--gap-v);
}

.agenda-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.6rem;
  align-items: center;
  width: 100%;
  text-align: start;
  padding: 0.55rem 0.6rem;
  border-inline-start: 5px solid var(--card-accent, var(--accent));
}

.agenda-item strong {
  grid-column: 1 / -1;
}

.agenda-item span {
  grid-column: 1 / -1;
  color: var(--muted);
  font-size: 0.85rem;
}

.table {
  display: grid;
  gap: var(--gap-v);
}

/* A list that may run to years of entries - a client's appointments, their
   notes - scrolls inside itself. Under half the screen tall, so on a phone
   there is always some of the window left to swipe the window itself by;
   and the swipe passes on to the window at the list's end rather than
   stopping dead. */
.scroll-frame {
  position: relative;
  min-width: 0;
}

.scroll-list {
  max-height: min(24rem, 45dvh);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-inline-end: 0.35rem;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--section-color, var(--accent)) 55%, transparent) transparent;
}

.scroll-list:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* The fades that say there is more above or below; they let touches through. */
.scroll-frame::before,
.scroll-frame::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 2.25rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}

.scroll-frame::before {
  top: 0;
  background: linear-gradient(#fff, rgb(255 255 255 / 0));
}

.scroll-frame::after {
  bottom: 0;
  background: linear-gradient(rgb(255 255 255 / 0), #fff);
}

.scroll-frame.has-more-above::before,
.scroll-frame.has-more-below::after {
  opacity: 1;
}

.row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.15rem;
  align-items: center;
  width: 100%;
  text-align: start;
  padding: 0.55rem 0.6rem;
}

.row span {
  color: var(--muted);
  font-size: 0.85rem;
}

/* ---------------------------------------------------------------- modal -- */

/* On a phone the dialog is the screen: there is no room around it worth
   keeping, and nothing to drag it across. It floats from tablet up. */
.modal {
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  overflow: auto;
  padding: var(--gap-v);
  margin: 0;
  border: 0;
  border-radius: 0;
}

.modal::backdrop {
  background: rgb(0 0 0 / 0.4);
}

/* The window's tint is its background and its sticky head's, so the head
   does not show as a white band across a tinted window. */
.modal {
  --modal-bg: var(--surface);
  background: var(--modal-bg);
}

.modal--appointment { --modal-bg: var(--tone-appointment); }
.modal--client { --modal-bg: var(--tone-client); }
.modal--course { --modal-bg: var(--tone-course); }
.modal--notice { --modal-bg: var(--tone-notice); }
.modal--staff { --modal-bg: var(--tone-staff); }
.modal--place { --modal-bg: var(--tone-place); }

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-h);
  position: sticky;
  top: calc(-1 * var(--gap-v));
  z-index: 1;
  padding-block: 0.35rem;
  margin-block-start: -0.35rem;
  background: var(--modal-bg);
}

.modal-head h2 {
  font-size: 1.1rem;
  margin: 0;
}

/* The X: a square the size of a fingertip, borderless until it is pointed
   at, with the cross large enough to see at a glance. */
.modal-close {
  flex: none;
  width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  border-color: transparent;
  background: transparent;
  color: var(--muted);
  font-size: 1.75rem;
  line-height: 1;
}

.modal-close:hover {
  border-color: var(--line);
  background: rgb(0 0 0 / 0.06);
  color: var(--text);
}

/* One row for the dialog's actions, wrapping only when the screen really has
   no room. The destructive one is pushed to the far end so it is not sitting
   under the thumb next to Save. */
.modal-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-h);
}

/* Every stack in a dialog - the dialog itself, its body, the form, a group
   of repeated rows, a history box - spaces its children with the one
   vertical gap. `[open]` because a closed dialog must stay hidden. */
.modal[open],
.modal-body,
.modal-body > div,
.modal-body form,
.modal .name-group,
.modal .subpanel,
.member-repeater,
.member-repeater__rows {
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
}

.modal .field,
.modal .name-group,
.modal .checkbox,
.modal .subpanel h3,
.modal .toast {
  margin: 0;
}

.modal .checkbox,
.modal .field-label {
  gap: var(--gap-h);
}

/* The browser's own margin on a checkbox would add to the gap beside it. */
.modal .checkbox input {
  margin: 0;
}

/* Boxes inside a dialog are padded by the same gap as the space around
   them. A legend is not a flex item, so the gap under it comes from the
   name group's top padding instead. */
.modal .subpanel,
.modal .name-group {
  padding: var(--gap-v);
}

.modal .table {
  gap: var(--gap-v);
}

/* One change in an appointment's history: what happened, then when and by
   whom beside it, then a line per field that changed. */
.history-entry {
  display: grid;
  gap: 0.15rem;
}

/* Its parts wrap on a narrow screen like the lines under them, 0.15rem
   apart: a wrapped head is still one line of the entry. */
.history-entry__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem var(--gap-h);
}

.history-entry span {
  color: var(--muted);
  font-size: 0.85rem;
}

/* A list entry that opens something - a course, a session - laid out like a
   history entry, framed as the button it is. */
.entry-button {
  display: grid;
  gap: 0.15rem;
  width: 100%;
  padding: 0.55rem 0.6rem;
  text-align: start;
}

.entry-button span {
  color: var(--muted);
  font-size: 0.85rem;
}

.course-state {
  font-weight: 600;
}

.course-entry--active .course-state { color: var(--accent-strong); }
.course-entry--expired .course-state,
.course-entry--cancelled .course-state { color: var(--muted); }

/* A client's course beside their name on the schedule: the session of the
   total, amber when money is owed, red on the course's last session. */
.course-mark {
  font-weight: 600;
  color: var(--accent-strong);
}

.course-mark.is-owed {
  color: #92400e;
}

.course-mark.is-last {
  color: var(--danger);
}

/* The header's bell, with the number of notices on it. */
.notice-button {
  position: relative;
  min-width: var(--tap);
}

.notice-count {
  position: absolute;
  inset-block-start: -0.35rem;
  inset-inline-end: -0.35rem;
  min-width: 1.25rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.25rem;
}

/* A notice's own actions sit under its text, a little apart from it. */
.notice .modal-actions {
  margin-block-start: 0.35rem;
}

/* The course choices in the appointment form, and what they mean. */
.course-picks {
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
}

.course-notes {
  display: grid;
  gap: 0.15rem;
}

.course-notes span {
  color: var(--muted);
  font-size: 0.85rem;
}

.entry-button .course-owes,
.history-entry .course-owes,
.course-notes .course-owes {
  color: #92400e;
  font-weight: 600;
}

/* A voided payment stays on the list, struck through. */
.history-entry.is-voided strong {
  text-decoration: line-through;
  color: var(--muted);
}

/* An action inside an entry's head: small text, but still a whole finger's
   height to tap on a phone. */
.link-button {
  min-height: var(--tap);
  padding: 0 0.6rem;
  font-size: 0.85rem;
}

/* A short form on one line - a payment, sessions given back - that wraps on
   a phone. Row and column gaps are the dialog's two gaps. */
.modal .inline-form {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--gap-v) var(--gap-h);
}

.modal .inline-form .field {
  flex: 1 1 9rem;
}

.course-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-v) var(--gap-h);
}

/* Six filters do not fit a small phone side by side, and squeezed they cut
   their own words short. They keep their full width and the row scrolls
   sideways, like the nav. */
.course-filter {
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.course-filter::-webkit-scrollbar {
  display: none;
}

.course-filter button {
  flex: 0 0 auto;
  overflow: visible;
}

.course-search {
  flex: 1 1 14rem;
  min-height: var(--tap);
  padding-inline: 0.6rem;
  border: 1px solid var(--line);
  border-radius: 6px;
}

/* A list row in a dialog is text, not a button: it lines up with the
   heading above it rather than sitting indented by a button's padding. */
.modal .table .row {
  padding: 0;
}

/* An empty message line takes back the gap in front of it, so a dialog with
   nothing to report has no hole where the message would go. The line stays
   displayed, so it is still a live region when a message arrives. */
.modal .toast:empty,
.modal .field-warning:empty {
  min-height: 0;
  margin-block: calc(-1 * var(--gap-v)) 0;
}

/* A warning belongs to the field above it, so it sits closer to that. */
.modal .field-warning {
  margin-block: calc(var(--gap-v) / -2) 0;
}

.modal-actions .danger {
  margin-inline-start: auto;
}

.split-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-v) var(--gap-h);
  align-items: start;
}

/* One row per person on an appointment, with its remove button beside it,
   level with the control's bottom edge. */
.member-repeater__row {
  display: flex;
  align-items: end;
  gap: var(--gap-h);
}

.member-repeater__row > .field {
  flex: 1 1 auto;
  min-width: 0;
}

.member-repeater__remove {
  flex: 0 0 auto;
  min-width: var(--tap);
}

/* A client's search field with the button to their profile beside it. */
.client-picker {
  display: flex;
  gap: var(--gap-h);
  min-width: 0;
}

.client-picker input {
  flex: 1 1 auto;
}

.client-profile-button {
  flex: 0 0 auto;
}

/* A password with Show beside it, and its rules beneath: a cross for each
   still missing, a tick once it is met. Colour is not the only sign - the
   marks differ, and a screen reader hears "met" or "not met". */
.password-input {
  display: flex;
  gap: var(--gap-h);
  min-width: 0;
}

.password-input input {
  flex: 1 1 auto;
}

.password-show {
  flex: none;
}

.password-rules {
  display: grid;
  gap: 0.3rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.9rem;
  color: var(--danger);
}

.password-rules li {
  display: flex;
  align-items: baseline;
  gap: var(--gap-h);
}

.password-rules li.is-met {
  color: var(--accent-strong);
}

.password-rules li.is-pending {
  color: var(--muted);
}

.password-rules__mark {
  flex: none;
  width: 1rem;
  font-weight: 700;
  text-align: center;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The first row cannot be removed, but once there are others it keeps the
   button's space, so every control in the group is the same width. */
.member-repeater__remove.is-placeholder {
  visibility: hidden;
}

/* "+ Add" and whatever else belongs to the group, on one line. */
.member-repeater__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-h);
}

.subpanel {
  display: flex;
  flex-direction: column;
  gap: var(--gap-v);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-v);
  background: #fff;
}

.subpanel h2,
.subpanel h3 {
  font-size: 1rem;
  margin: 0;
}

/* A section's colour: a stripe down its leading edge (the right one in
   Hebrew) and its heading. */
.subpanel[class*="subpanel--"] {
  border-inline-start: 4px solid var(--section-color);
}

.subpanel[class*="subpanel--"] > h3 {
  color: var(--section-heading, var(--section-color));
}

.subpanel--appointments { --section-color: var(--section-appointments); --section-heading: var(--accent-strong); }
.subpanel--courses { --section-color: var(--section-courses); --section-heading: #2f6128; }
.subpanel--phones { --section-color: var(--section-phones); --section-heading: #475569; }
.subpanel--history { --section-color: var(--section-history); --section-heading: #5b3fa0; }
.subpanel--notes { --section-color: var(--section-notes); --section-heading: var(--section-notes-text); }

/* Each note a card of its own, like a slip of paper. */
.subpanel--notes .table > .history-entry {
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--section-notes-line);
  border-radius: 6px;
  background: var(--section-notes-card);
}

.inline-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-v) var(--gap-h);
  align-items: end;
}

/* ------------------------------------------------------------- tablet ---- */
@media (min-width: 48rem) {
  :root {
    --time-col-width: 4.25rem;
    --resource-col-min: 11.5rem;
  }

  /* The header stays stacked (title row above nav row) through the tablet
     band. Putting nav beside the title row here would leave it about 370px -
     the title row's content width is fixed - and ten nav buttons would wrap
     into three rows. Given a full-width row they fit in two. */
  .app-header {
    padding: 0.6rem 1rem;
  }

  .app-nav {
    flex-wrap: wrap;
    overflow: visible;
  }

  .app-brand {
    font-size: 1.1rem;
  }

  .account-button__name {
    display: inline;
  }

  .account-button__icon {
    display: none;
  }

  /* Reads as the name it replaced, framed only when pointed at: the header
     beside the menu has no width to spare in Russian. */
  .account-button {
    min-width: 0;
    padding-inline: 0.2rem;
    border-color: transparent;
    background: transparent;
  }

  .account-button:hover {
    border-color: var(--line);
  }

  /* So a longer name than "Administrator" cannot push the menu onto two
     rows: it ends in an ellipsis, and the tooltip has it whole. */
  .account-button__name {
    max-width: 5.5rem;
  }

  .workspace {
    padding: 1rem;
  }

  .panel {
    padding: var(--gap-v);
  }

  .schedule-hint {
    display: block;
  }

  .schedule-new {
    font-size: inherit;
    line-height: inherit;
  }

  .schedule-new__full {
    display: inline;
  }

  .schedule-new__short {
    display: none;
  }

  .date-nav {
    flex: 0 0 auto;
  }

  .date-nav input[type="date"] {
    flex: 0 0 auto;
  }

  .row {
    grid-template-columns: 1.2fr 1fr auto;
    gap: 0.75rem;
  }

  .agenda-item {
    grid-template-columns: auto 1fr;
  }

  .agenda-item strong,
  .agenda-item span {
    grid-column: auto;
  }

  .split-grid {
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  }

  .inline-row {
    grid-template-columns: 1fr 8rem 8rem;
  }

  .segmented button {
    padding-inline: 0.7rem;
    font-size: inherit;
  }

  .modal {
    width: min(44rem, calc(100vw - 2rem));
    max-width: none;
    /* As tall as its content. `auto` on a dialog, which the browser
       positions against both edges of the screen, stretches it to the full
       height with the space left empty. */
    height: fit-content;
    max-height: calc(100dvh - 1.5rem);
    padding: var(--gap-v);
    margin: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }

  /* The header is the drag handle (see makeDraggable in app.js). */
  .modal-head {
    cursor: move;
    touch-action: none;
    user-select: none;
  }

  .modal-head button {
    cursor: pointer;
  }
}

/* ------------------------------------------------- laptop and larger ----- */
@media (min-width: 64rem) {
  :root {
    --hour-height: max(64px, 4rem);
    --resource-col-min: 13rem;
  }

  /* Wide enough for one row: `display: contents` promotes the title and the
     actions out of their wrapper so brand | nav | actions become three grid
     columns. */
  .app-header {
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
  }

  .app-header__bar {
    display: contents;
  }

  .app-brand {
    order: 0;
  }

  .app-nav {
    order: 1;
  }

  /* Slightly smaller and tighter, so the longest labels (Russian) still fit
     the eleven buttons on one row on a 1536px laptop. */
  .app-nav button {
    font-size: 0.875rem;
    padding-inline: 0.5rem;
  }

  .header-actions {
    order: 2;
  }
}

/* Sharpen the sticky gutter once there is room for a scrollbar shadow. */
@media (min-width: 48rem) {
  .time-body,
  .time-head {
    box-shadow: 1px 0 0 var(--line);
  }
}

/* --------------------------------------------------- short viewports -----
 * Mainly a phone held in landscape, where there are only a few hundred pixels
 * of height to spend. Everything chrome-like shrinks to one row so the grid
 * still shows a useful number of hours. This block is last so it wins over
 * the width-based rules above - a landscape phone matches those too.
 */
@media (max-height: 30rem) {
  :root {
    --hour-height: max(52px, 3.25rem);
  }

  .app-header {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.6rem;
  }

  .app-header__bar {
    display: flex;
    order: 2;
  }

  .app-brand,
  .account-button__name,
  .schedule-hint {
    display: none;
  }

  .account-button__icon {
    display: inline;
  }

  .app-nav {
    order: 1;
    flex-wrap: nowrap;
    overflow-x: auto;
  }

  .app-nav button,
  .schedule-toolbar button,
  .header-actions button,
  .header-actions select,
  .date-nav input[type="date"] {
    min-height: 2.25rem;
    padding-block: 0.25rem;
  }

  .workspace {
    padding: 0.4rem;
  }

  .panel {
    padding: 0.5rem;
  }

  .timeline-head {
    min-height: 2rem;
  }
}

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

/* A written notification keeps the lines it was typed with, and a long word
   wraps rather than widening the list on a phone. */
.notice-message {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The receivers' address field: looks like an input, holds the chosen names
   as chips, and opens its list of users beneath it, over what follows. */
.recipient-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-h);
  min-height: var(--tap);
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  cursor: text;
}

.recipient-box:focus-within {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.recipient-chips {
  display: contents;
}

.recipient-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  max-width: 100%;
  padding-inline-start: 0.6rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  overflow-wrap: anywhere;
}

/* A round X inside the chip; min-height 0 overrides the button's tap size,
   and the chip's own height (with the padding here) keeps it fingerable. */
.recipient-chip__remove {
  min-height: 0;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-size: 1.2rem;
  line-height: 1;
}

.recipient-chip__remove:hover {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

.modal .recipient-input,
.recipient-input {
  flex: 1 1 8rem;
  width: auto;
  min-width: 6rem;
  min-height: 2rem;
  border: 0;
  padding: 0;
  background: transparent;
}

.recipient-input:focus-visible {
  outline: none;
}

.recipient-list {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 100%;
  z-index: 2;
  max-height: 15rem;
  overflow: auto;
  margin: 0.25rem 0 0;
  padding: 0.25rem 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.15);
}

.recipient-option {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--gap-h);
  min-height: var(--tap);
  padding-inline: 0.75rem;
  cursor: pointer;
}

.recipient-option.is-active,
.recipient-option:hover {
  background: var(--accent-soft);
}

.recipient-option__role,
.recipient-option--none {
  color: var(--muted);
  font-size: 0.85rem;
}

.recipient-option--none {
  cursor: default;
}
