/*
 * The palette, and the scales everything else is built from.
 *
 * `--muted` is deliberately a step darker than it looks like it needs to be.
 * As a caption colour it usually sits on the page background, not on white, and
 * the lighter grey it used to be measured 4.43:1 there — under the 4.5:1 that
 * small text has to clear. Darkening one step is cheaper than remembering never
 * to use it on grey.
 *
 * The spacing values are named rather than picked per rule. Eight numbers (4, 6,
 * 8, 10, 12, 14, 16, 20) were all saying "distance between related things", which
 * is how a rhythm turns into a guess; the names below say *which* relationship,
 * and the comment on each is the rule for choosing.
 *
 * `--control-h` and `--touch-h` are two answers to the same question and are kept
 * apart on purpose: a dense desktop table is worth more than a taller one, so the
 * finger's size is applied by the pointer query further down instead of here.
 */
:root {
  --bg: #f4f5f7;
  --ink: #1b1d22;
  --muted: #5b6270;
  --accent: #114b8c;
  --accent-bright: #4f9bf5;
  --accent-soft: #e8eff8;
  --line: #e2e5ea;
  --ok: #1b7f4b;
  --danger: #b3261e;

  --s-1: 4px; /* inside one control */
  --s-2: 8px; /* between controls in a row */
  --s-3: 12px; /* between rows of one group */
  --s-4: 16px; /* between groups, and between cards */
  --s-5: 24px; /* above a panel */
  --s-6: 48px; /* below the page */

  --r-card: 10px;
  --r-group: 8px;
  --r-control: 7px;
  --r-pill: 999px;

  --control-h: 34px;
  --touch-h: 44px;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-size: 15px;
  -webkit-text-size-adjust: 100%;
}

/* ------------------------------------------------------------------ header */

.app-header {
  background: #1b1d22;
  color: #fff;
  padding: 14px 0 0;
  position: sticky;
  top: 0;
  z-index: 20;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
}

.brand {
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
}

/*
 * The slogan under the name. It is a caption, not a second title: on the same
 * line at 0.8rem it read as the rest of the name, so it sits below the name and
 * shrinks to a size a glance skips and a look finds. The wide tracking and the
 * upper case are what keep that size legible instead of merely faint.
 *
 * `line-height: 1` keeps the caption inside its own 10px: the header is sticky,
 * and every line added to it is a line of screen the panel does not get back.
 */
.brand-sub {
  margin-top: 1px;
  line-height: 1;
  color: #8b93a3;
  font-size: 0.62rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.clock {
  font-variant-numeric: tabular-nums;
  font-size: 1.05rem;
  margin-right: 12px;
  color: #dfe4ec;
}

.badge-running {
  background: var(--accent);
}
.badge-idle {
  background: #4b5563;
}
.badge-off {
  background: #7f1d1d;
}

.tabs {
  margin-top: 12px;
}

.tabs-list {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tabs-list::-webkit-scrollbar {
  display: none;
}

/* The burger only exists on narrow screens; see the media query below. */
.tabs-toggle {
  display: none;
}

.tab {
  background: transparent;
  border: 0;
  border-bottom: 3px solid transparent;
  color: #a8b0be;
  padding: 10px 14px;
  font-size: 0.92rem;
  white-space: nowrap;
  cursor: pointer;
}

.tab:hover {
  color: #fff;
}

.tab.is-active {
  color: #fff;
  border-bottom-color: var(--accent-bright);
}

/* ------------------------------------------------------------ burger menu */

/*
 * Below Bootstrap's md breakpoint the six tabs no longer fit and used to scroll
 * sideways, hiding half of them off-screen. They collapse into a dropdown
 * instead, and the trigger shows which section is open.
 */
@media (max-width: 767.98px) {
  .app-header {
    padding-bottom: 10px;
  }

  .tabs-toggle {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 10px;
    color: #fff;
    font-size: 0.95rem;
    cursor: pointer;
  }

  .tabs-toggle.is-open {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.24);
  }

  .tabs-toggle-label {
    flex: 1 1 auto;
    text-align: left;
  }

  .burger {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 18px;
    height: 14px;
    flex: 0 0 auto;
  }

  .burger span {
    display: block;
    height: 2px;
    background: currentColor;
    border-radius: 2px;
    transition: transform 0.18s ease, opacity 0.18s ease;
  }

  /* Open state: the middle bar fades out, the outer two cross into an X. */
  .tabs-toggle.is-open .burger span:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
  }
  .tabs-toggle.is-open .burger span:nth-child(2) {
    opacity: 0;
  }
  .tabs-toggle.is-open .burger span:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
  }

  .tabs-caret {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
    color: #9aa2b1;
    transition: transform 0.18s ease;
  }

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

  /* Stacked, full-width rows instead of a sideways-scrolling strip. */
  .tabs-list {
    display: none;
    flex-direction: column;
    gap: 0;
    margin-top: 6px;
    /*
     * Rows paint all the way to the container's rounded corners, and the active
     * row's 3px left indicator is a square border with no radius of its own. It
     * therefore covered the corner arc and stuck out past the rounding. Clipping
     * here makes every row follow the container's shape instead — and it keeps
     * doing so for whichever row happens to be active, rather than needing the
     * first and last child to be rounded by hand.
     */
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
  }

  .tabs.is-open .tabs-list {
    display: flex;
  }

  .tab {
    width: 100%;
    text-align: left;
    padding: 12px 14px;
    border-bottom: 0;
    border-left: 3px solid transparent;
    border-radius: 0;
    font-size: 0.95rem;
  }

  .tab:not(:last-child) {
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  }

  .tab.is-active {
    /* The indicator moves to the left edge now that rows are horizontal. */
    border-bottom-color: transparent;
    border-left-color: #4f9bf5;
    background: rgba(79, 155, 245, 0.14);
  }
}

/* ------------------------------------------------------------------ panels */

.panel {
  display: none;
  margin-top: 20px;
}
.panel.is-active {
  display: block;
}

.card {
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}

.card-header {
  background: #fff;
  border-bottom: 1px solid var(--line);
  font-weight: 600;
  font-size: 0.92rem;
  border-radius: 10px 10px 0 0 !important;
}

/*
 * Only the outside of a block is rounded, and a `.card-header` has already
 * rounded its own top corners. Whatever follows it inside the same card must
 * therefore not round its top ones again: two 10px arcs a hair apart read as a
 * card inside a card, which is exactly how the first row of a rounded list
 * looked. `.log-view` states the same rule for the bottom edge; this is the top
 * half of the pair, written once here so a new list cannot reintroduce it.
 */
.card > .card-header + * {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/*
 * A per-row report of the write a control triggered, on the model of the weekly
 * schedule's save column. Used by the tables whose controls have no column of
 * their own to report into — the active kennitala and the active zone — so a
 * click that changes nothing visible (clicking the row that is already active,
 * or a write the server refused) still says what happened.
 *
 * One line is always reserved, so a row does not grow the moment a report lands
 * in it and shrink again two seconds later.
 */
.row-status {
  display: block;
  min-height: 1.3em;
  font-size: 0.72rem;
  line-height: 1.3;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.row-status.is-saved {
  color: #1b7f4b;
}
.row-status.is-error {
  color: #b3261e;
}

/* ------------------------------------------------------------------- flashes */

/*
 * Messages float directly under the header rather than sitting in the flow. An
 * alert in the flow pushes the panel down, so every save rearranges the page for
 * one line of text — and on a phone that is most of the screen.
 *
 * `top` is driven by `--header-h`, which `toast.js` measures: the header is not a
 * fixed height, because the mobile burger opens and closes and the panel title
 * wraps at some widths.
 *
 * The stack sits below the header in z-order, not above it. Both are pinned to
 * the same corner, so the open burger menu has to be able to cover a message —
 * otherwise the menu would be unusable until the message expired.
 */
:root {
  --header-h: 0px;
}

.flash-stack {
  position: fixed;
  top: calc(var(--header-h) + 12px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 15;
  width: min(560px, calc(100vw - 24px));
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Only the messages themselves take clicks; the empty band stays transparent. */
  pointer-events: none;
}

.flash {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0;
  padding: 10px 12px;
  font-size: 0.85rem;
  line-height: 1.4;
  pointer-events: auto;
  box-shadow: 0 6px 20px rgba(15, 22, 36, 0.18);
  transition: opacity 0.18s ease, transform 0.18s ease;
}

/*
 * Hidden by opacity rather than `display: none`: a live region that is removed
 * from the accessibility tree announces nothing when it comes back.
 */
.flash.is-hidden {
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  /*
   * `pointer-events: none` hides a message from the mouse and from nothing else:
   * the slot still took focus, so Tab landed on a «Закрити повідомлення» button
   * nobody could see. `visibility` is what removes it from the tab order too.
   *
   * The delay is the one thing to be careful about — hiding it at once would cut
   * the fade-out off after its first frame, while showing it must not wait at all,
   * so the delay lives here (applied only while hidden) and not on `.flash`.
   */
  visibility: hidden;
  transition:
    opacity 0.18s ease,
    transform 0.18s ease,
    visibility 0s linear 0.18s;
}

.flash-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.flash-close {
  flex: 0 0 auto;
  margin: -2px -4px 0 0;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 1.1rem;
  line-height: 1.2;
  opacity: 0.6;
  cursor: pointer;
}

.flash-close:hover,
.flash-close:focus-visible {
  opacity: 1;
  outline: none;
}

/* -------------------------------------------------------------------- hints */

/*
 * The round "?" that replaced the muted paragraphs, and the one window every
 * hint on the page shares. Both are styled here rather than inline because the
 * button appears inside card headers, table cells and form labels alike, and it
 * has to look the same in all three.
 */
.hint-slot {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
}

.hint-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1px solid currentColor;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

.hint-btn:hover,
.hint-btn:focus-visible {
  color: var(--accent);
  outline: none;
}

.hint-btn[aria-expanded='true'] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/*
 * The few extra lines the last run wrote about itself — a failed attempt, a note
 * from the browser. It is a list of sentences, not a list of values, so it gets
 * the readable size and a bullet indent rather than a table cell's centring.
 */
.hint {
  margin: var(--s-2) 0 0;
  padding-left: 18px;
  font-size: 0.78rem;
  color: var(--muted);
}

/*
 * Fixed, and high enough to clear the sticky header (z-index 20). The window
 * cannot be clipped by the card or the table wrapper it was opened from, because
 * it is not inside them.
 */
.hint-layer {
  position: fixed;
  z-index: 40;
  max-width: min(340px, calc(100vw - 2 * 8px));
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: #fff;
  color: var(--ink);
  font-size: 0.82rem;
  line-height: 1.5;
  text-align: left;
  box-shadow: 0 8px 24px rgba(15, 22, 36, 0.22);
}

.hint-layer[hidden] {
  display: none;
}

/* A label and its "?" share a line, so the button sits with the word it explains. */
.field-head {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 22px;
}

/* --------------------------------------------------- schedule auto-save cell */

/*
 * The last column of the weekly table reports the outcome of the write that the
 * edit triggered. It is reserved width-wise by a short label, and it stays in the
 * layout even when empty so that the inputs do not shift sideways the moment a
 * save starts.
 */
.schedule-status {
  font-size: 0.78rem;
  line-height: 1.3;
  white-space: nowrap;
  min-width: 104px;
  /* Centred like the rest of the table; the width is still reserved by the
     shortest label so the inputs never shift when a save starts. */
  text-align: center;
}

.schedule-status.is-saving {
  color: var(--muted);
}

.schedule-status.is-saved {
  color: #1b7f4b;
}

/*
 * A rejection carries the server's own sentence, which is the only thing that
 * explains what to fix — so it may wrap rather than be clipped.
 */
.schedule-status.is-error {
  color: #b3261e;
  white-space: normal;
  min-width: 160px;
  max-width: 220px;
}

.status-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  margin-bottom: 16px;
  font-size: 0.9rem;
}
.status-grid dt {
  color: var(--muted);
  font-weight: 500;
}
.status-grid dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ tables */

.table {
  font-size: 0.86rem;
}
.table thead th {
  border-top: 0;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-weight: 600;
  text-transform: uppercase;
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  white-space: nowrap;
}
.table td,
.table th {
  vertical-align: middle;
}

/*
 * Every table in this panel is a data table: one short value per cell, named by
 * its column heading. Centring the heading and the value makes a column read down
 * its middle, which is what a table this dense needs — a left-aligned number and
 * a left-aligned label in the next column have no visual relationship at all.
 *
 * Applied per table rather than to `.table` on purpose: centring is a decision
 * about these five tables, not a rule that would silently swallow whatever table
 * gets added next (a log or a diff would want its own alignment).
 */
.data-table td,
.data-table th {
  text-align: center;
}

/*
 * `text-align: center` only moves inline content, so the three block-level things
 * these tables contain need a nudge of their own before a cell can centre them.
 */

/* The time and number fields of the weekly schedule. */
.data-table td input[type='text'],
.data-table td input[type='number'],
.data-table td input[type='time'] {
  display: inline-block;
  text-align: center;
}

/*
 * Bootstrap draws a switch's track and a radio's circle *outside* the element's
 * own box — a left padding with the mark absolutely positioned into it — so the
 * box and the mark disagree about where the control is, and centring the box
 * leaves the visible control a quarter of an em off.
 *
 * Here the disagreement is removed rather than compensated for. The control is a
 * centred flex box; the label — empty in the markup, so with no box of its own —
 * is given the exact rectangle of the mark it draws; and the transparent input
 * is stretched over that rectangle. The eye and the mouse therefore agree on one
 * and the same target, which is what makes a click aimed at the circle a click
 * on the control instead of a click on the cell around it.
 */
.data-table td .custom-control {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-left: 0;
  min-height: 1.5rem;
  vertical-align: middle;
}
.data-table td .custom-radio {
  width: 1rem;
}
.data-table td .custom-switch {
  width: 1.75rem;
}
/* The label's box *is* the mark, so `left: 0` and `top: 0` land exactly on it. */
.data-table td .custom-control-label {
  display: block;
  width: 100%;
  height: 1rem;
  margin-bottom: 0;
}
.data-table td .custom-control-label::before {
  top: 0;
  left: 0;
}
/*
 * Bootstrap puts the `::after` of *both* controls 1.5rem to the left of the
 * label, which is where it lives when the label is the padded text of a form.
 * With the label standing in for the mark itself, that offset is the width of
 * the control — so it has to be stated for each of the two. The radio's `::after`
 * is the dot inside its own 1rem circle, centred by its background position, so
 * it takes no inset at all; the switch's knob keeps Bootstrap's 2px.
 */
.data-table td .custom-radio .custom-control-label::after {
  top: 0;
  left: 0;
}
.data-table td .custom-switch .custom-control-label::after {
  top: 2px;
  left: 2px;
}
/*
 * The input is the only thing that toggles, and Bootstrap parks it at
 * `z-index: -1` — invisible to the mouse, reachable by the keyboard. Stretching
 * it over the control turns the whole visible mark into the click target while
 * leaving focus and the space bar working exactly as they did.
 */
.data-table td .custom-control-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  z-index: 1;
  cursor: pointer;
}

.table input[type='text'],
.table input[type='number'],
.table input[type='time'] {
  padding: 4px 6px;
  height: auto;
  font-size: 0.86rem;
}
.table input[type='time'] {
  width: 110px;
}
.table input[type='number'] {
  width: 80px;
}

/*
 * On a phone these tables stay tables.
 *
 * They used to become one card per row, every cell carrying its own heading.
 * That reads well for a long list and badly for exactly these five tables: each
 * one is a grid of short values whose whole point is the relationship between a
 * row and its columns, and stacking the columns destroys precisely that — a
 * «Кеннітала» line above an «Активна» line is not the same fact as a row you can
 * compare with the one above it.
 *
 * So the columns stay and the type shrinks instead. The widest compact table
 * (identity, five columns) then fits a card on a 390px screen; the weekly
 * schedule does not, and is left deliberately wider than its card, where
 * `.table-responsive` turns the overflow into a horizontal scroll.
 *
 * The breakpoint is the one the tab strip already collapses at. That is where
 * this panel admits it is not a desktop layout.
 */
@media (max-width: 767.98px) {
  /*
   * `min-width` rather than `width: 100%` so a narrow table still spans the card
   * when there are few columns, and `margin: 0 auto` keeps it centred in the
   * block it was given rather than pinned to one edge.
   */
  .data-table {
    width: auto;
    min-width: 100%;
    margin: 0 auto;
  }

  .data-table thead th,
  .data-table tbody td {
    padding: 4px 3px;
    /*
     * A value with nowhere to break — a kennitala, a coordinate pair — would
     * otherwise set the width of the whole table.
     */
    overflow-wrap: anywhere;
  }

  /*
   * The headings now carry the meaning on their own, so they keep a readable
   * size instead of shrinking to the letter-spaced decoration they were on
   * desktop. They wrap onto two lines rather than stretching the column.
   */
  .data-table thead th {
    /*
     * A heading is a word, not punctuation: at 0.62rem it was 9.92px, which is
     * the size below which a column stops being named and starts being textured.
     * 0.7rem is 11.2px, which is the smallest this panel asks anyone to read.
     */
    font-size: 0.7rem;
    letter-spacing: 0;
    white-space: normal;
    line-height: 1.2;
  }

  /* The action cell holds one button: give it the width of that button. */
  .data-table td[data-label=''] {
    width: 1%;
    white-space: nowrap;
  }

  /*
   * The weekly schedule is the one table wider than the card, so its three fixed
   * widths are tightened here — the two time fields and the save report are the
   * whole of the overflow.
   */
  .table input[type='time'] {
    width: 78px;
  }
  .table input[type='number'] {
    width: 52px;
  }
  .schedule-status {
    min-width: 0;
  }

  /*
   * Two classes on purpose: the shared `.data-table td` rules above carry more
   * weight than one class, and this way these rules outrank them without
   * `!important`.
   *
   * This is the one table whose columns must not be squeezed to fit. A cell that
   * may break anywhere absorbs the whole overflow by itself: «Понеділок» spends
   * it one letter per line, the row grows two hundred pixels tall, and the day
   * that names it stops being a row label and becomes a column of letters.
   */
  .data-table.table-schedule thead th,
  .data-table.table-schedule tbody td {
    overflow-wrap: normal;
  }

  /*
   * The day stays put while the rest of its row scrolls under it: a value being
   * edited has to keep its name on screen, or «16:35» could be either end of the
   * day. The background is opaque because the columns behind must not show
   * through it, and the right edge is a shadow rather than a border because a
   * collapsed border is not painted on a sticky cell.
   */
  .data-table.table-schedule .schedule-day,
  .data-table.table-schedule thead th:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: #fff;
    /* A weekday is one word, and it is the row's only label. */
    white-space: nowrap;
  }

  .data-table.table-schedule .schedule-day {
    /*
     * A floor under the day, so the label is a label: the automatic minimum of a
     * cell that may not break is its longest word, and 92px is room for that word
     * plus the padding, whatever the type does.
     */
    min-width: 92px;
    box-shadow: inset -1px 0 0 var(--line);
  }

  /* Above the cells it covers, below nothing else. */
  .data-table.table-schedule thead th:first-child {
    z-index: 2;
  }

  /* The three fixed widths the schedule's overflow is made of. */
  .data-table.table-schedule input[type='time'] {
    width: 74px;
  }
  .data-table.table-schedule input[type='number'] {
    width: 46px;
  }

  /*
   * A swipe that reaches the end of the table must not turn into the browser's
   * own "go back" gesture.
   */
  .table-schedule-box {
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }
}

/*
 * One band is left where a table has to be a table but does not quite fit.
 *
 * Between 768 and 991px the page container is 720px, which leaves 648px inside a
 * card. The schedule's seven columns add up to 665px at their natural size — the
 * two time fields and the status column are the whole of it — so the last 17px sit
 * behind a scrollbar that nothing on screen announces. Below 768px the tightened
 * phone widths leave room to spare; from 992px the container is 960px and there
 * was never a problem. Tightening the three fixed widths in this band alone
 * leaves the layout at every other size exactly as it was.
 */
@media (min-width: 768px) and (max-width: 991.98px) {
  .table input[type='time'] {
    width: 96px;
  }
  .schedule-status {
    min-width: 92px;
  }
}

/* ----------------------------------------------------------------- badges */

.tag {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.tag-pending {
  background: #fff4d6;
  color: #8a5b00;
}
.tag-running {
  background: #dbeafe;
  color: #1e40af;
}
/*
 * A projected slot: the plan the schedule implies for a date the scheduler has
 * not reached yet. Deliberately softer than `pending`, which means a stored row
 * that is waiting to fire — one exists, the other does not.
 */
.tag-planned {
  background: #eef2f7;
  color: #38506e;
  border: 1px dashed #b9c6d6;
}
.tag-done,
.tag-success {
  background: #dcfce7;
  color: #166534;
}
.tag-failed {
  background: #fee2e2;
  color: #991b1b;
}
.tag-skipped,
.tag-missed {
  background: #eceff3;
  color: #475569;
}
.tag-dry-run,
.tag-rehearsed {
  background: #ede9fe;
  color: #5b21b6;
}
.tag-inspect {
  background: #e0f2fe;
  color: #075985;
}

/* -------------------------------------------------------------------- map */

.geo-map-wrap {
  position: relative;
  /*
   * Leaflet paints its panes at z-index 200–700 inside whatever stacking
   * context it finds. `position: relative` alone does not make one, so those
   * numbers used to compete with the sticky header (z-index 20) in the root
   * context — and won, which is why the map scrolled over the panel header.
   * A z-index of 0 creates the context, and every internal layer stays inside
   * it, beneath the header. Raising the header instead would only hold until
   * the next Leaflet layer above 1000; this holds by construction.
   */
  z-index: 0;
}

.geo-map {
  height: 420px;
  width: 100%;
  background: #e8edf3;
  border-radius: 0;
}

/* Leaflet's own attribution must stay readable on a light map. */
.geo-map .leaflet-control-attribution {
  font-size: 0.68rem;
  background: rgba(255, 255, 255, 0.82);
}

.geo-map-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  text-align: center;
  background: repeating-linear-gradient(45deg, #f4f6f9, #f4f6f9 12px, #eef1f6 12px, #eef1f6 24px);
  color: #475569;
}

/* The zone being edited is highlighted in the table too. */
#geoBody tr.is-selected {
  background: #fff7e6;
}

@media (max-width: 991.98px) {
  .geo-map {
    height: 320px;
  }
}

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

/* Navigation is made of real anchors; strip the browser defaults only. */
.tab {
  display: inline-block;
  text-decoration: none;
}
.tab:hover,
.tab:focus {
  color: #fff;
  text-decoration: none;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}

.chip {
  display: inline-block;
  padding: 3px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  color: #3f4654;
  font-size: 0.82rem;
  text-decoration: none;
  white-space: nowrap;
}
.chip:hover {
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}
.chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* ------------------------------------------------------------- user chip */

/*
 * The signed-in identity, sitting after the live clock in the dark header. It
 * gets a hairline separator rather than a background: it belongs to the same
 * row of facts as the clock and the auto badge, and a second badge shape there
 * would read as another status.
 */
.user-chip {
  display: flex;
  align-items: center;
  margin-left: 12px;
  padding-left: 12px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
}

.user-role {
  color: #dfe4ec;
  font-size: 0.85rem;
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Bootstrap's `btn-link` is blue on white; this one lives on near-black. */
.user-chip .logout {
  padding: 0 0 0 10px;
  color: #9aa2b1;
  font-size: 0.85rem;
  line-height: inherit;
}
.user-chip .logout:hover,
.user-chip .logout:focus {
  color: #fff;
  text-decoration: none;
}

/*
 * The clock, the badge, the identity and the way out are one row of facts: they
 * belong together, and on a phone they fit. On the narrowest phone they do not —
 * the four together come to twenty pixels more than the screen, and the page
 * answers by scrolling sideways, which moves everything under a thumb for no
 * reason at all. Letting the row wrap keeps every fact and puts the identity
 * under the clock, instead of shrinking the type until it cannot be read.
 */
@media (max-width: 360px) {
  .header-status {
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 4px;
  }
}

/* ------------------------------------------------------------ site icon */

.icon-field {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.icon-preview {
  width: 32px;
  height: 32px;
  object-fit: contain;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
}

.icon-field-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.icon-status[data-kind='ok'] {
  color: #166534;
}
.icon-status[data-kind='error'] {
  color: #991b1b;
}

/* --------------------------------------------------------------- sign-in */

.login-body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;
}

/*
 * On a narrow phone the page's own margin is the difference between ten digits
 * having room and not: the boxes share whatever is left, and the card needs the
 * width more than the page needs the padding.
 */
@media (max-width: 400px) {
  .login-body {
    padding: 12px;
  }
}

.login-card {
  width: 100%;
  max-width: 360px;
  border: 1px solid var(--line);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

.login-title {
  margin-bottom: 2px;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.login-sub {
  margin-bottom: 20px;
  color: var(--muted);
  font-size: 0.85rem;
}

/* Space is reserved, so showing an error does not move the button under it. */
.login-error {
  min-height: 1.4rem;
  margin-bottom: 8px;
  font-size: 0.85rem;
}

/* ------------------------------------------------------- kennitala field */

/*
 * A kennitala is ten digits in two groups, and it is entered that way: one digit
 * per box, six, a dash, four. The boxes take `.form-control` for the border, the
 * focus ring and the height the field below them has, and are told to share one
 * row instead of each claiming the whole width.
 */
.kennitala-field {
  display: flex;
  align-items: center;
  /* The air that makes the digits read as separate boxes, and the wider gap the
     dash sits in. */
  gap: 4px;
}

.kennitala-field .kennitala-digit {
  flex: 1 1 0;
  /* A flex item will not shrink below its content, and ten boxes that cannot
     shrink push the card sideways on a phone. */
  min-width: 0;
  width: auto;
  /* Bootstrap's horizontal padding is sized for a word, not for one digit. */
  padding-right: 0;
  padding-left: 0;
  text-align: center;
}

.kennitala-dash {
  flex: none;
  color: var(--muted);
  line-height: 1;
}

/*
 * A refused number is one class on the container. Bootstrap's own
 * `.is-invalid ~ .invalid-feedback` expects the field and its message to be
 * siblings, but here the boxes sit inside the container while the message is
 * outside it — so the row is painted from the container down, and the message is
 * revealed the way Bootstrap would have revealed it.
 */
.kennitala-field.is-invalid .kennitala-digit {
  border-color: #dc3545;
}

.kennitala-field.is-invalid ~ .invalid-feedback {
  display: block;
}

/* ---------------------------------------------------------- route targets */

/* Whatever the URL points at is highlighted, so a deep link is visible. */
#scheduleBody tr.is-target > td {
  background: #fff8e1;
}

/* The roster is a row list, not a table, and a person can be the target of the URL. */
.person-row.is-target {
  background: #fff8e1;
  box-shadow: -10px 0 0 #fff8e1, 10px 0 0 #fff8e1;
}

#settingsForm [data-group].is-target {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ---------------------------------------------------------- journal filters */

.filter-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 6px;
}
.filter-row:last-child {
  margin-bottom: 0;
}

.filter-label {
  width: 54px;
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.filter-day {
  width: auto;
  max-width: 170px;
}

.filter-count {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.8rem;
}

/*
 * The day the plan is for, and the two ways out of it.
 *
 * What stood here was «‹» and «›» in a running line of text: 13x20 links a mouse
 * had to hunt for and a thumb could not find, with the date floating somewhere
 * between them and two buttons after that. They are one control now — a box
 * holding the steps and the day they step around — because they are one decision
 * seen three ways, and a target that moves a day has to be at least as easy to
 * hit as the table it moves.
 *
 * The height is set once, on the stepper, and the pieces stretch to it: arrows
 * and date stay one row of one height instead of three boxes that agree by
 * accident. The two buttons are a row of their own under it, so the header reads
 * as a day and the ways out of it rather than as one crowded line. 40px is the
 * floor; the tablet and finger size is 44px, further down.
 */
.plan-head {
  /* The title and the tools share one line when they fit and two when they do
     not, and the gap is what keeps a wrapped row from touching the title. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 10px;
}

/*
 * Two rows, not one: the arrows and the date answer «which day», and Сьогодні
 * and Стімпи answer «where to go from it». A column makes the split explicit
 * instead of leaving it to a wrap that falls the right way only at some widths,
 * and it keeps the two buttons from competing with the date for the last few
 * pixels on a phone. A wide card hugs the right edge of the header; the phone
 * band below stretches the column across the card instead.
 */
.plan-tools {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

/* The last-run line is a caption, and an empty one is not a row: without this
   it would still spend a gap's worth of height above the stepper. */
#planMeta:empty {
  display: none;
}

.plan-tools-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.plan-nav {
  display: inline-flex;
  align-items: stretch;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  /* One frame around three pieces: the corners belong to the box, not to the
     controls inside it. */
  overflow: hidden;
}

/* An `<a>`, not a `<button>`: a day is a URL, and a neighbour day can be opened
   in its own tab the way any other view can. */
.plan-step {
  flex: 0 0 auto;
  width: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  /* A shade off the card's white, so the two ends of the group read as the
     pushable part and the date in the middle reads as the value they change. */
  background: #f7f8fa;
  text-decoration: none;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.plan-step svg {
  width: 20px;
  height: 20px;
}
.plan-step:first-child {
  border-right: 1px solid var(--line);
}
.plan-step:last-child {
  border-left: 1px solid var(--line);
}
.plan-step:hover,
.plan-step:focus-visible {
  background: var(--accent-soft);
  color: var(--accent);
  text-decoration: none;
}
.plan-step:active {
  background: #d9e5f5;
}

/*
 * Inside the stepper the date gives up its own frame, so the three pieces read as
 * one control rather than as a form field with two decorations beside it. The
 * focus ring is given up with it and replaced by a filled field, because a ring
 * drawn on a borderless box in the middle of a bordered group looks like a bug.
 */
.plan-nav .plan-day {
  width: auto;
  min-width: 126px;
  max-width: 170px;
  height: 100%;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  font-size: 0.85rem;
}
.plan-nav .plan-day:focus {
  box-shadow: none;
  background: var(--accent-soft);
}

/* Сьогодні and Стімпи: a row of their own under the stepper, sharing its
   height so the header reads as a date and the ways out of it. */
.plan-action {
  min-height: 40px;
  display: inline-flex;
  align-items: center;
}

/*
 * On a phone the tools take the whole row and the stepper takes all of it, so
 * the day field ends up the widest thing on the card instead of the narrowest
 * and the two buttons sit under it instead of beside the date. The column is
 * stretched flat here, because a right-aligned stack leaves the card's left
 * edge doing nothing.
 */
@media (max-width: 767.98px) {
  .plan-tools {
    width: 100%;
    align-items: stretch;
  }
  .plan-nav {
    min-width: 0;
  }
  .plan-nav .plan-day {
    flex: 1 1 auto;
    min-width: 0;
    max-width: none;
  }
  .plan-tools-actions {
    justify-content: flex-start;
  }
}

/*
 * The kennitala in the plan card. The mask is the default state, so the button
 * has to look like text and still be findable: no padding to shift the line, a
 * dotted underline to say there is something behind it, and a pointer to agree
 * with that. `font: inherit` keeps it in the same muted small type as the name
 * it sits next to.
 */
.plan-kennitala {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

.plan-kennitala:hover,
.plan-kennitala.is-open {
  color: var(--accent);
}

.plan-kennitala:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/*
 * The hourglass that opens a day's stamps. At 0.45 opacity it measured 2.39:1 —
 * readable only if you already knew it was there, and on a touch screen there is
 * no hover to rescue it. It is dimmed, not hidden.
 */
.journal-day-link {
  text-decoration: none;
  opacity: 0.75;
}
.journal-day-link:hover,
.journal-day-link:focus-visible {
  opacity: 1;
  text-decoration: underline;
}

/* ------------------------------------------------------------------- misc */

.log-view {
  margin: 0;
  padding: 12px 14px;
  max-height: 340px;
  overflow: auto;
  background: #0f1115;
  color: #cfd6e4;
  border-radius: 0 0 10px 10px;
  font-size: 0.76rem;
  line-height: 1.5;
  /*
   * `pre` defaults to `white-space: pre`, which never wraps — so a Playwright
   * error or a long scope put the rest of the message behind a horizontal
   * scrollbar, and in a log that is the half you wanted. Wrap instead, and break
   * words that have nowhere to break (a URL, a hash).
   */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  /*
   * The browser's own scroll anchoring does half of what `log-view.js` does,
   * and doing both moves the text twice as far as it should go. Measured: with
   * the default, inserting a page above the viewport moved `scrollTop` by 2×
   * the inserted height and the line under the reader slid by one full page.
   * The module is the one that knows where the reader is, so it gets to decide;
   * Safari never implemented scroll anchoring, so this is a no-op there.
   */
  overflow-anchor: none;
}
/*
 * One block per entry rather than one text run with `\n` between the lines.
 * Lines are added at both ends of this list, and a block child can be inserted
 * before or removed from a position that a string of newlines cannot be.
 */
.log-view .log-line {
  display: block;
  min-height: 1.5em;
  /*
   * A long message wraps, and the continuation lines are indented to the depth
   * of the timestamp so the entries still read as separate blocks. Negative
   * because `text-indent` applies to the first line, which is the one that is
   * already positioned by the padding.
   */
  padding-left: 11ch;
  text-indent: -11ch;
}
/*
 * Says either where the older entries are, or that the server is unreachable.
 *
 * It is shown and hidden with the `hidden` attribute, so the rule is written as
 * `:not([hidden])` rather than plain `display: none` in both directions: there
 * is exactly one thing that decides whether it is on screen, and it is the same
 * thing the module sets.
 */
.log-view .log-note:not([hidden]) {
  display: block;
  margin-bottom: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  color: #8b93a3;
  font-style: italic;
}
.log-view .lvl-error {
  color: #ff8a8a;
}
.log-view .lvl-warn {
  color: #ffd479;
}
.log-view .lvl-success {
  color: #86efac;
}
.log-view .lvl-info {
  color: #8ecbff;
}
.log-view .lvl-debug {
  color: #8b93a3;
}

.btn {
  border-radius: 7px;
}

/*
 * Manual actions: one centred column, every button as wide as the widest one.
 * A single-column grid sizes that column to the largest max-content of its
 * items, so nothing is hard-coded — the row stays even when a label is edited,
 * and it re-measures itself instead of drifting out of sync with the text.
 */
.manual-actions {
  display: grid;
  grid-template-columns: max-content;
  justify-content: center;
  gap: 8px;
}
/*
 * The manual block, in the one band where the card is wide and the content is not.
 *
 * A centred column sized to its longest label is right on a phone, where the card
 * is 350px and that column is most of it, and right on a desktop, where the card is
 * 1110px and the column reads as a deliberate block. At 768–991px it is neither: a
 * 189px column in a 690px card, leaving about 250px of empty card on either side.
 *
 * Two even columns fill the width the card already has, and the four buttons make a
 * tidy 2x2. Nothing is hard-coded — the columns are `1fr`, so a fifth button would
 * make a third row rather than overflow.
 *
 * This override lives here, immediately after the rule above, and not in the
 * tablet block further up the file: both selectors are one class, so the cascade
 * is decided by source order alone and an override written earlier loses. Keeping
 * the pair adjacent is what makes that visible.
 */
@media (min-width: 768px) and (max-width: 991.98px) {
  .manual-actions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    justify-content: stretch;
  }
  .manual-actions .btn {
    width: 100%;
  }
}
.btn-sm {
  font-size: 0.78rem;
  padding: 3px 8px;
}

/* ------------------------------------------------------------------- people */

/*
 * One person, one row: name, kennitala, stamps, role, password and the clock.
 * Deliberately not a table, though it replaces one. A row now carries a role and
 * a password as well as a kennitala, and the column of controls that would take
 * is what pushes a four-column table off a phone screen — so the row wraps
 * instead, and the shape is the same on a phone and on a laptop.
 */
.person-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
}
.person-row:first-child {
  border-top: 0;
  padding-top: 0;
}

.person-who {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}
.person-name {
  font-weight: 600;
}
.person-mask,
.person-count {
  color: var(--muted);
  font-size: 0.78rem;
}

/*
 * The clock, said in words beside its mark, because «стімпає за» is the whole
 * meaning of the control and a bare dot in a line of values is not.
 *
 * This is not the bare-mark control the tables draw — there the label *was* the
 * circle — so the geometry is Bootstrap's, with the caption to the right of the
 * mark. The property worth keeping from the table's version is, and it is stated
 * rather than inherited: the input is stretched over the whole control, so the
 * mark and its caption are one tap target and the thing the mouse meets.
 */
/*
 * The device's kennitala, in the heading of the card it belongs to.
 *
 * It reads as a label and a value at a glance — «стімпає за …» — but the value is
 * a control, because it is the one thing on this card that is about the machine
 * rather than about a person. It stays out of the way until there is somebody to
 * choose from.
 */
.people-clock {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-weight: 400;
}
.people-clock-label {
  color: var(--muted);
  font-size: 0.8rem;
  white-space: nowrap;
}
.people-clock .form-control {
  width: auto;
  max-width: 200px;
  height: var(--control-h);
}

/* A choice, so it keeps its own width instead of taking a share of the row. */
.person-role {
  flex: 0 0 auto;
  width: auto;
  max-width: 100%;
}

/*
 * Changing your own password, on the card a `user` is shown instead of the
 * roster. The device-wide clock left the rows, but this one is about the person
 * reading it, so it stays where it is.
 */
.person-password {
  display: flex;
  flex: 1 1 190px;
  gap: var(--s-1);
  min-width: 0;
}
.person-password input {
  min-width: 0;
}

.person-row .row-status {
  flex: 1 1 100%;
}

textarea.ua {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
}

@media (max-width: 575.98px) {
  .app-header {
    padding-top: 10px;
  }
  .brand-sub {
    display: none;
  }
  .table {
    font-size: 0.8rem;
  }
  .table input[type='number'] {
    width: 66px;
  }
  .table input[type='time'] {
    width: 92px;
  }
}

/* ------------------------------------------------------- monthly planner */

/*
 * A scroll-snap rail of whole months, centred on the month being looked at: the
 * previous, the current and the next one. `position: relative` is what makes
 * `offsetLeft` on a month mean "distance from the left edge of the rail" — the
 * centring and the paging are both arithmetic on that number.
 */
.calendar-strip {
  position: relative;
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 6px;
  scrollbar-width: thin;
}

/*
 * `min(300px, 84vw)` rather than a fraction of the rail: on a phone the month has
 * to be narrower than the screen so that the next one shows at the edge and the
 * rail reads as scrollable, and on a wide screen all three fit side by side.
 */
.cal-month {
  flex: 0 0 auto;
  width: min(300px, 84vw);
  scroll-snap-align: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  padding: 6px 6px 8px;
}

.cal-month-title {
  margin-bottom: 6px;
  text-align: center;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: capitalize;
}

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

.cal-head {
  padding-bottom: 2px;
  text-align: center;
  font-size: 0.6rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--muted);
}

.cal-blank {
  display: block;
}

/*
 * A day cell is a button, and always is one — including in the past, where it
 * opens the day's journal rather than its editor. A calendar square is a small
 * target, so it is drawn as large as the column allows and the whole square
 * takes the click.
 */
.cal-cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: #f6f7f9;
  color: var(--muted);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.cal-cell:hover {
  border-color: var(--accent);
}
.cal-cell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* A day the plan works. */
.cal-cell.is-on {
  background: #fff;
  color: var(--ink);
  font-weight: 600;
}

/*
 * A date carrying an exception. The two kinds have to be tellable apart at a
 * glance, so they get opposite colours and a dot in the corner — the number
 * itself cannot also hold the hours at this size.
 */
.cal-cell.is-silenced {
  background: #fdeceb;
  color: #991b1b;
}
.cal-cell.is-extra {
  background: #e7f5ec;
  color: #166534;
}
/*
 * The dot marks "this day carries an exception". Both kinds used to mark it the
 * same way, in a colour that for a red-green colour-blind reader is one colour —
 * so the shape tells them apart as well: a round dot in the corner for the day
 * that was taken away, a square one for the day that was added.
 */
.cal-cell.is-silenced::after,
.cal-cell.is-extra::after {
  content: '';
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 4px;
  height: 4px;
  background: currentColor;
}
.cal-cell.is-silenced::after {
  border-radius: 50%;
}
.cal-cell.is-extra::after {
  border-radius: 1px;
  top: 3px;
  bottom: auto;
}

/*
 * Already lived through: plainly not the thing being planned, but still a number
 * somebody has to be able to read. This used to be the whole cell at 0.6 opacity,
 * which faded muted text into the background at about 2.3:1 — the past days were
 * the ones that became unreadable. Dimming the *fill* instead of the text keeps
 * the number at the same contrast as every other day while looking flatter.
 */
.cal-cell.is-past {
  background: transparent;
  border-color: var(--line);
}

.cal-cell.is-today {
  box-shadow: inset 0 0 0 2px var(--accent);
}

.cal-cell.is-selected {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.calendar-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 10px 0 2px;
}

.calendar-nav-label {
  min-width: 10rem;
  text-align: center;
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: capitalize;
}

.calendar-day {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.day-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.day-plans {
  margin-bottom: 12px;
}

.day-modes {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin-bottom: 10px;
}

.day-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px 12px;
}

.day-fields .field {
  min-width: 0;
}

/* The note is the one field that is a sentence rather than a value. */
.day-fields .field-wide {
  grid-column: 1 / -1;
}

.day-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}

/* Reserved, so the buttons do not shift the moment a save starts. */
.day-status {
  min-height: 1.2em;
  font-size: 0.78rem;
  color: var(--muted);
}
.day-status.is-saved {
  color: #1b7f4b;
}
.day-status.is-error {
  color: #b3261e;
}

.day-archive {
  margin-top: 16px;
}

.archive-title {
  margin-bottom: 6px;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.archive-empty {
  margin: 0;
  padding: 6px 0;
  color: var(--muted);
  font-size: 0.82rem;
}

.archive-list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.82rem;
}

.archive-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  padding: 5px 0;
  border-top: 1px solid var(--line);
}

.archive-meta {
  color: var(--muted);
  font-size: 0.78rem;
}

@media (max-width: 767.98px) {
  .cal-month {
    width: min(300px, 88vw);
  }
  .calendar-nav-label {
    min-width: 8rem;
  }
  .day-fields {
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  }
}

/* ------------------------------------------ one-time invitation link dialog */

/*
 * The link is the point of the dialog, so it gets the width and everything else
 * stays out of its way. Bootstrap's own modal needs its JavaScript, which this
 * panel does not load; a native `<dialog>` gives the backdrop, Esc and the focus
 * handling for free.
 */
.link-dialog {
  width: min(560px, 92vw);
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}

.link-dialog::backdrop {
  background: rgba(15, 23, 42, 0.45);
}

.link-title {
  margin-bottom: 4px;
  font-size: 1.05rem;
  font-weight: 600;
}

.link-note {
  margin-bottom: 10px;
  color: var(--muted);
  font-size: 0.85rem;
}

.link-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

/* A long token must not push the button off a phone. */
.link-row .form-control {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  min-width: 0;
}

.link-expiry {
  margin: 8px 0 4px;
}

.link-warn {
  color: var(--danger);
}

/* ---------------------------------------------------------- work calendar */

/*
 * GitHub-style contribution grid for the overview panel: weeks as columns,
 * weekdays as rows, one square per local day. A square is ten pixels and never
 * grows, so the width of the card decides how many weeks are drawn: the panel
 * measures the box, asks the server for that many columns, and the grid lands
 * with the last marked day in the middle of it. `.stats-scroll` stays a scroll
 * box only for the moment an answer is still on its way.
 *
 * The colour is the server's category and the legend below is built from the
 * same payload, so a square can never be painted a colour the legend does not
 * name.
 */
.stats-body {
  /* The tooltip is placed from this box, so it must be the offset parent. */
  position: relative;
}

.stats-scroll {
  /*
   * A flex line, so the board can be centred when it is narrower than the card:
   * the anchor's week is the grid's middle column, so centring the board is what
   * puts the anchor in the middle of the screen. The panel asks for exactly the
   * columns that fit, which normally leaves nothing to scroll at all.
   */
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* A little air, so the grid does not sit flush against the card's own edge. */
  padding: 3px 0 5px;
  scrollbar-width: thin;
}

.stats-board {
  position: relative;
  /*
   * The grid's tracks are fixed, so the board takes its content's width rather
   * than the card's, and refuses to be stretched by the flex line above — it is
   * the board's exact size that puts its middle column under the box's middle.
   */
  flex: 0 0 auto;
  width: max-content;
}

.stats-months {
  position: relative;
  height: 15px;
}

.stats-month {
  position: absolute;
  top: 1px;
  color: var(--muted);
  font-size: 0.62rem;
  text-transform: capitalize;
  white-space: nowrap;
}

.stats-grid {
  display: grid;
  grid-template-rows: repeat(7, 10px);
  grid-auto-flow: column;
  grid-auto-columns: 10px;
  gap: 2px;
  width: max-content;
  border-radius: 3px;
}

/* Keyboard users land on the grid as a whole, then walk it with the arrows. */
.stats-grid:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.stats-cell {
  display: block;
  /* The lit block draws its ring inside itself, so this is the ring's box. */
  position: relative;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: #eceff3;
  box-shadow: inset 0 0 0 1px rgba(27, 29, 34, 0.05);
}

/*
 * One rule per category, shared by the square and its legend swatch: the two
 * are the same fact drawn twice, and a colour written once cannot drift.
 * `color` is the ring the hover and focus highlight draws in `currentColor`.
 */
.stats-cell.is-worked,
.stats-swatch.is-worked {
  background: #3fb950;
  color: #1a7f37;
}
.stats-cell.is-overtime,
.stats-swatch.is-overtime {
  background: #f0883e;
  color: #b45309;
}
.stats-cell.is-weekend,
.stats-swatch.is-weekend {
  background: #e4e7ec;
  color: #6b7280;
}
.stats-cell.is-holiday,
.stats-swatch.is-holiday {
  background: #a371f7;
  color: #6b21a8;
}
.stats-cell.is-sick,
.stats-swatch.is-sick {
  background: #54aeff;
  color: #1d4ed8;
}
.stats-cell.is-unpaid,
.stats-swatch.is-unpaid {
  background: #ff9b8a;
  color: #b42318;
}
.stats-cell.is-leave,
.stats-swatch.is-leave {
  background: #2dd4bf;
  color: #0f766e;
}
.stats-cell.is-absent,
.stats-swatch.is-absent {
  background: #f3f4f6;
  color: #9aa1ac;
}
.stats-cell.is-future,
.stats-swatch.is-future {
  background: #fbfcfd;
  color: #cbd0d8;
}

/*
 * The day under the cursor or the keyboard is lit in its own colour.
 *
 * A ring drawn *around* the block has nowhere to go here: the gap between two
 * days is two pixels, so an outline or an outer shadow reaches into the day
 * beside it. This one is painted on a pseudo-element that is exactly the block's
 * own box, which makes overlapping impossible by construction — and the ring
 * itself is a gradient, transparent towards the middle and solid at the edge, so
 * the colour fades into the day instead of cutting a line around it.
 */
.stats-cell.is-hot::after {
  content: '';
  position: absolute;
  inset: 0;
  /* The block's own corner, one step rounder than a square corner. */
  border-radius: inherit;
  /*
   * The stops are read against the block's five-pixel radius: transparent out to
   * 62% of it, then a fade to the full ink at the edge. That leaves the inner
   * six pixels exactly the day's own colour and makes the ring about two pixels
   * wide — a soft edge rather than a frame drawn round a square this small.
   */
  background: radial-gradient(circle closest-side, transparent 62%, currentColor 100%);
}

.stats-tip {
  position: absolute;
  z-index: 5;
  max-width: min(240px, 72vw);
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  color: var(--ink);
  font-size: 0.74rem;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 6px 18px rgba(15, 22, 36, 0.18);
  /*
   * It follows the cursor, so taking its clicks would make the day underneath
   * it un-hoverable.
   */
  pointer-events: none;
  white-space: nowrap;
}

.stats-tip[hidden] {
  display: none;
}

.stats-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-top: 10px;
  font-size: 0.72rem;
  color: var(--muted);
}

.stats-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.stats-swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px rgba(27, 29, 34, 0.06);
}

.stats-summary {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: 0.8rem;
}

/*
 * A floor under the result card, before the first snapshot lands.
 *
 * The card is empty for a few hundred milliseconds and then fills with a status
 * line, a message and a small grid. Without a floor, everything below it on the
 * page slides down by about a screen and a half the moment the answer arrives —
 * measured as a 40% jump in document height — and a reader who had started
 * scrolling loses what they were reading. The floor is below what the card
 * normally holds, so it costs nothing once there is something to show.
 */
#lastResult {
  min-height: 140px;
}

/* ------------------------------------------------------ platform behaviour */

/*
 * iOS Safari magnifies the whole page when it focuses a field whose computed
 * font-size is under 16px, and it does not zoom back out when the field blurs —
 * the panel stays magnified until the reader pinches it back out. Six kinds of
 * field in this panel were under that threshold (the plan's day field at 13.6px,
 * the journal's at 14px, the table time fields at 13.76px, and the settings
 * fields at 14px).
 *
 * The size is raised only where a finger is in play. On a mouse the denser
 * 0.78rem fields are what let a settings card be read at a glance, and a mouse
 * never zooms anything to begin with. The table fields need naming individually
 * because `.table input[type='time']` is a more specific selector than
 * `.form-control-sm` and would otherwise win.
 */
@media (max-width: 991.98px), (any-pointer: coarse) {
  /*
   * Two of these are named by id rather than by width class. The panel already
   * sizes them with a selector that beats a single class — `.plan-nav .plan-day`
   * is two classes and `textarea.ua` is a class plus an element — and an id is
   * the shortest way to say "this field, on a touch screen, is 16px" without
   * reaching every other field in the card with it.
   */
  .form-control-sm,
  #planDay,
  #settingsForm .form-control,
  #passwordForm .form-control,
  .data-table input[type='text'],
  .data-table input[type='number'],
  .data-table input[type='time'] {
    font-size: 16px;
  }
}

/*
 * The panel animates two things nobody asked for: a message fading in and out,
 * and a spinner. The spinner is information and keeps turning; the fades stop for
 * a reader who has told their system that motion costs them something.
 */
@media (prefers-reduced-motion: reduce) {
  .flash,
  .flash.is-hidden {
    transition: none;
  }
}

/*
 * Finger-sized controls, last word on the subject.
 *
 * This block sits at the end of the file rather than beside the components it
 * sizes, and that placement is load-bearing: every selector here is one class, and
 * a media query contributes no specificity, so a declaration written above the
 * plain rule it means to override simply loses. The calendar square was the proof —
 * its 44px height sat 400 lines above the 30px one that won.
 *
 * Keeping the sizing in one block, at the end, is what lets the question stay
 * "is this device being used with a finger?" instead of "how tall is this
 * particular square".
 */
/*
 * A finger wants 44px, and a tablet is where a finger and a mouse both turn up,
 * so the size is asked for by pointer as well as by width. `any-pointer` rather
 * than `pointer`: the question is whether a finger *can* reach these controls,
 * not whether it is what the device uses by default — a laptop with a touchscreen
 * reports a fine primary pointer and is then held by hand all the same.
 */
@media (max-width: 991.98px), (any-pointer: coarse) {
  .plan-nav {
    height: 44px;
  }
  .plan-step {
    width: 44px;
  }
  .plan-step svg {
    width: 22px;
    height: 22px;
  }
  .plan-action {
    min-height: 44px;
  }

  /*
   * The rest of the finger-sized controls.
   *
   * They are gathered here rather than resized in their own rules because the
   * question being answered is not "how big should a chip be" — a chip is
   * perfectly fine small — but "is this device being used with a finger", and
   * that question is asked in exactly one place.
   */
  .hint-btn {
    position: relative;
  }
  /*
   * The circle itself stays 18px: it is a piece of punctuation beside a label,
   * and a 44px ring there would shout. What grows is the area that answers a
   * tap, as a transparent square centred on the mark.
   */
  .hint-btn::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--touch-h);
    height: var(--touch-h);
    transform: translate(-50%, -50%);
  }
  .chip {
    display: inline-flex;
    align-items: center;
    min-height: var(--touch-h);
  }
  .cal-cell {
    height: var(--touch-h);
  }
  /*
   * A cell's radio is drawn as its own mark and the invisible input is stretched
   * over it, so raising the control's height raises the target: the input is
   * `height: 100%` and follows. The width is left alone — the mark is 1rem wide
   * and widening the box would drag the mark off the centre it was placed on.
   */
  .data-table td .custom-control {
    min-height: var(--touch-h);
  }
  [data-act$='-del'] {
    min-width: var(--touch-h);
    min-height: var(--touch-h);
  }
  .people-clock .form-control {
    height: var(--touch-h);
  }
}
